feat: 将 HumanBreak 新内容移植入该样板

This commit is contained in:
unanmed 2026-02-28 22:56:59 +08:00
parent 6bf32174d8
commit bea2725d6a
128 changed files with 11984 additions and 573 deletions

View File

@ -633,7 +633,7 @@ export interface IWheelEvent extends IActionEvent {
1. 按下、抬起、点击**永远**保持为同一个 `identifier` 1. 按下、抬起、点击**永远**保持为同一个 `identifier`
2. 移动过程中,使用最后一个按下的按键的 `identifier` 作为移动事件的 `identifier` 2. 移动过程中,使用最后一个按下的按键的 `identifier` 作为移动事件的 `identifier`
3. 如果移动过程中,最后一个按下的按键抬起,那么依然会维持**原先的** `identifer`**不会**回退至上一个按下的按键 3. 如果移动过程中,最后一个按下的按键抬起,那么依然会维持**原先的** `identifier`**不会**回退至上一个按下的按键
除此之外,滚轮事件中的 `identifier` 永远为 -1。 除此之外,滚轮事件中的 `identifier` 永远为 -1。

View File

@ -10,7 +10,7 @@
"type": "vue-tsc --noEmit", "type": "vue-tsc --noEmit",
"lines": "tsx script/lines.ts packages packages-user", "lines": "tsx script/lines.ts packages packages-user",
"build:packages": "vue-tsc --noEmit && tsx script/build-packages.ts", "build:packages": "vue-tsc --noEmit && tsx script/build-packages.ts",
"build:game": "tsx script/declare.ts vue-tsc --noEmit && tsx script/build-game.ts", "build:game": "tsx script/declare.ts && vue-tsc --noEmit && tsx script/build-game.ts",
"build:lib": "vue-tsc --noEmit && tsx script/build-lib.ts", "build:lib": "vue-tsc --noEmit && tsx script/build-lib.ts",
"docs:dev": "concurrently -k -n SIDEBAR,VITEPRESS -c blue,green \"tsx docs/.vitepress/api.ts\" \"vitepress dev docs\"", "docs:dev": "concurrently -k -n SIDEBAR,VITEPRESS -c blue,green \"tsx docs/.vitepress/api.ts\" \"vitepress dev docs\"",
"docs:build": "vitepress build docs", "docs:build": "vitepress build docs",
@ -30,6 +30,7 @@
"jszip": "^3.10.1", "jszip": "^3.10.1",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"lz-string": "^1.5.0", "lz-string": "^1.5.0",
"maxrects-packer": "^2.7.3",
"mutate-animate": "^1.4.2", "mutate-animate": "^1.4.2",
"ogg-opus-decoder": "^1.6.14", "ogg-opus-decoder": "^1.6.14",
"opus-decoder": "^0.7.7", "opus-decoder": "^0.7.7",

View File

@ -0,0 +1,7 @@
{
"name": "@user/client-base",
"dependencies": {
"@motajs/render-assets": "workspace:*",
"@motajs/client-base": "workspace:*"
}
}

View File

@ -0,0 +1,7 @@
import { createMaterial } from './material';
export function create() {
createMaterial();
}
export * from './material';

View File

@ -0,0 +1,454 @@
import {
IRect,
ITextureRenderable,
SizedCanvasImageSource
} from '@motajs/render-assets';
import {
AutotileConnection,
AutotileType,
BlockCls,
IAutotileConnection,
IAutotileProcessor,
IMaterialFramedData,
IMaterialManager
} from './types';
import { isNil } from 'lodash-es';
interface ConnectedAutotile {
readonly lt: Readonly<IRect>;
readonly rt: Readonly<IRect>;
readonly rb: Readonly<IRect>;
readonly lb: Readonly<IRect>;
}
export interface IAutotileData {
/** 图像源 */
readonly source: SizedCanvasImageSource;
/** 自动元件帧数 */
readonly frames: number;
}
/** 3x4 自动元件的连接映射,元组表示将对应大小的自动元件按照格子 1/4 大小切分后对应的索引位置 */
const connectionMap3x4 = new Map<number, [number, number, number, number]>();
/** 2x3 自动元件的连接映射,元组表示将对应大小的自动元件按照格子 1/4 大小切分后对应的索引位置 */
const connectionMap2x3 = new Map<number, [number, number, number, number]>();
/** 3x4 自动元件各方向连接的矩形映射 */
const rectMap3x4 = new Map<number, ConnectedAutotile>();
/** 2x3 自动元件各方向连接的矩形映射 */
const rectMap2x3 = new Map<number, ConnectedAutotile>();
/** 不重复连接映射,用于平铺自动元件,一共 48 种 */
const distinctConnectionMap = new Map<number, number>();
export class AutotileProcessor implements IAutotileProcessor {
/** 自动元件父子关系映射,子元件 -> 父元件 */
readonly parentMap: Map<number, number> = new Map();
/** 自动元件父子关系映射,父元件 -> 子元件列表 */
readonly childMap: Map<number, Set<number>> = new Map();
constructor(readonly manager: IMaterialManager) {}
private ensureChildSet(num: number) {
const set = this.childMap.get(num);
if (set) return set;
const ensure = new Set<number>();
this.childMap.set(num, ensure);
return ensure;
}
setConnection(autotile: number, parent: number): void {
this.parentMap.set(autotile, parent);
const child = this.ensureChildSet(parent);
child.add(autotile);
}
private connectEdge(length: number, index: number, width: number): number {
// 最高位表示左上,低位依次顺时针旋转
// 如果地图大小只有 1
if (length === 1) {
return 0b1111_1111;
}
// 如果地图高度只有 1
if (length === width) {
if (index === 0) {
return 0b1110_1111;
} else if (index === length - 1) {
return 0b1111_1110;
} else {
return 0b1110_1110;
}
}
// 如果地图宽度只有 1
if (width === 1) {
if (index === 0) {
return 0b1111_1011;
} else if (index === length - 1) {
return 0b1011_1111;
} else {
return 0b1011_1011;
}
}
// 正常地图
const lastLine = length - width;
const x = index % width;
// 四个角,左上,右上,右下,左下
if (index === 0) {
return 0b1110_0011;
} else if (index === width - 1) {
return 0b1111_1000;
} else if (index === length - 1) {
return 0b0011_1110;
} else if (index === lastLine) {
return 0b1000_1111;
}
// 四条边,上,右,下,左
else if (index < width) {
return 0b1110_0000;
} else if (x === width - 1) {
return 0b0011_1000;
} else if (index > lastLine) {
return 0b0000_1110;
} else if (x === 0) {
return 0b1000_0011;
}
// 不在边缘
else {
return 0b0000_0000;
}
}
connect(
array: Uint32Array,
index: number,
width: number
): IAutotileConnection {
const block = array[index];
if (block === 0) {
return {
connection: 0,
center: 0
};
}
let res: number = this.connectEdge(array.length, index, width);
const childList = this.childMap.get(block);
// 最高位表示左上,低位依次顺时针旋转
const a7 = array[index - width - 1] ?? 0;
const a6 = array[index - width] ?? 0;
const a5 = array[index - width + 1] ?? 0;
const a4 = array[index + 1] ?? 0;
const a3 = array[index + width + 1] ?? 0;
const a2 = array[index + width] ?? 0;
const a1 = array[index + width - 1] ?? 0;
const a0 = array[index - 1] ?? 0;
// Benchmark https://www.measurethat.net/Benchmarks/Show/35271/0/convert-boolean-to-number
if (!childList || childList.size === 0) {
// 不包含子元件,那么直接跟相同的连接
res |=
+(a0 === block) |
(+(a1 === block) << 1) |
(+(a2 === block) << 2) |
(+(a3 === block) << 3) |
(+(a4 === block) << 4) |
(+(a5 === block) << 5) |
(+(a6 === block) << 6) |
(+(a7 === block) << 7);
} else {
res |=
+childList.has(a0) |
(+childList.has(a1) << 1) |
(+childList.has(a2) << 2) |
(+childList.has(a3) << 3) |
(+childList.has(a4) << 4) |
(+childList.has(a5) << 5) |
(+childList.has(a6) << 6) |
(+childList.has(a7) << 7);
}
return {
connection: res,
center: block
};
}
updateConnectionFor(
connection: number,
center: number,
target: number,
direction: AutotileConnection
): number {
const childList = this.childMap.get(center);
if (!childList || !childList.has(target)) {
return connection & ~direction;
} else {
return connection | direction;
}
}
/**
*
* @param tile
*/
private checkAutotile(tile: IMaterialFramedData) {
if (tile.cls !== BlockCls.Autotile) return false;
const { texture, frames } = tile;
if (texture.width !== 96 * frames) return false;
if (texture.height === 128 || texture.height === 144) return true;
else return false;
}
render(autotile: number, connection: number): ITextureRenderable | null {
const tile = this.manager.getTile(autotile);
if (!tile) return null;
if (!this.checkAutotile(tile)) return null;
return this.renderWithoutCheck(tile, connection);
}
renderWith(
tile: IMaterialFramedData,
connection: number
): ITextureRenderable | null {
if (!this.checkAutotile(tile)) return null;
return this.renderWithoutCheck(tile, connection);
}
renderWithoutCheck(
tile: IMaterialFramedData,
connection: number
): ITextureRenderable | null {
const { texture } = tile;
const size = texture.height === 32 * 48 ? 32 : 48;
const index = distinctConnectionMap.get(connection);
if (isNil(index)) return null;
const { rect } = texture.render();
return {
source: texture.source,
rect: { x: rect.x, y: rect.y + size * index, w: size, h: size }
};
}
*renderAnimated(
autotile: number,
connection: number
): Generator<ITextureRenderable, void> {
const tile = this.manager.getTile(autotile);
if (!tile) return;
yield* this.renderAnimatedWith(tile, connection);
}
*renderAnimatedWith(
tile: IMaterialFramedData,
connection: number
): Generator<ITextureRenderable, void> {
if (!this.checkAutotile(tile)) return;
const { texture, frames } = tile;
const size = texture.height === 128 ? 32 : 48;
const index = distinctConnectionMap.get(connection);
if (isNil(index)) return;
for (let i = 0; i < frames; i++) {
yield {
source: texture.source,
rect: { x: i * size, y: size * index, w: size, h: size }
};
}
}
/**
* 48
* @param image
*/
static flatten(image: IAutotileData): SizedCanvasImageSource | null {
const { source, frames } = image;
if (source.width !== frames * 96) return null;
if (source.height !== 128 && source.height !== 144) return null;
const type =
source.height === 128 ? AutotileType.Big3x4 : AutotileType.Small2x3;
const size = type === AutotileType.Big3x4 ? 32 : 48;
const width = frames * size;
const height = 48 * size;
// 画到画布上
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
if (!ctx) return null;
const half = size / 2;
const map = type === AutotileType.Big3x4 ? rectMap3x4 : rectMap2x3;
const used = new Set<number>();
// 遍历每个组合
distinctConnectionMap.forEach((index, conn) => {
if (used.has(conn)) return;
used.add(conn);
const { lt, rt, rb, lb } = map.get(conn)!;
const y = index * size;
for (let i = 0; i < frames; i++) {
const x = i * size;
// prettier-ignore
ctx.drawImage(source, lt.x + i * 96, lt.y, lt.w, lt.h, x, y, half, half);
// prettier-ignore
ctx.drawImage(source, rt.x + i * 96, rt.y, rt.w, rt.h, x + half, y, half, half);
// prettier-ignore
ctx.drawImage(source, rb.x + i * 96, rb.y, rb.w, rb.h, x + half, y + half, half, half);
// prettier-ignore
ctx.drawImage(source, lb.x + i * 96, lb.y, lb.w, lb.h, x, y + half, half, half);
}
});
return canvas;
}
}
/**
*
* @param target
* @param mode 1 3x42 2x3
*/
function mapAutotile(
target: Map<number, [number, number, number, number]>,
mode: 1 | 2
) {
const h = mode === 1 ? 2 : 1; // 横向偏移因子
const v = mode === 1 ? 12 : 4; // 纵向偏移因子
const luo = mode === 1 ? 12 : 8; // leftup origin
const ruo = mode === 1 ? 17 : 11; // rightup origin
const ldo = mode === 1 ? 42 : 20; // leftdown origin
const rdo = mode === 1 ? 47 : 23; // rightdown origin
const luc = mode === 1 ? 4 : 2; // leftup corner
const ruc = mode === 1 ? 5 : 3; // rightup corner
const rdc = mode === 1 ? 11 : 7; // rightdown corner
const ldc = mode === 1 ? 10 : 6; // leftdown corner
for (let i = 0; i <= 0b1111_1111; i++) {
// 自动元件由四个更小的矩形组合而成
// 初始状态下,四个矩形分别处在四个角的位置
// 而且对应角落的矩形只可能出现在每个大区块的对应角落
let lu = luo; // leftup
let ru = ruo; // rightup
let ld = ldo; // leftdown
let rd = rdo; // rightdown
// 先看四个方向,最后看斜角方向
if (i & 0b0000_0001) {
// 左侧有连接,左侧两个矩形向右偏移两个因子
lu += h * 2;
ld += h * 2;
// 如果右侧还有连接,那么右侧矩形和左侧矩形需要移动至中间
// 但是由于后面还处理了先右侧再左侧的情况,因此需要先向右偏移一个因子
// 结果就是先向右移动了一个因子,在后面又向左移动了两个因子,因此相当于向左移动了一个因子
if (i & 0b0001_0000) {
ru += h;
rd += h;
}
}
if (i & 0b0000_0100) {
// 下侧有连接,下侧两个矩形向上偏移两个因子
ld -= v * 2;
rd -= v * 2;
if (i & 0b0100_0000) {
lu -= v;
ru -= v;
}
}
if (i & 0b0001_0000) {
// 右侧有连接,右侧矩形向左移动两个因子
ru -= h * 2;
rd -= h * 2;
if (i & 0b0000_0001) {
lu -= h;
ld -= h;
}
}
if (i & 0b0100_0000) {
// 上侧有链接,上侧矩形向下移动两个因子
lu += v * 2;
ru += v * 2;
if (i & 0b0000_0100) {
ld += v;
rd += v;
}
}
// 斜角
// 如果左上仅与上和左连接
if ((i & 0b1100_0001) === 0b0100_0001) {
lu = luc;
}
// 如果右上仅与上和右连接
if ((i & 0b0111_0000) === 0b0101_0000) {
ru = ruc;
}
// 如果右下仅与右和下连接
if ((i & 0b0001_1100) === 0b0001_0100) {
rd = rdc;
}
// 如果左下仅与左和下连接
if ((i & 0b0000_0111) === 0b0000_0101) {
ld = ldc;
}
target.set(i, [lu, ru, rd, ld]);
}
}
export function createAutotile() {
mapAutotile(connectionMap3x4, 1);
mapAutotile(connectionMap2x3, 2);
connectionMap3x4.forEach((data, connection) => {
const [ltd, rtd, rbd, lbd] = data;
const ltx = (ltd % 6) * 16;
const lty = Math.floor(ltd / 6) * 16;
const rtx = (rtd % 6) * 16;
const rty = Math.floor(rtd / 6) * 16;
const rbx = (rbd % 6) * 16;
const rby = Math.floor(rbd / 6) * 16;
const lbx = (lbd % 6) * 16;
const lby = Math.floor(lbd / 6) * 16;
rectMap3x4.set(connection, {
lt: { x: ltx, y: lty, w: 16, h: 16 },
rt: { x: rtx, y: rty, w: 16, h: 16 },
rb: { x: rbx, y: rby, w: 16, h: 16 },
lb: { x: lbx, y: lby, w: 16, h: 16 }
});
});
connectionMap2x3.forEach((data, connection) => {
const [ltd, rtd, rbd, lbd] = data;
const ltx = (ltd % 4) * 24;
const lty = Math.floor(ltd / 4) * 24;
const rtx = (rtd % 4) * 24;
const rty = Math.floor(rtd / 4) * 24;
const rbx = (rbd % 4) * 24;
const rby = Math.floor(rbd / 4) * 24;
const lbx = (lbd % 4) * 24;
const lby = Math.floor(lbd / 4) * 24;
rectMap2x3.set(connection, {
lt: { x: ltx, y: lty, w: 24, h: 24 },
rt: { x: rtx, y: rty, w: 24, h: 24 },
rb: { x: rbx, y: rby, w: 24, h: 24 },
lb: { x: lbx, y: lby, w: 24, h: 24 }
});
});
const usedRect: [number, number, number, number][] = [];
let flag = 0;
// 2x3 和 3x4 的自动元件连接方式一样,因此没必要映射两次
connectionMap2x3.forEach((conn, num) => {
const index = usedRect.findIndex(
used =>
used[0] === conn[0] &&
used[1] === conn[1] &&
used[2] === conn[2] &&
used[3] === conn[3]
);
if (index === -1) {
distinctConnectionMap.set(num, flag);
usedRect.push(conn.slice() as [number, number, number, number]);
flag++;
} else {
distinctConnectionMap.set(num, index);
}
});
}

View File

@ -0,0 +1,159 @@
import {
ITextureStore,
ITexture,
ITextureComposedData,
ITextureStreamComposer,
TextureMaxRectsStreamComposer,
SizedCanvasImageSource
} from '@motajs/render-assets';
import { IAssetBuilder, IMaterialGetter, ITrackedAssetData } from './types';
import { logger, PrivateListDirtyTracker } from '@motajs/common';
export class AssetBuilder implements IAssetBuilder {
readonly composer: ITextureStreamComposer<void> =
new TextureMaxRectsStreamComposer(4096, 4096, 0);
private output: ITextureStore | null = null;
private started: boolean = false;
private readonly trackedData: TrackedAssetData;
/** 当前的索引 */
private index: number = -1;
/** 贴图更新的 promise */
private pending: Promise<void> = Promise.resolve();
constructor(readonly materials: IMaterialGetter) {
this.trackedData = new TrackedAssetData(materials, this);
}
pipe(store: ITextureStore): void {
if (this.started) {
logger.warn(76);
return;
}
this.output = store;
}
addTexture(texture: ITexture): ITextureComposedData {
this.started = true;
const res = [...this.composer.add([texture])];
const data = res[0];
if (this.output) {
if (data.index > this.index) {
this.output.addTexture(data.index, data.texture);
this.index = data.index;
}
}
this.pending = this.pending.then(() =>
this.trackedData.updateSource(data.index, data.texture.source)
);
return data;
}
private async updateSourceList(source: Set<ITextureComposedData>) {
for (const data of source) {
await this.trackedData.updateSource(
data.index,
data.texture.source
);
}
}
addTextureList(
texture: Iterable<ITexture>
): Iterable<ITextureComposedData> {
this.started = true;
const res = [...this.composer.add(texture)];
const toUpdate = new Set<ITextureComposedData>();
if (this.output) {
res.forEach(data => {
if (data.index > this.index) {
this.output!.addTexture(data.index, data.texture);
this.index = data.index;
toUpdate.add(data);
} else {
toUpdate.add(data);
}
});
}
this.pending = this.pending.then(() => this.updateSourceList(toUpdate));
return res;
}
tracked(): ITrackedAssetData {
return this.trackedData;
}
close(): void {
this.composer.close();
}
}
class TrackedAssetData
extends PrivateListDirtyTracker<number>
implements ITrackedAssetData
{
readonly sourceList: Map<number, ImageBitmap> = new Map();
readonly skipRef: Map<SizedCanvasImageSource, number> = new Map();
private originSourceMap: Map<number, SizedCanvasImageSource> = new Map();
private promises: Set<Promise<ImageBitmap>> = new Set();
constructor(
readonly materials: IMaterialGetter,
readonly builder: AssetBuilder
) {
super(0);
}
markDirty(index: number) {
if (index >= this.length) {
this.updateLength(index + 1);
}
this.dirty(index);
}
async updateSource(index: number, source: SizedCanvasImageSource) {
if (index >= this.length) {
this.updateLength(this.length + 1);
}
const origin = this.originSourceMap.get(index);
const prev = this.sourceList.get(index);
if (origin && origin !== source) {
this.skipRef.delete(origin);
}
if (prev) {
this.skipRef.delete(prev);
}
this.originSourceMap.set(index, source);
if (source instanceof ImageBitmap) {
if (this.skipRef.has(source)) return;
this.sourceList.set(index, source);
this.skipRef.set(source, index);
} else {
const promise = createImageBitmap(source);
this.promises.add(promise);
const bitmap = await promise;
this.promises.delete(promise);
this.sourceList.set(index, bitmap);
this.skipRef.set(bitmap, index);
// 要把源也加到映射中,因为这里的 bitmap 与外部源并不同引用
this.skipRef.set(source, index);
}
this.dirty(index);
}
async then(): Promise<void> {
await Promise.all([...this.promises]);
}
close(): void {}
}

View File

@ -0,0 +1,140 @@
import { ITexture } from '@motajs/render-assets';
import { materials } from './ins';
import { IBlockIdentifier, IIndexedIdentifier } from './types';
import { isNil } from 'lodash-es';
function extractClsBlocks<C extends Exclude<Cls, 'tileset'>>(
cls: C,
map: Record<string, number>,
icons: Record<string, number>
): IBlockIdentifier[] {
const max = Math.max(...Object.values(icons));
const arr = Array(max).fill(void 0);
for (const [key, value] of Object.entries(icons)) {
// 样板编辑器 bug 可能会导致多个 id 使用一个偏移,因此要判断下
if (!(key in map) || !isNil(arr[value])) continue;
const id = key as AllIdsOf<C>;
const num = map[id] as keyof NumberToId;
const identifier: IBlockIdentifier = {
id: id as string,
cls,
num
};
arr[value] = identifier;
}
return arr;
}
function addTileset(set: Set<number>, map?: readonly (readonly number[])[]) {
if (!map) return;
map.forEach(line => {
line.forEach(v => {
if (v >= 10000) set.add(v);
});
});
}
function addAutotile(set: Set<number>, map?: readonly (readonly number[])[]) {
if (!map) return;
map.forEach(line => {
line.forEach(v => {
const id = core.maps.blocksInfo[v as keyof NumberToId];
if (id?.cls === 'autotile') set.add(v);
});
});
}
/**
*
*/
export function fallbackLoad() {
// 基本素材
const icons = core.icons.icons;
const images = core.material.images;
const idNumMap: Record<string, number> = {};
for (const [key, value] of Object.entries(core.maps.blocksInfo)) {
const num = Number(key);
idNumMap[value.id] = Number(num);
if (!isNil(value.animate)) {
materials.setDefaultFrame(num, value.animate - 1);
}
}
const terrains = extractClsBlocks('terrains', idNumMap, icons.terrains);
const animates = extractClsBlocks('animates', idNumMap, icons.animates);
const items = extractClsBlocks('items', idNumMap, icons.items);
const enemys = extractClsBlocks('enemys', idNumMap, icons.enemys);
const npcs = extractClsBlocks('npcs', idNumMap, icons.npcs);
const enemy48 = extractClsBlocks('enemy48', idNumMap, icons.enemy48);
const npc48 = extractClsBlocks('npc48', idNumMap, icons.npc48);
// Grid
materials.addGrid(images.terrains, terrains);
materials.addGrid(images.items, items);
// Row Animates
materials.addRowAnimate(images.animates, animates, 32);
materials.addRowAnimate(images.enemys, enemys, 32);
materials.addRowAnimate(images.npcs, npcs, 32);
materials.addRowAnimate(images.enemy48, enemy48, 48);
materials.addRowAnimate(images.npc48, npc48, 48);
// Autotile
for (const key of Object.keys(icons.autotile)) {
const id = key as AllIdsOf<'autotile'>;
const img = images.autotile[id];
const identifier: IBlockIdentifier = {
id,
num: idNumMap[id],
cls: 'autotile'
};
materials.addAutotile(img, identifier);
}
// Tilesets
core.tilesets.forEach((v, i) => {
const img = images.tilesets[v];
const identifier: IIndexedIdentifier = {
index: i,
alias: v
};
materials.addTileset(img, identifier);
});
// Images
core.images.forEach((v, i) => {
const img = core.material.images.images[v];
materials.addImage(img, { index: i, alias: v });
});
// 地图上出现过的 tileset
const tilesetSet = new Set<number>();
const autotileSet = new Set<number>();
core.floorIds.forEach(v => {
const floor = core.floors[v];
addTileset(tilesetSet, floor.bgmap);
addTileset(tilesetSet, floor.bg2map);
addTileset(tilesetSet, floor.map);
addTileset(tilesetSet, floor.fgmap);
addTileset(tilesetSet, floor.fg2map);
addAutotile(autotileSet, floor.bgmap);
addAutotile(autotileSet, floor.bg2map);
addAutotile(autotileSet, floor.map);
addAutotile(autotileSet, floor.fgmap);
addAutotile(autotileSet, floor.fg2map);
});
const heroTextures: ITexture[] = [];
data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d.main.heroImages.forEach(v => {
const tex = materials.getImageByAlias(v);
if (tex) heroTextures.push(tex);
});
materials.buildAssets();
materials.cacheAutotileList(autotileSet);
materials.cacheTilesetList(tilesetSet);
materials.buildListToAsset(heroTextures);
}

View File

@ -0,0 +1,19 @@
import { loading } from '@user/data-base';
import { fallbackLoad } from './fallback';
import { createAutotile } from './autotile';
export function createMaterial() {
createAutotile();
loading.once('loaded', () => {
fallbackLoad();
loading.emit('assetBuilt');
});
}
export * from './autotile';
export * from './builder';
export * from './fallback';
export * from './ins';
export * from './manager';
export * from './types';
export * from './utils';

View File

@ -0,0 +1,5 @@
import { AutotileProcessor } from './autotile';
import { MaterialManager } from './manager';
export const materials = new MaterialManager();
export const autotile = new AutotileProcessor(materials);

View File

@ -0,0 +1,602 @@
import {
ITexture,
ITextureComposedData,
ITextureRenderable,
ITextureSplitter,
ITextureStore,
SizedCanvasImageSource,
Texture,
TextureGridSplitter,
TextureRowSplitter,
TextureStore
} from '@motajs/render-assets';
import {
IBlockIdentifier,
IMaterialData,
IMaterialManager,
IIndexedIdentifier,
IMaterialAssetData,
BlockCls,
IBigImageReturn,
IAssetBuilder,
IMaterialFramedData,
ITrackedAssetData
} from './types';
import { logger } from '@motajs/common';
import { getClsByString, getTextureFrame } from './utils';
import { isNil } from 'lodash-es';
import { AssetBuilder } from './builder';
import { AutotileProcessor } from './autotile';
interface TilesetCache {
/** 是否已经在贴图库中存在 */
readonly existed: boolean;
/** 贴图对象 */
readonly texture: ITexture;
}
export class MaterialManager implements IMaterialManager {
readonly tileStore: ITextureStore = new TextureStore();
readonly tilesetStore: ITextureStore = new TextureStore();
readonly imageStore: ITextureStore = new TextureStore();
readonly assetStore: ITextureStore = new TextureStore();
readonly bigImageStore: ITextureStore = new TextureStore();
/** 自动元件图像源映射 */
readonly autotileSource: Map<number, SizedCanvasImageSource> = new Map();
/** 图集信息存储 */
readonly assetDataStore: Map<number, ITextureComposedData> = new Map();
/** 贴图到图集索引的映射 */
readonly assetMap: Map<ITexture, number> = new Map();
/** 带有脏标记追踪的图集对象 */
readonly trackedAsset: ITrackedAssetData;
/** 大怪物数据 */
readonly bigImageData: Map<number, IMaterialFramedData> = new Map();
/** tileset 中 `Math.floor(id / 10000) + 1` 映射到 tileset 对应索引的映射,用于处理图块超出 10000 的 tileset */
readonly tilesetOffsetMap: Map<number, number> = new Map();
/** 图集打包器 */
readonly assetBuilder: IAssetBuilder;
/** 图块 id 到图块数字的映射 */
readonly idNumMap: Map<string, number> = new Map();
/** 图块数字到图块 id 的映射 */
readonly numIdMap: Map<number, string> = new Map();
/** 图块数字到图块类型的映射 */
readonly clsMap: Map<number, BlockCls> = new Map();
/** 图块的默认帧数 */
readonly defaultFrames: Map<number, number> = new Map();
/** 网格切分器 */
readonly gridSplitter: TextureGridSplitter = new TextureGridSplitter();
/** 行切分器 */
readonly rowSplitter: TextureRowSplitter = new TextureRowSplitter();
/** 大怪物贴图的标识符 */
private bigImageId: number = 0;
/** 当前 tileset 索引 */
private nowTilesetIndex: number = -1;
/** 当前 tileset 偏移 */
private nowTilesetOffset: number = 0;
/** 是否已经构建过素材 */
private built: boolean = false;
constructor() {
this.assetBuilder = new AssetBuilder(this);
this.assetBuilder.pipe(this.assetStore);
this.trackedAsset = this.assetBuilder.tracked();
}
/**
*
* @param source
* @param map id
* @param store
* @param splitter 使
* @param splitterData
* @param processTexture
*/
private addMappedSource<T>(
source: SizedCanvasImageSource,
map: ArrayLike<IBlockIdentifier>,
store: ITextureStore,
splitter: ITextureSplitter<T>,
splitterData: T,
processTexture?: (tex: ITexture) => void
): Iterable<IMaterialData> {
const tex = new Texture(source);
const textures = [...splitter.split(tex, splitterData)];
if (textures.length !== map.length) {
logger.warn(75, textures.length.toString(), map.length.toString());
}
const res: IMaterialData[] = textures.map((v, i) => {
if (!map[i]) {
return {
store,
texture: v,
identifier: -1,
alias: '@internal-unknown'
};
}
const { id, num, cls } = map[i];
store.addTexture(num, v);
store.alias(num, id);
this.clsMap.set(num, getClsByString(cls));
processTexture?.(v);
const data: IMaterialData = {
store,
texture: v,
identifier: num,
alias: id
};
return data;
});
return res;
}
addGrid(
source: SizedCanvasImageSource,
map: ArrayLike<IBlockIdentifier>
): Iterable<IMaterialData> {
return this.addMappedSource(
source,
map,
this.tileStore,
this.gridSplitter,
[32, 32]
);
}
addRowAnimate(
source: SizedCanvasImageSource,
map: ArrayLike<IBlockIdentifier>,
height: number
): Iterable<IMaterialData> {
return this.addMappedSource(
source,
map,
this.tileStore,
this.rowSplitter,
height
);
}
addAutotile(
source: SizedCanvasImageSource,
identifier: IBlockIdentifier
): void {
this.autotileSource.set(identifier.num, source);
this.tileStore.alias(identifier.num, identifier.id);
this.clsMap.set(identifier.num, BlockCls.Autotile);
}
addTileset(
source: SizedCanvasImageSource,
identifier: IIndexedIdentifier
): IMaterialData | null {
const tex = new Texture(source);
this.tilesetStore.addTexture(identifier.index, tex);
this.tilesetStore.alias(identifier.index, identifier.alias);
const width = Math.floor(source.width / 32);
const height = Math.floor(source.height / 32);
const count = width * height;
const offset = Math.ceil(count / 10000);
if (identifier.index === 0) {
this.tilesetOffsetMap.set(0, 0);
this.nowTilesetIndex = 0;
this.nowTilesetOffset = offset;
} else {
if (identifier.index - 1 !== this.nowTilesetIndex) {
logger.warn(78);
return null;
}
// 一个 tileset 可能不止 10000 个图块,需要计算偏移
const width = Math.floor(source.width / 32);
const height = Math.floor(source.height / 32);
const count = width * height;
const offset = Math.ceil(count / 10000);
const end = this.nowTilesetOffset + offset;
for (let i = this.nowTilesetOffset; i < end; i++) {
this.tilesetOffsetMap.set(i, identifier.index);
}
this.nowTilesetOffset = end;
this.nowTilesetIndex = identifier.index;
}
const data: IMaterialData = {
store: this.tilesetStore,
texture: tex,
identifier: identifier.index,
alias: identifier.alias
};
return data;
}
addImage(
source: SizedCanvasImageSource,
identifier: IIndexedIdentifier
): IMaterialData {
const texture = new Texture(source);
this.imageStore.addTexture(identifier.index, texture);
this.imageStore.alias(identifier.index, identifier.alias);
const data: IMaterialData = {
store: this.imageStore,
texture,
identifier: identifier.index,
alias: identifier.alias
};
return data;
}
setDefaultFrame(identifier: number, defaultFrame: number): void {
this.defaultFrames.set(identifier, defaultFrame);
const bigImageData = this.bigImageData.get(identifier);
if (bigImageData) {
bigImageData.defaultFrame = defaultFrame;
}
}
getDefaultFrame(identifier: number): number {
return this.defaultFrames.get(identifier) ?? -1;
}
getTile(identifier: number): Readonly<IMaterialFramedData> | null {
if (identifier < 10000) {
const cls = this.clsMap.get(identifier) ?? BlockCls.Unknown;
if (
cls === BlockCls.Autotile &&
this.autotileSource.has(identifier)
) {
this.cacheAutotile(identifier);
}
const texture = this.tileStore.getTexture(identifier);
if (!texture) return null;
return {
texture,
cls,
offset: 32,
frames: getTextureFrame(cls, texture),
defaultFrame: this.defaultFrames.get(identifier) ?? -1
};
} else {
const texture = this.cacheTileset(identifier);
if (!texture) return null;
return {
texture,
cls: BlockCls.Tileset,
offset: 32,
frames: 1,
defaultFrame: -1
};
}
}
getTileset(identifier: number): ITexture | null {
return this.tilesetStore.getTexture(identifier);
}
getImage(identifier: number): ITexture | null {
return this.imageStore.getTexture(identifier);
}
getTileByAlias(alias: string): Readonly<IMaterialFramedData> | null {
if (/X\d{5,}/.test(alias)) {
return this.getTile(parseInt(alias.slice(1)));
} else {
const identifier = this.tileStore.identifierOf(alias);
if (isNil(identifier)) return null;
return this.getTile(identifier);
}
}
getTilesetByAlias(alias: string): ITexture | null {
return this.tilesetStore.fromAlias(alias);
}
getImageByAlias(alias: string): ITexture | null {
return this.imageStore.fromAlias(alias);
}
private getTilesetOwnTexture(identifier: number): TilesetCache | null {
const texture = this.tileStore.getTexture(identifier);
if (texture) return { existed: true, texture };
// 如果 tileset 不存在,那么执行缓存操作
const offset = Math.floor(identifier / 10000);
const index = this.tilesetOffsetMap.get(offset - 1);
if (isNil(index)) return null;
// 获取对应的 tileset 贴图
const tileset = this.tilesetStore.getTexture(index);
if (!tileset) return null;
// 计算图块位置
const rest = identifier - offset * 10000;
const { width, height } = tileset;
const tileWidth = Math.floor(width / 32);
const tileHeight = Math.floor(height / 32);
// 如果图块位置超出了贴图范围
if (rest > tileWidth * tileHeight) return null;
// 裁剪 tileset生成贴图
const x = rest % tileWidth;
const y = Math.floor(rest / tileWidth);
const newTexture = new Texture(tileset.source);
newTexture.clip(x * 32, y * 32, 32, 32);
return { existed: false, texture: newTexture };
}
/**
*
* @param data
*/
private checkAssetDirty(data: ITextureComposedData) {
if (!this.built) return;
const asset = this.assetDataStore.get(data.index);
if (!asset) {
// 如果有新图集,需要添加
const alias = `asset-${data.index}`;
this.assetStore.alias(data.index, alias);
this.assetDataStore.set(data.index, data);
}
}
/**
*
* @param composedData
* @param textures
*/
private cacheToAsset(
composedData: ITextureComposedData[],
textures: ITexture[]
) {
textures.forEach(tex => {
const assetData = composedData.find(v => v.assetMap.has(tex));
if (!assetData) {
logger.error(38);
return;
}
tex.toAsset(assetData);
});
composedData.forEach(v => this.checkAssetDirty(v));
}
cacheTileset(identifier: number): ITexture | null {
const newTexture = this.getTilesetOwnTexture(identifier);
if (!newTexture) return null;
const { existed, texture } = newTexture;
if (existed) return texture;
// 缓存贴图
this.tileStore.addTexture(identifier, texture);
this.idNumMap.set(`X${identifier}`, identifier);
this.numIdMap.set(identifier, `X${identifier}`);
const data = this.assetBuilder.addTexture(texture);
texture.toAsset(data);
this.checkAssetDirty(data);
return texture;
}
cacheTilesetList(
identifierList: Iterable<number>
): Iterable<ITexture | null> {
const arr = [...identifierList];
const toAdd: ITexture[] = [];
arr.forEach(v => {
const newTexture = this.getTilesetOwnTexture(v);
if (!newTexture) return;
const { existed, texture } = newTexture;
if (existed) return;
toAdd.push(texture);
this.tileStore.addTexture(v, texture);
this.idNumMap.set(`X${v}`, v);
this.numIdMap.set(v, `X${v}`);
});
const data = this.assetBuilder.addTextureList(toAdd);
const res = [...data];
this.cacheToAsset(res, toAdd);
return toAdd;
}
/**
* `tileStore` `null`
* @param identifier
*/
private getFlattenedAutotile(
identifier: number
): SizedCanvasImageSource | null {
const cls = this.clsMap.get(identifier);
if (cls !== BlockCls.Autotile) return null;
if (this.tileStore.getTexture(identifier)) return null;
const source = this.autotileSource.get(identifier);
if (!source) return null;
const frames = source.width === 96 ? 1 : 4;
const flattened = AutotileProcessor.flatten({ source, frames });
if (!flattened) return null;
return flattened;
}
cacheAutotile(identifier: number): ITexture | null {
const flattened = this.getFlattenedAutotile(identifier);
if (!flattened) return null;
const tex = new Texture(flattened);
this.tileStore.addTexture(identifier, tex);
const data = this.assetBuilder.addTexture(tex);
tex.toAsset(data);
this.autotileSource.delete(identifier);
this.checkAssetDirty(data);
return tex;
}
cacheAutotileList(
identifierList: Iterable<number>
): Iterable<ITexture | null> {
const arr = [...identifierList];
const toAdd: ITexture[] = [];
arr.forEach(v => {
const flattened = this.getFlattenedAutotile(v);
if (!flattened) return;
const tex = new Texture(flattened);
this.tileStore.addTexture(v, tex);
toAdd.push(tex);
this.autotileSource.delete(v);
});
const data = this.assetBuilder.addTextureList(toAdd);
const res = [...data];
this.cacheToAsset(res, toAdd);
return toAdd;
}
buildAssets(): Iterable<IMaterialAssetData> {
if (this.built) {
logger.warn(79);
return [];
}
this.built = true;
return this.buildListToAsset(this.tileStore.values());
}
buildToAsset(texture: ITexture): IMaterialAssetData {
const data = this.assetBuilder.addTexture(texture);
const assetData: IMaterialAssetData = {
data: data,
identifier: data.index,
alias: `asset-${data.index}`,
store: this.assetStore
};
this.checkAssetDirty(data);
texture.toAsset(data);
return assetData;
}
buildListToAsset(
texture: Iterable<ITexture>
): Iterable<IMaterialAssetData> {
const data = this.assetBuilder.addTextureList(texture);
const arr = [...data];
const res: IMaterialAssetData[] = [];
arr.forEach(v => {
const alias = `asset-${v.index}`;
if (!this.assetDataStore.has(v.index)) {
this.assetDataStore.set(v.index, v);
}
const data: IMaterialAssetData = {
data: v,
identifier: v.index,
alias,
store: this.assetStore
};
for (const tex of v.assetMap.keys()) {
tex.toAsset(v);
}
res.push(data);
});
arr.forEach(v => {
this.checkAssetDirty(v);
});
return res;
}
getAsset(identifier: number): ITextureComposedData | null {
return this.assetDataStore.get(identifier) ?? null;
}
getAssetByAlias(alias: string): ITextureComposedData | null {
const id = this.assetStore.identifierOf(alias);
if (isNil(id)) return null;
return this.assetDataStore.get(id) ?? null;
}
private getTextureOf(identifier: number, cls: BlockCls): ITexture | null {
if (cls === BlockCls.Unknown) return null;
if (cls !== BlockCls.Tileset) {
return this.tileStore.getTexture(identifier);
}
if (identifier < 10000) return null;
return this.cacheTileset(identifier);
}
getRenderable(identifier: number): ITextureRenderable | null {
const cls = this.clsMap.get(identifier);
if (isNil(cls)) return null;
const texture = this.getTextureOf(identifier, cls);
if (!texture) return null;
return texture.render();
}
getRenderableByAlias(alias: string): ITextureRenderable | null {
const identifier = this.idNumMap.get(alias);
if (isNil(identifier)) return null;
return this.getRenderable(identifier);
}
getBlockCls(identifier: number): BlockCls {
return this.clsMap.get(identifier) ?? BlockCls.Unknown;
}
getBlockClsByAlias(alias: string): BlockCls {
const id = this.idNumMap.get(alias);
if (isNil(id)) return BlockCls.Unknown;
return this.clsMap.get(id) ?? BlockCls.Unknown;
}
getIdentifierByAlias(alias: string): number | undefined {
return this.idNumMap.get(alias);
}
getAliasByIdentifier(identifier: number): string | undefined {
return this.numIdMap.get(identifier);
}
setBigImage(
identifier: number,
image: ITexture,
frames: number
): IBigImageReturn {
const bigImageId = this.bigImageId++;
this.bigImageStore.addTexture(bigImageId, image);
const cls = this.clsMap.get(identifier) ?? BlockCls.Unknown;
const store: IMaterialFramedData = {
texture: image,
cls,
offset: image.width / 4,
frames,
defaultFrame: this.defaultFrames.get(identifier) ?? -1
};
this.bigImageData.set(identifier, store);
const data: IBigImageReturn = {
identifier: bigImageId,
store: this.bigImageStore
};
return data;
}
isBigImage(identifier: number): boolean {
return this.bigImageData.has(identifier);
}
getBigImage(identifier: number): Readonly<IMaterialFramedData> | null {
return this.bigImageData.get(identifier) ?? null;
}
getBigImageByAlias(alias: string): Readonly<IMaterialFramedData> | null {
const identifier = this.idNumMap.get(alias);
if (isNil(identifier)) return null;
return this.bigImageData.get(identifier) ?? null;
}
getIfBigImage(identifier: number): Readonly<IMaterialFramedData> | null {
const bigImage = this.bigImageData.get(identifier);
if (bigImage) return bigImage;
else return this.getTile(identifier);
}
assetContainsTexture(texture: ITexture): boolean {
return this.trackedAsset.skipRef.has(texture.source);
}
getTextureAsset(texture: ITexture): number | undefined {
return this.trackedAsset.skipRef.get(texture.source);
}
}

View File

@ -0,0 +1,528 @@
import { IDirtyTracker, IDirtyMarker } from '@motajs/common';
import {
ITexture,
ITextureComposedData,
ITextureRenderable,
ITextureStore,
SizedCanvasImageSource
} from '@motajs/render-assets';
export const enum BlockCls {
Unknown,
Terrains,
Animates,
Enemys,
Npcs,
Items,
Enemy48,
Npc48,
Tileset,
Autotile
}
export const enum AutotileType {
Small2x3,
Big3x4
}
export const enum AutotileConnection {
LeftUp = 0b1000_0000,
Up = 0b0100_0000,
RightUp = 0b0010_0000,
Right = 0b0001_0000,
RightDown = 0b0000_1000,
Down = 0b0000_0100,
LeftDown = 0b0000_0010,
Left = 0b0000_0001
}
export interface IMaterialData {
/** 此素材的贴图对象存入了哪个贴图存储对象 */
readonly store: ITextureStore;
/** 贴图对象 */
readonly texture: ITexture;
/** 此素材的贴图对象的数字 id一般对应到图块数字 */
readonly identifier: number;
/** 此素材的贴图对象的字符串别名,一般对应到图块 id */
readonly alias?: string;
}
export interface IBlockIdentifier {
/** 图块 id */
readonly id: string;
/** 图块数字 */
readonly num: number;
/** 图块类型 */
readonly cls: Cls;
}
export interface IIndexedIdentifier {
/** 标识符索引 */
readonly index: number;
/** 标识符别名 */
readonly alias: string;
}
export interface IMaterialAssetData {
/** 图集数据 */
readonly data: ITextureComposedData;
/** 贴图的标识符 */
readonly identifier: number;
/** 贴图的别名 */
readonly alias: string;
/** 贴图所属的存储对象 */
readonly store: ITextureStore;
}
export interface IAutotileConnection {
/** 连接方式,最高位表示左上,低位依次顺时针旋转 */
readonly connection: number;
/** 中心自动元件对应的图块数字 */
readonly center: number;
}
export interface IBigImageReturn {
/** 大怪物贴图在 store 中的标识符 */
readonly identifier: number;
/** 存储大怪物贴图的存储对象 */
readonly store: ITextureStore;
}
export interface IMaterialFramedData {
/** 贴图对象 */
texture: ITexture;
/** 图块类型 */
cls: BlockCls;
/** 贴图总帧数 */
frames: number;
/** 每帧的横向偏移量 */
offset: number;
/** 默认帧数 */
defaultFrame: number;
}
export interface IMaterialAsset
extends IDirtyTracker<boolean>,
IDirtyMarker<void> {
/** 图集的贴图数据 */
readonly data: ITextureComposedData;
}
export interface IAutotileProcessor {
/** 该自动元件处理器使用的素材管理器 */
readonly manager: IMaterialManager;
/**
*
*
* @param autotile
* @param target
*/
setConnection(autotile: number, target: number): void;
/**
*
* @param array
* @param index
* @param width
*/
connect(
array: Uint32Array,
index: number,
width: number
): IAutotileConnection;
/**
*
* @param connection
* @param center
* @param target
* @param direction
* @returns
*/
updateConnectionFor(
connection: number,
center: number,
target: number,
direction: AutotileConnection
): number;
/**
*
* @param autotile
* @param connection
* @returns
*/
render(autotile: number, connection: number): ITextureRenderable | null;
/**
*
* @param tile
* @param connection
* @returns
*/
renderWith(
tile: Readonly<IMaterialFramedData>,
connection: number
): ITextureRenderable | null;
/**
*
* @param tile
* @param connection
* @returns
*/
renderWithoutCheck(
tile: Readonly<IMaterialFramedData>,
connection: number
): ITextureRenderable | null;
/**
*
* @param autotile
* @param connection
* @returns
*/
renderAnimated(
autotile: number,
connection: number
): Generator<ITextureRenderable, void>;
/**
*
* @param autotile
* @param connection
* @returns
*/
renderAnimatedWith(
tile: Readonly<IMaterialFramedData>,
connection: number
): Generator<ITextureRenderable, void>;
}
export interface IMaterialGetter {
/**
*
* @param identifier
*/
getTile(identifier: number): Readonly<IMaterialFramedData> | null;
/**
*
* @param identifier
*/
getBlockCls(identifier: number): BlockCls;
/**
* `bigImage`
* @param identifier
*/
isBigImage(identifier: number): boolean;
/**
* `bigImage`
* @param identifier
*/
getBigImage(identifier: number): Readonly<IMaterialFramedData> | null;
/**
* `bigImage` `bigImage`
* `null`
* @param identifier
*/
getIfBigImage(identifier: number): Readonly<IMaterialFramedData> | null;
/**
*
* @param identifier
*/
getAsset(identifier: number): ITextureComposedData | null;
/**
*
* @param identifier
*/
getTileset(identifier: number): ITexture | null;
/**
*
* @param identifier
*/
getImage(identifier: number): ITexture | null;
}
export interface IMaterialAliasGetter {
/**
* id
* @param alias id
*/
getTileByAlias(alias: string): Readonly<IMaterialFramedData> | null;
/**
*
* @param alias
*/
getTilesetByAlias(alias: string): ITexture | null;
/**
*
* @param alias
*/
getImageByAlias(alias: string): ITexture | null;
/**
*
* @param alias
*/
getAssetByAlias(alias: string): ITextureComposedData | null;
/**
*
* @param alias id
*/
getBlockClsByAlias(alias: string): BlockCls;
/**
* `bigImage`
* @param alias id
*/
getBigImageByAlias(alias: string): Readonly<IMaterialFramedData> | null;
}
export interface IMaterialManager
extends IMaterialGetter,
IMaterialAliasGetter {
/** 贴图存储,把 terrains 等内容单独分开存储 */
readonly tileStore: ITextureStore;
/** tilesets 贴图存储,每个 tileset 是一个贴图对象 */
readonly tilesetStore: ITextureStore;
/** 存储注册的图像的存储对象 */
readonly imageStore: ITextureStore;
/** 图集存储,将常用贴图存入其中 */
readonly assetStore: ITextureStore;
/** bigImage 存储,存储大怪物数据 */
readonly bigImageStore: ITextureStore;
/** 图集信息存储 */
readonly assetDataStore: Iterable<[number, ITextureComposedData]>;
/** 带有脏标记追踪的图集信息 */
readonly trackedAsset: ITrackedAssetData;
/** 图块类型映射 */
readonly clsMap: Map<number, BlockCls>;
/**
* terrains items
* @param source
* @param map id
*/
addGrid(
source: SizedCanvasImageSource,
map: ArrayLike<IBlockIdentifier>
): Iterable<IMaterialData>;
/**
* animates enemys npcs enemy48 npc48
* @param source
* @param map id
* @param frames
* @param height
*/
addRowAnimate(
source: SizedCanvasImageSource,
map: ArrayLike<IBlockIdentifier>,
frames: number,
height: number
): Iterable<IMaterialData>;
/**
*
* @param source
* @param identifier id
* @returns 西
*/
addAutotile(
source: SizedCanvasImageSource,
identifier: IBlockIdentifier
): void;
/**
* tileset
* @param source
* @param alias tileset tilesets
*/
addTileset(
source: SizedCanvasImageSource,
identifier: IIndexedIdentifier
): IMaterialData | null;
/**
*
* @param source
* @param identifier images
*/
addImage(
source: SizedCanvasImageSource,
identifier: IIndexedIdentifier
): IMaterialData;
/**
*
* @param identifier
* @param defaultFrame
*/
setDefaultFrame(identifier: number, defaultFrame: number): void;
/**
* -1 使
* @param identifier
*/
getDefaultFrame(identifier: number): number;
/**
* tileset使 {@link cacheTilesetList}
* @param identifier tileset
*/
cacheTileset(identifier: number): ITexture | null;
/**
* tileset
* @param identifierList
*/
cacheTilesetList(
identifierList: Iterable<number>
): Iterable<ITexture | null>;
/**
* 使 {@link cacheAutotileList}
* @param identifier
*/
cacheAutotile(identifier: number): ITexture | null;
/**
*
* @param identifierList
*/
cacheAutotileList(
identifierList: Iterable<number>
): Iterable<ITexture | null>;
/**
* 使
*/
buildAssets(): Iterable<IMaterialAssetData>;
/**
*
* @param texture
*/
buildToAsset(texture: ITexture): IMaterialAssetData;
/**
*
* @param texture
*/
buildListToAsset(texture: Iterable<ITexture>): Iterable<IMaterialAssetData>;
/**
*
* @param identifier
*/
getRenderable(identifier: number): ITextureRenderable | null;
/**
*
* @param alias id
*/
getRenderableByAlias(alias: string): ITextureRenderable | null;
/**
*
* @param alias id
*/
getIdentifierByAlias(alias: string): number | undefined;
/**
* id
* @param identifier
*/
getAliasByIdentifier(identifier: number): string | undefined;
/**
* `bigImage`
* @param identifier
* @param image `bigImage`
* @param frames `bigImage`
*/
setBigImage(
identifier: number,
image: ITexture,
frames: number
): IBigImageReturn;
/**
*
* @param texture
*/
assetContainsTexture(texture: ITexture): boolean;
/**
*
* @param texture
*/
getTextureAsset(texture: ITexture): number | undefined;
}
export interface IAssetBuilder {
/**
*
* @param store
*/
pipe(store: ITextureStore): void;
/**
*
* @param texture
* @returns
*/
addTexture(texture: ITexture): ITextureComposedData;
/**
*
* @param texture
* @returns 使
*/
addTextureList(texture: Iterable<ITexture>): Iterable<ITextureComposedData>;
/**
*
*/
tracked(): ITrackedAssetData;
/**
*
*/
close(): void;
}
export interface ITrackedAssetData extends IDirtyTracker<Set<number>> {
/** 图像源列表 */
readonly sourceList: Map<number, ImageBitmap>;
/**
* `ImageBitmap` 使
* `ImageBitmap` `sourceList`
*/
readonly skipRef: Map<SizedCanvasImageSource, number>;
/** 贴图数据 */
readonly materials: IMaterialGetter;
/**
* 使
*/
close(): void;
/**
*
*/
then(): Promise<void>;
}

View File

@ -0,0 +1,47 @@
import { ITexture } from '@motajs/render-assets';
import { BlockCls } from './types';
export function getClsByString(cls: Cls): BlockCls {
switch (cls) {
case 'terrains':
return BlockCls.Terrains;
case 'animates':
return BlockCls.Animates;
case 'autotile':
return BlockCls.Autotile;
case 'enemys':
return BlockCls.Enemys;
case 'items':
return BlockCls.Items;
case 'npcs':
return BlockCls.Npcs;
case 'npc48':
return BlockCls.Npc48;
case 'enemy48':
return BlockCls.Enemy48;
case 'tileset':
return BlockCls.Tileset;
default:
return BlockCls.Unknown;
}
}
export function getTextureFrame(cls: BlockCls, texture: ITexture) {
switch (cls) {
case BlockCls.Animates:
case BlockCls.Enemy48:
case BlockCls.Npc48:
return 4;
case BlockCls.Autotile:
return texture.width === 384 ? 4 : 1;
case BlockCls.Enemys:
case BlockCls.Npcs:
return 2;
case BlockCls.Items:
case BlockCls.Terrains:
case BlockCls.Tileset:
return 1;
case BlockCls.Unknown:
return 0;
}
}

View File

@ -4,6 +4,7 @@
"@motajs/client-base": "workspace:*", "@motajs/client-base": "workspace:*",
"@motajs/common": "workspace:*", "@motajs/common": "workspace:*",
"@motajs/render": "workspace:*", "@motajs/render": "workspace:*",
"@motajs/render-assets": "workspace:*",
"@motajs/render-core": "workspace:*", "@motajs/render-core": "workspace:*",
"@motajs/legacy-common": "workspace:*", "@motajs/legacy-common": "workspace:*",
"@motajs/legacy-ui": "workspace:*", "@motajs/legacy-ui": "workspace:*",

View File

@ -76,6 +76,11 @@ gameKey
name: '快捷商店', name: '快捷商店',
defaults: KeyCode.KeyV defaults: KeyCode.KeyV
}) })
.register({
id: 'statistics',
name: '数据统计',
defaults: KeyCode.KeyB
})
.register({ .register({
id: 'viewMap_1', id: 'viewMap_1',
name: '浏览地图_1', name: '浏览地图_1',

View File

@ -17,8 +17,7 @@ export function patchAudio() {
}; };
patch.add('playBgm', function (bgm, startTime) { patch.add('playBgm', function (bgm, startTime) {
const name = core.getMappedName(bgm) as BgmIds; play(bgm, startTime);
play(name, startTime);
}); });
patch.add('pauseBgm', function () { patch.add('pauseBgm', function () {
pause(); pause();

View File

@ -6,7 +6,8 @@ import {
saveLoad, saveLoad,
openSettings, openSettings,
openViewMap, openViewMap,
openReplay openReplay,
openStatistics
} from './ui'; } from './ui';
import { ElementLocator } from '@motajs/render-core'; import { ElementLocator } from '@motajs/render-core';
@ -15,6 +16,9 @@ export function createAction() {
.realize('save', () => { .realize('save', () => {
saveSave(mainUIController, FULL_LOC); saveSave(mainUIController, FULL_LOC);
}) })
.realize('statistics', () => {
openStatistics(mainUIController);
})
.realize('load', () => { .realize('load', () => {
saveLoad(mainUIController, FULL_LOC); saveLoad(mainUIController, FULL_LOC);
}) })

View File

@ -0,0 +1,22 @@
import { state } from '@user/data-state';
import { materials } from '@user/client-base';
import { MapRenderer, MapExtensionManager } from './map';
/** 主地图渲染器,用于渲染游戏画面 */
export const mainMapRenderer = new MapRenderer(materials, state.layer);
/** 主地图渲染器拓展 */
export const mainMapExtension = new MapExtensionManager(mainMapRenderer);
/** 副地图渲染器,用于渲染缩略图、浏览地图等 */
// export const expandMapRenderer = new MapRenderer(materials, state.layer);
export async function createMainExtension() {
// 算是一种妥协吧,等之后加载系统重构之后应该会清晰很多
await materials.trackedAsset.then();
mainMapRenderer.useAsset(materials.trackedAsset);
const layer = state.layer.getLayerByAlias('event');
if (layer) {
mainMapExtension.addHero(state.hero, layer);
mainMapExtension.addDoor(layer);
}
}

View File

@ -33,6 +33,8 @@ import {
import { SetupComponentOptions } from '@motajs/system-ui'; import { SetupComponentOptions } from '@motajs/system-ui';
import { texture } from '../elements'; import { texture } from '../elements';
// todo: TextContent 应该改成渲染元素?
//#region TextContent //#region TextContent
export interface TextContentProps export interface TextContentProps

View File

@ -817,7 +817,7 @@ export class TextContentParser {
return pointer; return pointer;
} }
const time = parseInt(param); const time = parseInt(param);
this.addWaitRenderable(end, time); this.addWaitRenderable(end + 1, time);
return end; return end;
} }
@ -830,19 +830,19 @@ export class TextContentParser {
} }
if (/^\d+$/.test(param)) { if (/^\d+$/.test(param)) {
const num = Number(param); const num = Number(param);
this.addIconRenderable(end, num as AllNumbers); this.addIconRenderable(end + 1, num as AllNumbers);
} else { } else {
if (/^X\d+$/.test(param)) { if (/^X\d+$/.test(param)) {
// 额外素材 // 额外素材
const num = Number(param.slice(1)); const num = Number(param.slice(1));
this.addIconRenderable(end, num as AllNumbers); this.addIconRenderable(end + 1, num as AllNumbers);
} else { } else {
const num = texture.idNumberMap[param as AllIds]; const num = texture.idNumberMap[param as AllIds];
if (num === void 0) { if (num === void 0) {
logger.warn(59, param); logger.warn(59, param);
return end; return end;
} }
this.addIconRenderable(end, num); this.addIconRenderable(end + 1, num);
} }
} }
return end; return end;
@ -960,6 +960,7 @@ export class TextContentParser {
break; break;
case 'n': case 'n':
// 在这里预先将换行处理为多个 node会比在分行时再处理更方便 // 在这里预先将换行处理为多个 node会比在分行时再处理更方便
pointer++;
this.addTextNode(pointer + 1, true); this.addTextNode(pointer + 1, true);
break; break;
} }
@ -985,7 +986,9 @@ export class TextContentParser {
this.resolved += char; this.resolved += char;
} }
this.addTextNode(text.length, false); if (this.nodePointer < text.length) {
this.addTextNode(text.length, false);
}
return this.splitLines(width); return this.splitLines(width);
} }

View File

@ -1,8 +1,6 @@
import { logger } from '@motajs/common'; import { logger } from '@motajs/common';
import { import { MotaOffscreenCanvas2D } from '@motajs/render-core';
MotaOffscreenCanvas2D, import { SizedCanvasImageSource } from '@motajs/render-assets';
SizedCanvasImageSource
} from '@motajs/render-core';
// 经过测试https://www.measurethat.net/Benchmarks/Show/30741/1/drawimage-img-vs-canvas-vs-bitmap-cropping-fix-loading // 经过测试https://www.measurethat.net/Benchmarks/Show/30741/1/drawimage-img-vs-canvas-vs-bitmap-cropping-fix-loading
// 得出结论ImageBitmap和Canvas的绘制性能不如Image于是直接画Image就行所以缓存基本上就是存Image // 得出结论ImageBitmap和Canvas的绘制性能不如Image于是直接画Image就行所以缓存基本上就是存Image

View File

@ -1,4 +1,5 @@
import { RenderAdapter, SizedCanvasImageSource } from '@motajs/render-core'; import { RenderAdapter } from '@motajs/render-core';
import { SizedCanvasImageSource } from '@motajs/render-assets';
import { logger } from '@motajs/common'; import { logger } from '@motajs/common';
import { ILayerRenderExtends, Layer, LayerMovingRenderable } from './layer'; import { ILayerRenderExtends, Layer, LayerMovingRenderable } from './layer';
import EventEmitter from 'eventemitter3'; import EventEmitter from 'eventemitter3';

View File

@ -6,6 +6,10 @@ import { createViewport } from './viewport';
import { Icon, Winskin } from './misc'; import { Icon, Winskin } from './misc';
import { Animate } from './animate'; import { Animate } from './animate';
import { createItemDetail } from './itemDetail'; import { createItemDetail } from './itemDetail';
import { logger } from '@motajs/common';
import { MapRender, MapRenderer } from '../map';
import { state } from '@user/data-state';
import { materials } from '@user/client-base';
export function createElements() { export function createElements() {
createCache(); createCache();
@ -66,6 +70,31 @@ export function createElements() {
return new Animate(); return new Animate();
}); });
tagMap.register('icon', standardElementNoCache(Icon)); tagMap.register('icon', standardElementNoCache(Icon));
tagMap.register('map-render', (_0, _1, props) => {
if (!props) {
logger.error(42);
return new MapRender(
state.layer,
new MapRenderer(materials, state.layer)
);
}
const { layerState, renderer } = props;
if (!layerState) {
logger.error(42, 'layerState');
return new MapRender(
state.layer,
new MapRenderer(materials, state.layer)
);
}
if (!renderer) {
logger.error(42, 'renderer');
return new MapRender(
state.layer,
new MapRenderer(materials, state.layer)
);
}
return new MapRender(layerState, renderer);
});
} }
export * from './animate'; export * from './animate';

View File

@ -4,9 +4,9 @@ import {
RenderItem, RenderItem,
RenderItemPosition, RenderItemPosition,
MotaOffscreenCanvas2D, MotaOffscreenCanvas2D,
Transform, Transform
SizedCanvasImageSource
} from '@motajs/render-core'; } from '@motajs/render-core';
import { SizedCanvasImageSource } from '@motajs/render-assets';
import { isNil } from 'lodash-es'; import { isNil } from 'lodash-es';
import { RenderableData, AutotileRenderable, texture } from './cache'; import { RenderableData, AutotileRenderable, texture } from './cache';
import { IAnimateFrame, renderEmits } from './frame'; import { IAnimateFrame, renderEmits } from './frame';

View File

@ -1,5 +1,6 @@
import { BaseProps, TagDefine } from '@motajs/render-vue'; import { BaseProps, TagDefine } from '@motajs/render-vue';
import { CanvasStyle, Transform } from '@motajs/render-core'; import { ERenderItemEvent, Transform } from '@motajs/render-core';
import { CanvasStyle } from '@motajs/render-assets';
import { import {
ILayerGroupRenderExtends, ILayerGroupRenderExtends,
FloorLayer, FloorLayer,
@ -10,6 +11,8 @@ import {
import { EAnimateEvent } from './animate'; import { EAnimateEvent } from './animate';
import { EIconEvent, EWinskinEvent } from './misc'; import { EIconEvent, EWinskinEvent } from './misc';
import { IEnemyCollection } from '@motajs/types'; import { IEnemyCollection } from '@motajs/types';
import { ILayerState } from '@user/data-state';
import { IMapRenderer } from '../map';
export interface AnimateProps extends BaseProps {} export interface AnimateProps extends BaseProps {}
@ -59,6 +62,11 @@ export interface LayerProps extends BaseProps {
ex?: readonly ILayerRenderExtends[]; ex?: readonly ILayerRenderExtends[];
} }
export interface MapRenderProps extends BaseProps {
layerState: ILayerState;
renderer: IMapRenderer;
}
declare module 'vue/jsx-runtime' { declare module 'vue/jsx-runtime' {
namespace JSX { namespace JSX {
export interface IntrinsicElements { export interface IntrinsicElements {
@ -67,6 +75,7 @@ declare module 'vue/jsx-runtime' {
animation: TagDefine<AnimateProps, EAnimateEvent>; animation: TagDefine<AnimateProps, EAnimateEvent>;
icon: TagDefine<IconProps, EIconEvent>; icon: TagDefine<IconProps, EIconEvent>;
winskin: TagDefine<WinskinProps, EWinskinEvent>; winskin: TagDefine<WinskinProps, EWinskinEvent>;
'map-render': TagDefine<MapRenderProps, ERenderItemEvent>;
} }
} }
} }

View File

@ -7,7 +7,7 @@ import { EffectBase } from './base';
export class Image3DEffect export class Image3DEffect
extends EffectBase<void> extends EffectBase<void>
implements ITransformUpdatable implements ITransformUpdatable<Transform3D>
{ {
/** 图片的模型变换 */ /** 图片的模型变换 */
private model: Transform3D = new Transform3D(); private model: Transform3D = new Transform3D();
@ -65,7 +65,7 @@ export class Image3DEffect
* @param model * @param model
*/ */
setModel(model: Transform3D) { setModel(model: Transform3D) {
this.model.bind(); this.model.unbind(this);
this.model = model; this.model = model;
model.bind(this); model.bind(this);
} }
@ -75,7 +75,7 @@ export class Image3DEffect
* @param model * @param model
*/ */
setView(view: Transform3D) { setView(view: Transform3D) {
this.view.bind(); this.view.unbind(this);
this.view = view; this.view = view;
view.bind(this); view.bind(this);
} }
@ -85,7 +85,7 @@ export class Image3DEffect
* @param model * @param model
*/ */
setProj(proj: Transform3D) { setProj(proj: Transform3D) {
this.proj.bind(); this.proj.unbind(this);
this.proj = proj; this.proj = proj;
proj.bind(this); proj.bind(this);
} }

View File

@ -9,21 +9,19 @@ import { createAction } from './action';
import { sceneController } from './scene'; import { sceneController } from './scene';
import { GameTitleUI } from './ui/title'; import { GameTitleUI } from './ui/title';
import { createWeather } from './weather'; import { createWeather } from './weather';
import { createMainExtension } from './commonIns';
export function createGameRenderer() { export function createGameRenderer() {
const App = defineComponent(_props => { const App = defineComponent(_props => {
return () => ( return () => (
<container noanti width={MAIN_WIDTH} height={MAIN_HEIGHT}> <container width={MAIN_WIDTH} height={MAIN_HEIGHT}>
{sceneController.render()} {sceneController.render()}
</container> </container>
); );
}); });
mainRenderer.setAntiAliasing(false);
mainRenderer.hide(); mainRenderer.hide();
createApp(App).mount(mainRenderer); createApp(App).mount(mainRenderer);
console.log(mainRenderer);
} }
export function createRender() { export function createRender() {
@ -32,11 +30,15 @@ export function createRender() {
createAction(); createAction();
createWeather(); createWeather();
loading.on('loaded', () => { loading.once('loaded', () => {
sceneController.open(GameTitleUI, {}); sceneController.open(GameTitleUI, {});
mainRenderer.show(); mainRenderer.show();
}); });
loading.once('assetBuilt', () => {
createMainExtension();
});
hook.on('restart', () => { hook.on('restart', () => {
sceneController.closeAll(); sceneController.closeAll();
sceneController.open(GameTitleUI, {}); sceneController.open(GameTitleUI, {});
@ -45,6 +47,7 @@ export function createRender() {
Font.setDefaults(DEFAULT_FONT); Font.setDefaults(DEFAULT_FONT);
} }
export * from './commonIns';
export * from './components'; export * from './components';
export * from './elements'; export * from './elements';
export * from './fx'; export * from './fx';

View File

@ -0,0 +1,351 @@
import { clamp } from 'lodash-es';
import {
IBlockData,
IBlockIndex,
IBlockInfo,
IBlockSplitter,
IBlockSplitterConfig
} from './types';
export class BlockSplitter<T> implements IBlockSplitter<T> {
blockWidth: number = 0;
blockHeight: number = 0;
dataWidth: number = 0;
dataHeight: number = 0;
width: number = 1;
height: number = 1;
/** 分块映射 */
readonly blockMap: Map<number, IBlockData<T>> = new Map();
/** 数据宽度配置 */
private splitDataWidth: number = 0;
/** 数据高度配置 */
private splitDataHeight: number = 0;
/** 单个分块的宽度配置 */
private splitBlockWidth: number = 0;
/** 单个分块的高度配置 */
private splitBlockHeight: number = 0;
/**
*
* @param x
* @param y
*/
private checkLocRange(x: number, y: number) {
return x >= 0 && y >= 0 && x < this.width && y < this.height;
}
getBlockByLoc(x: number, y: number): IBlockData<T> | null {
if (!this.checkLocRange(x, y)) return null;
const index = y * this.width + x;
return this.blockMap.get(index) ?? null;
}
getBlockByIndex(index: number): IBlockData<T> | null {
return this.blockMap.get(index) ?? null;
}
setBlockByLoc(data: T, x: number, y: number): IBlockData<T> | null {
if (!this.checkLocRange(x, y)) return null;
const index = y * this.width + x;
const block = this.blockMap.get(index);
if (!block) return null;
block.data = data;
return block;
}
setBlockByIndex(data: T, index: number): IBlockData<T> | null {
const block = this.blockMap.get(index);
if (!block) return null;
block.data = data;
return block;
}
*iterateBlockByLoc(x: number, y: number): Generator<IBlockIndex, void> {
if (!this.checkLocRange(x, y)) return;
const index = y * this.width + x;
yield* this.iterateBlockByIndex(index);
}
*iterateBlockByIndex(index: number): Generator<IBlockIndex, void> {
const block = this.blockMap.get(index);
if (!block) return;
const startX = block.x * this.blockWidth;
const startY = block.y * this.blockHeight;
const endX = startX + block.width;
const endY = startY + block.height;
for (let ny = startY; ny < endY; ny++) {
for (let nx = startX; nx < endX; nx++) {
const index: IBlockIndex = {
x: nx,
y: ny,
dataX: nx * this.blockWidth,
dataY: ny * this.blockHeight,
index: ny * this.dataWidth + nx
};
yield index;
}
}
}
*iterateBlockByIndices(
indices: Iterable<number>
): Generator<IBlockIndex, void> {
for (const index of indices) {
yield* this.iterateBlockByIndex(index);
}
}
iterateBlocks(): Iterable<IBlockData<T>> {
return this.blockMap.values();
}
*iterateBlocksOfDataArea(
x: number,
y: number,
width: number,
height: number
): Generator<IBlockData<T>> {
const r = this.width - 1;
const b = this.height - 1;
const rx = x + width;
const by = y + height;
const left = clamp(Math.floor(x / this.blockWidth), 0, r);
const top = clamp(Math.floor(y / this.blockHeight), 0, b);
const right = clamp(Math.floor(rx / this.blockWidth), 0, r);
const bottom = clamp(Math.floor(by / this.blockHeight), 0, b);
for (let ny = left; ny <= right; ny++) {
for (let nx = top; nx <= bottom; nx++) {
const index = ny * this.width + nx;
const block = this.blockMap.get(index);
if (!block) continue;
yield block;
}
}
}
getIndexByLoc(x: number, y: number): number {
if (!this.checkLocRange(x, y)) return -1;
return y * this.width + x;
}
getLocByIndex(index: number): Loc | null {
if (index >= this.width * this.height) return null;
return {
x: index % this.width,
y: Math.floor(index / this.width)
};
}
getIndicesByLocList(list: Iterable<Loc>): Iterable<number> {
const res: number[] = [];
for (const { x, y } of list) {
res.push(this.getIndexByLoc(x, y));
}
return res;
}
getLocListByIndices(list: Iterable<number>): Iterable<Loc | null> {
const res: (Loc | null)[] = [];
for (const index of list) {
res.push(this.getLocByIndex(index));
}
return res;
}
getBlockByDataLoc(x: number, y: number): IBlockData<T> | null {
const bx = Math.floor(x / this.blockWidth);
const by = Math.floor(y / this.blockHeight);
if (!this.checkLocRange(bx, by)) return null;
const index = by * this.width + bx;
return this.blockMap.get(index) ?? null;
}
getBlockByDataIndex(index: number): IBlockData<T> | null {
const x = index % this.dataWidth;
const y = Math.floor(index / this.dataWidth);
return this.getBlockByDataLoc(x, y);
}
getIndicesByDataLocList(list: Iterable<Loc>): Set<number> {
const res = new Set<number>();
for (const { x, y } of list) {
const bx = Math.floor(x / this.blockWidth);
const by = Math.floor(y / this.blockHeight);
if (!this.checkLocRange(bx, by)) continue;
res.add(bx + by * this.width);
}
return res;
}
getIndicesByDataIndices(list: Iterable<number>): Set<number> {
const res = new Set<number>();
for (const index of list) {
const x = index % this.dataWidth;
const y = Math.floor(index / this.dataWidth);
const bx = Math.floor(x / this.blockWidth);
const by = Math.floor(y / this.blockHeight);
if (!this.checkLocRange(bx, by)) continue;
res.add(bx + by * this.width);
}
return res;
}
getBlocksByDataLocList(list: Iterable<Loc>): Set<IBlockData<T>> {
const res = new Set<IBlockData<T>>();
for (const { x, y } of list) {
const bx = Math.floor(x / this.blockWidth);
const by = Math.floor(y / this.blockHeight);
if (!this.checkLocRange(bx, by)) continue;
const index = bx + by * this.width;
const data = this.blockMap.get(index);
if (data) res.add(data);
}
return res;
}
getBlocksByDataIndices(list: Iterable<number>): Set<IBlockData<T>> {
const res = new Set<IBlockData<T>>();
for (const index of list) {
const x = index % this.dataWidth;
const y = Math.floor(index / this.dataWidth);
const bx = Math.floor(x / this.blockWidth);
const by = Math.floor(y / this.blockHeight);
if (!this.checkLocRange(bx, by)) continue;
const blockIndex = bx + by * this.width;
const data = this.blockMap.get(blockIndex);
if (data) res.add(data);
}
return res;
}
configSplitter(config: IBlockSplitterConfig): void {
this.splitDataWidth = config.dataWidth;
this.splitDataHeight = config.dataHeight;
this.splitBlockWidth = config.blockWidth;
this.splitBlockHeight = config.blockHeight;
}
private mapBlock(
x: number,
y: number,
realWidth: number,
width: number,
height: number,
fn: (block: IBlockInfo) => T
) {
const index = y * realWidth + x;
const block: IBlockInfo = {
index,
x,
y,
dataX: x * this.blockWidth,
dataY: y * this.blockHeight,
width,
height
};
const data = fn(block);
const blockData = new SplittedBlockData(this, block, data);
this.blockMap.set(index, blockData);
}
splitBlocks(mapFn: (block: IBlockInfo) => T): void {
this.blockMap.clear();
this.blockWidth = this.splitBlockWidth;
this.blockHeight = this.splitBlockHeight;
this.dataWidth = this.splitDataWidth;
this.dataHeight = this.splitDataHeight;
const restX = this.splitDataWidth % this.splitBlockWidth;
const restY = this.splitDataHeight % this.splitBlockHeight;
const width = Math.floor(this.splitDataWidth / this.splitBlockWidth);
const height = Math.floor(this.splitDataHeight / this.splitBlockHeight);
const hasXRest = restX > 0;
const hasYRest = restY > 0;
const realWidth = hasXRest ? width + 1 : width;
const bw = this.blockWidth;
const bh = this.blockHeight;
this.width = realWidth;
this.height = hasYRest ? height + 1 : height;
for (let ny = 0; ny < height; ny++) {
for (let nx = 0; nx < width; nx++) {
this.mapBlock(nx, ny, realWidth, bw, bh, mapFn);
}
}
if (hasXRest) {
for (let ny = 0; ny < height; ny++) {
this.mapBlock(width, ny, realWidth, restX, bh, mapFn);
}
}
if (hasYRest) {
for (let nx = 0; nx < width; nx++) {
this.mapBlock(nx, height, realWidth, bw, restY, mapFn);
}
}
if (hasXRest && hasYRest) {
this.mapBlock(width, height, realWidth, restX, restY, mapFn);
}
}
}
class SplittedBlockData<T> implements IBlockData<T> {
width: number;
height: number;
x: number;
y: number;
dataX: number;
dataY: number;
index: number;
data: T;
constructor(
readonly splitter: BlockSplitter<T>,
info: IBlockInfo,
data: T
) {
this.width = info.width;
this.height = info.height;
this.x = info.x;
this.y = info.y;
this.dataX = info.dataX;
this.dataY = info.dataY;
this.index = info.index;
this.data = data;
}
left(): IBlockData<T> | null {
return this.splitter.getBlockByLoc(this.x - 1, this.y);
}
right(): IBlockData<T> | null {
return this.splitter.getBlockByLoc(this.x + 1, this.y);
}
up(): IBlockData<T> | null {
return this.splitter.getBlockByLoc(this.x, this.y - 1);
}
down(): IBlockData<T> | null {
return this.splitter.getBlockByLoc(this.x, this.y + 1);
}
leftUp(): IBlockData<T> | null {
return this.splitter.getBlockByLoc(this.x - 1, this.y - 1);
}
leftDown(): IBlockData<T> | null {
return this.splitter.getBlockByLoc(this.x - 1, this.y + 1);
}
rightUp(): IBlockData<T> | null {
return this.splitter.getBlockByLoc(this.x + 1, this.y - 1);
}
rightDown(): IBlockData<T> | null {
return this.splitter.getBlockByLoc(this.x + 1, this.y + 1);
}
next(): IBlockData<T> | null {
return this.splitter.getBlockByIndex(this.index + 1);
}
}

View File

@ -0,0 +1,2 @@
/** 单个图块的实例化数据数量 */
export const INSTANCED_COUNT = 4 + 4 + 4 + 4;

View File

@ -0,0 +1,70 @@
import { MotaOffscreenCanvas2D, RenderItem } from '@motajs/render-core';
import { ILayerState } from '@user/data-state';
import { IMapRenderer } from './types';
import { ElementNamespace, ComponentInternalInstance } from 'vue';
import { CELL_HEIGHT, CELL_WIDTH, MAP_HEIGHT, MAP_WIDTH } from '../shared';
export class MapRender extends RenderItem {
/**
* @param layerState
* @param renderer
*/
constructor(
readonly layerState: ILayerState,
readonly renderer: IMapRenderer
) {
super('static', false, false);
renderer.setLayerState(layerState);
renderer.setCellSize(CELL_WIDTH, CELL_HEIGHT);
renderer.setRenderSize(MAP_WIDTH, MAP_HEIGHT);
this.delegateTicker(time => {
this.renderer.tick(time);
if (this.renderer.needUpdate()) {
this.update();
}
});
}
private sizeGL(width: number, height: number) {
const ratio = this.highResolution ? devicePixelRatio : 1;
const scale = ratio * this.scale;
const w = width * scale;
const h = height * scale;
this.renderer.setCanvasSize(w, h);
this.renderer.setViewport(0, 0, w, h);
}
onResize(scale: number): void {
super.onResize(scale);
this.sizeGL(this.width, this.height);
}
size(width: number, height: number): void {
super.size(width, height);
this.sizeGL(width, height);
}
protected render(canvas: MotaOffscreenCanvas2D): void {
this.renderer.clear(true, true);
const map = this.renderer.render();
canvas.ctx.drawImage(map, 0, 0, canvas.width, canvas.height);
}
patchProp(
key: string,
prevValue: any,
nextValue: any,
namespace?: ElementNamespace,
parentComponent?: ComponentInternalInstance | null
): void {
switch (key) {
case 'layerState': {
this.renderer.setLayerState(nextValue);
break;
}
}
super.patchProp(key, prevValue, nextValue, namespace, parentComponent);
}
}

View File

@ -0,0 +1,73 @@
import {
IMapLayer,
IMapLayerHookController,
IMapLayerHooks
} from '@user/data-state';
import { IMapDoorRenderer } from './types';
import { IMapRenderer } from '../types';
import { sleep } from 'mutate-animate';
import { DOOR_ANIMATE_INTERVAL } from '../../shared';
export class MapDoorRenderer implements IMapDoorRenderer {
/** 钩子控制器 */
readonly controller: IMapLayerHookController;
/** 动画间隔 */
private interval: number = DOOR_ANIMATE_INTERVAL;
constructor(
readonly renderer: IMapRenderer,
readonly layer: IMapLayer
) {
this.controller = layer.addHook(new MapDoorHook(this));
this.controller.load();
}
setAnimateInterval(interval: number): void {
this.interval = interval;
}
async openDoor(x: number, y: number): Promise<void> {
const status = this.renderer.getBlockStatus(this.layer, x, y);
if (!status) return;
const array = this.layer.getMapRef().array;
const index = y * this.layer.width + x;
const num = array[index];
const data = this.renderer.manager.getIfBigImage(num);
if (!data) return;
const frames = data.frames;
for (let i = 0; i < frames; i++) {
status.useSpecifiedFrame(i);
await sleep(this.interval);
}
}
async closeDoor(num: number, x: number, y: number): Promise<void> {
const data = this.renderer.manager.getIfBigImage(num);
if (!data) return;
const moving = this.renderer.addMovingBlock(this.layer, num, x, y);
const frames = data.frames;
for (let i = frames - 1; i >= 0; i--) {
moving.useSpecifiedFrame(i);
await sleep(this.interval);
}
moving.destroy();
}
destroy(): void {
this.controller.unload();
}
}
class MapDoorHook implements Partial<IMapLayerHooks> {
constructor(readonly renderer: MapDoorRenderer) {}
onOpenDoor(x: number, y: number): Promise<void> {
return this.renderer.openDoor(x, y);
}
onCloseDoor(num: number, x: number, y: number): Promise<void> {
return this.renderer.closeDoor(num, x, y);
}
}

View File

@ -0,0 +1,518 @@
import {
degradeFace,
FaceDirection,
getFaceMovement,
HeroAnimateDirection,
IHeroState,
IHeroStateHooks,
IMapLayer,
nextFaceDirection,
state
} from '@user/data-state';
import { IMapRenderer, IMapRendererTicker, IMovingBlock } from '../types';
import { isNil } from 'lodash-es';
import { IHookController, logger } from '@motajs/common';
import { BlockCls, IMaterialFramedData } from '@user/client-base';
import {
ITexture,
ITextureSplitter,
TextureRowSplitter
} from '@motajs/render-assets';
import { IMapHeroRenderer } from './types';
import { TimingFn } from 'mutate-animate';
/** 默认的移动时长 */
const DEFAULT_TIME = 100;
interface HeroRenderEntity {
/** 移动图块对象 */
readonly block: IMovingBlock;
/** 标识符,用于判定跟随者 */
readonly identifier: string;
/** 目标横坐标,移动时有效 */
targetX: number;
/** 目标纵坐标,移动时有效 */
targetY: number;
/** 当前的移动朝向 */
direction: FaceDirection;
/** 下一个跟随者的移动方向 */
nextDirection: FaceDirection;
/** 当前是否正在移动 */
moving: boolean;
/** 当前是否正在动画,移动跟动画要分开,有的操作比如跳跃就是在移动中但是没动画 */
animating: boolean;
/** 帧动画间隔 */
animateInterval: number;
/** 当一次动画时刻 */
lastAnimateTime: number;
/** 当前的动画帧数 */
animateFrame: number;
/** 移动的 `Promise`,移动完成时兑现,如果停止,则一直是兑现状态 */
promise: Promise<void>;
/** 勇士移动的动画方向 */
animateDirection: HeroAnimateDirection;
}
export class MapHeroRenderer implements IMapHeroRenderer {
private static readonly splitter: ITextureSplitter<number> =
new TextureRowSplitter();
/** 勇士钩子 */
readonly controller: IHookController<IHeroStateHooks>;
/** 勇士每个朝向的贴图对象 */
readonly textureMap: Map<FaceDirection, IMaterialFramedData> = new Map();
/** 勇士渲染实体,与 `entities[0]` 同引用 */
readonly heroEntity: HeroRenderEntity;
/**
* 0
*
*/
readonly entities: HeroRenderEntity[] = [];
/** 每帧执行的帧动画对象 */
readonly ticker: IMapRendererTicker;
constructor(
readonly renderer: IMapRenderer,
readonly layer: IMapLayer,
readonly hero: IHeroState
) {
this.controller = hero.addHook(new MapHeroHook(this));
this.controller.load();
const moving = this.addHeroMoving(renderer, layer, hero);
const heroEntity: HeroRenderEntity = {
block: moving,
identifier: '',
targetX: hero.x,
targetY: hero.y,
direction: hero.direction,
nextDirection: FaceDirection.Unknown,
moving: false,
animating: false,
animateInterval: 0,
lastAnimateTime: 0,
animateFrame: 0,
promise: Promise.resolve(),
animateDirection: HeroAnimateDirection.Forward
};
this.heroEntity = heroEntity;
this.entities.push(heroEntity);
this.ticker = renderer.requestTicker(time => this.tick(time));
}
/**
*
* @param renderer
* @param layer
* @param hero
*/
private addHeroMoving(
renderer: IMapRenderer,
layer: IMapLayer,
hero: IHeroState
) {
if (isNil(hero.image)) {
logger.warn(88);
return renderer.addMovingBlock(layer, 0, hero.x, hero.y);
}
const image = this.renderer.manager.getImageByAlias(hero.image);
if (!image) {
logger.warn(89, hero.image);
return renderer.addMovingBlock(layer, 0, hero.x, hero.y);
}
this.updateHeroTexture(image);
const tex = this.textureMap.get(degradeFace(hero.direction));
if (!tex) {
return renderer.addMovingBlock(layer, 0, hero.x, hero.y);
}
const block = renderer.addMovingBlock(layer, tex, hero.x, hero.y);
block.useSpecifiedFrame(0);
return block;
}
/**
*
* @param image 使
*/
private updateHeroTexture(image: ITexture) {
const textures = [
...image.split(MapHeroRenderer.splitter, image.height / 4)
];
if (textures.length !== 4) {
logger.warn(90, hero.image);
return;
}
const faceList = [
FaceDirection.Down,
FaceDirection.Left,
FaceDirection.Right,
FaceDirection.Up
];
faceList.forEach((v, i) => {
const dirImage = textures[i];
const data: IMaterialFramedData = {
offset: dirImage.width / 4,
texture: dirImage,
cls: BlockCls.Unknown,
frames: 4,
defaultFrame: 0
};
this.textureMap.set(v, data);
});
}
private tick(time: number) {
this.entities.forEach(v => {
if (v.animating) {
const dt = time - v.lastAnimateTime;
if (dt > v.animateInterval) {
if (v.animateDirection === HeroAnimateDirection.Forward) {
v.animateFrame++;
} else {
v.animateFrame--;
if (v.animateFrame < 0) {
// 小于 0则加上帧数的整数倍就写个 10000 倍吧
v.animateFrame += v.block.texture.frames * 10000;
}
}
v.lastAnimateTime = time;
v.block.useSpecifiedFrame(v.animateFrame);
}
} else {
if (v.animateFrame !== 0) {
v.animateFrame = 0;
v.block.useSpecifiedFrame(0);
}
}
});
}
setImage(image: ITexture): void {
this.updateHeroTexture(image);
const tex = this.textureMap.get(degradeFace(this.hero.direction));
if (!tex) return;
this.heroEntity.block.setTexture(tex);
}
setAlpha(alpha: number): void {
this.heroEntity.block.setAlpha(alpha);
}
setPosition(x: number, y: number): void {
this.entities.forEach(v => {
v.block.setPos(x, y);
v.nextDirection = FaceDirection.Unknown;
});
}
/**
*
* @param entity
* @param direction
* @param time
*/
private moveEntity(
entity: HeroRenderEntity,
direction: FaceDirection,
time: number
) {
const { x: dx, y: dy } = getFaceMovement(direction);
if (dx === 0 && dy === 0) return;
const block = entity.block;
const tx = block.x + dx;
const ty = block.y + dy;
const nextTile = state.roleFace.getFaceOf(block.tile, direction);
const nextTex = this.renderer.manager.getIfBigImage(
nextTile?.identifier ?? block.tile
);
entity.animateInterval = time;
entity.promise = entity.promise.then(async () => {
entity.moving = true;
entity.animating = true;
entity.direction = direction;
if (nextTex) block.setTexture(nextTex);
await block.lineTo(tx, ty, time);
entity.nextDirection = entity.direction;
});
}
/**
* 线
* @param dx
* @param dy
*/
private generateJumpFn(dx: number, dy: number): TimingFn<2> {
const distance = Math.hypot(dx, dy);
const peak = 3 + distance;
return (progress: number) => {
const x = dx * progress;
const y = progress * dy + (progress ** 2 - progress) * peak;
return [x, y];
};
}
/**
* `moveEntity`
* @param entity
* @param x
* @param y
* @param time
*/
private jumpEntity(
entity: HeroRenderEntity,
x: number,
y: number,
time: number
) {
const block = entity.block;
entity.promise = entity.promise.then(async () => {
const dx = block.x - x;
const dy = block.y - y;
const fn = this.generateJumpFn(dx, dy);
entity.moving = true;
entity.animating = false;
entity.animateFrame = 0;
await block.moveRelative(fn, time);
});
}
startMove(): void {
this.heroEntity.moving = true;
this.heroEntity.animating = true;
this.heroEntity.lastAnimateTime = this.ticker.timestamp;
}
private endEntityMoving(entity: HeroRenderEntity) {
entity.moving = false;
entity.animating = false;
entity.animateFrame = 0;
entity.block.useSpecifiedFrame(0);
}
async waitMoveEnd(): Promise<void> {
await Promise.all(this.entities.map(v => v.promise));
this.entities.forEach(v => this.endEntityMoving(v));
}
stopMove(): void {
this.entities.forEach(v => {
v.block.endMoving();
this.endEntityMoving(v);
});
}
async move(direction: FaceDirection, time: number): Promise<void> {
this.moveEntity(this.heroEntity, direction, time);
for (let i = 1; i < this.entities.length; i++) {
const last = this.entities[i - 1];
this.moveEntity(this.entities[i], last.nextDirection, time);
}
await Promise.all(this.entities.map(v => v.promise));
}
async jumpTo(
x: number,
y: number,
time: number,
waitFollower: boolean
): Promise<void> {
// 首先要把所有的跟随者移动到勇士所在位置
for (let i = 1; i < this.entities.length; i++) {
// 对于每一个跟随者,需要向前遍历每一个跟随者,然后朝向移动,这样就可以聚集在一起了
const now = this.entities[i];
for (let j = i - 1; j >= 0; j--) {
const last = this.entities[j];
this.moveEntity(now, last.nextDirection, DEFAULT_TIME);
}
}
this.entities.forEach(v => {
this.jumpEntity(v, x, y, time);
});
if (waitFollower) {
await Promise.all(this.entities.map(v => v.promise));
} else {
return this.heroEntity.promise;
}
}
addFollower(image: number, id: string): void {
const last = this.entities[this.entities.length - 1];
if (last.moving) {
logger.warn(92);
return;
}
const nowFace = degradeFace(last.nextDirection, FaceDirection.Down);
const faced = state.roleFace.getFaceOf(image, nowFace);
const tex = this.renderer.manager.getIfBigImage(faced?.face ?? image);
if (!tex) {
logger.warn(91, image.toString());
return;
}
const { x: dxn, y: dyn } = getFaceMovement(last.nextDirection);
const { x: dx, y: dy } = getFaceMovement(last.direction);
const x = last.block.x - dxn;
const y = last.block.y - dyn;
const moving = this.renderer.addMovingBlock(this.layer, tex, x, y);
const entity: HeroRenderEntity = {
block: moving,
identifier: id,
targetX: last.targetX - dx,
targetY: last.targetY - dy,
direction: nowFace,
nextDirection: FaceDirection.Unknown,
moving: false,
animating: false,
animateInterval: 0,
lastAnimateTime: 0,
animateFrame: 0,
promise: Promise.resolve(),
animateDirection: HeroAnimateDirection.Forward
};
moving.useSpecifiedFrame(0);
this.entities.push(entity);
}
async removeFollower(follower: string, animate: boolean): Promise<void> {
const index = this.entities.findIndex(v => v.identifier === follower);
if (index === -1) return;
if (this.entities[index].moving) {
logger.warn(93);
return;
}
if (index === this.entities.length - 1) {
this.entities[index].block.destroy();
this.entities.splice(index, 1);
return;
}
// 展示动画
if (animate) {
for (let i = index + 1; i < this.entities.length; i++) {
const last = this.entities[i - 1];
const moving = this.entities[i];
this.moveEntity(moving, last.nextDirection, DEFAULT_TIME);
}
this.entities[index].block.destroy();
this.entities.splice(index, 1);
await Promise.all(this.entities.map(v => v.promise));
return;
}
// 不展示动画
for (let i = index + 1; i < this.entities.length; i++) {
const last = this.entities[i - 1];
const moving = this.entities[i];
moving.block.setPos(last.block.x, last.block.y);
const nextFace = state.roleFace.getFaceOf(
moving.block.tile,
last.nextDirection
);
if (!nextFace) continue;
const tile = this.renderer.manager.getIfBigImage(
nextFace.identifier
);
if (!tile) continue;
moving.block.setTexture(tile);
moving.direction = last.nextDirection;
moving.nextDirection = moving.direction;
}
this.entities[index].block.destroy();
this.entities.splice(index, 1);
}
removeAllFollowers(): void {
for (let i = 1; i < this.entities.length; i++) {
this.entities[i].block.destroy();
}
this.entities.length = 1;
}
setFollowerAlpha(identifier: string, alpha: number): void {
const follower = this.entities.find(v => v.identifier === identifier);
if (!follower) return;
follower.block.setAlpha(alpha);
}
setHeroAnimateDirection(direction: HeroAnimateDirection): void {
this.heroEntity.animateDirection = direction;
}
turn(direction?: FaceDirection): void {
const next = isNil(direction)
? nextFaceDirection(this.heroEntity.direction)
: direction;
const tex = this.textureMap.get(next);
if (tex) {
this.heroEntity.block.setTexture(tex);
this.heroEntity.direction = next;
}
}
destroy() {
this.controller.unload();
}
}
class MapHeroHook implements Partial<IHeroStateHooks> {
constructor(readonly hero: MapHeroRenderer) {}
onSetImage(image: ImageIds): void {
const texture = this.hero.renderer.manager.getImageByAlias(image);
if (!texture) {
logger.warn(89, hero.image);
return;
}
this.hero.setImage(texture);
}
onSetPosition(x: number, y: number): void {
this.hero.setPosition(x, y);
}
onTurnHero(direction: FaceDirection): void {
this.hero.turn(direction);
}
onStartMove(): void {
this.hero.startMove();
}
onMoveHero(direction: FaceDirection, time: number): Promise<void> {
return this.hero.move(direction, time);
}
onEndMove(): Promise<void> {
return this.hero.waitMoveEnd();
}
onJumpHero(
x: number,
y: number,
time: number,
waitFollower: boolean
): Promise<void> {
return this.hero.jumpTo(x, y, time, waitFollower);
}
onSetAlpha(alpha: number): void {
this.hero.setAlpha(alpha);
}
onAddFollower(follower: number, identifier: string): void {
this.hero.addFollower(follower, identifier);
}
onRemoveFollower(identifier: string, animate: boolean): void {
this.hero.removeFollower(identifier, animate);
}
onRemoveAllFollowers(): void {
this.hero.removeAllFollowers();
}
onSetFollowerAlpha(identifier: string, alpha: number): void {
this.hero.setFollowerAlpha(identifier, alpha);
}
}

View File

@ -0,0 +1,3 @@
export * from './hero';
export * from './manager';
export * from './types';

View File

@ -0,0 +1,60 @@
import { IHeroState, IMapLayer } from '@user/data-state';
import {
IMapDoorRenderer,
IMapExtensionManager,
IMapHeroRenderer
} from './types';
import { IMapRenderer } from '../types';
import { MapHeroRenderer } from './hero';
import { logger } from '@motajs/common';
import { MapDoorRenderer } from './door';
export class MapExtensionManager implements IMapExtensionManager {
/** 勇士状态至勇士渲染器的映射 */
readonly heroMap: Map<IHeroState, IMapHeroRenderer> = new Map();
/** 地图图层到门渲染器的映射 */
readonly doorMap: Map<IMapLayer, IMapDoorRenderer> = new Map();
constructor(readonly renderer: IMapRenderer) {}
addHero(state: IHeroState, layer: IMapLayer): IMapHeroRenderer | null {
if (this.heroMap.has(state)) {
logger.error(45, 'hero renderer');
return null;
}
const heroRenderer = new MapHeroRenderer(this.renderer, layer, state);
this.heroMap.set(state, heroRenderer);
return heroRenderer;
}
removeHero(state: IHeroState): void {
const renderer = this.heroMap.get(state);
if (!renderer) return;
renderer.destroy();
this.heroMap.delete(state);
}
addDoor(layer: IMapLayer): IMapDoorRenderer | null {
if (this.doorMap.has(layer)) {
logger.error(45, 'door renderer');
return null;
}
const doorRenderer = new MapDoorRenderer(this.renderer, layer);
this.doorMap.set(layer, doorRenderer);
return doorRenderer;
}
removeDoor(layer: IMapLayer): void {
const renderer = this.doorMap.get(layer);
if (!renderer) return;
renderer.destroy();
this.doorMap.delete(layer);
}
destroy(): void {
this.heroMap.forEach(v => void v.destroy());
this.doorMap.forEach(v => void v.destroy());
this.heroMap.clear();
this.doorMap.clear();
}
}

View File

@ -0,0 +1,63 @@
import { IMapRenderer } from '../types';
import { IMapTextArea, IMapTextRenderable, IOnMapTextRenderer } from './types';
export class OnMapTextRenderer implements IOnMapTextRenderer {
/** 画布元素 */
readonly canvas: HTMLCanvasElement;
/** 画布 Canvas2D 上下文 */
readonly ctx: CanvasRenderingContext2D;
/** 图块索引到图块文本对象的映射 */
readonly areaMap: Map<number, MapTextArea> = new Map();
private dirty: boolean = false;
constructor(readonly renderer: IMapRenderer) {
this.canvas = document.createElement('canvas');
this.ctx = this.canvas.getContext('2d')!;
}
render(): HTMLCanvasElement {
return this.canvas;
}
requireBlockArea(x: number, y: number): Readonly<IMapTextArea> {
const index = y * this.renderer.mapWidth + x;
const exist = this.areaMap.get(index);
if (exist) return exist;
const area = new MapTextArea(this, x, y);
this.areaMap.set(index, area);
return area;
}
needUpdate(): boolean {
return this.dirty;
}
clear(): void {}
destroy(): void {}
}
class MapTextArea implements IMapTextArea {
index: number;
constructor(
readonly renderer: OnMapTextRenderer,
public mapX: number,
public mapY: number
) {
this.index = mapY * renderer.renderer.mapWidth + mapX;
}
addTextRenderable(renderable: IMapTextRenderable): void {
throw new Error('Method not implemented.');
}
removeTextRenderable(renderable: IMapTextRenderable): void {
throw new Error('Method not implemented.');
}
clear(): void {
throw new Error('Method not implemented.');
}
}

View File

@ -0,0 +1,246 @@
import { ITexture } from '@motajs/render-assets';
import {
FaceDirection,
HeroAnimateDirection,
IHeroState,
IMapLayer
} from '@user/data-state';
import { Font } from '@motajs/render-style';
export interface IMapExtensionManager {
/**
*
* @param state
* @param layer
*/
addHero(state: IHeroState, layer: IMapLayer): IMapHeroRenderer | null;
/**
*
* @param state
*/
removeHero(state: IHeroState): void;
/**
*
*/
addDoor(layer: IMapLayer): IMapDoorRenderer | null;
/**
*
*/
removeDoor(layer: IMapLayer): void;
/**
*
*/
destroy(): void;
}
export interface IMapHeroRenderer {
/**
*
* @param image 使
*/
setImage(image: ITexture): void;
/**
*
* @param image
* @param id id
*/
addFollower(image: number, id: string): void;
/**
*
* @param follower id
* @param animate `true` 使
*/
removeFollower(follower: string, animate: boolean): Promise<void>;
/**
*
*/
removeAllFollowers(): void;
/**
*
*/
setPosition(x: number, y: number): void;
/**
*
*/
startMove(): void;
/**
*
* @param waitFollower
*/
waitMoveEnd(waitFollower: boolean): Promise<void>;
/**
*
*/
stopMove(): void;
/**
*
* @param direction
*/
move(direction: FaceDirection, time: number): Promise<void>;
/**
*
* @param x
* @param y
* @param time
* @param waitFollower
*/
jumpTo(
x: number,
y: number,
time: number,
waitFollower: boolean
): Promise<void>;
/**
*
* @param alpha
*/
setAlpha(alpha: number): void;
/**
*
* @param identifier
* @param alpha
*/
setFollowerAlpha(identifier: string, alpha: number): void;
/**
* 退使使
* @param direction
*/
setHeroAnimateDirection(direction: HeroAnimateDirection): void;
/**
*
* @param direction
*/
turn(direction?: FaceDirection): void;
/**
*
*/
destroy(): void;
}
export interface IMapDoorRenderer {
/**
*
* @param x
* @param y
*/
openDoor(x: number, y: number): Promise<void>;
/**
*
* @param num
* @param x
* @param y
*/
closeDoor(num: number, x: number, y: number): Promise<void>;
/**
*
* @param interval
*/
setAnimateInterval(interval: number): void;
/**
*
*/
destroy(): void;
}
export interface IMapTextRenderable {
/** 文本内容 */
readonly text: string;
/** 文本字体 */
readonly font: Font;
/** 文本填充样式 */
readonly fillStyle: CanvasStyle;
/** 文本描边样式 */
readonly strokeStyle: CanvasStyle;
/** 文本横坐标,注意 {@link IMapTextArea.addTextRenderable} 的相对关系 */
readonly px: number;
/** 文本纵坐标,注意 {@link IMapTextArea.addTextRenderable} 的相对关系 */
readonly py: number;
/** 文本横向对齐方式 */
readonly textAlign: CanvasTextAlign;
/** 文本纵向对齐方式 */
readonly textBaseline: CanvasTextBaseline;
}
export interface IMapTextRequested {
/**
*
* @param blocks
*/
requestBlocks(blocks: IMapTextArea[]): void;
}
export interface IMapTextArea {
/** 图块在地图上的索引 */
index: number;
/** 图块横坐标 */
mapX: number;
/** 图块纵坐标 */
mapY: number;
/**
*
* @param renderable
*/
addTextRenderable(renderable: IMapTextRenderable): void;
/**
*
* @param renderable
*/
removeTextRenderable(renderable: IMapTextRenderable): void;
/**
*
*/
clear(): void;
}
export interface IOnMapTextRenderer {
/**
*
*/
render(): HTMLCanvasElement;
/**
*
* @param x
* @param y
*/
requireBlockArea(x: number, y: number): Readonly<IMapTextArea>;
/**
*
*/
needUpdate(): boolean;
/**
*
*/
clear(): void;
/**
*
*/
destroy(): void;
}

View File

@ -0,0 +1,11 @@
export * from './extension';
export * from './block';
export * from './constant';
export * from './element';
export * from './moving';
export * from './renderer';
export * from './status';
export * from './types';
export * from './vertex';
export * from './viewport';

View File

@ -0,0 +1,268 @@
import { linear, TimingFn } from 'mutate-animate';
import { IMapRenderer, IMapVertexGenerator, IMovingBlock } from './types';
import { IMaterialFramedData, IMaterialManager } from '@user/client-base';
import { logger } from '@motajs/common';
import { IMapLayer } from '@user/data-state';
import { DynamicBlockStatus } from './status';
export interface IMovingRenderer {
/** 素材管理器 */
readonly manager: IMaterialManager;
/** 顶点数组生成器 */
readonly vertex: IMapVertexGenerator;
/**
*
*/
getTimestamp(): number;
/**
*
* @param block
*/
deleteMoving(block: IMovingBlock): void;
}
export class MovingBlock extends DynamicBlockStatus implements IMovingBlock {
readonly tile: number;
readonly renderer: IMovingRenderer;
readonly layer: IMapLayer;
texture: IMaterialFramedData;
index: number;
x: number = 0;
y: number = 0;
/** 当前动画开始的时刻 */
private startTime: number = 0;
/** 是否是直线动画 */
private line: boolean = false;
/** 是否是相对模式 */
private relative: boolean = false;
/** 目标横坐标 */
private targetX: number = 0;
/** 目标纵坐标 */
private targetY: number = 0;
/** 直线移动的横坐标增量 */
private dx: number = 0;
/** 直线移动的纵坐标增量 */
private dy: number = 0;
/** 动画时长 */
private time: number = 0;
/** 速率曲线 */
private timing: TimingFn = () => 0;
/** 移动轨迹曲线 */
private curve: TimingFn<2> = () => [0, 0];
/** 动画开始时横坐标 */
private startX: number = 0;
/** 动画开始时纵坐标 */
private startY: number = 0;
/** 当前动画是否已经结束 */
private end: boolean = true;
/** 是否通过 `setPos` 设置了位置 */
private posUpdated: boolean = false;
/** 兑现函数 */
private promiseFunc: () => void = () => {};
constructor(
renderer: IMovingRenderer & IMapRenderer,
index: number,
layer: IMapLayer,
block: number | IMaterialFramedData
) {
super(layer, renderer.vertex, index);
this.renderer = renderer;
this.index = index;
this.layer = layer;
if (typeof block === 'number') {
this.texture = renderer.manager.getTile(block)!;
this.tile = block;
} else {
if (!renderer.manager.assetContainsTexture(block.texture)) {
logger.error(34);
}
if (renderer.getOffsetIndex(block.offset) === -1) {
logger.error(41);
}
this.texture = block;
this.tile = -1;
}
}
setPos(x: number, y: number): void {
if (!this.end) return;
this.x = x;
this.y = y;
this.posUpdated = true;
}
setTexture(texture: IMaterialFramedData): void {
if (texture === this.texture) return;
this.texture = texture;
this.renderer.vertex.updateMoving(this, true);
}
lineTo(
x: number,
y: number,
time: number,
timing?: TimingFn
): Promise<this> {
if (!this.end) return Promise.resolve(this);
this.startX = this.x;
this.startY = this.y;
this.targetX = x;
this.targetY = y;
this.dx = x - this.x;
this.dy = y - this.y;
this.time = time;
this.relative = false;
this.startTime = this.renderer.getTimestamp();
if (time === 0) {
this.x = x;
this.y = y;
this.end = true;
return Promise.resolve(this);
}
this.end = false;
this.timing = timing ?? linear();
this.line = true;
return new Promise(res => {
this.promiseFunc = () => res(this);
});
}
moveAs(curve: TimingFn<2>, time: number, timing?: TimingFn): Promise<this> {
if (!this.end) return Promise.resolve(this);
this.time = time;
this.line = false;
this.relative = false;
this.startX = this.x;
this.startY = this.y;
this.startTime = this.renderer.getTimestamp();
if (time === 0) {
const [tx, ty] = curve(1);
this.x = tx;
this.y = ty;
this.end = true;
return Promise.resolve(this);
}
this.end = false;
this.timing = timing ?? linear();
this.curve = curve;
return new Promise(res => {
this.promiseFunc = () => res(this);
});
}
moveRelative(
curve: TimingFn<2>,
time: number,
timing?: TimingFn
): Promise<this> {
if (!this.end) return Promise.resolve(this);
this.time = time;
this.line = false;
this.relative = true;
this.startX = this.x;
this.startY = this.y;
this.startTime = this.renderer.getTimestamp();
if (time === 0) {
const [tx, ty] = curve(1);
this.x = tx + this.startX;
this.y = ty + this.startY;
this.end = true;
return Promise.resolve(this);
}
this.end = false;
this.timing = timing ?? linear();
this.curve = curve;
return new Promise(res => {
this.promiseFunc = () => res(this);
});
}
stepMoving(timestamp: number): boolean {
if (this.end) {
if (this.posUpdated) {
this.posUpdated = false;
return true;
}
return false;
}
const dt = timestamp - this.startTime;
if (this.line) {
if (dt > this.time) {
this.x = this.targetX;
this.y = this.targetY;
this.end = true;
this.promiseFunc();
return true;
} else {
const timeProgress = dt / this.time;
const progress = this.timing(timeProgress);
this.x = this.startX + progress * this.dx;
this.y = this.startY + progress * this.dy;
}
} else {
if (dt > this.time) {
const [tx, ty] = this.curve(1);
if (this.relative) {
this.x = tx + this.startX;
this.y = ty + this.startY;
} else {
this.x = tx;
this.y = ty;
}
this.end = true;
this.promiseFunc();
return true;
} else {
const timeProgress = dt / this.time;
const progress = this.timing(timeProgress);
const [tx, ty] = this.curve(progress);
if (this.relative) {
this.x = tx + this.startX;
this.y = ty + this.startY;
} else {
this.x = tx;
this.y = ty;
}
}
}
return true;
}
endMoving(): void {
this.end = true;
if (this.line) {
this.x = this.targetX;
this.y = this.targetY;
} else {
const [x, y] = this.curve(1);
if (this.relative) {
this.x = x + this.startX;
this.y = y + this.startY;
} else {
this.x = x;
this.y = y;
}
}
this.promiseFunc();
this.posUpdated = true;
}
useDefaultFrame(): void {
if (!this.renderer.manager.getTile(this.tile)) return;
const defaultFrame = this.renderer.manager.getDefaultFrame(this.tile);
this.useSpecifiedFrame(defaultFrame);
}
destroy(): void {
this.renderer.deleteMoving(this);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,12 @@
#version 300 es
precision highp float;
precision highp sampler2DArray;
in vec3 v_texCoord;
out vec4 outColor;
uniform sampler2DArray u_sampler;
void main() {
outColor = texture(u_sampler, v_texCoord);
}

View File

@ -0,0 +1,16 @@
#version 300 es
precision highp float;
layout(location = 0) in vec2 a_position;
layout(location = 1) in vec2 a_texCoord;
out vec3 v_texCoord;
uniform float u_nowFrame;
uniform mat3 u_transform;
void main() {
vec3 transformed = u_transform * vec3(a_position, 1.0);
v_texCoord = vec3(a_texCoord, u_nowFrame);
gl_Position = vec4(transformed.xy, 0.95, 1.0);
}

View File

@ -0,0 +1,17 @@
#version 300 es
precision highp float;
precision highp sampler2DArray;
in vec4 v_texCoord;
out vec4 outColor;
uniform sampler2DArray u_sampler;
void main() {
vec4 texColor = texture(u_sampler, v_texCoord.xyz);
float alpha = texColor.a * v_texCoord.a;
// todo: 透明像素应该如何解决??
if (alpha < 0.1) discard;
outColor = vec4(texColor.rgb, alpha);
}

View File

@ -0,0 +1,36 @@
#version 300 es
precision highp float;
precision mediump int;
// 顶点坐标
layout(location = 0) in vec4 a_position;
// 实例化数据
// 图块坐标
layout(location = 1) in vec4 a_tilePos;
// 贴图坐标
layout(location = 2) in vec4 a_texCoord;
// x: 纵深y: 不透明度
layout(location = 3) in vec4 a_tileData;
// x: 当前帧数,负数表示使用 u_nowFramey: 最大帧数z: 偏移池索引w: 纹理数组索引
layout(location = 4) in vec4 a_texData;
// x,y,z: 纹理坐标w: 不透明度
out vec4 v_texCoord;
uniform float u_offsetPool[$1];
uniform float u_nowFrame;
uniform mat3 u_transform;
void main() {
// 坐标
vec2 pos = a_position.xy * a_tilePos.zw + a_tilePos.xy;
vec2 texCoord = a_position.zw * a_texCoord.zw + a_texCoord.xy;
// 偏移量
float offset = mod(a_texData.x < 0.0 ? u_nowFrame : a_texData.x, a_texData.y);
int offsetIndex = int(a_texData.z);
// 贴图偏移
texCoord.x += u_offsetPool[offsetIndex] * offset;
v_texCoord = vec4(texCoord.xy, a_texData.w, a_tileData.y);
vec3 transformed = u_transform * vec3(pos.xy, 1.0);
gl_Position = vec4(transformed.xy, a_tileData.x, 1.0);
}

View File

@ -0,0 +1,70 @@
import { IMapLayer } from '@user/data-state';
import { IBlockStatus, IMapVertexStatus } from './types';
export class StaticBlockStatus implements IBlockStatus {
/**
* @param layer
* @param vertex
* @param x
* @param y
*/
constructor(
readonly layer: IMapLayer,
readonly vertex: IMapVertexStatus,
readonly x: number,
readonly y: number
) {}
setAlpha(alpha: number): void {
this.vertex.setStaticAlpha(this.layer, alpha, this.x, this.y);
}
getAlpha(): number {
return this.vertex.getStaticAlpha(this.layer, this.x, this.y);
}
useGlobalFrame(): void {
this.vertex.setStaticFrame(this.layer, this.x, this.y, -1);
}
useSpecifiedFrame(frame: number): void {
this.vertex.setStaticFrame(this.layer, this.x, this.y, frame);
}
getFrame(): number {
return this.vertex.getStaticFrame(this.layer, this.x, this.y);
}
}
export class DynamicBlockStatus implements IBlockStatus {
/**
* @param layer
* @param vertex
* @param index
*/
constructor(
readonly layer: IMapLayer,
readonly vertex: IMapVertexStatus,
readonly index: number
) {}
setAlpha(alpha: number): void {
this.vertex.setDynamicAlpha(this.index, alpha);
}
getAlpha(): number {
return this.vertex.getDynamicAlpha(this.index);
}
useGlobalFrame(): void {
this.vertex.setDynamicFrame(this.index, -1);
}
useSpecifiedFrame(frame: number): void {
this.vertex.setDynamicFrame(this.index, frame);
}
getFrame(): number {
return this.vertex.getDynamicFrame(this.index);
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,138 @@
import { Transform } from '@motajs/render-core';
import {
IBlockData,
IMapRenderArea,
IMapRenderData,
IMapRenderer,
IMapVertexBlock,
IMapVertexGenerator,
IMapViewportController
} from './types';
import { clamp } from 'lodash-es';
export class MapViewport implements IMapViewportController {
transform: Transform = new Transform();
/** 顶点生成器 */
readonly vertex: IMapVertexGenerator;
constructor(readonly renderer: IMapRenderer) {
this.vertex = renderer.vertex;
}
private pushBlock(
list: IMapRenderArea[],
start: IBlockData<IMapVertexBlock>,
end: IBlockData<IMapVertexBlock>
) {
const startIndex = start.data.startIndex;
const endIndex = end.data.endIndex;
list.push({
startIndex,
endIndex,
count: endIndex - startIndex
});
}
private checkDynamic(
list: IMapRenderArea[],
dynamicStart: number,
dynamicCount: number
) {
const last = list[list.length - 1];
if (!last || last.endIndex < dynamicStart) {
list.push({
startIndex: dynamicStart,
endIndex: dynamicStart + dynamicCount,
count: dynamicCount
});
} else {
last.endIndex = dynamicStart + dynamicCount;
last.count += dynamicCount;
}
}
getRenderArea(): IMapRenderData {
const { cellWidth, cellHeight, renderWidth, renderHeight } =
this.renderer;
const { blockWidth, blockHeight, width, height } = this.vertex.block;
// 其实只需要算左上角和右下角就行了
const [left, top] = this.transform.untransformed(-1, -1);
const [right, bottom] = this.transform.untransformed(1, 1);
const cl = (left * renderWidth) / cellWidth;
const ct = (top * renderHeight) / cellHeight;
const cr = (right * renderWidth) / cellWidth;
const cb = (bottom * renderHeight) / cellHeight;
const blockLeft = clamp(Math.floor(cl / blockWidth), 0, width - 1);
const blockRight = clamp(Math.floor(cr / blockWidth), 0, width - 1);
const blockTop = clamp(Math.floor(ct / blockHeight), 0, height - 1);
const blockBottom = clamp(Math.floor(cb / blockHeight), 0, height - 1);
const renderArea: IMapRenderArea[] = [];
const updateArea: IMapRenderArea[] = [];
const blockList: IBlockData<IMapVertexBlock>[] = [];
// 内层横向外层纵向的话,索引在换行之前都是连续的,方便整合
for (let ny = blockTop; ny <= blockBottom; ny++) {
for (let nx = blockLeft; nx <= blockRight; nx++) {
const block = this.vertex.block.getBlockByLoc(nx, ny)!;
blockList.push(block);
}
}
if (blockList.length > 0) {
if (blockList.length === 1) {
const block = blockList[0];
if (block.data.renderDirty) {
this.pushBlock(updateArea, block, block);
}
this.pushBlock(renderArea, block, block);
} else {
// 更新区域
let updateStart: IBlockData<IMapVertexBlock> = blockList[0];
let updateEnd: IBlockData<IMapVertexBlock> = blockList[0];
let renderStart: IBlockData<IMapVertexBlock> = blockList[0];
let renderEnd: IBlockData<IMapVertexBlock> = blockList[0];
for (let i = 1; i < blockList.length; i++) {
const block = blockList[i];
const { renderDirty } = block.data;
// 连续则合并
// 渲染区域
if (block.index === renderEnd.index + 1) {
renderEnd = block;
} else {
this.pushBlock(renderArea, renderStart, renderEnd);
renderStart = block;
renderEnd = block;
}
// 缓冲区更新区域
if (renderDirty && block.index === updateEnd.index + 1) {
updateEnd = block;
} else {
this.pushBlock(updateArea, updateStart, updateEnd);
updateStart = block;
updateEnd = block;
}
}
this.pushBlock(updateArea, updateStart, updateEnd);
this.pushBlock(renderArea, renderStart, renderEnd);
}
}
const dynamicStart = this.vertex.dynamicStart;
const dynamicCount = this.vertex.dynamicCount;
this.checkDynamic(renderArea, dynamicStart, dynamicCount);
if (this.vertex.dynamicRenderDirty) {
this.checkDynamic(updateArea, dynamicStart, dynamicCount);
}
return {
render: renderArea,
dirty: updateArea,
blockList: blockList
};
}
bindTransform(transform: Transform): void {
this.transform = transform;
}
}

View File

@ -5,6 +5,10 @@ import { Font } from '@motajs/render-style';
//#region 地图 //#region 地图
/** 每个格子的默认宽度,现阶段用处不大 */
export const CELL_WIDTH = 32;
/** 每个格子的默认高度,现阶段用处不大 */
export const CELL_HEIGHT = 32;
/** 每个格子的宽高 */ /** 每个格子的宽高 */
export const CELL_SIZE = 32; export const CELL_SIZE = 32;
/** 地图格子宽度,此处仅影响画面,不影响游戏内逻辑,游戏内逻辑地图大小请在 core.js 中修改 */ /** 地图格子宽度,此处仅影响画面,不影响游戏内逻辑,游戏内逻辑地图大小请在 core.js 中修改 */
@ -19,6 +23,18 @@ export const MAP_HEIGHT = CELL_SIZE * MAP_BLOCK_HEIGHT;
export const HALF_MAP_WIDTH = MAP_WIDTH / 2; export const HALF_MAP_WIDTH = MAP_WIDTH / 2;
/** 地图高度的一半 */ /** 地图高度的一半 */
export const HALF_MAP_HEIGHT = MAP_HEIGHT / 2; export const HALF_MAP_HEIGHT = MAP_HEIGHT / 2;
/**
*
*
*/
export const DYNAMIC_RESERVE = 16;
/**
*
*
*/
export const MOVING_TOLERANCE = 60;
/** 开关门动画的动画时长 */
export const DOOR_ANIMATE_INTERVAL = 50;
//#region 状态栏 //#region 状态栏

View File

@ -8,6 +8,7 @@ export * from './controller';
export * from './main'; export * from './main';
export * from './save'; export * from './save';
export * from './settings'; export * from './settings';
export * from './statistics';
export * from './statusBar'; export * from './statusBar';
export * from './toolbar'; export * from './toolbar';
export * from './viewmap'; export * from './viewmap';

View File

@ -241,10 +241,10 @@ export const Save = defineComponent<SaveProps, SaveEmits, keyof SaveEmits>(
}; };
onMounted(() => { onMounted(() => {
const startIndex = getPosIndex(core.saves.saveIndex - 1); const startIndex = getPosIndex(core.saves.saveIndex);
selected.value = startIndex; selected.value = startIndex - 1;
pageRef.value?.changePage( pageRef.value?.changePage(
Math.floor((core.saves.saveIndex - 1) / (grid.value.count - 1)) Math.floor(core.saves.saveIndex / (grid.value.count - 1))
); );
updateDataList(now.value); updateDataList(now.value);
}); });
@ -380,10 +380,10 @@ export const Save = defineComponent<SaveProps, SaveEmits, keyof SaveEmits>(
'@save_right', '@save_right',
() => { () => {
const count = grid.value.count; const count = grid.value.count;
if (selected.value < count - 1) { if (selected.value < count) {
selected.value++; selected.value++;
} else { } else {
selected.value = 1; selected.value = 0;
pageRef.value?.movePage(1); pageRef.value?.movePage(1);
} }
}, },

View File

@ -20,6 +20,7 @@ import { generateKeyboardEvent } from '@motajs/system-action';
import { getVitualKeyOnce } from '@motajs/legacy-ui'; import { getVitualKeyOnce } from '@motajs/legacy-ui';
import { getAllSavesData, getSaveData, syncFromServer } from '../utils'; import { getAllSavesData, getSaveData, syncFromServer } from '../utils';
import { getInput } from '../components'; import { getInput } from '../components';
import { openStatistics } from './statistics';
import { saveWithExist } from './save'; import { saveWithExist } from './save';
import { compressToBase64 } from 'lz-string'; import { compressToBase64 } from 'lz-string';
import { ViewMapUI } from './viewmap'; import { ViewMapUI } from './viewmap';
@ -126,7 +127,7 @@ export const MainSettings = defineComponent<MainSettingsProps>(props => {
choices={choices} choices={choices}
width={POP_BOX_WIDTH} width={POP_BOX_WIDTH}
onChoose={choose} onChoose={choose}
maxHeight={MAIN_HEIGHT - 32} maxHeight={MAIN_HEIGHT - 64}
interval={8} interval={8}
scope={scope} scope={scope}
/> />
@ -245,7 +246,6 @@ export const ReplaySettings = defineComponent<MainSettingsProps>(props => {
onChoose={choose} onChoose={choose}
interval={8} interval={8}
scope={scope} scope={scope}
maxHeight={MAIN_HEIGHT - 32}
/> />
); );
}, mainSettingsProps); }, mainSettingsProps);
@ -275,12 +275,7 @@ export const GameInfo = defineComponent<MainSettingsProps>(props => {
const choose = async (key: ChoiceKey) => { const choose = async (key: ChoiceKey) => {
switch (key) { switch (key) {
case GameInfoChoice.Statistics: { case GameInfoChoice.Statistics: {
getConfirm( openStatistics(props.controller);
props.controller,
'数据统计尚未完工',
CENTER_LOC,
POP_BOX_WIDTH
);
break; break;
} }
case GameInfoChoice.Project: { case GameInfoChoice.Project: {

View File

@ -1,5 +1,5 @@
import { CloudLike } from './cloudLike'; import { CloudLike } from './cloudLike';
import { SizedCanvasImageSource } from '@motajs/render-core'; import { SizedCanvasImageSource } from '@motajs/render-assets';
export class CloudWeather extends CloudLike { export class CloudWeather extends CloudLike {
getImage(): SizedCanvasImageSource | null { getImage(): SizedCanvasImageSource | null {

View File

@ -1,6 +1,6 @@
import { MotaOffscreenCanvas2D, Sprite } from '@motajs/render-core'; import { MotaOffscreenCanvas2D, Sprite } from '@motajs/render-core';
import { Weather } from '../weather'; import { Weather } from '../weather';
import { SizedCanvasImageSource } from '@motajs/render-core'; import { SizedCanvasImageSource } from '@motajs/render-assets';
export abstract class CloudLike extends Weather<Sprite> { export abstract class CloudLike extends Weather<Sprite> {
/** 不透明度 */ /** 不透明度 */

View File

@ -1,6 +1,6 @@
import { MotaOffscreenCanvas2D } from '@motajs/render-core'; import { MotaOffscreenCanvas2D } from '@motajs/render-core';
import { CloudLike } from './cloudLike'; import { CloudLike } from './cloudLike';
import { SizedCanvasImageSource } from '@motajs/render-core'; import { SizedCanvasImageSource } from '@motajs/render-assets';
export class FogWeather extends CloudLike { export class FogWeather extends CloudLike {
/** 雾天气的图像比较小,因此将四个进行合并 */ /** 雾天气的图像比较小,因此将四个进行合并 */

View File

@ -11,6 +11,8 @@ interface GameLoadEvent {
coreInit: []; coreInit: [];
/** 当所有启动必要资源加载完毕后触发 */ /** 当所有启动必要资源加载完毕后触发 */
loaded: []; loaded: [];
/** 当资源构建完毕后触发,后续需要用新的加载系统替代 */
assetBuilt: [];
/** 当客户端(渲染端)和数据端都挂载完毕后触发 */ /** 当客户端(渲染端)和数据端都挂载完毕后触发 */
registered: []; registered: [];
/** 当数据端挂载完毕后触发 */ /** 当数据端挂载完毕后触发 */

View File

@ -0,0 +1,63 @@
import { logger } from '@motajs/common';
import { IFaceData, IRoleFaceBinder } from './types';
import { isNil } from 'lodash-es';
import { FaceDirection } from '.';
interface FaceInfo {
/** 此图块的朝向 */
readonly face: FaceDirection;
/** 此图块对应的映射 */
readonly map: Map<FaceDirection, number>;
}
export class RoleFaceBinder implements IRoleFaceBinder {
/** 每个图块对应的朝向信息 */
private faceMap: Map<number, FaceInfo> = new Map();
/** 主要朝向映射 */
private mainMap: Map<number, FaceDirection> = new Map();
malloc(identifier: number, main: FaceDirection): void {
this.mainMap.set(identifier, main);
const map = new Map<FaceDirection, number>();
map.set(main, identifier);
const info: FaceInfo = { face: main, map };
this.faceMap.set(identifier, info);
}
bind(identifier: number, main: number, face: FaceDirection): void {
const mainFace = this.mainMap.get(main);
if (isNil(mainFace)) {
logger.error(43, main.toString());
return;
}
if (mainFace === face) {
logger.error(44, main.toString());
return;
}
const { map } = this.faceMap.get(main)!;
map.set(face, identifier);
const info: FaceInfo = { face, map };
this.faceMap.set(identifier, info);
this.mainMap.set(identifier, mainFace);
}
getFaceOf(identifier: number, face: FaceDirection): IFaceData | null {
const info = this.faceMap.get(identifier);
if (!info) return null;
const target = info.map.get(face);
if (isNil(target)) return null;
const data: IFaceData = { identifier: target, face };
return data;
}
getFaceDirection(identifier: number): FaceDirection | undefined {
return this.faceMap.get(identifier)?.face;
}
getMainFace(identifier: number): IFaceData | null {
const face = this.mainMap.get(identifier);
if (isNil(face)) return null;
const data: IFaceData = { identifier, face };
return data;
}
}

View File

@ -0,0 +1,3 @@
export * from './face';
export * from './types';
export * from './utils';

View File

@ -0,0 +1,54 @@
export const enum FaceDirection {
Unknown,
Left,
Up,
Right,
Down,
LeftUp,
RightUp,
LeftDown,
RightDown
}
export interface IFaceData {
/** 图块数字 */
readonly identifier: number;
/** 图块朝向 */
readonly face: FaceDirection;
}
export interface IRoleFaceBinder {
/**
*
* @param identifier
* @param main
*/
malloc(identifier: number, main: FaceDirection): void;
/**
* {@link malloc}
* @param identifier
* @param main
* @param face
*/
bind(identifier: number, main: number, face: FaceDirection): void;
/**
*
* @param identifier
* @param face
*/
getFaceOf(identifier: number, face: FaceDirection): IFaceData | null;
/**
*
* @param identifier
*/
getFaceDirection(identifier: number): FaceDirection | undefined;
/**
*
* @param identifier
*/
getMainFace(identifier: number): IFaceData | null;
}

View File

@ -0,0 +1,182 @@
import { FaceDirection } from './types';
/**
*
* @param dir
*/
export function getFaceMovement(dir: FaceDirection): Loc {
switch (dir) {
case FaceDirection.Left:
return { x: -1, y: 0 };
case FaceDirection.Right:
return { x: 1, y: 0 };
case FaceDirection.Up:
return { x: 0, y: -1 };
case FaceDirection.Down:
return { x: 0, y: 1 };
case FaceDirection.LeftUp:
return { x: -1, y: -1 };
case FaceDirection.RightUp:
return { x: 1, y: -1 };
case FaceDirection.LeftDown:
return { x: -1, y: 1 };
case FaceDirection.RightDown:
return { x: 1, y: 1 };
case FaceDirection.Unknown:
return { x: 0, y: 0 };
}
}
/**
*
* @param dir
* @param unknown `FaceDirection.Unknown`
*/
export function degradeFace(
dir: FaceDirection,
unknown: FaceDirection = FaceDirection.Unknown
): FaceDirection {
switch (dir) {
case FaceDirection.LeftUp:
return FaceDirection.Left;
case FaceDirection.LeftDown:
return FaceDirection.Left;
case FaceDirection.RightUp:
return FaceDirection.Right;
case FaceDirection.RightDown:
return FaceDirection.Right;
case FaceDirection.Unknown:
return unknown;
}
return dir;
}
/**
*
* @param dir
* @param anticlockwise
* @param face8 使 `false` ->->->->->->
* `true` ->->->->->->->
*/
export function nextFaceDirection(
dir: FaceDirection,
anticlockwise: boolean = false,
face8: boolean = false
): FaceDirection {
if (face8) {
if (anticlockwise) {
switch (dir) {
case FaceDirection.Left:
return FaceDirection.LeftDown;
case FaceDirection.LeftDown:
return FaceDirection.Down;
case FaceDirection.Down:
return FaceDirection.RightDown;
case FaceDirection.RightDown:
return FaceDirection.Right;
case FaceDirection.Right:
return FaceDirection.RightUp;
case FaceDirection.RightUp:
return FaceDirection.Up;
case FaceDirection.Up:
return FaceDirection.LeftUp;
case FaceDirection.LeftUp:
return FaceDirection.Left;
case FaceDirection.Unknown:
return FaceDirection.Unknown;
}
} else {
switch (dir) {
case FaceDirection.Left:
return FaceDirection.LeftUp;
case FaceDirection.LeftUp:
return FaceDirection.Up;
case FaceDirection.Up:
return FaceDirection.RightUp;
case FaceDirection.RightUp:
return FaceDirection.Right;
case FaceDirection.Right:
return FaceDirection.RightDown;
case FaceDirection.RightDown:
return FaceDirection.Down;
case FaceDirection.Down:
return FaceDirection.LeftDown;
case FaceDirection.LeftDown:
return FaceDirection.Left;
case FaceDirection.Unknown:
return FaceDirection.Unknown;
}
}
} else {
if (anticlockwise) {
switch (dir) {
case FaceDirection.Left:
return FaceDirection.Down;
case FaceDirection.Down:
return FaceDirection.Right;
case FaceDirection.Right:
return FaceDirection.Up;
case FaceDirection.Up:
return FaceDirection.Left;
case FaceDirection.LeftUp:
return FaceDirection.LeftDown;
case FaceDirection.LeftDown:
return FaceDirection.RightDown;
case FaceDirection.RightDown:
return FaceDirection.RightUp;
case FaceDirection.RightUp:
return FaceDirection.LeftUp;
case FaceDirection.Unknown:
return FaceDirection.Unknown;
}
} else {
switch (dir) {
case FaceDirection.Left:
return FaceDirection.Up;
case FaceDirection.Up:
return FaceDirection.Right;
case FaceDirection.Right:
return FaceDirection.Down;
case FaceDirection.Down:
return FaceDirection.Left;
case FaceDirection.LeftUp:
return FaceDirection.RightUp;
case FaceDirection.RightUp:
return FaceDirection.RightDown;
case FaceDirection.RightDown:
return FaceDirection.LeftDown;
case FaceDirection.LeftDown:
return FaceDirection.LeftUp;
case FaceDirection.Unknown:
return FaceDirection.Unknown;
}
}
}
}
/**
*
* @param dir
*/
export function fromDirectionString(dir: Dir2): FaceDirection {
switch (dir) {
case 'left':
return FaceDirection.Left;
case 'right':
return FaceDirection.Right;
case 'up':
return FaceDirection.Up;
case 'down':
return FaceDirection.Down;
case 'leftup':
return FaceDirection.LeftUp;
case 'rightup':
return FaceDirection.RightUp;
case 'leftdown':
return FaceDirection.LeftDown;
case 'rightdown':
return FaceDirection.RightDown;
default:
return FaceDirection.Unknown;
}
}

View File

@ -0,0 +1,33 @@
import { ICoreState, IStateSaveData } from './types';
import { IHeroState, HeroState } from './hero';
import { ILayerState, LayerState } from './map';
import { IRoleFaceBinder, RoleFaceBinder } from './common';
export class CoreState implements ICoreState {
readonly layer: ILayerState;
readonly hero: IHeroState;
readonly roleFace: IRoleFaceBinder;
readonly idNumberMap: Map<string, number>;
readonly numberIdMap: Map<number, string>;
constructor() {
this.layer = new LayerState();
this.hero = new HeroState();
this.roleFace = new RoleFaceBinder();
this.idNumberMap = new Map();
this.numberIdMap = new Map();
}
saveState(): IStateSaveData {
return structuredClone({
followers: this.hero.followers
});
}
loadState(data: IStateSaveData): void {
this.hero.removeAllFollowers();
data?.followers.forEach(v => {
this.hero.addFollower(v.num, v.identifier);
});
}
}

View File

@ -1,4 +1,4 @@
import { getHeroStatusOf, getHeroStatusOn } from '../state/hero'; import { getHeroStatusOf, getHeroStatusOn } from '../legacy/hero';
import { Range, ensureArray, has, manhattan } from '@user/data-utils'; import { Range, ensureArray, has, manhattan } from '@user/data-utils';
import EventEmitter from 'eventemitter3'; import EventEmitter from 'eventemitter3';
import { hook } from '@user/data-base'; import { hook } from '@user/data-base';

View File

@ -1,5 +1,5 @@
import { EnemyInfo } from '@motajs/types'; import { EnemyInfo } from '@motajs/types';
import { getHeroStatusOn } from '../state/hero'; import { getHeroStatusOn } from '../legacy/hero';
export interface SpecialDeclaration { export interface SpecialDeclaration {
code: number; code: number;

View File

@ -0,0 +1,2 @@
export * from './state';
export * from './types';

View File

@ -0,0 +1,133 @@
import { Hookable, HookController, IHookController } from '@motajs/common';
import { IHeroFollower, IHeroState, IHeroStateHooks } from './types';
import { FaceDirection, getFaceMovement, nextFaceDirection } from '../common';
import { isNil } from 'lodash-es';
import { DEFAULT_HERO_IMAGE } from '../shared';
export class HeroState extends Hookable<IHeroStateHooks> implements IHeroState {
x: number = 0;
y: number = 0;
direction: FaceDirection = FaceDirection.Down;
image: ImageIds = DEFAULT_HERO_IMAGE;
/** 当前勇士是否正在移动 */
moving: boolean = false;
alpha: number = 1;
readonly followers: IHeroFollower[] = [];
protected createController(
hook: Partial<IHeroStateHooks>
): IHookController<IHeroStateHooks> {
return new HookController(this, hook);
}
setPosition(x: number, y: number): void {
this.x = x;
this.y = y;
this.forEachHook(hook => {
hook.onSetPosition?.(x, y);
});
}
turn(direction?: FaceDirection): void {
const next = isNil(direction)
? nextFaceDirection(this.direction)
: direction;
this.direction = next;
this.forEachHook(hook => {
hook.onTurnHero?.(next);
});
}
startMove(): void {
this.moving = true;
this.forEachHook(hook => {
hook.onStartMove?.();
});
}
async move(dir: FaceDirection, time: number = 100): Promise<void> {
await Promise.all(
this.forEachHook(hook => {
return hook.onMoveHero?.(dir, time);
})
);
const { x, y } = getFaceMovement(dir);
this.x += x;
this.y += y;
}
async endMove(): Promise<void> {
if (!this.moving) return;
await Promise.all(
this.forEachHook(hook => {
return hook.onEndMove?.();
})
);
this.moving = false;
}
async jumpHero(
x: number,
y: number,
time: number = 500,
waitFollower: boolean = false
): Promise<void> {
await Promise.all(
this.forEachHook(hook => {
return hook.onJumpHero?.(x, y, time, waitFollower);
})
);
this.x = x;
this.y = y;
}
setImage(image: ImageIds): void {
this.image = image;
this.forEachHook(hook => {
hook.onSetImage?.(image);
});
}
setAlpha(alpha: number): void {
this.alpha = alpha;
this.forEachHook(hook => {
hook.onSetAlpha?.(alpha);
});
}
setFollowerAlpha(identifier: string, alpha: number): void {
const follower = this.followers.find(v => v.identifier === identifier);
if (!follower) return;
follower.alpha = alpha;
this.forEachHook(hook => {
hook.onSetFollowerAlpha?.(identifier, alpha);
});
}
addFollower(follower: number, identifier: string): void {
this.followers.push({ num: follower, identifier, alpha: 1 });
this.forEachHook(hook => {
hook.onAddFollower?.(follower, identifier);
});
}
removeFollower(identifier: string, animate: boolean = false): void {
const index = this.followers.findIndex(
v => v.identifier === identifier
);
if (index === -1) return;
this.followers.splice(index, 1);
this.forEachHook(hook => {
hook.onRemoveFollower?.(identifier, animate);
});
}
removeAllFollowers(): void {
this.followers.length = 0;
this.forEachHook(hook => {
hook.onRemoveAllFollowers?.();
});
}
}

View File

@ -0,0 +1,204 @@
import { IHookBase, IHookable } from '@motajs/common';
import { FaceDirection } from '../common/types';
export const enum HeroAnimateDirection {
/** 正向播放动画 */
Forward,
/** 反向播放动画 */
Backward
}
export interface IHeroFollower {
/** 跟随者的图块数字 */
readonly num: number;
/** 跟随者的标识符 */
readonly identifier: string;
/** 跟随者的不透明度 */
alpha: number;
}
export interface IHeroStateHooks extends IHookBase {
/**
*
* @param controller
* @param x
* @param y
*/
onSetPosition(x: number, y: number): void;
/**
*
* @param direction
*/
onTurnHero(direction: FaceDirection): void;
/**
*
*/
onStartMove(): void;
/**
*
* @param controller
* @param direction
* @param time
*/
onMoveHero(direction: FaceDirection, time: number): Promise<void>;
/**
*
*/
onEndMove(): Promise<void>;
/**
*
* @param x
* @param y
* @param time
* @param waitFollower
*/
onJumpHero(
x: number,
y: number,
time: number,
waitFollower: boolean
): Promise<void>;
/**
*
* @param image id
*/
onSetImage(image: ImageIds): void;
/**
*
* @param alpha
*/
onSetAlpha(alpha: number): void;
/**
*
* @param follower
* @param identifier
*/
onAddFollower(follower: number, identifier: string): void;
/**
*
* @param identifier
* @param animate `true` 使
*/
onRemoveFollower(identifier: string, animate: boolean): void;
/**
*
*/
onRemoveAllFollowers(): void;
/**
*
* @param identifier
* @param alpha
*/
onSetFollowerAlpha(identifier: string, alpha: number): void;
}
export interface IHeroState extends IHookable<IHeroStateHooks> {
/** 勇士横坐标 */
readonly x: number;
/** 勇士纵坐标 */
readonly y: number;
/** 勇士朝向 */
readonly direction: FaceDirection;
/** 勇士图片 */
readonly image?: ImageIds;
/** 跟随者列表 */
readonly followers: readonly IHeroFollower[];
/** 勇士当前的不透明度 */
readonly alpha: number;
/**
*
* @param x
* @param y
*/
setPosition(x: number, y: number): void;
/**
*
* @param direction
*/
turn(direction?: FaceDirection): void;
/**
*
*/
startMove(): void;
/**
*
* @param dir
* @param time 100ms
* @returns `Promise`
*/
move(dir: FaceDirection, time?: number): Promise<void>;
/**
*
* @returns `Promise`
*/
endMove(): Promise<void>;
/**
*
* @param x
* @param y
* @param time 500ms
* @param waitFollower
* @returns `Promise`
*/
jumpHero(
x: number,
y: number,
time?: number,
waitFollower?: boolean
): Promise<void>;
/**
*
* @param image id
*/
setImage(image: ImageIds): void;
/**
*
* @param alpha
*/
setAlpha(alpha: number): void;
/**
*
* @param follower
* @param identifier
*/
addFollower(follower: number, identifier: string): void;
/**
*
* @param identifier
* @param animate `true` 使
*/
removeFollower(identifier: string, animate?: boolean): void;
/**
*
*/
removeAllFollowers(): void;
/**
*
* @param identifier
* @param alpha
*/
setFollowerAlpha(identifier: string, alpha: number): void;
}

View File

@ -1,2 +1,75 @@
import { loading } from '@user/data-base';
import { CoreState } from './core';
import { isNil } from 'lodash-es';
import { FaceDirection } from './common';
function createCoreState() {
//#region 地图部分
const width = core._WIDTH_;
const height = core._HEIGHT_;
const bg = state.layer.addLayer(width, height);
const bg2 = state.layer.addLayer(width, height);
const event = state.layer.addLayer(width, height);
const fg = state.layer.addLayer(width, height);
const fg2 = state.layer.addLayer(width, height);
state.layer.setLayerAlias(bg, 'bg');
state.layer.setLayerAlias(bg2, 'bg2');
state.layer.setLayerAlias(event, 'event');
state.layer.setLayerAlias(fg, 'fg');
state.layer.setLayerAlias(fg2, 'fg2');
//#endregion
//#region 图块部分
const data = Object.entries(core.maps.blocksInfo);
for (const [key, block] of data) {
const num = Number(key);
state.idNumberMap.set(block.id, num);
state.numberIdMap.set(num, block.id);
}
for (const [key, block] of data) {
if (!block.faceIds) continue;
const { down, up, left, right } = block.faceIds;
const downNum = state.idNumberMap.get(down);
if (downNum !== Number(key)) continue;
const upNum = state.idNumberMap.get(up);
const leftNum = state.idNumberMap.get(left);
const rightNum = state.idNumberMap.get(right);
state.roleFace.malloc(downNum, FaceDirection.Down);
if (!isNil(upNum)) {
state.roleFace.bind(upNum, downNum, FaceDirection.Up);
}
if (!isNil(leftNum)) {
state.roleFace.bind(leftNum, downNum, FaceDirection.Left);
}
if (!isNil(rightNum)) {
state.roleFace.bind(rightNum, downNum, FaceDirection.Right);
}
}
//#endregion
}
export function create() {
loading.once('loaded', () => {
// 加载后初始化全局状态
createCoreState();
});
}
/**
*
*
*
*/
export const state = new CoreState();
export * from './common';
export * from './core';
export * from './enemy'; export * from './enemy';
export * from './state'; export * from './hero';
export * from './map';
export * from './legacy';

View File

@ -0,0 +1,115 @@
/**
*
* @param name
* @param floorId
*/
export function getHeroStatusOn(name: 'all', floorId?: FloorIds): HeroStatus;
export function getHeroStatusOn(
name: (keyof HeroStatus)[],
floorId?: FloorIds
): Partial<HeroStatus>;
export function getHeroStatusOn<K extends keyof HeroStatus>(
name: K,
floorId?: FloorIds
): HeroStatus[K];
export function getHeroStatusOn(
name: keyof HeroStatus | 'all' | (keyof HeroStatus)[],
floorId?: FloorIds
) {
// @ts-expect-error 暂时无法推导
return getHeroStatusOf(core.status.hero, name, floorId);
}
/**
*
* @param status
* @param name
* @param floorId
*/
export function getHeroStatusOf(
status: Partial<HeroStatus>,
name: 'all',
floorId?: FloorIds
): HeroStatus;
export function getHeroStatusOf(
status: Partial<HeroStatus>,
name: (keyof HeroStatus)[],
floorId?: FloorIds
): Partial<HeroStatus>;
export function getHeroStatusOf<K extends keyof HeroStatus>(
status: Partial<HeroStatus>,
name: K,
floorId?: FloorIds
): HeroStatus[K];
export function getHeroStatusOf(
status: DeepPartial<HeroStatus>,
name: keyof HeroStatus | 'all' | (keyof HeroStatus)[],
floorId?: FloorIds
) {
return getRealStatus(status, name, floorId);
}
function getRealStatus(
status: DeepPartial<HeroStatus>,
name: keyof HeroStatus | 'all' | (keyof HeroStatus)[],
floorId: FloorIds = core.status.floorId
): any {
if (name instanceof Array) {
const res: any = {};
name.forEach(v => {
res[v] = getRealStatus(status, v, floorId);
});
return res;
}
if (name === 'all') {
const res: any = {};
for (const [key, value] of Object.entries(core.status.hero)) {
if (typeof value === 'number') {
res[key] = getRealStatus(
status,
key as keyof HeroStatus,
floorId
);
} else {
res[key] = value;
}
}
return res;
}
let s = (status[name] ?? core.status.hero[name]) as number;
if (s === null || s === void 0) {
throw new ReferenceError(
`Wrong hero status property name is delivered: ${name}`
);
}
if (typeof s !== 'number') return s;
// buff
s *= core.status.hero.buff[name] ?? 1;
s = Math.floor(s);
// 衰弱效果
if ((name === 'atk' || name === 'def') && flags.weak) {
const weak = core.values.weakValue;
if (weak < 1) {
// 百分比衰弱
s *= 1 - weak;
} else {
s -= weak;
}
}
return s;
}
// 下面的内容暂时无用
export interface IHeroStatusDefault {
atk: number;
def: number;
hp: number;
}

View File

@ -0,0 +1,3 @@
export * from './layerState';
export * from './mapLayer';
export * from './types';

View File

@ -0,0 +1,139 @@
import {
Hookable,
HookController,
IHookController,
logger
} from '@motajs/common';
import {
ILayerState,
ILayerStateHooks,
IMapLayer,
IMapLayerHookController,
IMapLayerHooks
} from './types';
import { MapLayer } from './mapLayer';
export class LayerState
extends Hookable<ILayerStateHooks>
implements ILayerState
{
readonly layerList: Set<IMapLayer> = new Set();
/** 图层到图层别名映射 */
readonly layerAliasMap: WeakMap<IMapLayer, string> = new WeakMap();
/** 图层别名到图层的映射 */
readonly aliasLayerMap: Map<symbol, IMapLayer> = new Map();
/** 背景图块 */
private backgroundTile: number = 0;
/** 图层钩子映射 */
private layerHookMap: Map<IMapLayer, IMapLayerHookController> = new Map();
addLayer(width: number, height: number): IMapLayer {
const array = new Uint32Array(width * height);
const layer = new MapLayer(array, width, height);
this.layerList.add(layer);
this.forEachHook(hook => {
hook.onUpdateLayer?.(this.layerList);
});
const controller = layer.addHook(new StateMapLayerHook(this, layer));
this.layerHookMap.set(layer, controller);
controller.load();
return layer;
}
removeLayer(layer: IMapLayer): void {
this.layerList.delete(layer);
const alias = this.layerAliasMap.get(layer);
if (alias) {
const symbol = Symbol.for(alias);
this.aliasLayerMap.delete(symbol);
this.layerAliasMap.delete(layer);
}
this.forEachHook(hook => {
hook.onUpdateLayer?.(this.layerList);
});
const controller = this.layerHookMap.get(layer);
if (!controller) return;
controller.unload();
this.layerHookMap.delete(layer);
}
hasLayer(layer: IMapLayer): boolean {
return this.layerList.has(layer);
}
setLayerAlias(layer: IMapLayer, alias: string): void {
const symbol = Symbol.for(alias);
if (this.aliasLayerMap.has(symbol)) {
logger.warn(84, alias);
return;
}
this.layerAliasMap.set(layer, alias);
this.aliasLayerMap.set(symbol, layer);
}
getLayerByAlias(alias: string): IMapLayer | null {
const symbol = Symbol.for(alias);
return this.aliasLayerMap.get(symbol) ?? null;
}
getLayerAlias(layer: IMapLayer): string | undefined {
return this.layerAliasMap.get(layer);
}
resizeLayer(
layer: IMapLayer,
width: number,
height: number,
keepBlock: boolean = false
): void {
if (keepBlock) {
layer.resize(width, height);
} else {
layer.resize2(width, height);
}
}
setBackground(tile: number): void {
this.backgroundTile = tile;
this.forEachHook(hook => {
hook.onChangeBackground?.(tile);
});
}
getBackground(): number {
return this.backgroundTile;
}
protected createController(
hook: Partial<ILayerStateHooks>
): IHookController<ILayerStateHooks> {
return new HookController(this, hook);
}
}
class StateMapLayerHook implements Partial<IMapLayerHooks> {
constructor(
readonly state: LayerState,
readonly layer: IMapLayer
) {}
onUpdateArea(x: number, y: number, width: number, height: number): void {
this.state.forEachHook(hook => {
hook.onUpdateLayerArea?.(this.layer, x, y, width, height);
});
}
onUpdateBlock(block: number, x: number, y: number): void {
this.state.forEachHook(hook => {
hook.onUpdateLayerBlock?.(this.layer, block, x, y);
});
}
onResize(width: number, height: number): void {
this.state.forEachHook(hook => {
hook.onResizeLayer?.(this.layer, width, height);
});
}
}

View File

@ -0,0 +1,263 @@
import { isNil } from 'lodash-es';
import {
IMapLayer,
IMapLayerData,
IMapLayerHookController,
IMapLayerHooks
} from './types';
import { Hookable, HookController, logger } from '@motajs/common';
// todo: 提供 core.setBlock 等方法的替代方法,同时添加 setBlockList以及前景背景的接口
export class MapLayer
extends Hookable<IMapLayerHooks, IMapLayerHookController>
implements IMapLayer
{
width: number;
height: number;
empty: boolean = true;
zIndex: number = 0;
/** 地图图块数组 */
private mapArray: Uint32Array;
/** 地图数据引用 */
private mapData: IMapLayerData;
constructor(array: Uint32Array, width: number, height: number) {
super();
this.width = width;
this.height = height;
const area = width * height;
this.mapArray = new Uint32Array(area);
// 超出的裁剪,不足的补零
this.mapArray.set(array);
this.mapData = {
expired: false,
array: this.mapArray
};
}
resize(width: number, height: number): void {
if (this.width === width && this.height === height) {
return;
}
this.mapData.expired = true;
const before = this.mapArray;
const beforeWidth = this.width;
const beforeHeight = this.height;
const beforeArea = beforeWidth * beforeHeight;
this.width = width;
this.height = height;
const area = width * height;
const newArray = new Uint32Array(area);
this.mapArray = newArray;
// 将原来的地图数组赋值给现在的
if (beforeArea > area) {
// 如果地图变小了,那么直接设置,不需要补零
for (let ny = 0; ny < height; ny++) {
const begin = ny * beforeWidth;
newArray.set(before.subarray(begin, begin + width), ny * width);
}
} else {
// 如果地图变大了,那么需要补零。因为新数组本来就是用 0 填充的,实际上只要赋值就可以了
for (let ny = 0; ny < beforeHeight; ny++) {
const begin = ny * beforeWidth;
newArray.set(
before.subarray(begin, begin + beforeWidth),
ny * width
);
}
}
this.mapData = {
expired: false,
array: this.mapArray
};
this.forEachHook(hook => {
hook.onResize?.(width, height);
});
}
resize2(width: number, height: number): void {
if (this.width === width && this.height === height) {
this.mapArray.fill(0);
return;
}
this.mapData.expired = true;
this.width = width;
this.height = height;
this.mapArray = new Uint32Array(width * height);
this.mapData = {
expired: false,
array: this.mapArray
};
this.empty = true;
this.forEachHook(hook => {
hook.onResize?.(width, height);
});
}
setBlock(block: number, x: number, y: number): void {
const index = y * this.width + x;
if (block === this.mapArray[index]) return;
this.mapArray[index] = block;
this.forEachHook(hook => {
hook.onUpdateBlock?.(block, x, y);
});
if (block !== 0) {
this.empty = false;
}
}
getBlock(x: number, y: number): number {
if (x < 0 || y < 0 || x >= this.width || y >= this.height) {
// 不在地图内,返回 -1
return -1;
}
return this.mapArray[y * this.width + x];
}
putMapData(array: Uint32Array, x: number, y: number, width: number): void {
if (array.length % width !== 0) {
logger.warn(8);
}
const height = Math.ceil(array.length / width);
if (width === this.width && height === this.height) {
this.mapArray.set(array);
this.forEachHook(hook => {
hook.onUpdateArea?.(x, y, width, height);
});
return;
}
const w = this.width;
const r = x + width;
const b = y + height;
if (x < 0 || y < 0 || r > w || b > this.height) {
logger.warn(9);
}
const nl = Math.max(x, 0);
const nt = Math.max(y, 0);
const nr = Math.min(r, w);
const nb = Math.min(b, this.height);
const nw = nr - nl;
const nh = nb - nt;
let empty = true;
for (let ny = 0; ny < nh; ny++) {
const start = ny * nw;
const offset = (ny + nt) * w + nl;
const sub = array.subarray(start, start + nw);
if (empty && sub.some(v => v !== 0)) {
// 空地图判断
empty = false;
}
this.mapArray.set(array.subarray(start, start + nw), offset);
}
this.forEachHook(hook => {
hook.onUpdateArea?.(x, y, width, height);
});
this.empty &&= empty;
}
getMapData(): Uint32Array;
getMapData(
x: number,
y: number,
width: number,
height: number
): Uint32Array;
getMapData(
x?: number,
y?: number,
width?: number,
height?: number
): Uint32Array {
if (isNil(x)) {
return new Uint32Array(this.mapArray);
}
if (isNil(y) || isNil(width) || isNil(height)) {
logger.warn(80);
return new Uint32Array();
}
const w = this.width;
const h = this.height;
const r = x + width;
const b = y + height;
if (x < 0 || y < 0 || r > w || b > h) {
logger.warn(81);
}
const res = new Uint32Array(width * height);
const arr = this.mapArray;
const nr = Math.min(r, w);
const nb = Math.min(b, h);
for (let nx = x; nx < nr; nx++) {
for (let ny = y; ny < nb; ny++) {
const origin = ny * w + nx;
const target = (ny - y) * width + (nx - x);
res[target] = arr[origin];
}
}
return res;
}
/**
*
*/
getMapRef(): IMapLayerData {
return this.mapData;
}
protected createController(
hook: Partial<IMapLayerHooks>
): IMapLayerHookController {
return new MapLayerHookController(this, hook);
}
setZIndex(zIndex: number): void {
this.zIndex = zIndex;
}
async openDoor(x: number, y: number): Promise<void> {
const index = y * this.width + x;
const num = this.mapArray[index];
if (num === 0) return;
await Promise.all(
this.forEachHook(hook => {
return hook.onOpenDoor?.(x, y);
})
);
this.setBlock(0, x, y);
}
async closeDoor(num: number, x: number, y: number): Promise<void> {
const index = y * this.width + x;
const nowNum = this.mapArray[index];
if (nowNum !== 0) {
logger.error(46, x.toString(), y.toString());
return;
}
await Promise.all(
this.forEachHook(hook => {
return hook.onCloseDoor?.(num, x, y);
})
);
this.setBlock(num, x, y);
}
}
class MapLayerHookController
extends HookController<IMapLayerHooks>
implements IMapLayerHookController
{
hookable: MapLayer;
constructor(
readonly layer: MapLayer,
hook: Partial<IMapLayerHooks>
) {
super(layer, hook);
this.hookable = layer;
}
getMapData(): Readonly<IMapLayerData> {
return this.layer.getMapRef();
}
}

View File

@ -0,0 +1,278 @@
import { IHookable, IHookBase, IHookController } from '@motajs/common';
export interface IMapLayerData {
/** 当前引用是否过期,当地图图层内部的地图数组引用更新时,此项会变为 `true` */
expired: boolean;
/** 地图图块数组,是对内部存储的直接引用 */
array: Uint32Array;
}
export interface IMapLayerHooks extends IHookBase {
/**
* `resize`
* @param width
* @param height
*/
onResize(width: number, height: number): void;
/**
*
* @param x
* @param y
* @param width
* @param height
*/
onUpdateArea(x: number, y: number, width: number, height: number): void;
/**
*
* @param block
* @param x
* @param y
*/
onUpdateBlock(block: number, x: number, y: number): void;
/**
* `Promise`
* @param x
* @param y
*/
onOpenDoor(x: number, y: number): Promise<void>;
/**
* `Promise`
* @param num
* @param x
* @param y
*/
onCloseDoor(num: number, x: number, y: number): Promise<void>;
}
export interface IMapLayerHookController
extends IHookController<IMapLayerHooks> {
/** 拓展所属的图层对象 */
readonly layer: IMapLayer;
/**
*
*/
getMapData(): Readonly<IMapLayerData>;
}
export interface IMapLayer
extends IHookable<IMapLayerHooks, IMapLayerHookController> {
/** 地图宽度 */
readonly width: number;
/** 地图高度 */
readonly height: number;
/**
*
* `true` `false`
*/
readonly empty: boolean;
/** 图层纵深 */
readonly zIndex: number;
/**
*
* @param width
* @param height
*/
resize(width: number, height: number): void;
/**
*
* @param width
* @param height
*/
resize2(width: number, height: number): void;
/**
*
* @param block
* @param x
* @param y
*/
setBlock(block: number, x: number, y: number): void;
/**
*
* @param x
* @param y
* @returns 0 -1
*/
getBlock(x: number, y: number): number;
/**
*
* @param array
* @param x
* @param y
* @param width
*/
putMapData(array: Uint32Array, x: number, y: number, width: number): void;
/**
*
*/
getMapData(): Uint32Array;
/**
*
* @param x
* @param y
* @param width
* @param height
*/
getMapData(
x: number,
y: number,
width: number,
height: number
): Uint32Array;
/**
*
*/
getMapRef(): IMapLayerData;
/**
*
* @param zIndex
*/
setZIndex(zIndex: number): void;
/**
*
* @param x
* @param y
*/
openDoor(x: number, y: number): Promise<void>;
/**
*
* @param num
* @param x
* @param y
*/
closeDoor(num: number, x: number, y: number): Promise<void>;
}
export interface ILayerStateHooks extends IHookBase {
/**
*
* @param tile
*/
onChangeBackground(tile: number): void;
/**
*
* @param layerList
*/
onUpdateLayer(layerList: Set<IMapLayer>): void;
/**
*
* @param layer
* @param x
* @param y
* @param width
* @param height
*/
onUpdateLayerArea(
layer: IMapLayer,
x: number,
y: number,
width: number,
height: number
): void;
/**
*
* @param layer
* @param block
* @param x
* @param y
*/
onUpdateLayerBlock(
layer: IMapLayer,
block: number,
x: number,
y: number
): void;
/**
*
* @param layer
* @param width
* @param height
*/
onResizeLayer(layer: IMapLayer, width: number, height: number): void;
}
export interface ILayerState extends IHookable<ILayerStateHooks> {
/** 地图列表 */
readonly layerList: Set<IMapLayer>;
/**
*
* @param width
* @param height
*/
addLayer(width: number, height: number): IMapLayer;
/**
*
* @param layer
*/
removeLayer(layer: IMapLayer): void;
/**
*
* @param layer
*/
hasLayer(layer: IMapLayer): boolean;
/**
*
* @param layer
* @param alias
*/
setLayerAlias(layer: IMapLayer, alias: string): void;
/**
*
* @param alias
*/
getLayerByAlias(alias: string): IMapLayer | null;
/**
*
* @param layer
*/
getLayerAlias(layer: IMapLayer): string | undefined;
/**
*
* @param layer
* @param width
* @param height
* @param keepBlock
*/
resizeLayer(
layer: IMapLayer,
width: number,
height: number,
keepBlock?: boolean
): void;
/**
*
* @param tile
*/
setBackground(tile: number): void;
/**
* 0
*/
getBackground(): number;
}

View File

@ -0,0 +1,2 @@
/** 默认的勇士图片 */
export const DEFAULT_HERO_IMAGE: ImageIds = 'hero.png';

View File

@ -1,332 +0,0 @@
import { logger } from '@motajs/common';
import { EventEmitter } from 'eventemitter3';
import { cloneDeep } from 'lodash-es';
/**
*
* @param name
* @param floorId
*/
export function getHeroStatusOn(name: 'all', floorId?: FloorIds): HeroStatus;
export function getHeroStatusOn(
name: (keyof HeroStatus)[],
floorId?: FloorIds
): Partial<HeroStatus>;
export function getHeroStatusOn<K extends keyof HeroStatus>(
name: K,
floorId?: FloorIds
): HeroStatus[K];
export function getHeroStatusOn(
name: keyof HeroStatus | 'all' | (keyof HeroStatus)[],
floorId?: FloorIds
) {
// @ts-expect-error 暂时无法推导
return getHeroStatusOf(core.status.hero, name, floorId);
}
/**
*
* @param status
* @param name
* @param floorId
*/
export function getHeroStatusOf(
status: Partial<HeroStatus>,
name: 'all',
floorId?: FloorIds
): HeroStatus;
export function getHeroStatusOf(
status: Partial<HeroStatus>,
name: (keyof HeroStatus)[],
floorId?: FloorIds
): Partial<HeroStatus>;
export function getHeroStatusOf<K extends keyof HeroStatus>(
status: Partial<HeroStatus>,
name: K,
floorId?: FloorIds
): HeroStatus[K];
export function getHeroStatusOf(
status: DeepPartial<HeroStatus>,
name: keyof HeroStatus | 'all' | (keyof HeroStatus)[],
floorId?: FloorIds
) {
return getRealStatus(status, name, floorId);
}
function getRealStatus(
status: DeepPartial<HeroStatus>,
name: keyof HeroStatus | 'all' | (keyof HeroStatus)[],
floorId: FloorIds = core.status.floorId
): any {
if (name instanceof Array) {
const res: any = {};
name.forEach(v => {
res[v] = getRealStatus(status, v, floorId);
});
return res;
}
if (name === 'all') {
const res: any = {};
for (const [key, value] of Object.entries(core.status.hero)) {
if (typeof value === 'number') {
res[key] = getRealStatus(
status,
key as keyof HeroStatus,
floorId
);
} else {
res[key] = value;
}
}
return res;
}
let s = (status[name] ?? core.status.hero[name]) as number;
if (s === null || s === void 0) {
throw new ReferenceError(
`Wrong hero status property name is delivered: ${name}`
);
}
if (typeof s !== 'number') return s;
// buff
s *= core.status.hero.buff[name] ?? 1;
s = Math.floor(s);
// 衰弱效果
if ((name === 'atk' || name === 'def') && flags.weak) {
const weak = core.values.weakValue;
if (weak < 1) {
// 百分比衰弱
s *= 1 - weak;
} else {
s -= weak;
}
}
return s;
}
// 下面的内容暂时无用
export interface IHeroStatusDefault {
atk: number;
def: number;
hp: number;
}
interface HeroStateEvent {
set: [key: string | number | symbol, value: any];
}
type HeroStatusCalculate = (
hero: HeroState<any>,
key: string | number | symbol,
value: any
) => any;
export class HeroState<
T extends object = IHeroStatusDefault
> extends EventEmitter<HeroStateEvent> {
readonly status: T;
readonly computedStatus: T;
readonly buffable: Set<keyof T> = new Set();
readonly buffMap: Map<keyof T, number> = new Map();
private static cal: HeroStatusCalculate = (_0, _1, value) => value;
constructor(init: T) {
super();
this.status = init;
this.computedStatus = cloneDeep(init);
}
/**
*
* @param key
* @param value
* @returns
*/
setStatus<K extends keyof T>(key: K, value: T[K]): boolean {
this.status[key] = value;
this.emit('set', key, value);
return this.refreshStatus(key);
}
/**
*
* @param key
* @param value
* @returns
*/
addStatus<K extends SelectKey<T, number>>(key: K, value: number): boolean {
if (typeof this.status[key] !== 'number') {
logger.warn(14, String(key));
return false;
}
return this.setStatus<K>(key, (this.status[key] + value) as T[K]);
}
/**
*
* @param key
* @returns
*/
getStatus<K extends keyof T>(key: K): T[K] {
return this.status[key];
}
/**
* 2.x所说的勇士真实属性
* @param key
*/
getComputedStatus<K extends keyof T>(key: K): T[K] {
return this.computedStatus[key];
}
/**
* buff加成
*/
markBuffable(key: SelectKey<T, number>): void {
if (typeof this.status[key] !== 'number') {
logger.warn(12, String(key));
return;
}
this.buffable.add(key);
this.buffMap.set(key, 1);
}
/**
* buff值
* @param key buff的属性
* @param value buff值
* @returns
*/
setBuff(key: SelectKey<T, number>, value: number): boolean {
if (!this.buffable.has(key) || typeof this.status[key] !== 'number') {
logger.warn(13, String(key));
return false;
}
this.buffMap.set(key, value);
return this.refreshStatus(key);
}
/**
* buff值
* @param key buff属性
* @param value buff增量
* @returns
*/
addBuff(key: SelectKey<T, number>, value: number): boolean {
if (!this.buffable.has(key) || typeof this.status[key] !== 'number') {
logger.warn(13, String(key));
return false;
}
return this.setBuff(key, this.buffMap.get(key)! + value);
}
/**
*
* @param key
* @returns
*/
refreshStatus(key?: keyof T): boolean {
if (key === void 0) {
for (const [key, value] of Object.entries(this.status)) {
// @ts-expect-error 暂时无法推导
this.computedStatus[key] = HeroState.cal(this, key, value);
}
return true;
}
this.computedStatus[key] = HeroState.cal(this, key, this.status[key]);
return true;
}
/**
* buff加成的属性
* @returns
*/
refreshBuffable(): boolean {
for (const key of this.buffable) {
this.computedStatus[key] = HeroState.cal(
this,
key,
this.status[key]
);
}
return true;
}
/**
*
* @param fn key表示属性名value表示属性值
*/
static overrideCalculate(fn: HeroStatusCalculate) {
this.cal = fn;
}
}
interface _IHeroItem {
items: Map<AllIdsOf<'items'>, number>;
/**
*
* @param item id
* @param value
* @returns
*/
setItem(item: AllIdsOf<'items'>, value: number): boolean;
/**
*
* @param item id
* @param value
* @returns
*/
addItem(item: AllIdsOf<'items'>, value: number): boolean;
/**
* 使
* @param item id
* @returns 使
*/
useItem(item: AllIdsOf<'items'>, x?: number, y?: number): boolean;
/**
*
* @param item id
* @param num
*/
getItem(item: AllIdsOf<'items'>, num: number): void;
/**
*
* @param item id
* @param x x坐标
* @param y y坐标
* @param floorId
* @param num
*/
getItem(
item: AllIdsOf<'items'>,
x: number,
y: number,
floorId?: FloorIds,
num?: number
): void;
/**
*
* @param item id
*/
itemCount(item: AllIdsOf<'items'>): number;
/**
*
* @param item id
*/
hasItem(item: AllIdsOf<'items'>): boolean;
}

View File

@ -0,0 +1,32 @@
import { ILayerState } from './map';
import { IHeroFollower, IHeroState } from './hero';
import { IRoleFaceBinder } from './common';
export interface IStateSaveData {
/** 跟随者列表 */
readonly followers: readonly IHeroFollower[];
}
export interface ICoreState {
/** 地图状态 */
readonly layer: ILayerState;
/** 勇士状态 */
readonly hero: IHeroState;
/** 朝向绑定 */
readonly roleFace: IRoleFaceBinder;
/** id 到图块数字的映射 */
readonly idNumberMap: Map<string, number>;
/** 图块数字到 id 的映射 */
readonly numberIdMap: Map<number, string>;
/**
*
*/
saveState(): IStateSaveData;
/**
*
* @param data
*/
loadState(data: IStateSaveData): void;
}

View File

@ -5,6 +5,7 @@
"@motajs/client-base": "workspace:*", "@motajs/client-base": "workspace:*",
"@motajs/common": "workspace:*", "@motajs/common": "workspace:*",
"@motajs/render": "workspace:*", "@motajs/render": "workspace:*",
"@motajs/render-assets": "workspace:*",
"@motajs/render-core": "workspace:*", "@motajs/render-core": "workspace:*",
"@motajs/render-elements": "workspace:*", "@motajs/render-elements": "workspace:*",
"@motajs/render-style": "workspace:*", "@motajs/render-style": "workspace:*",
@ -17,7 +18,8 @@
"@motajs/legacy-data": "workspace:*", "@motajs/legacy-data": "workspace:*",
"@motajs/legacy-ui": "workspace:*", "@motajs/legacy-ui": "workspace:*",
"@motajs/legacy-system": "workspace:*", "@motajs/legacy-system": "workspace:*",
"@user/client-base": "workspace:*",
"@user/client-modules": "workspace:*", "@user/client-modules": "workspace:*",
"@user/legacy-plugin-client": "workspace:*" "@user/legacy-plugin-client": "workspace:*"
} }
} }

View File

@ -5,6 +5,7 @@ import * as LegacyClient from '@motajs/legacy-client';
import * as LegacySystem from '@motajs/legacy-system'; import * as LegacySystem from '@motajs/legacy-system';
import * as LegacyUI from '@motajs/legacy-ui'; import * as LegacyUI from '@motajs/legacy-ui';
import * as Render from '@motajs/render'; import * as Render from '@motajs/render';
import * as RenderAssets from '@motajs/render-assets';
import * as RenderCore from '@motajs/render-core'; import * as RenderCore from '@motajs/render-core';
import * as RenderElements from '@motajs/render-elements'; import * as RenderElements from '@motajs/render-elements';
import * as RenderStyle from '@motajs/render-style'; import * as RenderStyle from '@motajs/render-style';
@ -12,6 +13,7 @@ import * as RenderVue from '@motajs/render-vue';
import * as System from '@motajs/system'; import * as System from '@motajs/system';
import * as SystemAction from '@motajs/system-action'; import * as SystemAction from '@motajs/system-action';
import * as SystemUI from '@motajs/system-ui'; import * as SystemUI from '@motajs/system-ui';
import * as UserClientBase from '@user/client-base';
import * as ClientModules from '@user/client-modules'; import * as ClientModules from '@user/client-modules';
import * as LegacyPluginClient from '@user/legacy-plugin-client'; import * as LegacyPluginClient from '@user/legacy-plugin-client';
import * as MutateAnimate from 'mutate-animate'; import * as MutateAnimate from 'mutate-animate';
@ -28,6 +30,7 @@ export function create() {
Mota.register('@motajs/legacy-system', LegacySystem); Mota.register('@motajs/legacy-system', LegacySystem);
Mota.register('@motajs/legacy-ui', LegacyUI); Mota.register('@motajs/legacy-ui', LegacyUI);
Mota.register('@motajs/render', Render); Mota.register('@motajs/render', Render);
Mota.register('@motajs/render-assets', RenderAssets);
Mota.register('@motajs/render-core', RenderCore); Mota.register('@motajs/render-core', RenderCore);
Mota.register('@motajs/render-elements', RenderElements); Mota.register('@motajs/render-elements', RenderElements);
Mota.register('@motajs/render-style', RenderStyle); Mota.register('@motajs/render-style', RenderStyle);
@ -35,6 +38,7 @@ export function create() {
Mota.register('@motajs/system', System); Mota.register('@motajs/system', System);
Mota.register('@motajs/system-action', SystemAction); Mota.register('@motajs/system-action', SystemAction);
Mota.register('@motajs/system-ui', SystemUI); Mota.register('@motajs/system-ui', SystemUI);
Mota.register('@user/client-base', UserClientBase);
Mota.register('@user/client-modules', ClientModules); Mota.register('@user/client-modules', ClientModules);
Mota.register('@user/legacy-plugin-client', LegacyPluginClient); Mota.register('@user/legacy-plugin-client', LegacyPluginClient);
Mota.register('MutateAnimate', MutateAnimate); Mota.register('MutateAnimate', MutateAnimate);
@ -45,8 +49,9 @@ export function create() {
} }
async function createModule() { async function createModule() {
LegacyUI.create(); UserClientBase.create();
ClientModules.create(); ClientModules.create();
LegacyUI.create();
await import('ant-design-vue/dist/antd.dark.css'); await import('ant-design-vue/dist/antd.dark.css');
main.renderLoaded = true; main.renderLoaded = true;

View File

@ -3,12 +3,20 @@ import { create } from './create';
import { patchAll } from '@user/data-fallback'; import { patchAll } from '@user/data-fallback';
import { loading } from '@user/data-base'; import { loading } from '@user/data-base';
import { Patch } from '@motajs/legacy-common'; import { Patch } from '@motajs/legacy-common';
import { logger } from '@motajs/common';
export function createData() { export function createData() {
createMota(); createMota();
patchAll(); patchAll();
create(); create();
if (main.replayChecking) {
logger.log(
`如果需要调试录像验证,请在 script/build-game.ts 中将 DEBUG_REPLAY 设为 true` +
`此时录像验证中可以看到完整正确的报错栈。调试完毕后,记得将它重新设为 false`
);
}
loading.once('coreInit', () => { loading.once('coreInit', () => {
Patch.patchAll(); Patch.patchAll();
}); });

View File

@ -1,25 +1,27 @@
import type { RenderAdapter } from '@motajs/render'; import type { RenderAdapter } from '@motajs/render';
import type { TimingFn } from 'mutate-animate'; import type { TimingFn } from 'mutate-animate';
import { BlockMover, heroMoveCollection, MoveStep } from '@user/data-state'; import {
BlockMover,
fromDirectionString,
heroMoveCollection,
MoveStep,
state
} from '@user/data-state';
import { hook, loading } from '@user/data-base'; import { hook, loading } from '@user/data-base';
import { Patch, PatchClass } from '@motajs/legacy-common'; import { Patch, PatchClass } from '@motajs/legacy-common';
import type { import type {
HeroRenderer,
LayerDoorAnimate, LayerDoorAnimate,
LayerGroupAnimate, LayerGroupAnimate,
Layer,
FloorViewport, FloorViewport,
LayerFloorBinder,
LayerGroup LayerGroup
} from '@user/client-modules'; } from '@user/client-modules';
import { isNil } from 'lodash-es';
// 向后兼容用,会充当两个版本间过渡的作用 // 向后兼容用,会充当两个版本间过渡的作用
interface Adapters { interface Adapters {
'hero-adapter'?: RenderAdapter<HeroRenderer>;
'door-animate'?: RenderAdapter<LayerDoorAnimate>; 'door-animate'?: RenderAdapter<LayerDoorAnimate>;
animate?: RenderAdapter<LayerGroupAnimate>; animate?: RenderAdapter<LayerGroupAnimate>;
layer?: RenderAdapter<Layer>;
viewport?: RenderAdapter<FloorViewport>; viewport?: RenderAdapter<FloorViewport>;
} }
@ -30,16 +32,12 @@ export function initFallback() {
if (!main.replayChecking && main.mode === 'play') { if (!main.replayChecking && main.mode === 'play') {
const Adapter = Mota.require('@motajs/render').RenderAdapter; const Adapter = Mota.require('@motajs/render').RenderAdapter;
const hero = Adapter.get<HeroRenderer>('hero-adapter');
const doorAnimate = Adapter.get<LayerDoorAnimate>('door-animate'); const doorAnimate = Adapter.get<LayerDoorAnimate>('door-animate');
const animate = Adapter.get<LayerGroupAnimate>('animate'); const animate = Adapter.get<LayerGroupAnimate>('animate');
const layer = Adapter.get<Layer>('layer');
const viewport = Adapter.get<FloorViewport>('viewport'); const viewport = Adapter.get<FloorViewport>('viewport');
adapters['hero-adapter'] = hero;
adapters['door-animate'] = doorAnimate; adapters['door-animate'] = doorAnimate;
adapters['animate'] = animate; adapters['animate'] = animate;
adapters['layer'] = layer;
adapters['viewport'] = viewport; adapters['viewport'] = viewport;
} }
@ -74,7 +72,7 @@ export function initFallback() {
/** /**
* *
*/ */
function generateJumpFn(dx: number, dy: number): TimingFn<3> { function generateJumpFn(dx: number, dy: number): TimingFn<2> {
const distance = Math.hypot(dx, dy); const distance = Math.hypot(dx, dy);
const peak = 3 + distance; const peak = 3 + distance;
@ -82,7 +80,7 @@ export function initFallback() {
const x = dx * progress; const x = dx * progress;
const y = progress * dy + (progress ** 2 - progress) * peak; const y = progress * dy + (progress ** 2 - progress) * peak;
return [x, y, Math.ceil(y)]; return [x, y];
}; };
} }
@ -112,31 +110,28 @@ export function initFallback() {
patch.add('_moveAction_moving', () => {}); patch.add('_moveAction_moving', () => {});
patch2.add( patch2.add('_action_moveAction', function () {
'_action_moveAction', if (core.canMoveHero()) {
function (data: any, x: number, y: number, prefix: any) { const nx = core.nextX(),
if (core.canMoveHero()) { ny = core.nextY();
const nx = core.nextX(), // 检查noPass决定是撞击还是移动
ny = core.nextY(); if (core.noPass(nx, ny)) {
// 检查noPass决定是撞击还是移动 core.insertAction([{ type: 'trigger', loc: [nx, ny] }]);
if (core.noPass(nx, ny)) { } else {
core.insertAction([{ type: 'trigger', loc: [nx, ny] }]); // 先移动一格,然后尝试触发事件
} else { core.insertAction([
// 先移动一格,然后尝试触发事件 {
core.insertAction([ type: 'function',
{ function:
type: 'function', 'function() { core.moveAction(core.doAction); }',
function: async: true
'function() { core.moveAction(core.doAction); }', },
async: true { type: '_label' }
}, ]);
{ type: '_label' }
]);
}
} }
core.doAction();
} }
); core.doAction();
});
patch2.add( patch2.add(
'eventMoveHero', 'eventMoveHero',
@ -184,21 +179,28 @@ export function initFallback() {
if (!core.status.hero) return; if (!core.status.hero) return;
// @ts-ignore // @ts-ignore
core.status.hero.loc[name] = value; core.status.hero.loc[name] = value;
if ((name === 'x' || name === 'y') && !noGather) {
core.control.gatherFollowers();
}
if (name === 'direction') { if (name === 'direction') {
adapters['hero-adapter']?.sync('turn', value); const dir = fromDirectionString(value as Dir);
adapters['hero-adapter']?.sync('setAnimateDir', value); state.hero.turn(dir);
setHeroDirection(value as Dir); setHeroDirection(value as Dir);
} else if (name === 'x') { } else if (name === 'x') {
// 为了防止逆天样板出问题 // 为了防止逆天样板出问题
core.bigmap.posX = value as number; core.bigmap.posX = value as number;
adapters['hero-adapter']?.sync('setHeroLoc', value); if (!noGather) {
state.hero.setPosition(
value as number,
core.status.hero.loc.y
);
}
} else { } else {
// 为了防止逆天样板出问题 // 为了防止逆天样板出问题
core.bigmap.posY = value as number; core.bigmap.posY = value as number;
adapters['hero-adapter']?.sync('setHeroLoc', void 0, value); if (!noGather) {
state.hero.setPosition(
core.status.hero.loc.x,
value as number
);
}
} }
} }
); );
@ -243,10 +245,8 @@ export function initFallback() {
); );
patch2.add('setHeroIcon', function (name: ImageIds) { patch2.add('setHeroIcon', function (name: ImageIds) {
const img = core.material.images.images[name];
if (!img) return;
core.status.hero.image = name; core.status.hero.image = name;
adapters['hero-adapter']?.sync('setImage', img); state.hero.setImage(name);
}); });
patch.add('isMoving', function () { patch.add('isMoving', function () {
@ -279,10 +279,10 @@ export function initFallback() {
// 找寻自动寻路路线 // 找寻自动寻路路线
const moveStep = core.automaticRoute(destX, destY); const moveStep = core.automaticRoute(destX, destY);
if ( if (
moveStep.length == 0 && moveStep.length === 0 &&
(destX != core.status.hero.loc.x || (destX !== core.status.hero.loc.x ||
destY != core.status.hero.loc.y || destY !== core.status.hero.loc.y ||
stepPostfix.length == 0) stepPostfix.length === 0)
) )
return; return;
moveStep.push(...stepPostfix); moveStep.push(...stepPostfix);
@ -341,9 +341,9 @@ export function initFallback() {
const locked = core.status.lockControl; const locked = core.status.lockControl;
core.lockControl(); core.lockControl();
core.status.replay.animate = true; core.status.replay.animate = true;
core.removeBlock(x, y);
const cb = () => { const cb = () => {
core.removeBlock(x, y);
core.maps._removeBlockFromMap( core.maps._removeBlockFromMap(
core.status.floorId, core.status.floorId,
block block
@ -357,10 +357,10 @@ export function initFallback() {
y y
); );
callback?.(); callback?.();
delete core.animateFrame.asyncId[animate];
}; };
adapters['door-animate']?.all('openDoor', block).then(cb); const layer = state.layer.getLayerByAlias('event')!;
layer.openDoor(x, y).then(cb);
const animate = fallbackIds++; const animate = fallbackIds++;
core.animateFrame.lastAsyncId = animate; core.animateFrame.lastAsyncId = animate;
@ -376,10 +376,10 @@ export function initFallback() {
id = id || ''; id = id || '';
if ( if (
// @ts-ignore // @ts-ignore
(core.material.icons.animates[id] == null && (isNil(core.material.icons.animates[id]) &&
// @ts-ignore // @ts-ignore
core.material.icons.npc48[id] == null) || isNil(core.material.icons.npc48[id])) ||
core.getBlock(x, y) != null !isNil(core.getBlock(x, y))
) { ) {
if (callback) callback(); if (callback) callback();
return; return;
@ -407,11 +407,9 @@ export function initFallback() {
if (core.status.replay.speed === 24) { if (core.status.replay.speed === 24) {
cb(); cb();
} else { } else {
adapters['door-animate'] const num = state.idNumberMap.get(id)!;
?.all('closeDoor', block) const layer = state.layer.getLayerByAlias('event')!;
.then(() => { layer.closeDoor(num, x, y).then(cb);
cb();
});
const animate = fallbackIds++; const animate = fallbackIds++;
core.animateFrame.lastAsyncId = animate; core.animateFrame.lastAsyncId = animate;
@ -439,10 +437,10 @@ export function initFallback() {
if ( if (
core.isReplaying() || core.isReplaying() ||
!core.material.animates[name] || !core.material.animates[name] ||
x == null || isNil(x) ||
y == null isNil(y)
) { ) {
if (callback) callback(); callback?.();
return -1; return -1;
} }
@ -515,19 +513,10 @@ export function initFallback() {
mover.insertMove(...[start, ...resolved]); mover.insertMove(...[start, ...resolved]);
const controller = mover.startMove(); const controller = mover.startMove();
const id = fallbackIds++;
core.animateFrame.asyncId[id] = () => {
if (!keep) {
core.removeBlock(mover.x, mover.y);
}
};
if (controller) { if (controller) {
await controller.onEnd; await controller.onEnd;
} }
delete core.animateFrame.asyncId[id];
if (!keep) { if (!keep) {
core.removeBlock(mover.x, mover.y); core.removeBlock(mover.x, mover.y);
} }
@ -557,36 +546,24 @@ export function initFallback() {
const dy = ey - sy; const dy = ey - sy;
const fn = generateJumpFn(dx, dy); const fn = generateJumpFn(dx, dy);
// 先使用 mainMapRenderer 妥协
const list = adapters.layer?.items ?? []; const { mainMapRenderer: renderer } = Mota.require(
const items = [...list].filter(v => { '@user/client-modules'
if (v.layer !== 'event') return false;
const ex = v.getExtends('floor-binder') as LayerFloorBinder;
if (!ex) return false;
return ex.getFloor() === core.status.floorId;
});
const width = core.status.thisMap.width;
const index = sx + sy * width;
const promise = Promise.all(
items.map(v => {
return v.moveAs(index, ex, ey, fn, time, keep);
})
); );
if (renderer.layerState !== state.layer) {
core.updateStatusBar(); callback?.();
return;
}
const layer = state.layer.getLayerByAlias('event');
if (!layer) {
callback?.();
return;
}
core.removeBlock(sx, sy); core.removeBlock(sx, sy);
const moving = renderer.addMovingBlock(layer, block.id, sx, sy);
const id = fallbackIds++; core.updateStatusBar();
core.animateFrame.asyncId[id] = () => { await moving.moveRelative(fn, time);
if (keep) { moving.destroy();
core.setBlock(block.id, ex, ey);
}
};
await promise;
delete core.animateFrame.asyncId[id];
if (keep) { if (keep) {
core.setBlock(block.id, ex, ey); core.setBlock(block.id, ex, ey);
@ -607,30 +584,15 @@ export function initFallback() {
) { ) {
if (heroMover.moving) return; if (heroMover.moving) return;
const sx = core.getHeroLoc('x');
const sy = core.getHeroLoc('y');
adapters.viewport?.all('mutateTo', ex, ey, time); adapters.viewport?.all('mutateTo', ex, ey, time);
const locked = core.status.lockControl; const locked = core.status.lockControl;
core.lockControl(); core.lockControl();
const list = adapters['hero-adapter']?.items ?? [];
const items = [...list];
time /= core.status.replay.speed; time /= core.status.replay.speed;
if (core.status.replay.speed === 24) time = 1; if (core.status.replay.speed === 24) time = 1;
const fn = generateJumpFn(ex - sx, ey - sy);
await Promise.all( await state.hero.jumpHero(ex, ey, time);
items.map(v => {
if (!v.renderable) return Promise.reject();
return v.layer.moveRenderable(
v.renderable,
sx,
sy,
fn,
time
);
})
);
if (!locked) core.unlockControl(); if (!locked) core.unlockControl();
core.setHeroLoc('x', ex); core.setHeroLoc('x', ex);

View File

@ -0,0 +1,86 @@
import { logger } from '@motajs/common';
export interface ICompiledProgram {
/** 着色器程序 */
readonly program: WebGLProgram;
/** 顶点着色器对象 */
readonly vertexShader: WebGLShader;
/** 片段着色器对象 */
readonly fragmentShader: WebGLShader;
}
/**
*
* @param gl WebGL2
* @param type
* @param source
*/
export function compileShader(
gl: WebGL2RenderingContext,
type: number,
source: string
): WebGLShader | null {
const shader = gl.createShader(type);
if (!shader) return null;
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
const info = gl.getShaderInfoLog(shader);
const typeStr = type === gl.VERTEX_SHADER ? 'vertex' : 'fragment';
logger.error(10, typeStr, info ?? '');
return null;
}
return shader;
}
/**
*
* @param gl WebGL2
* @param vs
* @param fs
*/
export function compileProgram(
gl: WebGL2RenderingContext,
vs: WebGLShader,
fs: WebGLShader
) {
const program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
const info = gl.getProgramInfoLog(program);
logger.error(9, info ?? '');
return null;
}
return program;
}
/**
* 使
* @param gl WebGL2
* @param vs
* @param fs
*/
export function compileProgramWith(
gl: WebGL2RenderingContext,
vs: string,
fs: string
): ICompiledProgram | null {
const vsShader = compileShader(gl, gl.VERTEX_SHADER, vs);
const fsShader = compileShader(gl, gl.FRAGMENT_SHADER, fs);
if (!vsShader || !fsShader) return null;
const program = compileProgram(gl, vsShader, fsShader);
if (!program) return null;
return {
program,
vertexShader: vsShader,
fragmentShader: fsShader
};
}

View File

@ -1,2 +1,3 @@
export * from './glUtils';
export * from './keyCodes'; export * from './keyCodes';
export * from './types'; export * from './types';

View File

@ -0,0 +1,133 @@
import { isNil } from 'lodash-es';
import { IDirtyMark, IDirtyTracker } from './types';
/**
* `dirtySince` `true`
*/
export class PrivateBooleanDirtyTracker implements IDirtyTracker<boolean> {
/** 标记映射 */
private markMap: WeakMap<IDirtyMark, number> = new WeakMap();
/** 脏标记 */
private dirtyFlag: number = 0;
mark(): IDirtyMark {
const symbol = {};
this.markMap.set(symbol, this.dirtyFlag);
return symbol;
}
unmark(mark: IDirtyMark): void {
this.markMap.delete(mark);
}
dirtySince(mark: IDirtyMark): boolean {
const num = this.markMap.get(mark);
if (isNil(num)) return true;
return num < this.dirtyFlag;
}
hasMark(symbol: IDirtyMark): boolean {
return this.markMap.has(symbol);
}
/**
*
*/
protected dirty() {
this.dirtyFlag++;
}
}
/**
* `dirtySince`
*/
export class PrivateListDirtyTracker<T extends number>
implements IDirtyTracker<Set<T>>
{
/** 标记映射,键表示在索引,值表示其对应的标记数字 */
private readonly markMap: Map<T, number> = new Map();
/** 标记 symbol 映射,值表示这个 symbol 对应的标记数字 */
private readonly symbolMap: WeakMap<{}, number> = new WeakMap();
/** 脏标记数字 */
private dirtyFlag: number = 0;
constructor(protected length: number) {}
mark(): IDirtyMark {
const symbol = {};
this.symbolMap.set(symbol, this.dirtyFlag);
return symbol;
}
unmark(mark: IDirtyMark): void {
this.symbolMap.delete(mark);
}
dirtySince(mark: IDirtyMark): Set<T> {
const num = this.symbolMap.get(mark);
const res = new Set<T>();
if (isNil(num)) return res;
this.markMap.forEach((v, k) => {
if (v > num) res.add(k);
});
return res;
}
hasMark(symbol: IDirtyMark): boolean {
return this.symbolMap.has(symbol);
}
protected dirty(data: T): void {
if (data >= this.length) return;
this.dirtyFlag++;
this.markMap.set(data, this.dirtyFlag);
}
protected updateLength(length: number) {
this.length = length;
}
}
export class PrivateMapDirtyTracker<T extends string>
implements IDirtyTracker<Record<T, boolean>>
{
/** 标记映射,键表示名称,值表示其对应的标记数字 */
private readonly markMap: Map<T, number> = new Map();
/** 标记 symbol 映射,值表示这个 symbol 对应的标记数字 */
private readonly symbolMap: WeakMap<{}, number> = new WeakMap();
/** 脏标记数字 */
private dirtyFlag: number = 0;
constructor(protected length: number) {}
mark(): IDirtyMark {
const symbol = {};
this.symbolMap.set(symbol, this.dirtyFlag);
return symbol;
}
unmark(mark: IDirtyMark): void {
this.symbolMap.delete(mark);
}
dirtySince(mark: IDirtyMark): Record<T, boolean> {
const num = this.symbolMap.get(mark) ?? 0;
const obj: Partial<Record<T, boolean>> = {};
this.markMap.forEach((v, k) => {
if (v > num) obj[k] = true;
else obj[k] = false;
});
return obj as Record<T, boolean>;
}
hasMark(symbol: IDirtyMark): boolean {
return this.symbolMap.has(symbol);
}
protected dirty(data: T): void {
this.dirtyFlag++;
this.markMap.set(data, this.dirtyFlag);
}
}

View File

@ -0,0 +1,86 @@
import { logger } from './logger';
import { IHookable, IHookBase, IHookController, IHookObject } from './types';
export abstract class Hookable<
H extends IHookBase = IHookBase,
C extends IHookController<H> = IHookController<H>
> implements IHookable<H, C>
{
/** 加载完成的钩子列表 */
protected readonly loadedList: Set<IHookObject<H, C>> = new Set();
/** 钩子列表 */
private readonly hookList: Set<IHookObject<H, C>> = new Set();
/** 钩子对象到钩子存储对象的映射 */
private readonly hookMap: Map<Partial<H>, IHookObject<H, C>> = new Map();
/** 钩子控制器到钩子存储对象的映射 */
private readonly controllerMap: Map<C, IHookObject<H, C>> = new Map();
/**
*
* @param hook
*/
protected abstract createController(hook: Partial<H>): C;
addHook(hook: Partial<H>): C {
const controller = this.createController(hook);
const obj: IHookObject<H, C> = { hook, controller };
this.hookMap.set(hook, obj);
this.controllerMap.set(controller, obj);
this.hookList.add(obj);
return controller;
}
loadHook(hook: Partial<H>): void {
const obj = this.hookMap.get(hook);
if (!obj) {
logger.warn(85);
return;
}
hook.awake?.(obj.controller);
this.loadedList.add(obj);
}
removeHook(hook: Partial<H>): void {
const obj = this.hookMap.get(hook);
if (!obj) return;
obj.hook.destroy?.(obj.controller);
this.hookList.delete(obj);
this.loadedList.delete(obj);
this.hookMap.delete(hook);
this.controllerMap.delete(obj.controller);
}
removeHookByController(hook: C): void {
const obj = this.controllerMap.get(hook);
if (!obj) return;
obj.hook.destroy?.(obj.controller);
this.hookList.delete(obj);
this.loadedList.delete(obj);
this.controllerMap.delete(hook);
this.hookMap.delete(obj.hook);
}
forEachHook<T>(fn: (hook: Partial<H>, controller: C) => T): T[] {
const arr: T[] = [];
this.loadedList.forEach(v => {
arr.push(fn(v.hook, v.controller));
});
return arr;
}
}
export class HookController<H extends IHookBase> implements IHookController<H> {
constructor(
readonly hookable: IHookable<H, IHookController<H>>,
readonly hook: Partial<H>
) {}
load(): void {
this.hookable.loadHook(this.hook);
}
unload(): void {
this.hookable.removeHookByController(this);
}
}

View File

@ -1,2 +1,5 @@
export * from './dirtyTracker';
export * from './hook';
export * from './logger'; export * from './logger';
export * from './utils'; export * from './utils';
export * from './types';

View File

@ -26,6 +26,26 @@
"24": "Cannot decode source type of '$1', since there is no registered decoder for that type.", "24": "Cannot decode source type of '$1', since there is no registered decoder for that type.",
"25": "Unknown audio type. Header: '$1'", "25": "Unknown audio type. Header: '$1'",
"26": "Uncaught error when fetching stream data from '$1'. Error info: $2.", "26": "Uncaught error when fetching stream data from '$1'. Error info: $2.",
"27": "No autotile connection data, please ensure you have created autotile connection map.",
"28": "Cannot compile map render shader.",
"29": "Cannot get uniform location of map render shader.",
"30": "",
"31": "No asset data is specified when rending map.",
"32": "Every layer added to map renderer must share the same size. Different layer: $1.",
"33": "Map layer transfered to vertex generator must belong to the renderer that the generator use.",
"34": "The texture of moving block must be a part of built asset.",
"35": "Tile background transfered to map renderer does not exists.",
"36": "Tile background transfered to map renderer has no frame data.",
"37": "Frame of tile background transfered to map renderer must share the same size.",
"38": "Cached texture cannot be convert to asset. This is likely an internal bug, please contact us.",
"39": "Offset pool size exceeds WebGL2 limitation, ensure size type of your big image is less than $1.",
"40": "Material used by map block $1 is not found in built asset. Please ensure you have pack it into asset.",
"41": "You are trying to use a texture on moving block whose offset is not in the offset pool, please build it into asset after loading.",
"42": "The '$1' property of map-render element is required.",
"43": "Cannot bind face direction to main block $1, please call malloc in advance.",
"44": "Cannot bind face direction to main block $1, since main direction cannot be override.",
"45": "Cannot add $1 map renderer extension, since $1 already exists for the given state.",
"46": "Cannot execute close door action on $1,$2, since the given position is not empty.",
"1201": "Floor-damage extension needs 'floor-binder' extension as dependency." "1201": "Floor-damage extension needs 'floor-binder' extension as dependency."
}, },
"warn": { "warn": {
@ -94,6 +114,34 @@
"63": "Uncaught promise error in waiting box component. Error reason: $1", "63": "Uncaught promise error in waiting box component. Error reason: $1",
"64": "Text node type and block type mismatch: '$1' vs '$2'", "64": "Text node type and block type mismatch: '$1' vs '$2'",
"65": "Cannot bind a weather controller twice.", "65": "Cannot bind a weather controller twice.",
"66": "Texture identifier repeated: $1",
"67": "Cannot set alias '$1' for texture $2, since there is already an alias '$3' for it.",
"68": "Cannot set alias '$1' for texture $2, since '$1' is already an alias for $3.",
"69": "Clip totally exceeds texture's dimensions, no operation will be alppied.",
"70": "An TextureAnimater can only be created once.",
"71": "Cannot start animate when animater not created on texture.",
"72": "Frame count delivered to frame-based animater need to exactly divide texture's height.",
"73": "Consistent size is needed when using WebGL2 composer.",
"74": "Frame size not match texture's size, incomplete data will be skipped.",
"75": "Material count splitted by grid does not match alias count, some material may have no alias or some alias may not map to a texture. Splitted: $1, alias: $2.",
"76": "Cannot pipe texture store when asset builder has been started.",
"77": "Texture is clipped to an area of 0. Ensure that the texture contains your clip rect and clip rect's area is not zero.",
"78": "Adding tileset source must follow index order.",
"79": "Assets can only be built once.",
"80": "Parameter count of MapLayer.getMapData must be 0 or 4.",
"81": "Map data to get is partially (or totally) out of range. Overflowed area will be filled with zero.",
"82": "Big image offset size is larger than 64. Considier reduce big image offset bucket.",
"83": "It seems that you call '$1' too frequently. This will extremely affect game's performance, so considering integrate them into one 'updateArea' or 'updateBlockList' call.",
"84": "Cannot set alias '$1' for layer, since '$1' is already an alias for another layer.",
"85": "Hook to load does not belong to current hookable object.",
"86": "Cannot restore vertex data since delivered state does not belong to current vertex generator instance.",
"87": "Map texture asset cache not hit. This has no effect on rendering, but may extremely affect performance, please see the following link to find solution.",
"88": "No hero image is specified, please set image in advance.",
"89": "Cannot set hero image, since there is no image named '$1'.",
"90": "Cannot set hero image, since delivered image has incorrect format or size.",
"91": "Cannot add follower, since specified follower number $1 does not exist.",
"92": "Followers can only be added when the last follower is not moving.",
"93": "Followers can only be removed when the last follower is not moving.",
"1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency." "1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency."
} }
} }

View File

@ -72,8 +72,15 @@ export class Logger {
let paramNum = ''; let paramNum = '';
while (++pointer < text.length) { while (++pointer < text.length) {
const char = text[pointer]; const char = text[pointer];
const next = text[pointer + 1];
if (char === '$' && text[pointer - 1] !== '\\') { if (char === '\\' && next === '$') {
str += '$';
pointer++;
continue;
}
if (char === '$' && nums.has(next)) {
inParam = true; inParam = true;
continue; continue;
} }
@ -82,7 +89,7 @@ export class Logger {
if (nums.has(char)) { if (nums.has(char)) {
paramNum += char; paramNum += char;
} }
if (!nums.has(text[pointer + 1])) { if (!nums.has(next)) {
inParam = false; inParam = false;
const num = Number(paramNum); const num = Number(paramNum);
paramNum = ''; paramNum = '';
@ -125,8 +132,7 @@ export class Logger {
hideTipText(); hideTipText();
} }
const n = Math.floor(code / 50) + 1; const n = Math.floor(code / 50) + 1;
const n2 = code % 50; const url = `${location.origin}/_docs/logger/error/error${n}.html#error-code-${code}`;
const url = `${location.origin}/_docs/logger/error/error${n}.html#error-code-${n2}`;
console.error(`[ERROR Code ${code}] ${text} See ${url}`); console.error(`[ERROR Code ${code}] ${text} See ${url}`);
} }
} }
@ -159,8 +165,7 @@ export class Logger {
hideTipText(); hideTipText();
} }
const n = Math.floor(code / 50) + 1; const n = Math.floor(code / 50) + 1;
const n2 = code % 50; const url = `${location.origin}/_docs/logger/warn/warn${n}.html#warn-code-${code}`;
const url = `${location.origin}/_docs/logger/warn/warn${n}.html#warn-code-${n2}`;
console.warn(`[WARNING Code ${code}] ${text} See ${url}`); console.warn(`[WARNING Code ${code}] ${text} See ${url}`);
} }
} }

View File

@ -0,0 +1,117 @@
//#region 脏标记
export interface IDirtyMarker<T> {
/**
*
* @param data
*/
dirty(data: T): void;
}
export interface IDirtyMark {}
export interface IDirtyTracker<T> {
/**
*
*/
mark(): IDirtyMark;
/**
*
* @param mark
*/
unmark(mark: IDirtyMark): void;
/**
*
* @param mark
*/
dirtySince(mark: IDirtyMark): T;
/**
*
* @param symbol
*/
hasMark(symbol: IDirtyMark): boolean;
}
//#endregion
//#region 钩子
export interface IHookObject<
H extends IHookBase,
C extends IHookController<H>
> {
/** 钩子对象 */
readonly hook: Partial<H>;
/** 钩子控制器 */
readonly controller: C;
}
export interface IHookController<H extends IHookBase = IHookBase> {
/** 控制器的钩子对象 */
readonly hook: Partial<H>;
/**
*
*/
load(): void;
/**
*
*/
unload(): void;
}
export interface IHookBase {
/**
*
* @param controller
*/
awake(controller: IHookController<this>): void;
/**
*
* @param controller
*/
destroy(controller: IHookController<this>): void;
}
export interface IHookable<
H extends IHookBase = IHookBase,
C extends IHookController<H> = IHookController<H>
> {
/**
*
* @param hook
*/
addHook(hook: Partial<H>): C;
/**
*
* @param hook
*/
loadHook(hook: Partial<H>): void;
/**
* `destroy`
* @param hook
*/
removeHook(hook: Partial<H>): void;
/**
*
* @param hook
*/
removeHookByController(hook: C): void;
/**
*
* @param fn
* @returns
*/
forEachHook<T>(fn: (hook: Partial<H>, controller: C) => T): T[];
}
//#endregion

View File

@ -0,0 +1,6 @@
{
"name": "@motajs/render-assets",
"dependencies": {
"@motajs/client-base": "workspace:*"
}
}

View File

@ -0,0 +1,134 @@
import { ITexture, ITextureAnimater, ITextureRenderable } from './types';
/**
*
*/
export class TextureRowAnimater implements ITextureAnimater<number> {
*once(texture: ITexture, frames: number): Generator<ITextureRenderable> {
if (frames <= 0) return;
const renderable = texture.render();
const { x: ox, y: oy } = renderable.rect;
const { width: w, height } = texture!;
const h = height / frames;
for (let i = 0; i < frames; i++) {
const renderable: ITextureRenderable = {
source: texture.source,
rect: texture.clampRect({ x: i * w + ox, y: oy, w, h })
};
yield renderable;
}
}
*cycled(texture: ITexture, frames: number): Generator<ITextureRenderable> {
if (frames <= 0) return;
const renderable = texture.render();
const { x: ox, y: oy } = renderable.rect;
const { width: w, height } = texture;
const h = height / frames;
let i = 0;
while (true) {
const renderable: ITextureRenderable = {
source: texture.source,
rect: texture.clampRect({ x: i * w + ox, y: oy, w, h })
};
yield renderable;
i++;
if (i === frames) i = 0;
}
}
}
/**
*
*/
export class TextureColumnAnimater implements ITextureAnimater<number> {
*once(texture: ITexture, frames: number): Generator<ITextureRenderable> {
if (frames <= 0) return;
const renderable = texture.render();
const { x: ox, y: oy, w: width, h } = renderable.rect;
const w = width / frames;
for (let i = 0; i < frames; i++) {
const renderable: ITextureRenderable = {
source: texture.source,
rect: texture.clampRect({ x: i * w + ox, y: oy, w, h })
};
yield renderable;
}
}
*cycled(texture: ITexture, frames: number): Generator<ITextureRenderable> {
if (frames <= 0) return null;
const renderable = texture.render();
const { x: ox, y: oy } = renderable.rect;
const { width, height: h } = texture;
const w = width / frames;
let i = 0;
while (true) {
const renderable: ITextureRenderable = {
source: texture.source,
rect: texture.clampRect({ x: i * w + ox, y: oy, w, h })
};
yield renderable;
i++;
if (i === frames) i = 0;
}
}
}
export interface IScanAnimaterData {
/** 每帧的宽度 */
readonly width: number;
/** 每帧的高度 */
readonly height: number;
/** 总帧数 */
readonly frames: number;
}
/**
*
*/
export class TextureScanAnimater
implements ITextureAnimater<IScanAnimaterData>
{
*once(
texture: ITexture,
data: IScanAnimaterData
): Generator<ITextureRenderable, void> {
const w = texture.width;
const h = texture.height;
let frame = 0;
for (let y = 0; y < data.width; y++) {
for (let x = 0; x < data.height; x++) {
const renderable: ITextureRenderable = {
source: texture.source,
rect: texture.clampRect({ x: x * w, y: y * h, w, h })
};
yield renderable;
frame++;
if (frame === data.frames) break;
}
}
}
*cycled(
texture: ITexture,
data: IScanAnimaterData
): Generator<ITextureRenderable, void> {
const w = texture.width;
const h = texture.height;
let index = 0;
while (true) {
const x = index % data.width;
const y = Math.floor(index / data.height);
const renderable: ITextureRenderable = {
source: texture.source,
rect: texture.clampRect({ x: x * w, y: y * h, w, h })
};
yield renderable;
index++;
if (index === data.frames) index = 0;
}
}
}

View File

@ -0,0 +1,451 @@
import {
IOption,
IRectangle,
MaxRectsPacker,
Rectangle
} from 'maxrects-packer';
import { Texture } from './texture';
import {
IRect,
ITexture,
ITextureComposedData,
ITextureComposer,
SizedCanvasImageSource
} from './types';
import vert from './shader/pack.vert?raw';
import frag from './shader/pack.frag?raw';
import { logger } from '@motajs/common';
import { isNil } from 'lodash-es';
import { compileProgramWith } from '@motajs/client-base';
interface IndexMarkedComposedData {
/** 组合数据 */
readonly asset: ITextureComposedData;
/** 组合时最后一个用到的贴图的索引 */
readonly index: number;
}
export interface IGridComposerData {
/** 单个贴图的宽度,与之不同的贴图将会被剔除并警告 */
readonly width: number;
/** 单个贴图的宽度,与之不同的贴图将会被剔除并警告 */
readonly height: number;
}
export class TextureGridComposer
implements ITextureComposer<IGridComposerData>
{
/**
*
*
* @param maxWidth
* @param maxHeight
*/
constructor(
readonly maxWidth: number,
readonly maxHeight: number
) {}
private nextAsset(
tex: ITexture[],
start: number,
data: IGridComposerData,
rows: number,
cols: number,
index: number
): IndexMarkedComposedData {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d')!;
canvas.width = this.maxWidth;
canvas.height = this.maxHeight;
const count = Math.min(rows * cols, tex.length - start);
const map = new Map<ITexture, IRect>();
let x = 0;
let y = 0;
for (let i = 0; i < count; i++) {
const dx = x * data.width;
const dy = y * data.height;
const texture = tex[i + start];
const renderable = texture.render();
const { x: sx, y: sy, w: sw, h: sh } = renderable.rect;
ctx.drawImage(renderable.source, sx, sy, sw, sh, dx, dy, sw, sh);
map.set(texture, { x: dx, y: dy, w: sw, h: sh });
x++;
if (x === cols) {
y++;
x = 0;
}
}
const texture = new Texture(canvas);
const composed: ITextureComposedData = {
index,
texture,
assetMap: map
};
return { asset: composed, index: start + count };
}
*compose(
input: Iterable<ITexture>,
data: IGridComposerData
): Generator<ITextureComposedData, void> {
const arr = [...input];
const rows = Math.floor(this.maxWidth / data.width);
const cols = Math.floor(this.maxHeight / data.height);
let arrindex = 0;
let i = 0;
while (arrindex < arr.length) {
const res = this.nextAsset(arr, arrindex, data, rows, cols, i);
arrindex = res.index + 1;
i++;
yield res.asset;
}
}
}
export interface IMaxRectsComposerData extends IOption {
/** 贴图之间的间距 */
readonly padding: number;
}
interface MaxRectsRectangle extends IRectangle {
/** 这个矩形对应的贴图对象 */
readonly data: ITexture;
}
export class TextureMaxRectsComposer
implements ITextureComposer<IMaxRectsComposerData>
{
/**
* 使 Max Rects {@link IMaxRectsComposerData}
* {@link TextureMaxRectsWebGL2Composer}
* @param maxWidth
* @param maxHeight
*/
constructor(
public readonly maxWidth: number,
public readonly maxHeight: number
) {}
*compose(
input: Iterable<ITexture>,
data: IMaxRectsComposerData
): Generator<ITextureComposedData, void> {
const packer = new MaxRectsPacker<MaxRectsRectangle>(
this.maxWidth,
this.maxHeight,
data.padding,
data
);
const arr = [...input];
const rects = arr.map<MaxRectsRectangle>(v => {
const rect = v.render().rect;
const toPack = new Rectangle(rect.w, rect.h);
toPack.data = v;
return toPack;
});
packer.addArray(rects);
let index = 0;
for (const bin of packer.bins) {
const map = new Map<ITexture, IRect>();
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d')!;
canvas.width = this.maxWidth;
canvas.height = this.maxHeight;
ctx.imageSmoothingEnabled = false;
bin.rects.forEach(v => {
const rect: IRect = { x: v.x, y: v.y, w: v.width, h: v.height };
map.set(v.data, rect);
const renderable = v.data.render();
const { x, y, w, h } = renderable.rect;
const source = renderable.source;
ctx.drawImage(source, x, y, w, h, v.x, v.y, v.width, v.height);
});
const texture = new Texture(canvas);
const data: ITextureComposedData = {
index: index++,
texture,
assetMap: map
};
yield data;
}
}
}
interface RectProcessed {
/** 贴图位置映射 */
readonly texMap: Map<ITexture, Readonly<IRect>>;
/** 顶点数组 */
readonly attrib: Float32Array;
}
export class TextureMaxRectsWebGL2Composer
implements ITextureComposer<IMaxRectsComposerData>
{
/** 使用的画布 */
readonly canvas: HTMLCanvasElement;
/** 画布上下文 */
readonly gl: WebGL2RenderingContext;
/** WebGL2 程序 */
readonly program: WebGLProgram;
/** 顶点数组缓冲区 */
readonly vertBuffer: WebGLBuffer;
/** 纹理数组对象 */
readonly texArray: WebGLTexture;
/** 纹理数组对象的位置 */
readonly texArrayPos: WebGLUniformLocation;
/** `a_position` 的索引 */
readonly posPos: number;
/** `a_texCoord` 的索引 */
readonly texPos: number;
/** 本次处理的贴图宽度 */
private opWidth: number = 0;
/** 本次处理的贴图高度 */
private opHeight: number = 0;
/**
* 使 Max Rects 使 WebGL2
* {@link IMaxRectsComposerData}
* `compose`
* 使 `toBitmap`,
* `next`
* @param maxWidth
* @param maxHeight
*/
constructor(
public readonly maxWidth: number,
public readonly maxHeight: number
) {
this.canvas = document.createElement('canvas');
this.canvas.width = maxWidth;
this.canvas.height = maxHeight;
this.gl = this.canvas.getContext('webgl2')!;
const { program } = compileProgramWith(this.gl, vert, frag)!;
this.program = program;
// 初始化画布数据
const texture = this.gl.createTexture();
this.texArray = texture;
const location = this.gl.getUniformLocation(program, 'u_textArray')!;
this.texArrayPos = location;
this.posPos = this.gl.getAttribLocation(program, 'a_position');
this.texPos = this.gl.getAttribLocation(program, 'a_texCoord');
this.vertBuffer = this.gl.createBuffer();
this.gl.useProgram(program);
}
/**
*
* @param textures
*/
private mapTextures(
textures: ITexture[]
): Map<SizedCanvasImageSource, number> {
const map = new Map<SizedCanvasImageSource, number>();
const { width, height } = textures[0].source;
textures.forEach(v => {
const source = v.source;
if (map.has(source)) return;
if (source.width !== width || source.height !== height) {
logger.warn(73);
return;
}
map.set(source, map.size);
});
this.opWidth = width;
this.opHeight = height;
return map;
}
/**
*
* @param texMap
*/
private setTexture(texMap: Map<SizedCanvasImageSource, number>) {
const gl = this.gl;
gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.texArray);
gl.texStorage3D(
gl.TEXTURE_2D_ARRAY,
1,
gl.RGBA8,
this.opWidth,
this.opHeight,
texMap.size
);
texMap.forEach((index, source) => {
gl.texSubImage3D(
gl.TEXTURE_2D_ARRAY,
0,
0,
0,
index,
this.opWidth,
this.opHeight,
1,
gl.RGBA,
gl.UNSIGNED_BYTE,
source
);
});
gl.texParameteri(
gl.TEXTURE_2D_ARRAY,
gl.TEXTURE_MAG_FILTER,
gl.NEAREST
);
gl.texParameteri(
gl.TEXTURE_2D_ARRAY,
gl.TEXTURE_MIN_FILTER,
gl.NEAREST
);
gl.texParameteri(
gl.TEXTURE_2D_ARRAY,
gl.TEXTURE_WRAP_S,
gl.CLAMP_TO_EDGE
);
gl.texParameteri(
gl.TEXTURE_2D_ARRAY,
gl.TEXTURE_WRAP_T,
gl.CLAMP_TO_EDGE
);
}
/**
* WebGL2
* @param rects
* @param texMap
*/
private processRects(
rects: MaxRectsRectangle[],
texMap: Map<SizedCanvasImageSource, number>
): RectProcessed {
const { width: ow, height: oh } = this.canvas;
const map = new Map<ITexture, IRect>();
const attrib = new Float32Array(rects.length * 5 * 6);
rects.forEach((v, i) => {
const rect: IRect = { x: v.x, y: v.y, w: v.width, h: v.height };
map.set(v.data, rect);
const renderable = v.data.render();
const { width: tw, height: th } = v.data.source;
const { x, y, w, h } = renderable.rect;
// 画到目标画布上的位置
const ol = (v.x / ow) * 2 - 1;
const ob = (v.y / oh) * 2 - 1;
const or = ((v.x + v.width) / ow) * 2 - 1;
const ot = ((v.y + v.height) / oh) * 2 - 1;
// 原始贴图位置
const tl = x / tw;
const tt = y / tw;
const tr = (x + w) / tw;
const tb = (y + h) / th;
// 贴图索引
const ti = texMap.get(v.data.source);
if (isNil(ti)) return;
// Benchmark https://www.measurethat.net/Benchmarks/Show/35246/2/different-method-to-write-a-typedarray
// prettier-ignore
const data = [
// x y u v i
ol, -ot, tl, tt, ti, // 左上角
ol, -ob, tl, tb, ti, // 左下角
or, -ot, tr, tt, ti, // 右上角
or, -ot, tr, tt, ti, // 右上角
ol, -ob, tl, tb, ti, // 左下角
or, -ob, tr, tb, ti // 右下角
];
attrib.set(data, i * 30);
});
const data: RectProcessed = {
texMap: map,
attrib
};
return data;
}
/**
*
* @param attrib
*/
private renderAtlas(attrib: Float32Array) {
const gl = this.gl;
gl.clearColor(0, 0, 0, 0);
gl.clearDepth(1);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertBuffer);
gl.bufferData(gl.ARRAY_BUFFER, attrib, gl.DYNAMIC_DRAW);
gl.vertexAttribPointer(this.posPos, 2, gl.FLOAT, false, 5 * 4, 0);
gl.vertexAttribPointer(this.texPos, 3, gl.FLOAT, false, 5 * 4, 2 * 4);
gl.enableVertexAttribArray(this.posPos);
gl.enableVertexAttribArray(this.texPos);
gl.uniform1i(this.texArrayPos, 0);
gl.drawArrays(gl.TRIANGLES, 0, attrib.length / 5);
}
*compose(
input: Iterable<ITexture>,
data: IMaxRectsComposerData
): Generator<ITextureComposedData, void> {
this.opWidth = 0;
this.opHeight = 0;
const packer = new MaxRectsPacker<MaxRectsRectangle>(
this.maxWidth,
this.maxHeight,
data.padding,
data
);
const arr = [...input];
const rects = arr.map<MaxRectsRectangle>(v => {
const rect = v.render().rect;
const toPack = new Rectangle(rect.w, rect.h);
toPack.data = v;
return toPack;
});
packer.addArray(rects);
const indexMap = this.mapTextures(arr);
this.setTexture(indexMap);
let index = 0;
for (const bin of packer.bins) {
const { texMap, attrib } = this.processRects(bin.rects, indexMap);
this.renderAtlas(attrib);
const texture = new Texture(this.canvas);
const data: ITextureComposedData = {
index: index++,
texture,
assetMap: texMap
};
yield data;
}
this.gl.disableVertexAttribArray(this.posPos);
this.gl.disableVertexAttribArray(this.texPos);
}
}

View File

@ -0,0 +1,7 @@
export * from './animater';
export * from './composer';
export * from './splitter';
export * from './store';
export * from './streamComposer';
export * from './texture';
export * from './types';

View File

@ -0,0 +1,12 @@
#version 300 es
precision highp float;
precision highp sampler2DArray;
in vec3 v_texCoord;
out vec4 outColor;
uniform sampler2DArray u_texArray;
void main() {
outColor = texture(u_texArray, v_texCoord);
}

View File

@ -0,0 +1,12 @@
#version 300 es
precision highp float;
in vec2 a_position;
in vec3 a_texCoord;
out vec3 v_texCoord;
void main() {
v_texCoord = a_texCoord;
gl_Position = vec4(a_position, 0.0, 1.0);
}

View File

@ -0,0 +1,68 @@
import { Texture } from './texture';
import { ITexture, ITextureSplitter, IRect } from './types';
/**
*
*
*/
export class TextureRowSplitter implements ITextureSplitter<number> {
*split(texture: ITexture, data: number): Generator<ITexture> {
const lines = Math.ceil(texture.height / data);
const { x, y } = texture.render().rect;
for (let i = 0; i < lines; i++) {
const tex = new Texture(texture.source);
tex.clip(x, y + i * data, texture.width, data);
yield tex;
}
}
}
/**
*
*
*/
export class TextureColumnSplitter implements ITextureSplitter<number> {
*split(texture: ITexture, data: number): Generator<ITexture> {
const lines = Math.ceil(texture.width / data);
const { x, y } = texture.render().rect;
for (let i = 0; i < lines; i++) {
const tex = new Texture(texture.source);
tex.clip(x + i * data, y, data, texture.height);
yield tex;
}
}
}
/**
*
*
*/
export class TextureGridSplitter implements ITextureSplitter<[number, number]> {
*split(texture: ITexture, data: [number, number]): Generator<ITexture> {
const [w, h] = data;
const rows = Math.ceil(texture.width / w);
const lines = Math.ceil(texture.height / h);
const { x, y } = texture.render().rect;
for (let ny = 0; ny < lines; ny++) {
for (let nx = 0; nx < rows; nx++) {
const tex = new Texture(texture.source);
tex.clip(x + nx * w, y + ny * h, w, h);
yield tex;
}
}
}
}
/**
*
*
*/
export class TextureAssetSplitter implements ITextureSplitter<IRect[]> {
*split(texture: ITexture, data: IRect[]): Generator<ITexture> {
for (const { x, y, w, h } of data) {
const tex = new Texture(texture.source);
tex.clip(x, y, w, h);
yield tex;
}
}
}

View File

@ -0,0 +1,107 @@
import { isNil } from 'lodash-es';
import { ITexture, ITextureStore } from './types';
import { logger } from '@motajs/common';
export class TextureStore<T extends ITexture = ITexture>
implements ITextureStore<T>
{
private readonly texMap: Map<number, T> = new Map();
private readonly invMap: Map<T, number> = new Map();
private readonly aliasMap: Map<string, number> = new Map();
private readonly aliasInvMap: Map<number, string> = new Map();
[Symbol.iterator](): Iterator<[key: number, tex: T]> {
return this.texMap.entries();
}
entries(): Iterable<[key: number, tex: T]> {
return this.texMap.entries();
}
keys(): Iterable<number> {
return this.texMap.keys();
}
values(): Iterable<T> {
return this.texMap.values();
}
addTexture(identifier: number, texture: T): void {
if (this.texMap.has(identifier)) {
logger.warn(66, identifier.toString());
return;
}
this.texMap.set(identifier, texture);
this.invMap.set(texture, identifier);
}
private removeBy(id: number, tex: T, alias?: string) {
this.texMap.delete(id);
this.invMap.delete(tex);
if (alias) {
this.aliasMap.delete(alias);
this.aliasInvMap.delete(id);
}
}
removeTexture(identifier: number | string | T): void {
if (typeof identifier === 'string') {
const id = this.aliasMap.get(identifier);
if (isNil(id)) return;
const tex = this.texMap.get(id);
if (isNil(tex)) return;
this.removeBy(id, tex, identifier);
} else if (typeof identifier === 'number') {
const tex = this.texMap.get(identifier);
if (isNil(tex)) return;
const alias = this.aliasInvMap.get(identifier);
this.removeBy(identifier, tex, alias);
} else {
const id = this.invMap.get(identifier);
if (isNil(id)) return;
const alias = this.aliasInvMap.get(id);
this.removeBy(id, identifier, alias);
}
}
hasTexture(identifier: number): boolean {
return this.texMap.has(identifier);
}
getTexture(identifier: number): T | null {
return this.texMap.get(identifier) ?? null;
}
alias(identifier: number, alias: string): void {
const id = this.aliasMap.get(alias);
const al = this.aliasInvMap.get(identifier);
if (!isNil(al)) {
logger.warn(67, alias, identifier.toString(), al);
return;
}
if (!isNil(id)) {
logger.warn(68, alias, identifier.toString(), id.toString());
return;
}
this.aliasMap.set(alias, identifier);
this.aliasInvMap.set(identifier, alias);
}
fromAlias(alias: string): ITexture | null {
const id = this.aliasMap.get(alias);
if (isNil(id)) return null;
return this.texMap.get(id) ?? null;
}
idOf(texture: T): number | undefined {
return this.invMap.get(texture);
}
aliasOf(identifier: number): string | undefined {
return this.aliasInvMap.get(identifier);
}
identifierOf(alias: string): number | undefined {
return this.aliasMap.get(alias);
}
}

View File

@ -0,0 +1,201 @@
import {
Bin,
IOption,
IRectangle,
MaxRectsPacker,
Rectangle
} from 'maxrects-packer';
import { Texture } from './texture';
import {
IRect,
ITexture,
ITextureComposedData,
ITextureStreamComposer
} from './types';
export class TextureGridStreamComposer implements ITextureStreamComposer<void> {
readonly rows: number;
readonly cols: number;
private nowIndex: number = 0;
private outputIndex: number = -1;
private nowTexture: ITexture;
private nowCanvas: HTMLCanvasElement;
private nowCtx: CanvasRenderingContext2D;
private nowMap: Map<ITexture, Readonly<IRect>>;
/**
*
*
* @param width
* @param height
* @param maxWidth
* @param maxHeight
*/
constructor(
readonly width: number,
readonly height: number,
readonly maxWidth: number,
readonly maxHeight: number
) {
this.rows = Math.floor(maxHeight / height);
this.cols = Math.floor(maxWidth / width);
this.nowCanvas = document.createElement('canvas');
this.nowCtx = this.nowCanvas.getContext('2d')!;
this.nowMap = new Map();
this.nowTexture = new Texture(this.nowCanvas);
}
private nextCanvas() {
this.nowCanvas = document.createElement('canvas');
this.nowCtx = this.nowCanvas.getContext('2d')!;
this.nowMap = new Map();
this.nowIndex = 0;
this.nowTexture = new Texture(this.nowCanvas);
this.outputIndex++;
}
*add(textures: Iterable<ITexture>): Generator<ITextureComposedData, void> {
let index = this.nowIndex;
const max = this.cols * this.rows;
for (const tex of textures) {
const nowRow = Math.floor(index / this.cols);
const nowCol = index % this.cols;
const { source, rect } = tex.render();
const { x: cx, y: cy, w: cw, h: ch } = rect;
const x = nowRow * this.width;
const y = nowCol * this.height;
this.nowCtx.drawImage(source, cx, cy, cw, ch, x, y, cw, ch);
this.nowMap.set(tex, { x, y, w: cw, h: ch });
if (++index === max) {
const data: ITextureComposedData = {
index: this.outputIndex,
texture: this.nowTexture,
assetMap: this.nowMap
};
yield data;
this.nextCanvas();
}
}
const data: ITextureComposedData = {
index: this.outputIndex,
texture: this.nowTexture,
assetMap: this.nowMap
};
yield data;
}
close(): void {
// We need to do nothing.
}
}
interface MaxRectsRectangle extends IRectangle {
/** 这个矩形对应的贴图对象 */
readonly data: ITexture;
}
export class TextureMaxRectsStreamComposer
implements ITextureStreamComposer<void>
{
/** Max Rects 打包器 */
readonly packer: MaxRectsPacker<MaxRectsRectangle>;
private outputIndex: number = -1;
private nowTexture!: ITexture;
private nowCanvas!: HTMLCanvasElement;
private nowCtx!: CanvasRenderingContext2D;
private nowMap!: Map<ITexture, Readonly<IRect>>;
private nowBin: number = 0;
/**
* 使 Max Rects
* @param maxWidth
* @param maxHeight
* @param padding
* @param options
*/
constructor(
readonly maxWidth: number,
readonly maxHeight: number,
readonly padding: number,
options?: IOption
) {
this.packer = new MaxRectsPacker<MaxRectsRectangle>(
this.maxWidth,
this.maxHeight,
padding,
options
);
this.nextCanvas();
}
private nextCanvas() {
this.nowCanvas = document.createElement('canvas');
this.nowCtx = this.nowCanvas.getContext('2d')!;
this.nowCanvas.width = this.maxWidth;
this.nowCanvas.height = this.maxHeight;
this.nowMap = new Map();
this.outputIndex++;
this.nowTexture = new Texture(this.nowCanvas);
}
*add(textures: Iterable<ITexture>): Generator<ITextureComposedData, void> {
const arr = [...textures];
const rects = arr.map<MaxRectsRectangle>(v => {
const rect = v.render().rect;
const toPack = new Rectangle(rect.w, rect.h);
toPack.data = v;
return toPack;
});
this.packer.addArray(rects);
const bins = this.packer.bins;
if (bins.length === 0) return;
const toYield: Bin<MaxRectsRectangle>[] = [bins[bins.length - 1]];
if (bins.length > this.nowBin) {
toYield.push(...bins.slice(this.nowBin));
}
for (let i = this.nowBin; i < bins.length; i++) {
const rects = bins[i].rects;
rects.forEach(v => {
if (this.nowMap.has(v.data)) return;
const target: IRect = {
x: v.x,
y: v.y,
w: v.width,
h: v.height
};
this.nowMap.set(v.data, target);
const { source, rect } = v.data.render();
const { x: cx, y: cy, w: cw, h: ch } = rect;
this.nowCtx.drawImage(source, cx, cy, cw, ch, v.x, v.y, cw, ch);
});
const data: ITextureComposedData = {
index: this.outputIndex,
texture: this.nowTexture,
assetMap: this.nowMap
};
yield data;
if (i < bins.length - 1) {
this.nextCanvas();
}
}
this.nowBin = bins.length - 1;
}
close(): void {
// We need to do nothing.
}
}

View File

@ -0,0 +1,193 @@
import { logger } from '@motajs/common';
import {
IRect,
ITexture,
ITextureComposedData,
ITextureRenderable,
ITextureSplitter,
SizedCanvasImageSource
} from './types';
import { clamp } from 'lodash-es';
export class Texture implements ITexture {
source: SizedCanvasImageSource;
width: number;
height: number;
isBitmap: boolean = false;
/** 裁剪矩形的左边框位置 */
private cl: number;
/** 裁剪矩形的上边框位置 */
private ct: number;
/** 裁剪矩形的右边框位置 */
private cr: number;
/** 裁剪矩形的下边框位置 */
private cb: number;
constructor(source: SizedCanvasImageSource) {
this.source = source;
this.width = source.width;
this.height = source.height;
this.cl = 0;
this.ct = 0;
this.cr = source.width;
this.cb = source.height;
}
/**
*
* @param x
* @param y
* @param w
* @param h
*/
clip(x: number, y: number, w: number, h: number) {
const left = clamp(this.cl + x, this.cl, this.cr);
const top = clamp(this.ct + y, this.ct, this.cb);
const right = clamp(this.cl + x + w, this.cl, this.cr);
const bottom = clamp(this.ct + y + h, this.ct, this.cb);
if (left === right || top === bottom) {
logger.warn(69);
return;
}
const width = right - left;
const height = bottom - top;
if (width <= 0 || height <= 0) {
logger.warn(77);
return;
}
this.cl = left;
this.ct = top;
this.cr = right;
this.cb = bottom;
this.width = right - left;
this.height = bottom - top;
}
async toBitmap(): Promise<void> {
if (this.source instanceof ImageBitmap) return;
this.source = await createImageBitmap(
this.source,
this.cl,
this.ct,
this.width,
this.height
);
this.cl = 0;
this.ct = 0;
this.cr = this.width;
this.cb = this.height;
this.isBitmap = true;
}
split<U>(splitter: ITextureSplitter<U>, data: U): Generator<ITexture> {
return splitter.split(this, data);
}
render(): ITextureRenderable {
return {
source: this.source,
rect: { x: this.cl, y: this.ct, w: this.width, h: this.height }
};
}
clampRect(rect: Readonly<IRect>): Readonly<IRect> {
const l = clamp(rect.x, this.cl, this.cr);
const t = clamp(rect.y, this.ct, this.cb);
const r = clamp(rect.x + rect.w, this.cl, this.cr);
const b = clamp(rect.y + rect.h, this.ct, this.cb);
return { x: l, y: t, w: r - l, h: b - t };
}
clipped(rect: Readonly<IRect>): ITextureRenderable {
return {
source: this.source,
rect: this.clampRect(rect)
};
}
dispose(): void {
if (this.source instanceof ImageBitmap) {
this.source.close();
}
}
toAsset(asset: ITextureComposedData): boolean {
const rect = asset.assetMap.get(this);
if (!rect) return false;
if (this.isBitmap && this.source instanceof ImageBitmap) {
this.source.close();
}
this.isBitmap = false;
this.source = asset.texture.source;
this.cl = rect.x;
this.ct = rect.y;
this.width = rect.w;
this.height = rect.h;
this.cr = rect.x + rect.w;
this.cb = rect.y + rect.h;
return true;
}
/**
*
*
* @param texture
* @param animate
* @param ox
* @param oy
*/
static *translated(
texture: ITexture,
animate: Generator<ITextureRenderable, void> | null,
ox: number,
oy: number
): Generator<ITextureRenderable, void> | null {
if (!animate) return null;
while (true) {
const next = animate.next();
if (next.done) break;
const renderable = next.value;
const { x, y, w, h } = renderable.rect;
const translated: IRect = { x: x + ox, y: y + oy, w, h };
const res: ITextureRenderable = {
source: texture.source,
rect: texture.clampRect(translated)
};
yield res;
}
}
/**
* `fx` `fy`
* @param texture
* @param origin
* @param animate
* @param fx
* @param fy
*/
static *fixed(
texture: ITexture,
origin: ITexture,
animate: Generator<ITextureRenderable, void> | null,
fx: number,
fy: number
): Generator<ITextureRenderable, void> | null {
if (!animate) return null;
const { x: ox, y: oy } = origin.render().rect;
while (true) {
const next = animate.next();
if (next.done) break;
const renderable = next.value;
const { x, y, w, h } = renderable.rect;
const translated: IRect = { x: x - ox + fx, y: y - oy + fy, w, h };
const res: ITextureRenderable = {
source: texture.source,
rect: texture.clampRect(translated)
};
yield res;
}
}
}

Some files were not shown because too many files have changed in this diff Show More