Compare commits

..

No commits in common. "fe67939a058b2d162c45547d232adf09f133aa58" and "93fb788befe662fe8acc3d366266f4077ac9ad20" have entirely different histories.

242 changed files with 13332 additions and 12755 deletions

View File

@ -1,39 +1,35 @@
import { basename, join, resolve } from 'node:path'; import fs from 'fs-extra';
import path from 'path';
import chokidar from 'chokidar';
import { DefaultTheme } from 'vitepress'; import { DefaultTheme } from 'vitepress';
import { readdir, stat, writeFile } from 'node:fs/promises';
const apiDir = resolve('./docs/api'); const apiDir = path.resolve('./docs/api');
const sidebarConfigPath = resolve('./docs/.vitepress/apiSidebar.ts'); const sidebarConfigPath = path.resolve('./docs/.vitepress/apiSidebar.ts');
const weight: Record<string, number> = { const weight: Record<string, number> = {
主页: 10, 主页: 10,
函数: 5 函数: 5
}; };
export async function generateSidebar(): Promise<void> { function generateSidebar(): void {
const sidebar: DefaultTheme.SidebarItem[] = [ const sidebar: DefaultTheme.SidebarItem[] = [
{ text: '目录', link: '/api/' } { text: '目录', link: '/api/' }
]; ];
// 遍历 api 目录,查找 package 目录 // 遍历 api 目录,查找 package 目录
const dir = await readdir(apiDir); const packages = fs
const packages = []; .readdirSync(apiDir)
for (const pkg of dir) { .filter(pkg => fs.statSync(path.join(apiDir, pkg)).isDirectory());
const stats = await stat(join(apiDir, pkg));
if (stats.isDirectory()) {
packages.push(pkg);
}
}
await Promise.all( packages.forEach(pkg => {
packages.map(async pkg => { const pkgPath = path.join(apiDir, pkg);
const pkgPath = join(apiDir, pkg); const files = fs
const dir = await readdir(pkgPath); .readdirSync(pkgPath)
const files = dir.filter(file => file.endsWith('.md')); .filter(file => file.endsWith('.md'));
const items: DefaultTheme.SidebarItem[] = files.map(file => { const items: DefaultTheme.SidebarItem[] = files.map(file => {
const filePath = `api/${pkg}/${file}`; const filePath = `api/${pkg}/${file}`;
const fileName = basename(file, '.md'); const fileName = path.basename(file, '.md');
return { return {
text: text:
@ -57,8 +53,7 @@ export async function generateSidebar(): Promise<void> {
collapsed: true, collapsed: true,
items items
}); });
}) });
);
// 生成 sidebar.ts // 生成 sidebar.ts
const sidebarContent = `import { DefaultTheme } from 'vitepress'; const sidebarContent = `import { DefaultTheme } from 'vitepress';
@ -68,6 +63,35 @@ export default ${JSON.stringify(
null, null,
4 4
)} as DefaultTheme.SidebarItem[];`; )} as DefaultTheme.SidebarItem[];`;
await writeFile(sidebarConfigPath, sidebarContent); fs.writeFileSync(sidebarConfigPath, sidebarContent);
console.log('✅ Sidebar 配置已更新'); console.log('✅ Sidebar 配置已更新');
} }
// 初次运行
generateSidebar();
// 监听文件变动
chokidar
.watch(apiDir, { ignoreInitial: true })
.on('add', filePath => {
console.log(`📄 文件新增: ${filePath}`);
generateSidebar();
})
.on('unlink', filePath => {
console.log(`❌ 文件删除: ${filePath}`);
generateSidebar();
})
.on('addDir', dirPath => {
console.log(`📁 目录新增: ${dirPath}`);
generateSidebar();
})
.on('unlinkDir', dirPath => {
console.log(`📁 目录删除: ${dirPath}`);
generateSidebar();
})
.on('raw', (event, path, details) => {
if (event === 'rename') {
console.log(`🔄 文件或文件夹重命名: ${path}`);
generateSidebar();
}
});

View File

@ -1,39 +1,7 @@
import { defineConfig, Plugin } from 'vitepress'; import { defineConfig } from 'vitepress';
import { MermaidMarkdown, MermaidPlugin } from 'vitepress-plugin-mermaid'; import { MermaidMarkdown, MermaidPlugin } from 'vitepress-plugin-mermaid';
import api from './apiSidebar'; import api from './apiSidebar';
import { join } from 'path'; import { join } from 'path';
import { generateSidebar } from './api';
function listenSidebar(): Plugin {
return {
name: 'sidebar-listen',
configureServer(server) {
server.watcher
.on('add', filePath => {
console.log(`📄 文件新增: ${filePath}`);
generateSidebar();
})
.on('unlink', filePath => {
console.log(`❌ 文件删除: ${filePath}`);
generateSidebar();
})
.on('addDir', dirPath => {
console.log(`📁 目录新增: ${dirPath}`);
generateSidebar();
})
.on('unlinkDir', dirPath => {
console.log(`📁 目录删除: ${dirPath}`);
generateSidebar();
})
.on('raw', (event, path, _) => {
if (event === 'rename') {
console.log(`🔄 文件或文件夹重命名: ${path}`);
generateSidebar();
}
});
}
};
}
// https://vitepress.dev/reference/site-config // https://vitepress.dev/reference/site-config
export default defineConfig({ export default defineConfig({
@ -189,7 +157,7 @@ export default defineConfig({
}, },
vite: { vite: {
// @ts-expect-error 类型错误 // @ts-expect-error 类型错误
plugins: [MermaidPlugin(), listenSidebar()], plugins: [MermaidPlugin()],
optimizeDeps: { optimizeDeps: {
include: ['mermaid'] include: ['mermaid']
}, },

View File

@ -1,5 +0,0 @@
import { generateSidebar } from './api';
(() => {
generateSidebar();
})();

View File

@ -5,7 +5,6 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "tsx script/dev.ts", "dev": "tsx script/dev.ts",
"test": "vitest",
"preview": "vite preview", "preview": "vite preview",
"declare": "tsx script/declare.ts", "declare": "tsx script/declare.ts",
"type": "vue-tsc --noEmit", "type": "vue-tsc --noEmit",
@ -13,83 +12,84 @@
"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": "tsx docs/.vitepress/init.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",
"docs:preview": "vitepress preview docs", "docs:preview": "vitepress preview docs",
"pack:template": "tsx script/pack-template.ts" "pack:template": "tsx script/pack-template.ts"
}, },
"dependencies": { "dependencies": {
"@ant-design/icons-vue": "^6.1.0", "@ant-design/icons-vue": "^6.1.0",
"@wasm-audio-decoders/ogg-vorbis": "^0.1.20", "@wasm-audio-decoders/ogg-vorbis": "^0.1.16",
"anon-tokyo": "0.0.0-alpha.0", "anon-tokyo": "0.0.0-alpha.0",
"ant-design-vue": "^3.2.20", "ant-design-vue": "^3.2.20",
"axios": "^1.13.6", "axios": "^1.8.4",
"chart.js": "^4.5.1", "chart.js": "^4.4.8",
"codec-parser": "^2.5.0", "codec-parser": "^2.5.0",
"eventemitter3": "^5.0.4", "eventemitter3": "^5.0.1",
"gl-matrix": "^3.4.4", "gl-matrix": "^3.4.3",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"lodash-es": "^4.17.23", "lodash-es": "^4.17.21",
"lz-string": "^1.5.0", "lz-string": "^1.5.0",
"maxrects-packer": "^2.7.3", "maxrects-packer": "^2.7.3",
"mutate-animate": "^1.4.2", "mutate-animate": "^1.4.2",
"ogg-opus-decoder": "^1.7.3", "ogg-opus-decoder": "^1.6.14",
"opus-decoder": "^0.7.11", "opus-decoder": "^0.7.7",
"vue": "^3.5.29" "vue": "^3.5.20"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "^7.28.6", "@babel/cli": "^7.26.4",
"@babel/core": "^7.29.0", "@babel/core": "^7.26.10",
"@babel/preset-env": "^7.29.0", "@babel/preset-env": "^7.26.9",
"@eslint/js": "^9.39.4", "@eslint/js": "^9.24.0",
"@rollup/plugin-babel": "^6.1.0", "@rollup/plugin-babel": "^6.0.4",
"@rollup/plugin-commonjs": "^25.0.8", "@rollup/plugin-commonjs": "^25.0.8",
"@rollup/plugin-json": "^6.1.0", "@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.3.1", "@rollup/plugin-node-resolve": "^15.3.1",
"@rollup/plugin-replace": "^5.0.7", "@rollup/plugin-replace": "^5.0.7",
"@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^11.1.6", "@rollup/plugin-typescript": "^11.1.6",
"@types/archiver": "^6.0.4", "@types/archiver": "^6.0.3",
"@types/babel__core": "^7.20.5", "@types/babel__core": "^7.20.5",
"@types/express": "^5.0.6", "@types/express": "^5.0.3",
"@types/fontmin": "^0.9.5", "@types/fontmin": "^0.9.5",
"@types/fs-extra": "^11.0.4", "@types/fs-extra": "^11.0.4",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/node": "^22.19.15", "@types/node": "^22.18.0",
"@types/ws": "^8.18.1", "@types/ws": "^8.18.0",
"@vitejs/plugin-legacy": "^7.2.1", "@vitejs/plugin-legacy": "^7.2.1",
"@vitejs/plugin-vue": "^6.0.4", "@vitejs/plugin-vue": "^6.0.1",
"@vitejs/plugin-vue-jsx": "^5.1.4", "@vitejs/plugin-vue-jsx": "^5.1.1",
"archiver": "^7.0.1", "archiver": "^7.0.1",
"chokidar": "^3.6.0", "chokidar": "^3.6.0",
"compressing": "^1.10.4", "compressing": "^1.10.1",
"eslint": "^9.39.4", "concurrently": "^9.1.2",
"eslint": "^9.22.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5", "eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"eslint-plugin-vue": "^9.33.0", "eslint-plugin-vue": "^9.33.0",
"express": "^5.2.1", "express": "^5.1.0",
"fontmin": "^2.0.3", "fontmin": "^2.0.3",
"fs-extra": "^11.3.4", "form-data": "^4.0.2",
"glob": "^11.1.0", "fs-extra": "^11.3.1",
"glob": "^11.0.1",
"globals": "^15.15.0", "globals": "^15.15.0",
"less": "^4.5.1", "less": "^4.2.2",
"madge": "^8.0.0", "madge": "^8.0.0",
"markdown-it-mathjax3": "^4.3.2", "markdown-it-mathjax3": "^4.3.2",
"mermaid": "^11.12.3", "mermaid": "^11.5.0",
"postcss-preset-env": "^9.6.0", "postcss-preset-env": "^9.6.0",
"prettier": "^3.8.1", "prettier": "^3.6.2",
"rollup": "^4.59.0", "rollup": "^4.49.0",
"terser": "^5.46.0", "terser": "^5.39.0",
"tsx": "^4.21.0", "tsx": "^4.20.5",
"typescript": "6.0.1-rc", "typescript": "^5.9.2",
"typescript-eslint": "^8.57.0", "typescript-eslint": "^8.27.0",
"vite": "^7.3.1", "vite": "^7.0.0",
"vite-plugin-dts": "^4.5.4", "vite-plugin-dts": "^4.5.4",
"vitepress": "^1.6.4", "vitepress": "^1.6.3",
"vitepress-plugin-mermaid": "^2.0.17", "vitepress-plugin-mermaid": "^2.0.17",
"vitest": "^4.0.18", "vue-tsc": "^2.2.8",
"vue-tsc": "^2.2.12", "ws": "^8.18.1"
"ws": "^8.19.0"
} }
} }

View File

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

View File

@ -1,16 +1,7 @@
import { loading } from '@user/data-base'; import { createMaterial } from './material';
import { createMaterial, fallbackLoad } from './material';
import { materials } from './ins';
export function create() { export function create() {
createMaterial(); createMaterial();
loading.once('loaded', () => {
fallbackLoad(materials);
loading.emit('assetBuilt');
});
} }
export * from './load';
export * from './material'; export * from './material';
export * from './ins';

View File

@ -1,47 +0,0 @@
import {
AudioType,
BGMPlayer,
MotaAudioContext,
OpusDecoder,
SoundPlayer,
VorbisDecoder
} from '@motajs/audio';
import { MotaAssetsLoader } from './load/loader';
import { AutotileProcessor, MaterialManager } from './material';
import { dataLoader, loadProgress } from '@user/data-base';
//#region 音频实例
/** 游戏全局音频上下文 */
export const audioContext = new MotaAudioContext();
/** 音效播放器 */
export const soundPlayer = new SoundPlayer(audioContext);
/** 音乐播放器 */
export const bgmPlayer = new BGMPlayer(audioContext);
audioContext.registerDecoder(AudioType.Opus, () => new OpusDecoder());
audioContext.registerDecoder(AudioType.Ogg, () => new VorbisDecoder());
//#endregion
//#region 素材实例
/** 素材管理器 */
export const materials = new MaterialManager();
/** 自动元件处理器 */
export const autotile = new AutotileProcessor(materials);
//#endregion
//#region 加载实例
/** 客户端加载实例 */
export const loader = new MotaAssetsLoader(
loadProgress,
dataLoader,
audioContext,
soundPlayer,
materials
);
//#endregion

View File

@ -1,37 +0,0 @@
export const iconNames: string[] = [
'floor',
'lv',
'hpmax',
'hp',
'atk',
'def',
'mdef',
'money',
'exp',
'up',
'book',
'fly',
'toolbox',
'keyboard',
'shop',
'save',
'load',
'settings',
'play',
'pause',
'stop',
'speedDown',
'speedUp',
'rewind',
'equipbox',
'mana',
'skill',
'btn1',
'btn2',
'btn3',
'btn4',
'btn5',
'btn6',
'btn7',
'btn8'
];

View File

@ -1 +0,0 @@
export * from './loader';

View File

@ -1,505 +0,0 @@
import {
ILoadProgressTotal,
LoadDataType,
ILoadTask,
LoadTask,
ILoadTaskProcessor
} from '@motajs/loader';
import {
CompressedUsage,
CustomLoadFunc,
ICompressedMotaAssetsData,
ICompressedMotaAssetsLoadList,
IMotaAssetsLoader
} from './types';
import JSZip from 'jszip';
import {
LoadAudioProcessor,
LoadFontProcessor,
LoadImageProcessor,
LoadTextProcessor,
LoadZipProcessor
} from '@user/data-base';
import { IMotaAudioContext, ISoundPlayer } from '@motajs/audio';
import { loading } from '@user/data-base';
import { IMaterialManager } from '../material';
import { ITextureSplitter, Texture, TextureRowSplitter } from '@motajs/render';
import { iconNames } from './data';
import { IMotaDataLoader } from '@user/data-base';
export class MotaAssetsLoader implements IMotaAssetsLoader {
/** 当前是否正在进行加载 */
loading: boolean = false;
/** 当前加载工作是否已经完成 */
loaded: boolean = false;
readonly imageProcessor: ILoadTaskProcessor<LoadDataType.Blob, ImageBitmap>;
readonly audioProcessor: ILoadTaskProcessor<
LoadDataType.Uint8Array,
AudioBuffer | null
>;
readonly fontProcessor: ILoadTaskProcessor<
LoadDataType.ArrayBuffer,
FontFace
>;
readonly textProcessor: ILoadTaskProcessor<LoadDataType.Text, string>;
readonly zipProcessor: ILoadTaskProcessor<LoadDataType.ArrayBuffer, JSZip>;
/** 素材索引 */
private materialsCounter: number = 0;
/** 贴图行分割器,用于处理遗留 `icons.png` */
private readonly rowSplitter: ITextureSplitter<number>;
constructor(
readonly progress: ILoadProgressTotal,
private readonly dataLoader: IMotaDataLoader,
private readonly ac: IMotaAudioContext,
private readonly sounds: ISoundPlayer<SoundIds>,
private readonly materials: IMaterialManager
) {
this.imageProcessor = new LoadImageProcessor();
this.audioProcessor = new LoadAudioProcessor(ac);
this.fontProcessor = new LoadFontProcessor();
this.textProcessor = new LoadTextProcessor();
this.zipProcessor = new LoadZipProcessor();
this.rowSplitter = new TextureRowSplitter();
}
//#region 其他处理
private splitMaterialIcons(image: ImageBitmap) {
const tex = new Texture(image);
const splitted = [...this.rowSplitter.split(tex, 32)];
for (let i = 0; i < splitted.length; i++) {
const name = iconNames[i] ? `icon-${iconNames[i]}` : `icons-${i}`;
// todo: 早晚删除 icons.png
const index = this.materialsCounter++;
this.materials.imageStore.addTexture(index, splitted[i]);
this.materials.imageStore.alias(index, name);
}
}
//#region 加载后处理
/**
*
* @param font
* @param fontFace `FontFace`
*/
private fontLoaded(font: string, fontFace: FontFace) {
const suffix = font.lastIndexOf('.');
const family = font.slice(0, suffix);
fontFace.family = family;
document.fonts.add(fontFace);
return Promise.resolve();
}
/**
*
* @param name
* @param image `ImageBitmap`
*/
private customImagesLoaded(name: ImageIds, image: ImageBitmap) {
core.material.images.images[name] = image;
this.materials.addImage(image, {
index: this.materialsCounter++,
alias: name
});
return Promise.resolve();
}
/**
*
* @param name
* @param buffer `AudioBuffer`
*/
private soundLoaded(name: SoundIds, buffer: AudioBuffer | null) {
if (buffer) {
this.sounds.add(name, buffer);
}
return Promise.resolve();
}
/**
* tileset
* @param name tileset
* @param image `ImageBitmap`
*/
private tilesetLoaded(name: string, image: ImageBitmap) {
core.material.images.tilesets[name] = image;
// this.materials.addTileset(image, {
// index: this.materialsCounter++,
// alias: name
// });
return Promise.resolve();
}
/**
*
* @param autotiles
* @param name
* @param image `ImageBitmap`
*/
private autotileLoaded(
autotiles: Partial<Record<AllIdsOf<'autotile'>, ImageBitmap>>,
name: AllIdsOf<'autotile'>,
image: ImageBitmap
) {
autotiles[name] = image;
loading.addAutotileLoaded();
loading.onAutotileLoaded(autotiles);
core.material.images.autotile[name] = image;
// const num = icon.autotile[name];
// this.materials.addAutotile(image, {
// id: name,
// num,
// cls: 'autotile'
// });
return Promise.resolve();
}
/**
*
* @param name
* @param image `ImageBitmap`
*/
private materialLoaded(name: string, image: ImageBitmap) {
core.material.images[
name.slice(0, -4) as SelectKey<MaterialImages, ImageBitmap>
] = image;
if (name === 'icons.png') {
this.splitMaterialIcons(image);
}
return Promise.resolve();
}
/**
*
* @param animation
*/
private animationLoaded(animation: string) {
const data = data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d;
const rows = animation.split('@@@~~~###~~~@@@');
rows.forEach((value, i) => {
const id = data.main.animates[i];
if (value.length === 0) {
throw new Error(`Cannot find animate: '${id}'`);
}
core.material.animates[id] = core.loader._loadAnimate(value);
});
return Promise.resolve();
}
//#endregion
//#region 加载流程
/**
*
*/
private developingLoad() {
const data = data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d;
const icon = icons_4665ee12_3a1f_44a4_bea3_0fccba634dc1;
// font
data.main.fonts.forEach(font => {
const url = `project/fonts/${font}`;
const task = new LoadTask<LoadDataType.ArrayBuffer, FontFace>({
url,
identifier: `@system-font/${font}`,
dataType: LoadDataType.ArrayBuffer,
processor: this.fontProcessor,
progress: this.progress
});
this.addCustomLoadTask(task, data => this.fontLoaded(font, data));
});
// image
data.main.images.forEach(image => {
const url = `project/images/${image}`;
const task = new LoadTask<LoadDataType.Blob, ImageBitmap>({
url,
identifier: `@system-image/${image}`,
dataType: LoadDataType.Blob,
processor: this.imageProcessor,
progress: this.progress
});
this.addCustomLoadTask(task, data =>
this.customImagesLoaded(image, data)
);
});
// sound
data.main.sounds.forEach(sound => {
const url = `project/sounds/${sound}`;
const task = new LoadTask<
LoadDataType.Uint8Array,
AudioBuffer | null
>({
url,
identifier: `@system-sound/${sound}`,
dataType: LoadDataType.Uint8Array,
processor: this.audioProcessor,
progress: this.progress
});
this.addCustomLoadTask(task, data => this.soundLoaded(sound, data));
});
// tileset
data.main.tilesets.forEach(tileset => {
const url = `project/tilesets/${tileset}`;
const task = new LoadTask<LoadDataType.Blob, ImageBitmap>({
url,
identifier: `@system-tileset/${tileset}`,
dataType: LoadDataType.Blob,
processor: this.imageProcessor,
progress: this.progress
});
this.addCustomLoadTask(task, data =>
this.tilesetLoaded(tileset, data)
);
});
// autotile
const autotiles: Partial<Record<AllIdsOf<'autotile'>, ImageBitmap>> =
{};
Object.keys(icon.autotile).forEach(key => {
const url = `project/autotiles/${key}.png`;
const task = new LoadTask<LoadDataType.Blob, ImageBitmap>({
url,
identifier: `@system-autotile/${key}`,
dataType: LoadDataType.Blob,
processor: this.imageProcessor,
progress: this.progress
});
this.addCustomLoadTask(task, data =>
this.autotileLoaded(
autotiles,
key as AllIdsOf<'autotile'>,
data
)
);
});
// material
const materialImages = core.materials.slice() as SelectKey<
MaterialImages,
ImageBitmap
>[];
materialImages.push('keyboard');
materialImages
.map(v => `${v}.png`)
.forEach(materialName => {
const url = `project/materials/${materialName}`;
const task = new LoadTask<LoadDataType.Blob, ImageBitmap>({
url,
identifier: `@system-material/${materialName}`,
dataType: LoadDataType.Blob,
processor: this.imageProcessor,
progress: this.progress
});
this.addCustomLoadTask(task, data =>
this.materialLoaded(materialName, data)
);
});
// animate
const animatesUrl = `all/__all_animates__?v=${main.version}&id=${data.main.animates.join(',')}`;
const animateTask = new LoadTask<LoadDataType.Text, string>({
url: animatesUrl,
identifier: '@system-animates',
dataType: LoadDataType.Text,
processor: this.textProcessor,
progress: this.progress
});
this.addCustomLoadTask(animateTask, data => this.animationLoaded(data));
}
/**
* `JSZip`
* @param type
*/
private getZipOutputType(type: LoadDataType): JSZip.OutputType {
switch (type) {
case LoadDataType.Text:
case LoadDataType.JSON:
return 'string';
case LoadDataType.ArrayBuffer:
return 'arraybuffer';
case LoadDataType.Blob:
return 'blob';
case LoadDataType.Uint8Array:
return 'uint8array';
default:
return 'uint8array';
}
}
/**
*
* @param usage
*/
private getZipFolderByUsage(usage: CompressedUsage): string {
switch (usage) {
case CompressedUsage.Image:
return 'image';
case CompressedUsage.Tileset:
return 'tileset';
case CompressedUsage.Autotile:
return 'autotile';
case CompressedUsage.Material:
return 'material';
case CompressedUsage.Font:
return 'font';
case CompressedUsage.Sound:
return 'sound';
case CompressedUsage.Animate:
return 'animate';
}
}
/**
*
* @param name
* @param value
* @param usage
*/
private async processZipFile(
name: string,
value: unknown,
usage: CompressedUsage
) {
switch (usage) {
case CompressedUsage.Image: {
const image = await createImageBitmap(value as Blob);
await this.customImagesLoaded(name as ImageIds, image);
break;
}
case CompressedUsage.Tileset: {
const image = await createImageBitmap(value as Blob);
await this.tilesetLoaded(name, image);
break;
}
case CompressedUsage.Material: {
const image = await createImageBitmap(value as Blob);
await this.materialLoaded(name, image);
break;
}
case CompressedUsage.Font: {
const fontFace = new FontFace(
name.slice(0, -4),
value as ArrayBuffer
);
await fontFace.load();
await this.fontLoaded(name, fontFace);
break;
}
case CompressedUsage.Sound: {
const buffer = await this.ac.decodeToAudioBuffer(
value as Uint8Array<ArrayBuffer>
);
await this.soundLoaded(name as SoundIds, buffer);
break;
}
case CompressedUsage.Animate: {
await this.animationLoaded(value as string);
break;
}
}
}
/**
*
* @param list
* @param zip
*/
private async handleZip(list: ICompressedMotaAssetsData[], zip: JSZip) {
const autotiles: Partial<Record<AllIdsOf<'autotile'>, ImageBitmap>> =
{};
const materialImages = core.materials.slice() as SelectKey<
MaterialImages,
ImageBitmap
>[];
materialImages.push('keyboard');
const promises = list.map(async item => {
const { readAs, name, usage } = item;
const folder = this.getZipFolderByUsage(usage);
const file = zip.file(`${folder}/${name}`);
if (!file) return;
const value = await file.async(this.getZipOutputType(readAs));
if (usage === CompressedUsage.Autotile) {
const image = await createImageBitmap(value as Blob);
await this.autotileLoaded(
autotiles,
name.slice(0, -4) as AllIdsOf<'autotile'>,
image
);
}
await this.processZipFile(name, value, usage);
});
await Promise.all(promises);
}
/**
*
*/
private async playingLoad() {
const loadListTask = new LoadTask<
LoadDataType.JSON,
ICompressedMotaAssetsLoadList
>({
url: `loadList.json`,
dataType: LoadDataType.JSON,
identifier: '@system-loadList',
processor: this.dataLoader.jsonProcessor,
progress: { onProgress() {} }
});
loadListTask.start();
const loadList = await loadListTask.loaded();
const zipTask = new LoadTask<LoadDataType.ArrayBuffer, JSZip>({
url: loadList.file,
identifier: `@system-zip/${loadList.file}`,
dataType: LoadDataType.ArrayBuffer,
processor: this.zipProcessor,
progress: this.progress
});
this.addCustomLoadTask(zipTask, zip => {
return this.handleZip(loadList.content, zip);
});
}
//#endregion
//#region 对外接口
initSystemLoadTask(): void {
if (import.meta.env.DEV) {
this.developingLoad();
} else {
this.playingLoad();
}
}
addCustomLoadTask<R>(
task: ILoadTask<LoadDataType, R>,
onLoaded: CustomLoadFunc<R>
): Promise<R> {
return this.dataLoader.addCustomLoadTask(task, onLoaded);
}
async load(): Promise<void> {
this.loading = true;
this.loaded = false;
await this.dataLoader.load();
this.loading = false;
this.loaded = true;
}
//#endregion
}

View File

@ -1,84 +0,0 @@
import {
ILoadProgressTotal,
ILoadTask,
ILoadTaskProcessor,
LoadDataType
} from '@motajs/loader';
import JSZip from 'jszip';
export type CustomLoadFunc<R> = (data: R) => Promise<void>;
export const enum CompressedUsage {
// ---- 系统加载内容,不可更改
Font,
Image,
Sound,
Tileset,
Autotile,
Material,
Animate
}
export interface ICompressedMotaAssetsData {
/** 此内容的名称 */
readonly name: string;
/** 此内容应该由什么方式读取 */
readonly readAs: LoadDataType;
/** 此内容的应用方式 */
readonly usage: CompressedUsage;
}
export interface ICompressedMotaAssetsLoadList {
/** 压缩文件名称 */
readonly file: string;
/** 压缩包所包含的内容 */
readonly content: ICompressedMotaAssetsData[];
}
export interface IMotaAssetsLoader {
/** 加载进度对象 */
readonly progress: ILoadProgressTotal;
/** 当前是否正在加载 */
readonly loading: boolean;
/** 当前是否已经加载完毕 */
readonly loaded: boolean;
/** 图片处理器 */
readonly imageProcessor: ILoadTaskProcessor<LoadDataType.Blob, ImageBitmap>;
/** 音频处理器 */
readonly audioProcessor: ILoadTaskProcessor<
LoadDataType.Uint8Array,
AudioBuffer | null
>;
/** 字体处理器 */
readonly fontProcessor: ILoadTaskProcessor<
LoadDataType.ArrayBuffer,
FontFace
>;
/** 文字处理器 */
readonly textProcessor: ILoadTaskProcessor<LoadDataType.Text, string>;
/** `zip` 压缩包处理器 */
readonly zipProcessor: ILoadTaskProcessor<LoadDataType.ArrayBuffer, JSZip>;
/**
*
*/
initSystemLoadTask(): void;
/**
*
* @param task
* @param onLoad
* @returns `Promise` `onLoad` `Promise`
*/
addCustomLoadTask<R>(
task: ILoadTask<LoadDataType, R>,
onLoad: CustomLoadFunc<R>
): Promise<R>;
/**
*
* @returns `Promise`
*/
load(): Promise<void>;
}

View File

@ -2,7 +2,7 @@ import {
IRect, IRect,
ITextureRenderable, ITextureRenderable,
SizedCanvasImageSource SizedCanvasImageSource
} from '@motajs/render'; } from '@motajs/render-assets';
import { import {
AutotileConnection, AutotileConnection,
AutotileType, AutotileType,

View File

@ -5,7 +5,7 @@ import {
ITextureStreamComposer, ITextureStreamComposer,
TextureMaxRectsStreamComposer, TextureMaxRectsStreamComposer,
SizedCanvasImageSource SizedCanvasImageSource
} from '@motajs/render'; } from '@motajs/render-assets';
import { IAssetBuilder, IMaterialGetter, ITrackedAssetData } from './types'; import { IAssetBuilder, IMaterialGetter, ITrackedAssetData } from './types';
import { logger, PrivateListDirtyTracker } from '@motajs/common'; import { logger, PrivateListDirtyTracker } from '@motajs/common';

View File

@ -1,9 +1,6 @@
import { ITexture } from '@motajs/render'; import { ITexture } from '@motajs/render-assets';
import { import { materials } from './ins';
IBlockIdentifier, import { IBlockIdentifier, IIndexedIdentifier } from './types';
IIndexedIdentifier,
IMaterialManager
} from './types';
import { isNil } from 'lodash-es'; import { isNil } from 'lodash-es';
function extractClsBlocks<C extends Exclude<Cls, 'tileset'>>( function extractClsBlocks<C extends Exclude<Cls, 'tileset'>>(
@ -50,7 +47,7 @@ function addAutotile(set: Set<number>, map?: readonly (readonly number[])[]) {
/** /**
* *
*/ */
export function fallbackLoad(materials: IMaterialManager) { export function fallbackLoad() {
// 基本素材 // 基本素材
const icons = core.icons.icons; const icons = core.icons.icons;
const images = core.material.images; const images = core.material.images;
@ -105,6 +102,12 @@ export function fallbackLoad(materials: IMaterialManager) {
materials.addTileset(img, identifier); 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 // 地图上出现过的 tileset
const tilesetSet = new Set<number>(); const tilesetSet = new Set<number>();
const autotileSet = new Set<number>(); const autotileSet = new Set<number>();

View File

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

@ -9,7 +9,7 @@ import {
TextureGridSplitter, TextureGridSplitter,
TextureRowSplitter, TextureRowSplitter,
TextureStore TextureStore
} from '@motajs/render'; } from '@motajs/render-assets';
import { import {
IBlockIdentifier, IBlockIdentifier,
IMaterialData, IMaterialData,

View File

@ -5,7 +5,7 @@ import {
ITextureRenderable, ITextureRenderable,
ITextureStore, ITextureStore,
SizedCanvasImageSource SizedCanvasImageSource
} from '@motajs/render'; } from '@motajs/render-assets';
export const enum BlockCls { export const enum BlockCls {
Unknown, Unknown,
@ -102,7 +102,8 @@ export interface IMaterialFramedData {
} }
export interface IMaterialAsset export interface IMaterialAsset
extends IDirtyTracker<boolean>, IDirtyMarker<void> { extends IDirtyTracker<boolean>,
IDirtyMarker<void> {
/** 图集的贴图数据 */ /** 图集的贴图数据 */
readonly data: ITextureComposedData; readonly data: ITextureComposedData;
} }
@ -289,7 +290,8 @@ export interface IMaterialAliasGetter {
} }
export interface IMaterialManager export interface IMaterialManager
extends IMaterialGetter, IMaterialAliasGetter { extends IMaterialGetter,
IMaterialAliasGetter {
/** 贴图存储,把 terrains 等内容单独分开存储 */ /** 贴图存储,把 terrains 等内容单独分开存储 */
readonly tileStore: ITextureStore; readonly tileStore: ITextureStore;
/** tilesets 贴图存储,每个 tileset 是一个贴图对象 */ /** tilesets 贴图存储,每个 tileset 是一个贴图对象 */
@ -329,6 +331,7 @@ export interface IMaterialManager
addRowAnimate( addRowAnimate(
source: SizedCanvasImageSource, source: SizedCanvasImageSource,
map: ArrayLike<IBlockIdentifier>, map: ArrayLike<IBlockIdentifier>,
frames: number,
height: number height: number
): Iterable<IMaterialData>; ): Iterable<IMaterialData>;

View File

@ -1,4 +1,4 @@
import { ITexture } from '@motajs/render'; import { ITexture } from '@motajs/render-assets';
import { BlockCls } from './types'; import { BlockCls } from './types';
export function getClsByString(cls: Cls): BlockCls { export function getClsByString(cls: Cls): BlockCls {

View File

@ -1,15 +1,16 @@
{ {
"name": "@user/client-modules", "name": "@user/client-modules",
"dependencies": { "dependencies": {
"@motajs/animate": "workspace:*",
"@motajs/client-base": "workspace:*", "@motajs/client-base": "workspace:*",
"@motajs/common": "workspace:*", "@motajs/common": "workspace:*",
"@motajs/render": "workspace:*", "@motajs/render": "workspace:*",
"@motajs/render-vue": "workspace:*", "@motajs/render-assets": "workspace:*",
"@motajs/render-core": "workspace:*",
"@motajs/legacy-common": "workspace:*", "@motajs/legacy-common": "workspace:*",
"@motajs/legacy-ui": "workspace:*", "@motajs/legacy-ui": "workspace:*",
"@motajs/types": "workspace:*", "@motajs/types": "workspace:*",
"@motajs/system": "workspace:*", "@motajs/system-action": "workspace:*",
"@motajs/system-ui": "workspace:*",
"@user/data-base": "workspace:*", "@user/data-base": "workspace:*",
"@user/data-state": "workspace:*", "@user/data-state": "workspace:*",
"@user/legacy-plugin-data": "workspace:*" "@user/legacy-plugin-data": "workspace:*"

View File

@ -1,5 +1,5 @@
import { KeyCode } from '@motajs/client-base'; import { KeyCode } from '@motajs/client-base';
import { gameKey, HotkeyJSON } from '@motajs/system'; import { gameKey, HotkeyJSON } from '@motajs/system-action';
import { GameStorage } from '@motajs/legacy-system'; import { GameStorage } from '@motajs/legacy-system';
export const mainScope = Symbol.for('@key_main'); export const mainScope = Symbol.for('@key_main');

View File

@ -1,5 +1,5 @@
import { KeyCode } from '@motajs/client-base'; import { KeyCode } from '@motajs/client-base';
import { Hotkey, HotkeyData } from '@motajs/system'; import { Hotkey, HotkeyData } from '@motajs/system-action';
import { HeroMover, IMoveController } from '@user/data-state'; import { HeroMover, IMoveController } from '@user/data-state';
import { Ticker } from 'mutate-animate'; import { Ticker } from 'mutate-animate';
import { mainScope } from './hotkey'; import { mainScope } from './hotkey';

View File

@ -0,0 +1,268 @@
import EventEmitter from 'eventemitter3';
import { audioPlayer, AudioPlayer, AudioRoute, AudioStatus } from './player';
import { guessTypeByExt, isAudioSupport } from './support';
import { logger } from '@motajs/common';
import { StreamLoader } from '../loader';
import { linear, sleep, Transition } from 'mutate-animate';
import { VolumeEffect } from './effect';
interface BgmVolume {
effect: VolumeEffect;
transition: Transition;
}
interface BgmControllerEvent {
play: [];
pause: [];
resume: [];
stop: [];
}
export class BgmController<
T extends string = BgmIds
> extends EventEmitter<BgmControllerEvent> {
/** bgm音频名称的前缀 */
prefix: string = 'bgms.';
/** 每个 bgm 的音量控制器 */
readonly gain: Map<T, BgmVolume> = new Map();
/** 正在播放的 bgm */
playingBgm?: T;
/** 是否正在播放 */
playing: boolean = false;
/** 是否已经启用 */
enabled: boolean = true;
/** 主音量控制器 */
private readonly mainGain: VolumeEffect;
/** 是否屏蔽所有的音乐切换 */
private blocking: boolean = false;
/** 渐变时长 */
private transitionTime: number = 2000;
constructor(public readonly player: AudioPlayer) {
super();
this.mainGain = player.createVolumeEffect();
}
/**
*
* @param time
*/
setTransitionTime(time: number) {
this.transitionTime = time;
for (const [, value] of this.gain) {
value.transition.time(time);
}
}
/**
*
*/
blockChange() {
this.blocking = true;
}
/**
*
*/
unblockChange() {
this.blocking = false;
}
/**
*
* @param volume
*/
setVolume(volume: number) {
this.mainGain.setVolume(volume);
}
/**
*
*/
getVolume() {
return this.mainGain.getVolume();
}
/**
*
* @param enabled
*/
setEnabled(enabled: boolean) {
if (enabled) this.resume();
else this.stop();
this.enabled = enabled;
}
/**
* bgm
*/
setPrefix(prefix: string) {
this.prefix = prefix;
}
private getId(name: T) {
return `${this.prefix}${name}`;
}
/**
* bgm AudioRoute
* @param id
*/
get(id: T) {
return this.player.getRoute(this.getId(id));
}
/**
* bgm
* @param id bgm
* @param url bgm
*/
addBgm(id: T, url: string = `project/bgms/${id}`) {
const type = guessTypeByExt(id);
if (!type) {
logger.warn(50, id.split('.').slice(0, -1).join('.'));
return;
}
const gain = this.player.createVolumeEffect();
if (isAudioSupport(type)) {
const source = audioPlayer.createElementSource();
source.setSource(url);
source.setLoop(true);
const route = new AudioRoute(source, audioPlayer);
route.addEffect([gain, this.mainGain]);
audioPlayer.addRoute(this.getId(id), route);
this.setTransition(id, route, gain);
} else {
const source = audioPlayer.createStreamSource();
const stream = new StreamLoader(url);
stream.pipe(source);
source.setLoop(true);
const route = new AudioRoute(source, audioPlayer);
route.addEffect([gain, this.mainGain]);
audioPlayer.addRoute(this.getId(id), route);
this.setTransition(id, route, gain);
}
}
/**
* bgm
* @param id bgm
*/
removeBgm(id: T) {
this.player.removeRoute(this.getId(id));
const gain = this.gain.get(id);
gain?.transition.ticker.destroy();
this.gain.delete(id);
}
private setTransition(id: T, route: AudioRoute, gain: VolumeEffect) {
const transition = new Transition();
transition
.time(this.transitionTime)
.mode(linear())
.transition('volume', 0);
const tick = () => {
gain.setVolume(transition.value.volume);
};
/**
* @param expect
*/
const setTick = async (expect: AudioStatus) => {
transition.ticker.remove(tick);
transition.ticker.add(tick);
const identifier = route.stopIdentifier;
await sleep(this.transitionTime + 500);
if (
route.status === expect &&
identifier === route.stopIdentifier
) {
transition.ticker.remove(tick);
if (route.status === AudioStatus.Playing) {
gain.setVolume(1);
} else {
gain.setVolume(0);
}
}
};
route.onStart(async () => {
transition.transition('volume', 1);
setTick(AudioStatus.Playing);
});
route.onEnd(() => {
transition.transition('volume', 0);
setTick(AudioStatus.Paused);
});
route.setEndTime(this.transitionTime);
this.gain.set(id, { effect: gain, transition });
}
/**
* bgm
* @param id bgm
*/
play(id: T, when?: number) {
if (this.blocking) return;
if (id !== this.playingBgm && this.playingBgm) {
this.player.pause(this.getId(this.playingBgm));
}
this.playingBgm = id;
if (!this.enabled) return;
this.player.play(this.getId(id), when);
this.playing = true;
this.emit('play');
}
/**
* bgm
*/
resume() {
if (this.blocking || !this.enabled || this.playing) return;
if (this.playingBgm) {
this.player.resume(this.getId(this.playingBgm));
}
this.playing = true;
this.emit('resume');
}
/**
* bgm
*/
pause() {
if (this.blocking || !this.enabled) return;
if (this.playingBgm) {
this.player.pause(this.getId(this.playingBgm));
}
this.playing = false;
this.emit('pause');
}
/**
* bgm
*/
stop() {
if (this.blocking || !this.enabled) return;
if (this.playingBgm) {
this.player.stop(this.getId(this.playingBgm));
}
this.playing = false;
this.emit('stop');
}
}
export const bgmController = new BgmController<BgmIds>(audioPlayer);
export function loadAllBgm() {
const { loading } = Mota.require('@user/data-base');
loading.once('coreInit', () => {
const data = data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d;
for (const bgm of data.main.bgms) {
bgmController.addBgm(bgm);
}
});
}

View File

@ -0,0 +1,203 @@
import { logger } from '@motajs/common';
import { OggVorbisDecoderWebWorker } from '@wasm-audio-decoders/ogg-vorbis';
import { OggOpusDecoderWebWorker } from 'ogg-opus-decoder';
import { AudioType, isAudioSupport } from './support';
import type { AudioPlayer } from './player';
const fileSignatures: [AudioType, number[]][] = [
[AudioType.Mp3, [0x49, 0x44, 0x33]],
[AudioType.Ogg, [0x4f, 0x67, 0x67, 0x53]],
[AudioType.Wav, [0x52, 0x49, 0x46, 0x46]],
[AudioType.Flac, [0x66, 0x4c, 0x61, 0x43]],
[AudioType.Aac, [0xff, 0xf1]],
[AudioType.Aac, [0xff, 0xf9]]
];
const oggHeaders: [AudioType, number[]][] = [
[AudioType.Opus, [0x4f, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61, 0x64]]
];
export function checkAudioType(data: Uint8Array) {
let audioType: AudioType | '' = '';
// 检查头文件获取音频类型仅检查前256个字节
const toCheck = data.slice(0, 256);
for (const [type, value] of fileSignatures) {
if (value.every((v, i) => toCheck[i] === v)) {
audioType = type;
break;
}
}
if (audioType === AudioType.Ogg) {
// 如果是ogg的话进一步判断是不是opus
for (const [key, value] of oggHeaders) {
const has = toCheck.some((_, i) => {
return value.every((v, ii) => toCheck[i + ii] === v);
});
if (has) {
audioType = key;
break;
}
}
}
return audioType;
}
export interface IAudioDecodeError {
/** 错误信息 */
message: string;
}
export interface IAudioDecodeData {
/** 每个声道的音频信息 */
channelData: Float32Array<ArrayBuffer>[];
/** 已经被解码的 PCM 采样数 */
samplesDecoded: number;
/** 音频采样率 */
sampleRate: number;
/** 解码错误信息 */
errors: IAudioDecodeError[];
}
export abstract class AudioDecoder {
static readonly decoderMap: Map<AudioType, new () => AudioDecoder> =
new Map();
/**
*
* @param type
* @param decoder
*/
static registerDecoder(type: AudioType, decoder: new () => AudioDecoder) {
if (this.decoderMap.has(type)) {
logger.warn(47, type);
return;
}
this.decoderMap.set(type, decoder);
}
/**
*
* @param data
* @param player AudioPlayer实例
*/
static async decodeAudioData(data: Uint8Array, player: AudioPlayer) {
// 检查头文件获取音频类型仅检查前256个字节
const toCheck = data.slice(0, 256);
const type = checkAudioType(data);
if (type === '') {
logger.error(
25,
[...toCheck]
.map(v => v.toString(16).padStart(2, '0'))
.join(' ')
.toUpperCase()
);
return null;
}
if (isAudioSupport(type)) {
if (data.buffer instanceof ArrayBuffer) {
return player.ac.decodeAudioData(data.buffer);
} else {
return null;
}
} else {
const Decoder = this.decoderMap.get(type);
if (!Decoder) {
return null;
} else {
const decoder = new Decoder();
await decoder.create();
const decodedData = await decoder.decodeAll(data);
if (!decodedData) return null;
const buffer = player.ac.createBuffer(
decodedData.channelData.length,
decodedData.channelData[0].length,
decodedData.sampleRate
);
decodedData.channelData.forEach((v, i) => {
buffer.copyToChannel(v, i);
});
decoder.destroy();
return buffer;
}
}
}
/**
*
*/
abstract create(): Promise<void>;
/**
*
*/
abstract destroy(): void;
/**
*
* @param data
*/
abstract decode(data: Uint8Array): Promise<IAudioDecodeData | undefined>;
/**
*
* @param data
*/
abstract decodeAll(data: Uint8Array): Promise<IAudioDecodeData | undefined>;
/**
* 使
*/
abstract flush(): Promise<IAudioDecodeData | undefined>;
}
export class VorbisDecoder extends AudioDecoder {
decoder?: OggVorbisDecoderWebWorker;
async create(): Promise<void> {
this.decoder = new OggVorbisDecoderWebWorker();
await this.decoder.ready;
}
destroy(): void {
this.decoder?.free();
}
async decode(data: Uint8Array): Promise<IAudioDecodeData | undefined> {
return this.decoder?.decode(data) as Promise<IAudioDecodeData>;
}
async decodeAll(data: Uint8Array): Promise<IAudioDecodeData | undefined> {
return this.decoder?.decodeFile(data) as Promise<IAudioDecodeData>;
}
async flush(): Promise<IAudioDecodeData | undefined> {
return this.decoder?.flush() as Promise<IAudioDecodeData>;
}
}
export class OpusDecoder extends AudioDecoder {
decoder?: OggOpusDecoderWebWorker;
async create(): Promise<void> {
this.decoder = new OggOpusDecoderWebWorker({
speechQualityEnhancement: 'none'
});
await this.decoder.ready;
}
destroy(): void {
this.decoder?.free();
}
async decode(data: Uint8Array): Promise<IAudioDecodeData | undefined> {
return this.decoder?.decode(data) as Promise<IAudioDecodeData>;
}
async decodeAll(data: Uint8Array): Promise<IAudioDecodeData | undefined> {
return this.decoder?.decodeFile(data) as Promise<IAudioDecodeData>;
}
async flush(): Promise<IAudioDecodeData | undefined> {
return this.decoder?.flush() as Promise<IAudioDecodeData>;
}
}

View File

@ -1,26 +1,23 @@
import { isNil } from 'lodash-es'; import { isNil } from 'lodash-es';
import { import { sleep } from 'mutate-animate';
IAudioEffect,
IAudioInput,
IAudioStereoEffect,
IAudioChannelVolumeEffect,
IAudioDelayEffect,
IMotaAudioContext,
IAudioEchoEffect
} from './types';
import { sleep } from '@motajs/common';
export abstract class AudioEffect implements IAudioEffect { export interface IAudioInput {
/** 输入节点 */
input: AudioNode;
}
export interface IAudioOutput {
/** 输出节点 */
output: AudioNode;
}
export abstract class AudioEffect implements IAudioInput, IAudioOutput {
/** 输出节点 */ /** 输出节点 */
abstract output: AudioNode; abstract output: AudioNode;
/** 输入节点 */ /** 输入节点 */
abstract input: AudioNode; abstract input: AudioNode;
readonly ac: AudioContext; constructor(public readonly ac: AudioContext) {}
constructor(public readonly motaAC: IMotaAudioContext) {
this.ac = motaAC.ac;
}
/** /**
* *
@ -69,13 +66,13 @@ export abstract class AudioEffect implements IAudioEffect {
} }
} }
export class StereoEffect extends AudioEffect implements IAudioStereoEffect { export class StereoEffect extends AudioEffect {
output: PannerNode; output: PannerNode;
input: PannerNode; input: PannerNode;
constructor(ac: IMotaAudioContext) { constructor(ac: AudioContext) {
super(ac); super(ac);
const panner = ac.ac.createPanner(); const panner = ac.createPanner();
this.input = panner; this.input = panner;
this.output = panner; this.output = panner;
} }
@ -113,9 +110,9 @@ export class VolumeEffect extends AudioEffect {
output: GainNode; output: GainNode;
input: GainNode; input: GainNode;
constructor(ac: IMotaAudioContext) { constructor(ac: AudioContext) {
super(ac); super(ac);
const gain = ac.ac.createGain(); const gain = ac.createGain();
this.input = gain; this.input = gain;
this.output = gain; this.output = gain;
} }
@ -140,24 +137,21 @@ export class VolumeEffect extends AudioEffect {
start(): void {} start(): void {}
} }
export class ChannelVolumeEffect export class ChannelVolumeEffect extends AudioEffect {
extends AudioEffect
implements IAudioChannelVolumeEffect
{
output: ChannelMergerNode; output: ChannelMergerNode;
input: ChannelSplitterNode; input: ChannelSplitterNode;
/** 所有的音量控制节点 */ /** 所有的音量控制节点 */
private readonly gain: GainNode[] = []; private readonly gain: GainNode[] = [];
constructor(ac: IMotaAudioContext) { constructor(ac: AudioContext) {
super(ac); super(ac);
const splitter = ac.ac.createChannelSplitter(); const splitter = ac.createChannelSplitter();
const merger = ac.ac.createChannelMerger(); const merger = ac.createChannelMerger();
this.output = merger; this.output = merger;
this.input = splitter; this.input = splitter;
for (let i = 0; i < 6; i++) { for (let i = 0; i < 6; i++) {
const gain = ac.ac.createGain(); const gain = ac.createGain();
splitter.connect(gain, i); splitter.connect(gain, i);
gain.connect(merger, 0, i); gain.connect(merger, 0, i);
this.gain.push(gain); this.gain.push(gain);
@ -188,13 +182,13 @@ export class ChannelVolumeEffect
start(): void {} start(): void {}
} }
export class DelayEffect extends AudioEffect implements IAudioDelayEffect { export class DelayEffect extends AudioEffect {
output: DelayNode; output: DelayNode;
input: DelayNode; input: DelayNode;
constructor(ac: IMotaAudioContext) { constructor(ac: AudioContext) {
super(ac); super(ac);
const delay = ac.ac.createDelay(); const delay = ac.createDelay();
this.input = delay; this.input = delay;
this.output = delay; this.output = delay;
} }
@ -219,7 +213,7 @@ export class DelayEffect extends AudioEffect implements IAudioDelayEffect {
start(): void {} start(): void {}
} }
export class EchoEffect extends AudioEffect implements IAudioEchoEffect { export class EchoEffect extends AudioEffect {
output: GainNode; output: GainNode;
input: GainNode; input: GainNode;
@ -232,10 +226,10 @@ export class EchoEffect extends AudioEffect implements IAudioEchoEffect {
/** 是否正在播放 */ /** 是否正在播放 */
private playing: boolean = false; private playing: boolean = false;
constructor(ac: IMotaAudioContext) { constructor(ac: AudioContext) {
super(ac); super(ac);
const delay = ac.ac.createDelay(); const delay = ac.createDelay();
const gain = ac.ac.createGain(); const gain = ac.createGain();
gain.gain.value = 0.5; gain.gain.value = 0.5;
delay.delayTime.value = 0.05; delay.delayTime.value = 0.05;
delay.connect(gain); delay.connect(gain);

View File

@ -0,0 +1,18 @@
import { loadAllBgm } from './bgm';
import { OpusDecoder, VorbisDecoder } from './decoder';
import { AudioType } from './support';
import { AudioDecoder } from './decoder';
export function createAudio() {
loadAllBgm();
AudioDecoder.registerDecoder(AudioType.Ogg, VorbisDecoder);
AudioDecoder.registerDecoder(AudioType.Opus, OpusDecoder);
}
export * from './support';
export * from './effect';
export * from './player';
export * from './source';
export * from './bgm';
export * from './decoder';
export * from './sound';

View File

@ -0,0 +1,605 @@
import EventEmitter from 'eventemitter3';
import {
AudioBufferSource,
AudioElementSource,
AudioSource,
AudioStreamSource
} from './source';
import {
AudioEffect,
ChannelVolumeEffect,
DelayEffect,
EchoEffect,
IAudioOutput,
StereoEffect,
VolumeEffect
} from './effect';
import { isNil } from 'lodash-es';
import { logger } from '@motajs/common';
import { sleep } from 'mutate-animate';
import { AudioDecoder } from './decoder';
interface AudioPlayerEvent {}
export class AudioPlayer extends EventEmitter<AudioPlayerEvent> {
/** 音频播放上下文 */
readonly ac: AudioContext;
/** 所有的音频播放路由 */
readonly audioRoutes: Map<string, AudioRoute> = new Map();
/** 音量节点 */
readonly gain: GainNode;
constructor() {
super();
this.ac = new AudioContext();
this.gain = this.ac.createGain();
this.gain.connect(this.ac.destination);
}
/**
*
* @param data
*/
decodeAudioData(data: Uint8Array) {
return AudioDecoder.decodeAudioData(data, this);
}
/**
*
* @param volume
*/
setVolume(volume: number) {
this.gain.gain.value = volume;
}
/**
*
*/
getVolume() {
return this.gain.gain.value;
}
/**
*
* @param Source
*/
createSource<T extends AudioSource>(
Source: new (ac: AudioContext) => T
): T {
return new Source(this.ac);
}
/**
* opus ogg
*/
createStreamSource() {
return new AudioStreamSource(this.ac);
}
/**
* audio
*/
createElementSource() {
return new AudioElementSource(this.ac);
}
/**
* AudioBuffer
*/
createBufferSource() {
return new AudioBufferSource(this.ac);
}
/**
*
*/
getDestination() {
return this.gain;
}
/**
*
* @param Effect
*/
createEffect<T extends AudioEffect>(
Effect: new (ac: AudioContext) => T
): T {
return new Effect(this.ac);
}
/**
*
* ```txt
* |----------|
* Input ----> | GainNode | ----> Output
* |----------|
* ```
*/
createVolumeEffect() {
return new VolumeEffect(this.ac);
}
/**
*
* ```txt
* |------------|
* Input ----> | PannerNode | ----> Output
* |------------|
* ```
*/
createStereoEffect() {
return new StereoEffect(this.ac);
}
/**
*
* ```txt
* |----------|
* -> | GainNode | \
* |--------------| / |----------| -> |------------|
* Input ----> | SplitterNode | ...... | MergerNode | ----> Output
* |--------------| \ |----------| -> |------------|
* -> | GainNode | /
* |----------|
* ```
*/
createChannelVolumeEffect() {
return new ChannelVolumeEffect(this.ac);
}
/**
*
* ```txt
* |-----------|
* Input ----> | DelayNode | ----> Output
* |-----------|
* ```
*/
createDelayEffect() {
return new DelayEffect(this.ac);
}
/**
*
* ```txt
* |----------|
* Input ----> | GainNode | ----> Output
* ^ |----------| |
* | |
* | |------------|
* |-- | Delay Node | <--
* |------------|
* ```
*/
createEchoEffect() {
return new EchoEffect(this.ac);
}
/**
*
* @param source
*/
createRoute(source: AudioSource) {
return new AudioRoute(source, this);
}
/**
*
* @param id
* @param route
*/
addRoute(id: string, route: AudioRoute) {
if (this.audioRoutes.has(id)) {
logger.warn(45, id);
}
this.audioRoutes.set(id, route);
}
/**
*
* @param id
*/
getRoute(id: string) {
return this.audioRoutes.get(id);
}
/**
*
* @param id
*/
removeRoute(id: string) {
const route = this.audioRoutes.get(id);
if (route) {
route.destroy();
}
this.audioRoutes.delete(id);
}
/**
*
* @param id
* @param when
*/
play(id: string, when: number = 0) {
const route = this.getRoute(id);
if (!route) {
logger.warn(53, 'play', id);
return;
}
route.play(when);
}
/**
*
* @param id
* @returns
*/
pause(id: string) {
const route = this.getRoute(id);
if (!route) {
logger.warn(53, 'pause', id);
return;
}
return route.pause();
}
/**
*
* @param id
* @returns
*/
stop(id: string) {
const route = this.getRoute(id);
if (!route) {
logger.warn(53, 'stop', id);
return;
}
return route.stop();
}
/**
*
* @param id
*/
resume(id: string) {
const route = this.getRoute(id);
if (!route) {
logger.warn(53, 'resume', id);
return;
}
route.resume();
}
/**
* x正方向水平向右y正方向垂直于地面向上z正方向垂直屏幕远离用户
* @param x x坐标
* @param y y坐标
* @param z z坐标
*/
setListenerPosition(x: number, y: number, z: number) {
const listener = this.ac.listener;
listener.positionX.value = x;
listener.positionY.value = y;
listener.positionZ.value = z;
}
/**
* x正方向水平向右y正方向垂直于地面向上z正方向垂直屏幕远离用户
* @param x x坐标
* @param y y坐标
* @param z z坐标
*/
setListenerOrientation(x: number, y: number, z: number) {
const listener = this.ac.listener;
listener.forwardX.value = x;
listener.forwardY.value = y;
listener.forwardZ.value = z;
}
/**
* x正方向水平向右y正方向垂直于地面向上z正方向垂直屏幕远离用户
* @param x x坐标
* @param y y坐标
* @param z z坐标
*/
setListenerUp(x: number, y: number, z: number) {
const listener = this.ac.listener;
listener.upX.value = x;
listener.upY.value = y;
listener.upZ.value = z;
}
}
export const enum AudioStatus {
Playing,
Pausing,
Paused,
Stoping,
Stoped
}
type AudioStartHook = (route: AudioRoute) => void;
type AudioEndHook = (time: number, route: AudioRoute) => void;
interface AudioRouteEvent {
updateEffect: [];
play: [];
stop: [];
pause: [];
resume: [];
}
export class AudioRoute
extends EventEmitter<AudioRouteEvent>
implements IAudioOutput
{
output: AudioNode;
/** 效果器路由图 */
readonly effectRoute: AudioEffect[] = [];
/** 结束时长,当音频暂停或停止时,会经过这么长时间之后才真正终止播放,期间可以做音频淡入淡出等效果 */
endTime: number = 0;
/** 当前播放状态 */
status: AudioStatus = AudioStatus.Stoped;
/** 暂停时刻 */
private pauseTime: number = 0;
/** 暂停时播放了多长时间 */
private pauseCurrentTime: number = 0;
/** 音频时长,单位秒 */
get duration() {
return this.source.duration;
}
/** 当前播放了多长时间,单位秒 */
get currentTime() {
if (this.status === AudioStatus.Paused) {
return this.pauseCurrentTime;
} else {
return this.source.currentTime;
}
}
set currentTime(time: number) {
this.source.stop();
this.source.play(time);
}
private shouldStop: boolean = false;
/**
*
*
*/
stopIdentifier: number = 0;
private audioStartHook?: AudioStartHook;
private audioEndHook?: AudioEndHook;
constructor(
public readonly source: AudioSource,
public readonly player: AudioPlayer
) {
super();
this.output = source.output;
source.on('end', () => {
if (this.status === AudioStatus.Playing) {
this.status = AudioStatus.Stoped;
}
});
source.on('play', () => {
if (this.status !== AudioStatus.Playing) {
this.status = AudioStatus.Playing;
}
});
}
/**
*
* @param time
*/
setEndTime(time: number) {
this.endTime = time;
}
/**
*
* @param fn
*/
onStart(fn?: AudioStartHook) {
this.audioStartHook = fn;
}
/**
*
* @param fn
*
*/
onEnd(fn?: AudioEndHook) {
this.audioEndHook = fn;
}
/**
*
* @param when
*/
async play(when: number = 0) {
if (this.status === AudioStatus.Playing) return;
this.link();
await this.player.ac.resume();
if (this.effectRoute.length > 0) {
const first = this.effectRoute[0];
this.source.connect(first);
const last = this.effectRoute.at(-1)!;
last.connect({ input: this.player.getDestination() });
} else {
this.source.connect({ input: this.player.getDestination() });
}
this.source.play(when);
this.status = AudioStatus.Playing;
this.pauseTime = 0;
this.audioStartHook?.(this);
this.startAllEffect();
this.emit('play');
}
/**
*
*/
async pause() {
if (this.status !== AudioStatus.Playing) return;
this.status = AudioStatus.Pausing;
this.stopIdentifier++;
const identifier = this.stopIdentifier;
if (this.audioEndHook) {
this.audioEndHook(this.endTime, this);
await sleep(this.endTime);
}
if (
this.status !== AudioStatus.Pausing ||
this.stopIdentifier !== identifier
) {
return;
}
this.pauseCurrentTime = this.source.currentTime;
const time = this.source.stop();
this.pauseTime = time;
if (this.shouldStop) {
this.status = AudioStatus.Stoped;
this.endAllEffect();
this.emit('stop');
this.shouldStop = false;
} else {
this.status = AudioStatus.Paused;
this.endAllEffect();
this.emit('pause');
}
}
/**
*
*/
resume() {
if (this.status === AudioStatus.Playing) return;
if (
this.status === AudioStatus.Pausing ||
this.status === AudioStatus.Stoping
) {
this.audioStartHook?.(this);
this.emit('resume');
return;
}
if (this.status === AudioStatus.Paused) {
this.play(this.pauseTime);
} else {
this.play(0);
}
this.status = AudioStatus.Playing;
this.pauseTime = 0;
this.audioStartHook?.(this);
this.startAllEffect();
this.emit('resume');
}
/**
*
*/
async stop() {
if (this.status !== AudioStatus.Playing) {
if (this.status === AudioStatus.Pausing) {
this.shouldStop = true;
}
return;
}
this.status = AudioStatus.Stoping;
this.stopIdentifier++;
const identifier = this.stopIdentifier;
if (this.audioEndHook) {
this.audioEndHook(this.endTime, this);
await sleep(this.endTime);
}
if (
this.status !== AudioStatus.Stoping ||
this.stopIdentifier !== identifier
) {
return;
}
this.source.stop();
this.status = AudioStatus.Stoped;
this.pauseTime = 0;
this.endAllEffect();
this.emit('stop');
}
/**
*
* @param effect
* @param index 0
*/
addEffect(effect: AudioEffect | AudioEffect[], index?: number) {
if (isNil(index)) {
if (effect instanceof Array) {
this.effectRoute.push(...effect);
} else {
this.effectRoute.push(effect);
}
} else {
if (effect instanceof Array) {
this.effectRoute.splice(index, 0, ...effect);
} else {
this.effectRoute.splice(index, 0, effect);
}
}
this.setOutput();
if (this.source.playing) this.link();
this.emit('updateEffect');
}
/**
*
* @param effect
*/
removeEffect(effect: AudioEffect) {
const index = this.effectRoute.indexOf(effect);
if (index === -1) return;
this.effectRoute.splice(index, 1);
effect.disconnect();
this.setOutput();
if (this.source.playing) this.link();
this.emit('updateEffect');
}
destroy() {
this.effectRoute.forEach(v => v.disconnect());
}
private setOutput() {
const effect = this.effectRoute.at(-1);
if (!effect) this.output = this.source.output;
else this.output = effect.output;
}
/**
*
*/
private link() {
this.effectRoute.forEach(v => v.disconnect());
this.effectRoute.forEach((v, i) => {
const next = this.effectRoute[i + 1];
if (next) {
v.connect(next);
}
});
}
private startAllEffect() {
this.effectRoute.forEach(v => v.start());
}
private endAllEffect() {
this.effectRoute.forEach(v => v.end());
}
}
export const audioPlayer = new AudioPlayer();
// window.audioPlayer = audioPlayer;

View File

@ -1,11 +1,15 @@
import EventEmitter from 'eventemitter3';
import { audioPlayer, AudioPlayer } from './player';
import { logger } from '@motajs/common'; import { logger } from '@motajs/common';
import { IAudioVolumeEffect, IMotaAudioContext, ISoundPlayer } from './types'; import { VolumeEffect } from './effect';
type LocationArray = [number, number, number]; type LocationArray = [number, number, number];
interface SoundPlayerEvent {}
export class SoundPlayer< export class SoundPlayer<
T extends string = SoundIds T extends string = SoundIds
> implements ISoundPlayer<T> { > extends EventEmitter<SoundPlayerEvent> {
/** 每个音效的唯一标识符 */ /** 每个音效的唯一标识符 */
private num: number = 0; private num: number = 0;
@ -14,13 +18,14 @@ export class SoundPlayer<
/** 所有正在播放的音乐 */ /** 所有正在播放的音乐 */
readonly playing: Set<number> = new Set(); readonly playing: Set<number> = new Set();
/** 音量节点 */ /** 音量节点 */
readonly gain: IAudioVolumeEffect; readonly gain: VolumeEffect;
/** 是否已经启用 */ /** 是否已经启用 */
enabled: boolean = true; enabled: boolean = true;
constructor(public readonly ac: IMotaAudioContext) { constructor(public readonly player: AudioPlayer) {
this.gain = ac.createVolumeEffect(); super();
this.gain = player.createVolumeEffect();
} }
/** /**
@ -52,17 +57,13 @@ export class SoundPlayer<
* @param id * @param id
* @param data Uint8Array数据 * @param data Uint8Array数据
*/ */
async add(id: T, data: Uint8Array | AudioBuffer) { async add(id: T, data: Uint8Array) {
if (data instanceof Uint8Array) { const buffer = await this.player.decodeAudioData(data);
const buffer = await this.ac.decodeToAudioBuffer(data);
if (!buffer) { if (!buffer) {
logger.warn(51, id); logger.warn(51, id);
return; return;
} }
this.buffer.set(id, buffer); this.buffer.set(id, buffer);
} else {
this.buffer.set(id, data);
}
} }
/** /**
@ -83,19 +84,19 @@ export class SoundPlayer<
return -1; return -1;
} }
const soundNum = this.num++; const soundNum = this.num++;
const source = this.ac.createBufferSource(); const source = this.player.createBufferSource();
source.setBuffer(buffer); source.setBuffer(buffer);
const route = this.ac.createRoute(source); const route = this.player.createRoute(source);
const stereo = this.ac.createStereoEffect(); const stereo = this.player.createStereoEffect();
stereo.setPosition(position[0], position[1], position[2]); stereo.setPosition(position[0], position[1], position[2]);
stereo.setOrientation(orientation[0], orientation[1], orientation[2]); stereo.setOrientation(orientation[0], orientation[1], orientation[2]);
route.addEffect([stereo, this.gain]); route.addEffect([stereo, this.gain]);
this.ac.addRoute(`sounds.${soundNum}`, route); this.player.addRoute(`sounds.${soundNum}`, route);
route.play(); route.play();
// 清理垃圾 // 清理垃圾
source.output.addEventListener('ended', () => { source.output.addEventListener('ended', () => {
this.playing.delete(soundNum); this.playing.delete(soundNum);
this.ac.removeRoute(`sounds.${soundNum}`); this.player.removeRoute(`sounds.${soundNum}`);
}); });
this.playing.add(soundNum); this.playing.add(soundNum);
return soundNum; return soundNum;
@ -107,10 +108,10 @@ export class SoundPlayer<
*/ */
stop(num: number) { stop(num: number) {
const id = `sounds.${num}`; const id = `sounds.${num}`;
const route = this.ac.getRoute(id); const route = this.player.getRoute(id);
if (route) { if (route) {
route.stop(); route.stop();
this.ac.removeRoute(id); this.player.removeRoute(id);
this.playing.delete(num); this.playing.delete(num);
} }
} }
@ -121,12 +122,14 @@ export class SoundPlayer<
stopAllSounds() { stopAllSounds() {
this.playing.forEach(v => { this.playing.forEach(v => {
const id = `sounds.${v}`; const id = `sounds.${v}`;
const route = this.ac.getRoute(id); const route = this.player.getRoute(id);
if (route) { if (route) {
route.stop(); route.stop();
this.ac.removeRoute(id); this.player.removeRoute(id);
} }
}); });
this.playing.clear(); this.playing.clear();
} }
} }
export const soundPlayer = new SoundPlayer<SoundIds>(audioPlayer);

View File

@ -1,22 +1,61 @@
import { IStreamController, IStreamReader } from '@motajs/loader'; import EventEmitter from 'eventemitter3';
import { IStreamController, IStreamReader } from '../loader';
import { IAudioInput, IAudioOutput } from './effect';
import { logger } from '@motajs/common'; import { logger } from '@motajs/common';
import { AudioType } from './support';
import CodecParser, { CodecFrame, MimeType, OggPage } from 'codec-parser'; import CodecParser, { CodecFrame, MimeType, OggPage } from 'codec-parser';
import { isNil } from 'lodash-es'; import { isNil } from 'lodash-es';
import { import { IAudioDecodeData, AudioDecoder, checkAudioType } from './decoder';
AudioType,
EAudioSourceEvent,
IAudioBufferSource,
IAudioDecodeData,
IAudioDecoder,
IAudioElementSource,
IAudioInput,
IAudioStreamSource,
IMotaAudioContext
} from './types';
import EventEmitter from 'eventemitter3';
const mimeTypeMap: Record<AudioType, MimeType | 'unknown'> = { interface AudioSourceEvent {
[AudioType.Unknown]: 'unknown', play: [];
end: [];
}
export abstract class AudioSource
extends EventEmitter<AudioSourceEvent>
implements IAudioOutput
{
/** 音频源的输出节点 */
abstract readonly output: AudioNode;
/** 是否正在播放 */
playing: boolean = false;
/** 获取音频时长 */
abstract get duration(): number;
/** 获取当前音频播放了多长时间 */
abstract get currentTime(): number;
constructor(public readonly ac: AudioContext) {
super();
}
/**
*
*/
abstract play(when?: number): void;
/**
*
* @returns
*/
abstract stop(): number;
/**
*
* @param target
*/
abstract connect(target: IAudioInput): void;
/**
*
* @param loop
*/
abstract setLoop(loop: boolean): void;
}
const mimeTypeMap: Record<AudioType, MimeType> = {
[AudioType.Aac]: 'audio/aac', [AudioType.Aac]: 'audio/aac',
[AudioType.Flac]: 'audio/flac', [AudioType.Flac]: 'audio/flac',
[AudioType.Mp3]: 'audio/mpeg', [AudioType.Mp3]: 'audio/mpeg',
@ -29,16 +68,11 @@ function isOggPage(data: any): data is OggPage {
return !isNil(data.isFirstPage); return !isNil(data.isFirstPage);
} }
export class AudioStreamSource export class AudioStreamSource extends AudioSource implements IStreamReader {
extends EventEmitter<EAudioSourceEvent>
implements IAudioStreamSource, IStreamReader
{
readonly ac: AudioContext;
/** 音频源节点 */
output: AudioBufferSourceNode; output: AudioBufferSourceNode;
/** 音频数据 */ /** 音频数据 */
buffer: AudioBuffer | null = null; buffer?: AudioBuffer;
/** 是否已经完全加载完毕 */ /** 是否已经完全加载完毕 */
loaded: boolean = false; loaded: boolean = false;
@ -46,8 +80,6 @@ export class AudioStreamSource
buffered: number = 0; buffered: number = 0;
/** 已经缓冲的采样点数量 */ /** 已经缓冲的采样点数量 */
bufferedSamples: number = 0; bufferedSamples: number = 0;
/** 当前是否正在播放 */
playing: boolean = false;
/** 歌曲时长,加载完毕之前保持为 0 */ /** 歌曲时长,加载完毕之前保持为 0 */
duration: number = 0; duration: number = 0;
/** 当前已经播放了多长时间 */ /** 当前已经播放了多长时间 */
@ -59,7 +91,7 @@ export class AudioStreamSource
/** 音频的采样率,未成功解析出之前保持为 0 */ /** 音频的采样率,未成功解析出之前保持为 0 */
sampleRate: number = 0; sampleRate: number = 0;
private controller: IStreamController | null = null; private controller?: IStreamController;
private loop: boolean = false; private loop: boolean = false;
private target?: IAudioInput; private target?: IAudioInput;
@ -76,9 +108,9 @@ export class AudioStreamSource
/** 音频类型 */ /** 音频类型 */
private audioType: AudioType | '' = ''; private audioType: AudioType | '' = '';
/** 音频解码器 */ /** 音频解码器 */
private decoder: IAudioDecoder | null = null; private decoder?: AudioDecoder;
/** 音频解析器 */ /** 音频解析器 */
private parser: CodecParser | null = null; private parser?: CodecParser;
/** 每多长时间组成一个缓存 Float32Array */ /** 每多长时间组成一个缓存 Float32Array */
private bufferChunkSize: number = 10; private bufferChunkSize: number = 10;
/** 缓存音频数据,每 bufferChunkSize 秒钟组成一个 Float32Array用于流式解码 */ /** 缓存音频数据,每 bufferChunkSize 秒钟组成一个 Float32Array用于流式解码 */
@ -86,10 +118,9 @@ export class AudioStreamSource
private errored: boolean = false; private errored: boolean = false;
constructor(readonly motaAC: IMotaAudioContext) { constructor(context: AudioContext) {
super(); super(context);
this.ac = motaAC.ac; this.output = context.createBufferSource();
this.output = motaAC.ac.createBufferSource();
} }
/** /**
@ -101,42 +132,17 @@ export class AudioStreamSource
this.bufferChunkSize = size; this.bufferChunkSize = size;
} }
free(): void {
this.stop();
this.audioData = [];
this.decoder?.destroy();
this.decoder = null;
this.parser = null;
this.audioType = '';
this.headerRecieved = false;
this.errored = false;
this.duration = 0;
this.buffered = 0;
this.bufferedSamples = 0;
this.loaded = false;
this.sampleRate = 0;
this.buffer = null;
this.output.buffer = null;
}
piped(controller: IStreamController): void { piped(controller: IStreamController): void {
this.controller = controller; this.controller = controller;
} }
unpiped(controller: IStreamController): void {
if (this.controller === controller) {
this.controller = null;
}
}
async pump(data: Uint8Array | undefined, done: boolean): Promise<void> { async pump(data: Uint8Array | undefined, done: boolean): Promise<void> {
if (!data || this.errored) return; if (!data || this.errored) return;
if (!this.headerRecieved) { if (!this.headerRecieved) {
// 检查头文件获取音频类型仅检查前256个字节 // 检查头文件获取音频类型仅检查前256个字节
const toCheck = data.slice(0, 256); const toCheck = data.slice(0, 256);
const type = this.motaAC.getAudioTypeFromData(data); this.audioType = checkAudioType(data);
this.audioType = type; if (!this.audioType) {
if (type === AudioType.Unknown) {
logger.error( logger.error(
25, 25,
[...toCheck] [...toCheck]
@ -146,24 +152,23 @@ export class AudioStreamSource
); );
return; return;
} }
const decoder = this.motaAC.createDecoder(type); // 创建解码器
if (!decoder) { const Decoder = AudioDecoder.decoderMap.get(this.audioType);
if (!Decoder) {
this.errored = true; this.errored = true;
logger.error(24, this.audioType); logger.error(24, this.audioType);
return Promise.reject( return Promise.reject(
`Cannot decode stream source type of '${this.audioType}', since there is no registered decoder for that type.` `Cannot decode stream source type of '${this.audioType}', since there is no registered decoder for that type.`
); );
} }
this.decoder = decoder; this.decoder = new Decoder();
// 创建数据解析器 // 创建数据解析器
const mime = mimeTypeMap[this.audioType]; const mime = mimeTypeMap[this.audioType];
if (mime !== 'unknown') {
const parser = new CodecParser(mime); const parser = new CodecParser(mime);
this.parser = parser; this.parser = parser;
await decoder.create(); await this.decoder.create();
this.headerRecieved = true; this.headerRecieved = true;
} }
}
const decoder = this.decoder; const decoder = this.decoder;
const parser = this.parser; const parser = this.parser;
@ -204,7 +209,7 @@ export class AudioStreamSource
*/ */
private async decodeData( private async decodeData(
data: Uint8Array, data: Uint8Array,
decoder: IAudioDecoder, decoder: AudioDecoder,
parser: CodecParser parser: CodecParser
) { ) {
// 解析音频数据 // 解析音频数据
@ -225,7 +230,7 @@ export class AudioStreamSource
/** /**
* *
*/ */
private async decodeFlushData(decoder: IAudioDecoder, parser: CodecParser) { private async decodeFlushData(decoder: AudioDecoder, parser: CodecParser) {
const audioData = await decoder.flush(); const audioData = await decoder.flush();
if (!audioData) return; if (!audioData) return;
// @ts-expect-error 库类型声明错误 // @ts-expect-error 库类型声明错误
@ -343,7 +348,7 @@ export class AudioStreamSource
} }
async start() { async start() {
this.buffer = null; delete this.buffer;
this.headerRecieved = false; this.headerRecieved = false;
this.audioType = ''; this.audioType = '';
this.errored = false; this.errored = false;
@ -360,14 +365,13 @@ export class AudioStreamSource
end(done: boolean, reason?: string): void { end(done: boolean, reason?: string): void {
if (done && this.buffer) { if (done && this.buffer) {
this.loaded = true; this.loaded = true;
this.controller = null; delete this.controller;
this.mergeBuffers(); this.mergeBuffers();
this.duration = this.buffered; this.duration = this.buffered;
this.audioData = []; this.audioData = [];
this.decoder?.destroy(); this.decoder?.destroy();
this.decoder = null; delete this.decoder;
this.parser = null; delete this.parser;
this.emit('load');
} else { } else {
logger.warn(44, reason ?? ''); logger.warn(44, reason ?? '');
} }
@ -377,14 +381,14 @@ export class AudioStreamSource
if (!this.buffer) return; if (!this.buffer) return;
this.lastStartTime = this.ac.currentTime; this.lastStartTime = this.ac.currentTime;
if (this.playing) this.output.stop(); if (this.playing) this.output.stop();
this.emit('play');
this.createSourceNode(this.buffer); this.createSourceNode(this.buffer);
this.output.start(0, when); this.output.start(0, when);
this.playing = true; this.playing = true;
this.emit('play');
this.output.addEventListener('ended', () => { this.output.addEventListener('ended', () => {
this.playing = false; this.playing = false;
if (this.loop && !this.output.loop) this.play(0);
this.emit('end'); this.emit('end');
if (this.loop && !this.output.loop) this.play(0);
}); });
} }
@ -424,16 +428,9 @@ export class AudioStreamSource
} }
} }
export class AudioElementSource export class AudioElementSource extends AudioSource {
extends EventEmitter<EAudioSourceEvent>
implements IAudioElementSource
{
readonly ac: AudioContext;
output: MediaElementAudioSourceNode; output: MediaElementAudioSourceNode;
/** 当前是否正在播放 */
playing: boolean = false;
/** audio 元素 */ /** audio 元素 */
readonly audio: HTMLAudioElement; readonly audio: HTMLAudioElement;
@ -444,12 +441,11 @@ export class AudioElementSource
return this.audio.currentTime; return this.audio.currentTime;
} }
constructor(readonly motaAC: IMotaAudioContext) { constructor(context: AudioContext) {
super(); super(context);
this.ac = motaAC.ac;
const audio = new Audio(); const audio = new Audio();
audio.preload = 'none'; audio.preload = 'none';
this.output = motaAC.ac.createMediaElementSource(audio); this.output = context.createMediaElementSource(audio);
this.audio = audio; this.audio = audio;
audio.addEventListener('play', () => { audio.addEventListener('play', () => {
this.playing = true; this.playing = true;
@ -459,11 +455,6 @@ export class AudioElementSource
this.playing = false; this.playing = false;
this.emit('end'); this.emit('end');
}); });
audio.addEventListener('load', () => {
if (audio.src.length > 0) {
this.emit('load');
}
});
} }
/** /**
@ -474,12 +465,6 @@ export class AudioElementSource
this.audio.src = url; this.audio.src = url;
} }
free(): void {
this.stop();
this.audio.src = '';
this.audio.load();
}
play(when: number = 0): void { play(when: number = 0): void {
if (this.playing) return; if (this.playing) return;
this.audio.currentTime = when; this.audio.currentTime = when;
@ -489,6 +474,7 @@ export class AudioElementSource
stop(): number { stop(): number {
this.audio.pause(); this.audio.pause();
this.playing = false; this.playing = false;
this.emit('end');
return this.audio.currentTime; return this.audio.currentTime;
} }
@ -501,21 +487,14 @@ export class AudioElementSource
} }
} }
export class AudioBufferSource export class AudioBufferSource extends AudioSource {
extends EventEmitter<EAudioSourceEvent>
implements IAudioBufferSource
{
readonly ac: AudioContext;
output: AudioBufferSourceNode; output: AudioBufferSourceNode;
/** 音频数据 */ /** 音频数据 */
buffer: AudioBuffer | null = null; buffer?: AudioBuffer;
/** 是否循环 */ /** 是否循环 */
private loop: boolean = false; private loop: boolean = false;
/** 当前是否正在播放 */
playing: boolean = false;
duration: number = 0; duration: number = 0;
get currentTime(): number { get currentTime(): number {
return this.ac.currentTime - this.lastStartTime + this.lastStartWhen; return this.ac.currentTime - this.lastStartTime + this.lastStartWhen;
@ -527,10 +506,9 @@ export class AudioBufferSource
private lastStartTime: number = 0; private lastStartTime: number = 0;
private target?: IAudioInput; private target?: IAudioInput;
constructor(readonly motaAC: IMotaAudioContext) { constructor(context: AudioContext) {
super(); super(context);
this.ac = motaAC.ac; this.output = context.createBufferSource();
this.output = motaAC.ac.createBufferSource();
} }
/** /**
@ -544,26 +522,19 @@ export class AudioBufferSource
this.buffer = buffer; this.buffer = buffer;
} }
this.duration = this.buffer.duration; this.duration = this.buffer.duration;
this.emit('load');
}
free(): void {
this.stop();
this.output.buffer = null;
this.buffer = null;
} }
play(when?: number): void { play(when?: number): void {
if (this.playing || !this.buffer) return; if (this.playing || !this.buffer) return;
this.playing = true; this.playing = true;
this.lastStartTime = this.ac.currentTime; this.lastStartTime = this.ac.currentTime;
this.emit('play');
this.createSourceNode(this.buffer); this.createSourceNode(this.buffer);
this.output.start(0, when); this.output.start(0, when);
this.emit('play');
this.output.addEventListener('ended', () => { this.output.addEventListener('ended', () => {
this.playing = false; this.playing = false;
if (this.loop && !this.output.loop) this.play(0);
this.emit('end'); this.emit('end');
if (this.loop && !this.output.loop) this.play(0);
}); });
} }

View File

@ -1,9 +1,16 @@
import { AudioType } from './types';
const audio = new Audio(); const audio = new Audio();
const supportMap = new Map<string, boolean>(); const supportMap = new Map<string, boolean>();
export const enum AudioType {
Mp3 = 'audio/mpeg',
Wav = 'audio/wav; codecs="1"',
Flac = 'audio/flac',
Opus = 'audio/ogg; codecs="opus"',
Ogg = 'audio/ogg; codecs="vorbis"',
Aac = 'audio/aac'
}
/** /**
* *
* @param type * @param type

View File

@ -1,7 +1,7 @@
import { Patch, PatchClass } from '@motajs/legacy-common'; import { Patch, PatchClass } from '@motajs/legacy-common';
import { audioContext, bgmPlayer, soundPlayer } from '@user/client-base'; import { audioPlayer, bgmController, soundPlayer } from '../audio';
import { mainSetting } from '@motajs/legacy-ui'; import { mainSetting } from '@motajs/legacy-ui';
import { sleep } from '@motajs/common'; import { sleep } from 'mutate-animate';
import { isNil } from 'lodash-es'; import { isNil } from 'lodash-es';
// todo: 添加弃用警告 logger.warn(56) // todo: 添加弃用警告 logger.warn(56)
@ -10,10 +10,10 @@ export function patchAudio() {
const patch = new Patch(PatchClass.Control); const patch = new Patch(PatchClass.Control);
const play = (bgm: BgmIds, when?: number) => { const play = (bgm: BgmIds, when?: number) => {
bgmPlayer.play(bgm, when); bgmController.play(bgm, when);
}; };
const pause = () => { const pause = () => {
bgmPlayer.pause(); bgmController.pause();
}; };
patch.add('playBgm', function (bgm, startTime) { patch.add('playBgm', function (bgm, startTime) {
@ -23,13 +23,13 @@ export function patchAudio() {
pause(); pause();
}); });
patch.add('resumeBgm', function () { patch.add('resumeBgm', function () {
bgmPlayer.resume(); bgmController.resume();
}); });
patch.add('checkBgm', function () { patch.add('checkBgm', function () {
if (bgmPlayer.playing) return; if (bgmController.playing) return;
if (mainSetting.getValue('audio.bgmEnabled')) { if (mainSetting.getValue('audio.bgmEnabled')) {
if (bgmPlayer.playingBgm) { if (bgmController.playingBgm) {
bgmPlayer.play(bgmPlayer.playingBgm); bgmController.play(bgmController.playingBgm);
} else { } else {
play(main.startBgm, 0); play(main.startBgm, 0);
} }
@ -38,8 +38,8 @@ export function patchAudio() {
} }
}); });
patch.add('triggerBgm', function () { patch.add('triggerBgm', function () {
if (bgmPlayer.playing) bgmPlayer.pause(); if (bgmController.playing) bgmController.pause();
else bgmPlayer.resume(); else bgmController.resume();
}); });
patch.add( patch.add(
@ -47,7 +47,7 @@ export function patchAudio() {
function (sound, _pitch, callback, position, orientation) { function (sound, _pitch, callback, position, orientation) {
const name = core.getMappedName(sound) as SoundIds; const name = core.getMappedName(sound) as SoundIds;
const num = soundPlayer.play(name, position, orientation); const num = soundPlayer.play(name, position, orientation);
const route = audioContext.getRoute(`sounds.${num}`); const route = audioPlayer.getRoute(`sounds.${num}`);
if (!route) { if (!route) {
callback?.(); callback?.();
return -1; return -1;

View File

@ -1,9 +1,11 @@
import { loading } from '@user/data-base'; import { loading } from '@user/data-base';
import { createAudio } from './audio';
import { patchAll } from './fallback'; import { patchAll } from './fallback';
import { createGameRenderer, createRender } from './render'; import { createGameRenderer, createRender } from './render';
export function create() { export function create() {
patchAll(); patchAll();
createAudio();
createRender(); createRender();
loading.once('coreInit', () => { loading.once('coreInit', () => {
createGameRenderer(); createGameRenderer();
@ -11,5 +13,7 @@ export function create() {
} }
export * from './action'; export * from './action';
export * from './audio';
export * from './fallback'; export * from './fallback';
export * from './loader';
export * from './render'; export * from './render';

View File

@ -0,0 +1 @@
export * from './stream';

View File

@ -1,15 +1,76 @@
import { logger } from '@motajs/common'; import { logger } from '@motajs/common';
import { IStreamLoader, IStreamReader } from './types'; import EventEmitter from 'eventemitter3';
export class StreamLoader implements IStreamLoader { export interface IStreamController<T = void> {
readonly loading: boolean;
/**
*
*/
start(): Promise<T>;
/**
*
* @param reason
*/
cancel(reason?: string): void;
}
export interface IStreamReader<T = any> {
/**
*
* @param data
* @param done
*/
pump(
data: Uint8Array | undefined,
done: boolean,
response: Response
): Promise<void>;
/**
*
* @param controller
*/
piped(controller: IStreamController<T>): void;
/**
*
* @param stream
* @param controller
*/
start(
stream: ReadableStream,
controller: IStreamController<T>,
response: Response
): Promise<void>;
/**
*
* @param done false
* @param reason
*/
end(done: boolean, reason?: string): void;
}
interface StreamLoaderEvent {
data: [data: Uint8Array | undefined, done: boolean];
}
export class StreamLoader
extends EventEmitter<StreamLoaderEvent>
implements IStreamController<void>
{
/** 传输目标 */ /** 传输目标 */
private target: Set<IStreamReader> = new Set(); private target: Set<IStreamReader> = new Set();
/** 读取流对象 */ /** 读取流对象 */
private stream: ReadableStream | null = null; private stream?: ReadableStream;
loading: boolean = false; loading: boolean = false;
constructor(public readonly url: string) {} constructor(public readonly url: string) {
super();
}
/** /**
* *
@ -22,14 +83,7 @@ export class StreamLoader implements IStreamLoader {
} }
this.target.add(reader); this.target.add(reader);
reader.piped(this); reader.piped(this);
} return this;
unpipe(reader: IStreamReader): void {
if (this.loading) {
logger.warn(46);
return;
}
this.target.delete(reader);
} }
async start() { async start() {

View File

@ -1,4 +1,4 @@
import { gameKey } from '@motajs/system'; import { gameKey } from '@motajs/system-action';
import { POP_BOX_WIDTH, CENTER_LOC, FULL_LOC } from './shared'; import { POP_BOX_WIDTH, CENTER_LOC, FULL_LOC } from './shared';
import { import {
saveSave, saveSave,
@ -9,7 +9,7 @@ import {
openReplay, openReplay,
openStatistics openStatistics
} from './ui'; } from './ui';
import { ElementLocator } from '@motajs/render'; import { ElementLocator } from '@motajs/render-core';
export function createAction() { export function createAction() {
gameKey gameKey

View File

@ -1,13 +1,12 @@
import { ElementLocator, Font } from '@motajs/render'; import { DefaultProps, ElementLocator, Font } from '@motajs/render';
import { computed, defineComponent, reactive, ref } from 'vue'; import { computed, defineComponent, reactive, ref } from 'vue';
import { Background, Selection } from './misc'; import { Background, Selection } from './misc';
import { TextContent, TextContentProps } from './textbox'; import { TextContent, TextContentProps } from './textbox';
import { TextAlign } from './textboxTyper'; import { TextAlign } from './textboxTyper';
import { Page, PageExpose } from './page'; import { Page, PageExpose } from './page';
import { GameUI, IUIMountable, SetupComponentOptions } from '@motajs/system'; import { GameUI, IUIMountable, SetupComponentOptions } from '@motajs/system-ui';
import { useKey } from '../use'; import { useKey } from '../use';
import { sleep } from 'mutate-animate'; import { sleep } from 'mutate-animate';
import { DefaultProps } from '@motajs/render-vue';
export interface ConfirmBoxProps extends DefaultProps, TextContentProps { export interface ConfirmBoxProps extends DefaultProps, TextContentProps {
/** 确认框的提示文本内容 */ /** 确认框的提示文本内容 */
@ -139,11 +138,11 @@ export const ConfirmBox = defineComponent<
height.value = textHeight + pad.value * 4; height.value = textHeight + pad.value * 4;
}; };
const setYes = (width: number, height: number) => { const setYes = (_: string, width: number, height: number) => {
yesSize.value = [width, height]; yesSize.value = [width, height];
}; };
const setNo = (width: number, height: number) => { const setNo = (_: string, width: number, height: number) => {
noSize.value = [width, height]; noSize.value = [width, height];
}; };
@ -190,7 +189,7 @@ export const ConfirmBox = defineComponent<
zIndex={15} zIndex={15}
onClick={() => emit('yes')} onClick={() => emit('yes')}
onEnter={() => (selected.value = true)} onEnter={() => (selected.value = true)}
onResize={setYes} onSetText={setYes}
/> />
<text <text
loc={noLoc.value} loc={noLoc.value}
@ -201,7 +200,7 @@ export const ConfirmBox = defineComponent<
zIndex={15} zIndex={15}
onClick={() => emit('no')} onClick={() => emit('no')}
onEnter={() => (selected.value = false)} onEnter={() => (selected.value = false)}
onResize={setNo} onSetText={setNo}
/> />
</container> </container>
); );
@ -459,7 +458,7 @@ export const Choices = defineComponent<
contentHeight.value = height; contentHeight.value = height;
}; };
const updateTitleHeight = (_: number, height: number) => { const updateTitleHeight = (_0: string, _1: number, height: number) => {
titleHeight.value = height; titleHeight.value = height;
}; };
@ -519,7 +518,7 @@ export const Choices = defineComponent<
font={props.titleFont ?? new Font(void 0, 18)} font={props.titleFont ?? new Font(void 0, 18)}
fillStyle={props.titleFill ?? 'gold'} fillStyle={props.titleFill ?? 'gold'}
zIndex={5} zIndex={5}
onResize={updateTitleHeight} onSetText={updateTitleHeight}
/> />
<TextContent <TextContent
{...attrs} {...attrs}
@ -556,7 +555,7 @@ export const Choices = defineComponent<
zIndex={5} zIndex={5}
fillStyle={props.selFill} fillStyle={props.selFill}
onClick={() => emit('choose', v[0])} onClick={() => emit('choose', v[0])}
onResize={(width, height) => onSetText={(_, width, height) =>
updateChoiceSize(i, width, height) updateChoiceSize(i, width, height)
} }
onEnter={() => (selected.value = i)} onEnter={() => (selected.value = i)}

View File

@ -1,9 +1,10 @@
import { DefaultProps } from '@motajs/render-vue'; import { DefaultProps } from '@motajs/render-vue';
import { SetupComponentOptions } from '@motajs/system'; import { SetupComponentOptions } from '@motajs/system-ui';
import { clamp, isNil } from 'lodash-es'; import { clamp, isNil } from 'lodash-es';
import { computed, defineComponent, onMounted, ref, watch } from 'vue'; import { computed, defineComponent, onMounted, ref, watch } from 'vue';
import { Scroll, ScrollExpose } from './scroll'; import { Scroll, ScrollExpose } from './scroll';
import { MotaOffscreenCanvas2D, Font } from '@motajs/render'; import { Font } from '@motajs/render-style';
import { MotaOffscreenCanvas2D } from '@motajs/render-core';
import { import {
HALF_STATUS_WIDTH, HALF_STATUS_WIDTH,
STATUS_BAR_HEIGHT, STATUS_BAR_HEIGHT,
@ -219,7 +220,7 @@ export const FloorSelector = defineComponent<
lineWidth={1} lineWidth={1}
strokeStyle="#aaa" strokeStyle="#aaa"
/> />
<custom <sprite
zIndex={20} zIndex={20}
loc={[0, 0, 144, SCROLL_HEIGHT]} loc={[0, 0, 144, SCROLL_HEIGHT]}
nocache nocache

View File

@ -1,6 +1,5 @@
import { ElementLocator } from '@motajs/render'; import { DefaultProps, ElementLocator, GraphicPropsBase } from '@motajs/render';
import { DefaultProps, GraphicPropsBase } from '@motajs/render-vue'; import { SetupComponentOptions } from '@motajs/system-ui';
import { SetupComponentOptions } from '@motajs/system';
import { import {
computed, computed,
defineComponent, defineComponent,

View File

@ -1,20 +1,19 @@
import { DefaultProps } from '@motajs/render-vue'; import { DefaultProps } from '@motajs/render-vue';
import { computed, defineComponent, onUnmounted, ref, watch } from 'vue'; import { computed, defineComponent, onUnmounted, ref, watch } from 'vue';
import { TextContent, TextContentProps } from './textbox'; import { TextContent, TextContentProps } from './textbox';
import { RectRCircleParams } from '@motajs/render-elements';
import { import {
Container, Container,
ElementLocator, ElementLocator,
MotaRenderer, MotaRenderer,
Transform, RenderItem,
Font, Transform
RectRCircleParams, } from '@motajs/render-core';
IRenderItem, import { Font } from '@motajs/render-style';
IRenderTreeRoot
} from '@motajs/render';
import { transitionedColor, useKey } from '../use'; import { transitionedColor, useKey } from '../use';
import { linear } from 'mutate-animate'; import { linear } from 'mutate-animate';
import { Background, Selection } from './misc'; import { Background, Selection } from './misc';
import { GameUI, IUIMountable, SetupComponentOptions } from '@motajs/system'; import { GameUI, IUIMountable, SetupComponentOptions } from '@motajs/system-ui';
import { KeyCode } from '@motajs/client-base'; import { KeyCode } from '@motajs/client-base';
export interface InputProps extends DefaultProps, Partial<TextContentProps> { export interface InputProps extends DefaultProps, Partial<TextContentProps> {
@ -162,9 +161,9 @@ export const Input = defineComponent<InputProps, InputEmits, keyof InputEmits>(
if (!ele) return; if (!ele) return;
// 计算当前绝对位置 // 计算当前绝对位置
const chain: IRenderItem[] = []; const chain: RenderItem[] = [];
let now: IRenderItem | null = root.value ?? null; let now: RenderItem | undefined = root.value;
let renderer: IRenderTreeRoot | null = null; let renderer: MotaRenderer | undefined;
if (!now) return; if (!now) return;
while (now) { while (now) {
chain.unshift(now); chain.unshift(now);
@ -442,11 +441,11 @@ export const InputBox = defineComponent<
emit('input', value); emit('input', value);
}; };
const setYes = (width: number, height: number) => { const setYes = (_: string, width: number, height: number) => {
yesSize.value = [width, height]; yesSize.value = [width, height];
}; };
const setNo = (width: number, height: number) => { const setNo = (_: string, width: number, height: number) => {
noSize.value = [width, height]; noSize.value = [width, height];
}; };
@ -501,7 +500,7 @@ export const InputBox = defineComponent<
zIndex={15} zIndex={15}
onClick={confirm} onClick={confirm}
onEnter={() => (selected.value = true)} onEnter={() => (selected.value = true)}
onResize={setYes} onSetText={setYes}
/> />
<text <text
loc={noLoc.value} loc={noLoc.value}
@ -512,7 +511,7 @@ export const InputBox = defineComponent<
zIndex={15} zIndex={15}
onClick={cancel} onClick={cancel}
onEnter={() => (selected.value = false)} onEnter={() => (selected.value = false)}
onResize={setNo} onSetText={setNo}
/> />
</container> </container>
); );

View File

@ -1,8 +1,9 @@
import { DefaultProps } from '@motajs/render-vue'; import { DefaultProps } from '@motajs/render-vue';
import { computed, defineComponent, ref, SlotsType, VNode } from 'vue'; import { computed, defineComponent, ref, SlotsType, VNode } from 'vue';
import { Selection } from './misc'; import { Selection } from './misc';
import { ElementLocator, Font } from '@motajs/render'; import { ElementLocator } from '@motajs/render-core';
import { SetupComponentOptions } from '@motajs/system'; import { Font } from '@motajs/render-style';
import { SetupComponentOptions } from '@motajs/system-ui';
import { Scroll } from './scroll'; import { Scroll } from './scroll';
export interface ListProps extends DefaultProps { export interface ListProps extends DefaultProps {

View File

@ -1,18 +1,19 @@
import { import {
DefaultProps,
ElementLocator, ElementLocator,
CustomRenderItem, onTick,
MotaOffscreenCanvas2D PathProps,
Sprite
} from '@motajs/render'; } from '@motajs/render';
import { DefaultProps, PathProps } from '@motajs/render-vue';
import { computed, defineComponent, ref, SetupContext, watch } from 'vue'; import { computed, defineComponent, ref, SetupContext, watch } from 'vue';
import { MotaOffscreenCanvas2D } from '@motajs/render';
import { TextContent, TextContentProps } from './textbox'; import { TextContent, TextContentProps } from './textbox';
import { Scroll, ScrollExpose, ScrollProps } from './scroll'; import { Scroll, ScrollExpose, ScrollProps } from './scroll';
import { transitioned } from '../use'; import { transitioned } from '../use';
import { hyper } from 'mutate-animate';
import { logger } from '@motajs/common'; import { logger } from '@motajs/common';
import { GameUI, IUIMountable, SetupComponentOptions } from '@motajs/system'; import { GameUI, IUIMountable, SetupComponentOptions } from '@motajs/system-ui';
import { clamp } from 'lodash-es'; import { clamp } from 'lodash-es';
import { using } from '../renderer';
import { cosh, CurveMode } from '@motajs/animate';
interface ProgressProps extends DefaultProps { interface ProgressProps extends DefaultProps {
/** 进度条的位置 */ /** 进度条的位置 */
@ -42,7 +43,7 @@ const progressProps = {
* ``` * ```
*/ */
export const Progress = defineComponent<ProgressProps>(props => { export const Progress = defineComponent<ProgressProps>(props => {
const element = ref<CustomRenderItem>(); const element = ref<Sprite>();
const render = (canvas: MotaOffscreenCanvas2D) => { const render = (canvas: MotaOffscreenCanvas2D) => {
const { ctx } = canvas; const { ctx } = canvas;
@ -72,7 +73,7 @@ export const Progress = defineComponent<ProgressProps>(props => {
}); });
return () => { return () => {
return <custom ref={element} loc={props.loc} render={render}></custom>; return <sprite ref={element} loc={props.loc} render={render}></sprite>;
}; };
}, progressProps); }, progressProps);
@ -222,7 +223,7 @@ export const ScrollText = defineComponent<
let paused = false; let paused = false;
let nowScroll = 0; let nowScroll = 0;
using.onExcitedFunc(() => { onTick(() => {
if (paused || !scroll.value) return; if (paused || !scroll.value) return;
const now = Date.now(); const now = Date.now();
const dt = now - lastFixedTime; const dt = now - lastFixedTime;
@ -301,11 +302,7 @@ const selectionProps = {
export const Selection = defineComponent<SelectionProps>(props => { export const Selection = defineComponent<SelectionProps>(props => {
const minAlpha = computed(() => props.alphaRange?.[0] ?? 0.25); const minAlpha = computed(() => props.alphaRange?.[0] ?? 0.25);
const maxAlpha = computed(() => props.alphaRange?.[1] ?? 0.55); const maxAlpha = computed(() => props.alphaRange?.[1] ?? 0.55);
const alpha = transitioned( const alpha = transitioned(minAlpha.value, 2000, hyper('sin', 'in-out'))!;
minAlpha.value,
2000,
cosh(2, CurveMode.EaseInOut)
)!;
const isWinskin = computed(() => !!props.winskin); const isWinskin = computed(() => !!props.winskin);
const winskinImage = computed(() => const winskinImage = computed(() =>
@ -335,7 +332,7 @@ export const Selection = defineComponent<SelectionProps>(props => {
ctx.drawImage(image, 158, 66, 2, 28, width - 2, 2, 2, height - 4); ctx.drawImage(image, 158, 66, 2, 28, width - 2, 2, 2, height - 4);
}; };
using.onExcitedFunc(() => { onTick(() => {
if (alpha.value === maxAlpha.value) { if (alpha.value === maxAlpha.value) {
alpha.set(minAlpha.value); alpha.set(minAlpha.value);
} }
@ -346,7 +343,7 @@ export const Selection = defineComponent<SelectionProps>(props => {
return () => return () =>
isWinskin.value ? ( isWinskin.value ? (
<custom <sprite
loc={props.loc} loc={props.loc}
render={renderWinskin} render={renderWinskin}
alpha={alpha.ref.value} alpha={alpha.ref.value}
@ -395,7 +392,7 @@ export const Background = defineComponent<BackgroundProps>(props => {
return () => return () =>
isWinskin.value ? ( isWinskin.value ? (
<winskin imageName={props.winskin!} loc={props.loc} noanti /> <winskin image={props.winskin!} loc={props.loc} noanti />
) : ( ) : (
<g-rectr <g-rectr
loc={fixedLoc.value} loc={fixedLoc.value}
@ -410,7 +407,8 @@ export const Background = defineComponent<BackgroundProps>(props => {
}, backgroundProps); }, backgroundProps);
export interface WaitBoxProps<T> export interface WaitBoxProps<T>
extends Partial<BackgroundProps>, Partial<TextContentProps> { extends Partial<BackgroundProps>,
Partial<TextContentProps> {
loc: ElementLocator; loc: ElementLocator;
width: number; width: number;
promise?: Promise<T>; promise?: Promise<T>;

View File

@ -9,9 +9,8 @@ import {
watch watch
} from 'vue'; } from 'vue';
import { clamp, isNil } from 'lodash-es'; import { clamp, isNil } from 'lodash-es';
import { ElementLocator, Font } from '@motajs/render'; import { DefaultProps, ElementLocator, Font } from '@motajs/render';
import { SetupComponentOptions } from '@motajs/system'; import { SetupComponentOptions } from '@motajs/system-ui';
import { DefaultProps } from '@motajs/render-vue';
/** 圆角矩形页码距离容器的边框大小,与 pageSize 相乘 */ /** 圆角矩形页码距离容器的边框大小,与 pageSize 相乘 */
const RECT_PAD = 0.1; const RECT_PAD = 0.1;

View File

@ -12,8 +12,10 @@ import {
} from 'vue'; } from 'vue';
import { import {
Container, Container,
DefaultProps,
ElementLocator, ElementLocator,
CustomRenderItem, RenderItem,
Sprite,
Transform, Transform,
MotaOffscreenCanvas2D, MotaOffscreenCanvas2D,
IActionEvent, IActionEvent,
@ -21,16 +23,14 @@ import {
MouseType, MouseType,
EventProgress, EventProgress,
ActionEventMap, ActionEventMap,
ContainerCustom,
ActionType, ActionType,
CustomContainerPropagateOrigin, CustomContainerPropagateOrigin
IRenderItem,
ICustomContainer
} from '@motajs/render'; } from '@motajs/render';
import { hyper, linear, Transition } from 'mutate-animate'; import { hyper, linear, Transition } from 'mutate-animate';
import { clamp } from 'lodash-es'; import { clamp } from 'lodash-es';
import { transitioned } from '../use'; import { transitioned } from '../use';
import { SetupComponentOptions } from '@motajs/system'; import { SetupComponentOptions } from '@motajs/system-ui';
import { DefaultProps } from '@motajs/render-vue';
export const enum ScrollDirection { export const enum ScrollDirection {
Horizontal, Horizontal,
@ -110,10 +110,10 @@ export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
/** 滚动条的定位 */ /** 滚动条的定位 */
const sp = ref<ElementLocator>([0, 0, 1, 1]); const sp = ref<ElementLocator>([0, 0, 1, 1]);
const listenedChild: Set<IRenderItem> = new Set(); const listenedChild: Set<RenderItem> = new Set();
const areaMap: Map<IRenderItem, [number, number]> = new Map(); const areaMap: Map<RenderItem, [number, number]> = new Map();
const content = ref<Container>(); const content = ref<Container>();
const scroll = ref<CustomRenderItem>(); const scroll = ref<Sprite>();
const scrollAlpha = transitioned(0.5, 100, linear())!; const scrollAlpha = transitioned(0.5, 100, linear())!;
@ -187,7 +187,7 @@ export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
/** /**
* *
*/ */
const getArea = (item: IRenderItem, rect: DOMRectReadOnly) => { const getArea = (item: RenderItem, rect: DOMRectReadOnly) => {
if (direction.value === ScrollDirection.Horizontal) { if (direction.value === ScrollDirection.Horizontal) {
areaMap.set(item, [rect.left - width.value, rect.right]); areaMap.set(item, [rect.left - width.value, rect.right]);
} else { } else {
@ -198,7 +198,7 @@ export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
/** /**
* *
*/ */
const checkItem = (item: IRenderItem) => { const checkItem = (item: RenderItem) => {
const area = areaMap.get(item); const area = areaMap.get(item);
if (!area) { if (!area) {
item.show(); item.show();
@ -222,7 +222,7 @@ export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
/** /**
* *
*/ */
const onTransform = (item: IRenderItem) => { const onTransform = (item: RenderItem) => {
const rect = item.getBoundingRect(); const rect = item.getBoundingRect();
const pad = props.padEnd ?? 0; const pad = props.padEnd ?? 0;
if (item.parent === content.value) { if (item.parent === content.value) {
@ -340,7 +340,7 @@ export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
const renderContent = ( const renderContent = (
canvas: MotaOffscreenCanvas2D, canvas: MotaOffscreenCanvas2D,
children: IRenderItem[], children: RenderItem[],
transform: Transform transform: Transform
) => { ) => {
const ctx = canvas.ctx; const ctx = canvas.ctx;
@ -367,7 +367,7 @@ export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
type: T, type: T,
progress: EventProgress, progress: EventProgress,
event: ActionEventMap[T], event: ActionEventMap[T],
_: ICustomContainer, _: ContainerCustom,
origin: CustomContainerPropagateOrigin origin: CustomContainerPropagateOrigin
) => { ) => {
if (progress === EventProgress.Capture) { if (progress === EventProgress.Capture) {
@ -562,7 +562,7 @@ export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
> >
{slots.default?.()} {slots.default?.()}
</container-custom> </container-custom>
<custom <sprite
nocache nocache
hidden={props.noscroll} hidden={props.noscroll}
loc={sp.value} loc={sp.value}
@ -573,7 +573,7 @@ export const Scroll = defineComponent<ScrollProps, {}, string, ScrollSlots>(
zIndex={10} zIndex={10}
onEnter={enter} onEnter={enter}
onLeave={leave} onLeave={leave}
></custom> ></sprite>
</container> </container>
); );
}; };

View File

@ -1,7 +1,8 @@
import { import {
ElementLocator, ElementLocator,
Font, Font,
CustomRenderItem, Sprite,
DefaultProps,
Text, Text,
MotaOffscreenCanvas2D MotaOffscreenCanvas2D
} from '@motajs/render'; } from '@motajs/render';
@ -29,16 +30,16 @@ import {
WordBreak, WordBreak,
TextAlign TextAlign
} from './textboxTyper'; } from './textboxTyper';
import { SetupComponentOptions } from '@motajs/system'; import { SetupComponentOptions } from '@motajs/system-ui';
import { texture } from '../elements'; import { texture } from '../elements';
import { DefaultProps } from '@motajs/render-vue';
// todo: TextContent 应该改成渲染元素? // todo: TextContent 应该改成渲染元素?
//#region TextContent //#region TextContent
export interface TextContentProps export interface TextContentProps
extends DefaultProps, Partial<ITextContentConfig> { extends DefaultProps,
Partial<ITextContentConfig> {
/** 显示的文字 */ /** 显示的文字 */
text?: string; text?: string;
/** 是否填充 */ /** 是否填充 */
@ -171,7 +172,7 @@ export const TextContent = defineComponent<
expose<TextContentExpose>({ retype, showAll, getHeight }); expose<TextContentExpose>({ retype, showAll, getHeight });
const spriteElement = shallowRef<CustomRenderItem>(); const spriteElement = shallowRef<Sprite>();
const renderContent = (canvas: MotaOffscreenCanvas2D) => { const renderContent = (canvas: MotaOffscreenCanvas2D) => {
const ctx = canvas.ctx; const ctx = canvas.ctx;
ctx.textBaseline = 'top'; ctx.textBaseline = 'top';
@ -225,11 +226,11 @@ export const TextContent = defineComponent<
return () => { return () => {
return ( return (
<custom <sprite
loc={loc.value} loc={loc.value}
ref={spriteElement} ref={spriteElement}
render={renderContent} render={renderContent}
></custom> ></sprite>
); );
}; };
}, textContentOptions); }, textContentOptions);
@ -493,7 +494,7 @@ export const Textbox = defineComponent<
slots.title(data) slots.title(data)
) : props.winskin ? ( ) : props.winskin ? (
<winskin <winskin
imageName={props.winskin} image={props.winskin}
loc={[0, 0, tw.value, th.value]} loc={[0, 0, tw.value, th.value]}
></winskin> ></winskin>
) : ( ) : (
@ -514,7 +515,7 @@ export const Textbox = defineComponent<
slots.default(data) slots.default(data)
) : props.winskin ? ( ) : props.winskin ? (
<winskin <winskin
imageName={props.winskin} image={props.winskin}
loc={[0, contentY.value, data.width!, backHeight.value]} loc={[0, contentY.value, data.width!, backHeight.value]}
></winskin> ></winskin>
) : ( ) : (

View File

@ -1,9 +1,8 @@
import { logger } from '@motajs/common'; import { logger } from '@motajs/common';
import { Font, MotaOffscreenCanvas2D } from '@motajs/render'; import { Font, onTick, MotaOffscreenCanvas2D } from '@motajs/render';
import EventEmitter from 'eventemitter3'; import EventEmitter from 'eventemitter3';
import { isNil } from 'lodash-es'; import { isNil } from 'lodash-es';
import { RenderableData, AutotileRenderable, texture } from '../elements'; import { RenderableData, AutotileRenderable, texture } from '../elements';
import { using } from '../renderer';
/** 文字的安全填充,会填充在文字的上侧和下侧,防止削顶和削底 */ /** 文字的安全填充,会填充在文字的上侧和下侧,防止削顶和削底 */
const SAFE_PAD = 1; const SAFE_PAD = 1;
@ -151,7 +150,8 @@ interface ISizedTextContentBlock {
} }
export interface ITextContentTextBlock export interface ITextContentTextBlock
extends ITextContentBlockBase, ISizedTextContentBlock { extends ITextContentBlockBase,
ISizedTextContentBlock {
readonly type: TextContentType.Text; readonly type: TextContentType.Text;
/** 文本 block 的文字内容 */ /** 文本 block 的文字内容 */
readonly text: string; readonly text: string;
@ -164,7 +164,8 @@ export interface ITextContentTextBlock
} }
export interface ITextContentIconBlock export interface ITextContentIconBlock
extends ITextContentBlockBase, ISizedTextContentBlock { extends ITextContentBlockBase,
ISizedTextContentBlock {
readonly type: TextContentType.Icon; readonly type: TextContentType.Icon;
/** 图标 block 显示的图标 */ /** 图标 block 显示的图标 */
readonly icon: AllNumbers; readonly icon: AllNumbers;
@ -198,7 +199,8 @@ export interface ITyperRenderableBase {
} }
export interface ITyperTextRenderable export interface ITyperTextRenderable
extends ITextContentTextBlock, ITyperRenderableBase { extends ITextContentTextBlock,
ITyperRenderableBase {
/** 文本左上角的横坐标 */ /** 文本左上角的横坐标 */
readonly x: number; readonly x: number;
/** 文本左上角的纵坐标 */ /** 文本左上角的纵坐标 */
@ -208,7 +210,8 @@ export interface ITyperTextRenderable
} }
export interface ITyperIconRenderable export interface ITyperIconRenderable
extends ITextContentIconBlock, ITyperRenderableBase { extends ITextContentIconBlock,
ITyperRenderableBase {
/** 图标左上角的横坐标 */ /** 图标左上角的横坐标 */
readonly x: number; readonly x: number;
/** 图标左上角的纵坐标 */ /** 图标左上角的纵坐标 */
@ -216,7 +219,8 @@ export interface ITyperIconRenderable
} }
export interface ITyperWaitRenderable export interface ITyperWaitRenderable
extends ITextContentWaitBlock, ITyperRenderableBase { extends ITextContentWaitBlock,
ITyperRenderableBase {
/** 当然是否已经等待了多少个字符 */ /** 当然是否已经等待了多少个字符 */
waited: number; waited: number;
} }
@ -309,7 +313,7 @@ export class TextContentTyper extends EventEmitter<TextContentTyperEvent> {
this.config this.config
); );
using.onExcitedFunc(() => this.tick()); onTick(() => this.tick());
} }
/** /**

View File

@ -1,13 +1,13 @@
import { import {
ElementLocator, ElementLocator,
MotaOffscreenCanvas2D, MotaOffscreenCanvas2D,
CustomRenderItem Sprite
} from '@motajs/render'; } from '@motajs/render-core';
import { CustomProps } from '@motajs/render-vue'; import { SpriteProps } from '@motajs/render-vue';
import { defineComponent, ref, watch } from 'vue'; import { defineComponent, ref, watch } from 'vue';
import { SetupComponentOptions } from '@motajs/system'; import { SetupComponentOptions } from '@motajs/system-ui';
export interface ThumbnailProps extends CustomProps { export interface ThumbnailProps extends SpriteProps {
/** 缩略图的位置 */ /** 缩略图的位置 */
loc: ElementLocator; loc: ElementLocator;
/** 楼层 ID */ /** 楼层 ID */
@ -41,7 +41,7 @@ const thumbnailProps = {
} satisfies SetupComponentOptions<ThumbnailProps>; } satisfies SetupComponentOptions<ThumbnailProps>;
export const Thumbnail = defineComponent<ThumbnailProps>(props => { export const Thumbnail = defineComponent<ThumbnailProps>(props => {
const spriteRef = ref<CustomRenderItem>(); const spriteRef = ref<Sprite>();
const update = () => { const update = () => {
spriteRef.value?.update(); spriteRef.value?.update();
@ -79,6 +79,6 @@ export const Thumbnail = defineComponent<ThumbnailProps>(props => {
watch(props, update); watch(props, update);
return () => ( return () => (
<custom noanti ref={spriteRef} loc={props.loc} render={drawThumbnail} /> <sprite noanti ref={spriteRef} loc={props.loc} render={drawThumbnail} />
); );
}, thumbnailProps); }, thumbnailProps);

View File

@ -1,12 +1,11 @@
import { ElementLocator, Font } from '@motajs/render'; import { DefaultProps, ElementLocator, Font } from '@motajs/render';
import { logger } from '@motajs/common'; import { logger } from '@motajs/common';
import { computed, defineComponent, onUnmounted, ref } from 'vue'; import { computed, defineComponent, onUnmounted, ref } from 'vue';
import { transitioned } from '../use'; import { transitioned } from '../use';
import { hyper } from 'mutate-animate'; import { hyper } from 'mutate-animate';
import { debounce } from 'lodash-es'; import { debounce } from 'lodash-es';
import { texture } from '../elements'; import { texture } from '../elements';
import { SetupComponentOptions } from '@motajs/system'; import { SetupComponentOptions } from '@motajs/system-ui';
import { DefaultProps } from '@motajs/render-vue';
export interface TipProps extends DefaultProps { export interface TipProps extends DefaultProps {
/** 显示的位置 */ /** 显示的位置 */
@ -106,7 +105,7 @@ export const Tip = defineComponent<TipProps>((props, { expose }) => {
hide(); hide();
}; };
const onSetText = (width: number) => { const onSetText = (_: string, width: number) => {
textWidth.value = width; textWidth.value = width;
}; };
@ -137,7 +136,7 @@ export const Tip = defineComponent<TipProps>((props, { expose }) => {
<text <text
loc={textLoc.value} loc={textLoc.value}
text={text.value} text={text.value}
onResize={onSetText} onSetText={onSetText}
font={font} font={font}
/> />
</container> </container>

View File

@ -0,0 +1,278 @@
import {
RenderAdapter,
transformCanvas,
ERenderItemEvent,
RenderItem,
Transform,
MotaOffscreenCanvas2D
} from '@motajs/render-core';
import { HeroRenderer } from './hero';
import { ILayerGroupRenderExtends, LayerGroup } from './layer';
export class LayerGroupAnimate implements ILayerGroupRenderExtends {
static animateList: Set<LayerGroupAnimate> = new Set();
id: string = 'animate';
group!: LayerGroup;
hero?: HeroRenderer;
animate!: Animate;
private animation: Set<AnimateData> = new Set();
/**
*
* @param name id
*/
async drawHeroAnimate(name: AnimationIds) {
const animate = this.animate.animate(name, 0, 0);
this.updatePosition(animate);
await this.animate.draw(animate);
this.animation.delete(animate);
}
private updatePosition(animate: AnimateData) {
if (!this.checkHero()) return;
if (!this.hero?.renderable) return;
const { x, y } = this.hero.renderable;
const cell = this.group.cellSize;
const half = cell / 2;
animate.centerX = x * cell + half;
animate.centerY = y * cell + half;
}
private onMoveTick = (x: number, y: number) => {
const cell = this.group.cellSize;
const half = cell / 2;
const ax = x * cell + half;
const ay = y * cell + half;
this.animation.forEach(v => {
v.centerX = ax;
v.centerY = ay;
});
};
private listen() {
if (this.checkHero()) {
this.hero!.on('moveTick', this.onMoveTick);
}
}
private checkHero() {
if (this.hero) return true;
const ex = this.group.getLayer('event')?.getExtends('floor-hero');
if (ex instanceof HeroRenderer) {
this.hero = ex;
return true;
}
return false;
}
awake(group: LayerGroup): void {
this.group = group;
this.animate = new Animate();
this.animate.size(group.width, group.height);
this.animate.setHD(true);
this.animate.setZIndex(100);
group.appendChild(this.animate);
LayerGroupAnimate.animateList.add(this);
this.listen();
}
onDestroy(_group: LayerGroup): void {
if (this.checkHero()) {
this.hero!.off('moveTick', this.onMoveTick);
LayerGroupAnimate.animateList.delete(this);
}
}
}
interface AnimateData {
obj: globalThis.Animate;
/** 第一帧是全局第几帧 */
readonly start: number;
/** 当前是第几帧 */
index: number;
/** 是否需要播放音频 */
sound: boolean;
centerX: number;
centerY: number;
onEnd?: () => void;
readonly absolute: boolean;
}
export interface EAnimateEvent extends ERenderItemEvent {}
export class Animate extends RenderItem<EAnimateEvent> {
/** 绝对位置的动画 */
private absoluteAnimates: Set<AnimateData> = new Set();
/** 静态位置的动画 */
private staticAnimates: Set<AnimateData> = new Set();
private delegation: number;
private frame: number = 0;
private lastTime: number = 0;
constructor() {
super('absolute', false, true);
this.delegation = this.delegateTicker(time => {
if (time - this.lastTime < 50) return;
this.lastTime = time;
this.frame++;
if (
this.absoluteAnimates.size > 0 ||
this.staticAnimates.size > 0
) {
this.update(this);
}
});
adapter.add(this);
}
protected render(
canvas: MotaOffscreenCanvas2D,
transform: Transform
): void {
if (
this.absoluteAnimates.size === 0 &&
this.staticAnimates.size === 0
) {
return;
}
this.drawAnimates(this.absoluteAnimates, canvas);
transformCanvas(canvas, transform);
this.drawAnimates(this.staticAnimates, canvas);
}
private drawAnimates(
data: Set<AnimateData>,
canvas: MotaOffscreenCanvas2D
) {
if (data.size === 0) return;
const { ctx } = canvas;
const toDelete = new Set<AnimateData>();
data.forEach(v => {
const obj = v.obj;
const index = v.index;
const frame = obj.frames[index];
const ratio = obj.ratio;
if (!v.sound) {
const se = (index % obj.frame) + 1;
core.playSound(v.obj.se[se], v.obj.pitch[se]);
v.sound = true;
}
const centerX = v.centerX;
const centerY = v.centerY;
frame.forEach(v => {
const img = obj.images[v.index];
if (!img) return;
const realWidth = (img.width * ratio * v.zoom) / 100;
const realHeight = (img.height * ratio * v.zoom) / 100;
ctx.globalAlpha = v.opacity / 255;
const cx = centerX + v.x;
const cy = centerY + v.y;
const ix = -realWidth / 2;
const iy = -realHeight / 2;
const angle = v.angle ? (-v.angle * Math.PI) / 180 : 0;
ctx.save();
ctx.translate(cx, cy);
if (v.mirror) {
ctx.scale(-1, 1);
}
ctx.rotate(angle);
ctx.drawImage(img, ix, iy, realWidth, realHeight);
ctx.restore();
});
const now = this.frame - v.start;
if (now !== v.index) v.sound = true;
v.index = now;
if (v.index === v.obj.frame) {
toDelete.add(v);
}
});
toDelete.forEach(v => {
data.delete(v);
v.onEnd?.();
});
}
/**
*
* @param name
* @param absolute transform的影响
*/
animate(
name: AnimationIds,
x: number,
y: number,
absolute: boolean = false
) {
const animate = core.material.animates[name];
const data: AnimateData = {
index: 0,
start: this.frame,
obj: animate,
centerX: x,
centerY: y,
absolute,
sound: false
};
return data;
}
/**
* Promise
* @param animate
* @returns
*/
draw(animate: AnimateData): Promise<void> {
return new Promise(res => {
if (animate.absolute) {
this.absoluteAnimates.add(animate);
} else {
this.staticAnimates.add(animate);
}
animate.onEnd = () => {
res();
};
});
}
/**
*
* @param name
* @param absolute
*/
drawAnimate(
name: AnimationIds,
x: number,
y: number,
absolute: boolean = false
) {
return this.draw(this.animate(name, x, y, absolute));
}
destroy(): void {
super.destroy();
this.removeTicker(this.delegation);
adapter.remove(this);
}
}
const adapter = new RenderAdapter<Animate>('animate');
adapter.receive('drawAnimate', (item, name, x, y, absolute) => {
return item.drawAnimate(name, x, y, absolute);
});
adapter.receiveGlobal('drawHeroAnimate', name => {
const execute: Promise<void>[] = [];
LayerGroupAnimate.animateList.forEach(v => {
execute.push(v.drawHeroAnimate(name));
});
return Promise.all(execute);
});

View File

@ -0,0 +1,319 @@
import { EventEmitter } from 'eventemitter3';
import { logger } from '@motajs/common';
import { MotaOffscreenCanvas2D, RenderItem } from '@motajs/render-core';
interface BlockCacherEvent {
split: [];
beforeClear: [index: number];
}
interface BlockData {
/** 横向宽度包括rest的那一个块 */
width: number;
/** 纵向宽度包括rest的那一个块 */
height: number;
/** 横向最后一个块的宽度 */
restWidth: number;
/** 纵向最后一个块的高度 */
restHeight: number;
}
export interface IBlockCacheable {
/**
*
*/
destroy(): void;
}
/**
*
* 13x13划分缓存13x13的缓存分块
* 便`xx -> yy`
* xx说明传入的数据是元素还是分块的数据yy表示其返回值或转换为的值
*/
export class BlockCacher<
T extends IBlockCacheable
> extends EventEmitter<BlockCacherEvent> {
/** 区域宽度 */
width: number;
/** 区域高度 */
height: number;
/** 区域面积 */
area: number = 0;
/** 分块大小 */
blockSize: number;
/** 分块信息 */
blockData: BlockData = {
width: 0,
height: 0,
restWidth: 0,
restHeight: 0
};
/** 缓存深度例如填4的时候表示每格包含4个缓存 */
cacheDepth: number = 1;
/** 缓存内容,计算公式为 (x + y * width) * depth + deep */
cache: Map<number, T> = new Map();
constructor(
width: number,
height: number,
size: number,
depth: number = 1
) {
super();
this.width = width;
this.height = height;
this.blockSize = size;
this.cacheDepth = depth;
this.split();
}
/**
*
* @param width
* @param height
*/
size(width: number, height: number) {
this.width = width;
this.height = height;
this.split();
}
/**
*
*/
setBlockSize(size: number) {
this.blockSize = size;
this.split();
}
/**
* 31
* @param depth
*/
setCacheDepth(depth: number) {
if (depth > 31) {
logger.error(11);
return;
}
const old = this.cache;
const before = this.cacheDepth;
this.cache = new Map();
old.forEach((v, k) => {
const index = Math.floor(k / before);
const deep = k % before;
this.cache.set(index * depth + deep, v);
});
old.clear();
this.cacheDepth = depth;
}
/**
*
*/
split() {
this.blockData = {
width: Math.ceil(this.width / this.blockSize),
height: Math.ceil(this.height / this.blockSize),
restWidth: this.width % this.blockSize,
restHeight: this.height % this.blockSize
};
this.area = this.blockData.width * this.blockData.height;
this.emit('split');
}
/**
* ->void
* @param index
* @param deep 310b111就是清除前三层的索引
*/
clearCache(index: number, deep: number) {
const depth = this.cacheDepth;
for (let i = 0; i < depth; i++) {
if (deep & (1 << i)) {
const nowIndex = index * this.cacheDepth + i;
const item = this.cache.get(nowIndex);
item?.destroy();
this.cache.delete(nowIndex);
}
}
}
/**
* {@link clearCache} ->void
*/
clearCacheByIndex(index: number) {
const item = this.cache.get(index);
item?.destroy();
this.cache.delete(index);
}
/**
*
*/
clearAllCache() {
this.cache.forEach(v => v.destroy());
this.cache.clear();
}
/**
* ->
*/
getIndex(x: number, y: number) {
return x + y * this.blockData.width;
}
/**
* ->
*/
getIndexByLoc(x: number, y: number) {
return this.getIndex(
Math.floor(x / this.blockSize),
Math.floor(y / this.blockSize)
);
}
/**
* ->
*/
getBlockXYByIndex(index: number): LocArr {
const width = this.blockData.width;
return [index % width, Math.floor(index / width)];
}
/**
* 使->
*/
getBlockXY(x: number, y: number): LocArr {
return [Math.floor(x / this.blockSize), Math.floor(y / this.blockSize)];
}
/**
* deep获取一个分块的精确索引->
*/
getPreciseIndex(x: number, y: number, deep: number) {
return (x + y * this.blockSize) * this.cacheDepth + deep;
}
/**
* deep获取元素所在块的精确索引->
*/
getPreciseIndexByLoc(x: number, y: number, deep: number) {
return this.getPreciseIndex(...this.getBlockXY(x, y), deep);
}
/**
* ->
* @param deep
* @returns
*/
updateElementArea(
x: number,
y: number,
width: number,
height: number,
deep: number = 2 ** 31 - 1
) {
const [bx, by] = this.getBlockXY(x, y);
const [ex, ey] = this.getBlockXY(x + width - 1, y + height - 1);
return this.updateArea(bx, by, ex - bx, ey - by, deep);
}
/**
* ->
* @param deep
* @returns
*/
updateArea(
x: number,
y: number,
width: number,
height: number,
deep: number = 2 ** 31 - 1
) {
const blocks = this.getIndexOf(x, y, width, height);
blocks.forEach(v => {
this.clearCache(v, deep);
});
return blocks;
}
/**
* ->
*/
getIndexOf(x: number, y: number, width: number, height: number) {
const res = new Set<number>();
const sx = Math.max(x, 0);
const sy = Math.max(y, 0);
const ex = Math.min(x + width, this.blockData.width);
const ey = Math.min(y + height, this.blockData.height);
for (let nx = sx; nx <= ex; nx++) {
for (let ny = sy; ny <= ey; ny++) {
const index = this.getIndex(nx, ny);
res.add(index);
}
}
return res;
}
/**
* ->
*/
getIndexOfElement(x: number, y: number, width: number, height: number) {
const [bx, by] = this.getBlockXY(x, y);
const [ex, ey] = this.getBlockXY(x + width, y + height);
return this.getIndexOf(bx, by, ex - bx, ey - by);
}
/**
* ->
* @param block
*/
getRectOfIndex(block: number) {
const [x, y] = this.getBlockXYByIndex(block);
return this.getRectOfBlockXY(x, y);
}
/**
* ->
* @param x
* @param y
*/
getRectOfBlockXY(x: number, y: number) {
return [
x * this.blockSize,
y * this.blockSize,
(x + 1) * this.blockSize,
(y + 1) * this.blockSize
];
}
/**
*
*/
destroy() {
this.clearAllCache();
}
}
export interface ICanvasCacheItem extends IBlockCacheable {
readonly canvas: MotaOffscreenCanvas2D;
symbol: number;
}
export class CanvasCacheItem implements ICanvasCacheItem {
constructor(
public readonly canvas: MotaOffscreenCanvas2D,
public readonly symbol: number,
public readonly element: RenderItem<any>
) {}
destroy(): void {
this.element.deleteCanvas(this.canvas);
}
}

View File

@ -1,11 +1,12 @@
import { logger } from '@motajs/common'; import { logger } from '@motajs/common';
import { MotaOffscreenCanvas2D, SizedCanvasImageSource } from '@motajs/render'; import { MotaOffscreenCanvas2D } from '@motajs/render-core';
import { SizedCanvasImageSource } from '@motajs/render-assets';
// 经过测试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
type ImageMapKeys = Exclude<Cls, 'tileset' | 'autotile'>; type ImageMapKeys = Exclude<Cls, 'tileset' | 'autotile'>;
type ImageMap = Record<ImageMapKeys, ImageBitmap>; type ImageMap = Record<ImageMapKeys, HTMLImageElement>;
const i = (img: ImageMapKeys) => { const i = (img: ImageMapKeys) => {
return core.material.images[img]; return core.material.images[img];
@ -21,10 +22,10 @@ interface AutotileCache {
type AutotileCaches = Record<AllNumbersOf<'autotile'>, AutotileCache>; type AutotileCaches = Record<AllNumbersOf<'autotile'>, AutotileCache>;
interface TextureRequire { interface TextureRequire {
tileset: Record<string, ImageBitmap>; tileset: Record<string, HTMLImageElement>;
material: Record<ImageMapKeys, ImageBitmap>; material: Record<ImageMapKeys, HTMLImageElement>;
autotile: AutotileCaches; autotile: AutotileCaches;
images: Record<ImageIds, ImageBitmap>; images: Record<ImageIds, HTMLImageElement>;
} }
interface RenderableDataBase { interface RenderableDataBase {
@ -49,10 +50,10 @@ export interface AutotileRenderable extends RenderableDataBase {
} }
class TextureCache { class TextureCache {
tileset!: Record<string, ImageBitmap>; tileset!: Record<string, HTMLImageElement>;
material: Record<ImageMapKeys, ImageBitmap>; material: Record<ImageMapKeys, HTMLImageElement>;
autotile!: AutotileCaches; autotile!: AutotileCaches;
images!: Record<ImageIds, ImageBitmap>; images!: Record<ImageIds, HTMLImageElement>;
idNumberMap!: IdToNumber; idNumberMap!: IdToNumber;
@ -76,7 +77,7 @@ class TextureCache {
characterTurn2: Dir2[] = ['leftup', 'rightup', 'rightdown', 'leftdown']; characterTurn2: Dir2[] = ['leftup', 'rightup', 'rightdown', 'leftdown'];
constructor() { constructor() {
this.material = imageMap as Record<ImageMapKeys, ImageBitmap>; this.material = imageMap as Record<ImageMapKeys, HTMLImageElement>;
} }
init() { init() {

View File

@ -0,0 +1,624 @@
import { Animation, TimingFn, Transition } from 'mutate-animate';
import { RenderItem, Transform } from '@motajs/render-core';
import { logger } from '@motajs/common';
import EventEmitter from 'eventemitter3';
export interface ICameraTranslate {
readonly type: 'translate';
readonly from: Camera;
x: number;
y: number;
}
export interface ICameraRotate {
readonly type: 'rotate';
readonly from: Camera;
/** 旋转角,单位弧度 */
angle: number;
}
export interface ICameraScale {
readonly type: 'scale';
readonly from: Camera;
x: number;
y: number;
}
type CameraOperation = ICameraTranslate | ICameraScale | ICameraRotate;
interface CameraEvent {
destroy: [];
}
export class Camera extends EventEmitter<CameraEvent> {
/** 当前绑定的渲染元素 */
readonly binded: RenderItem;
/** 目标变换矩阵,默认与 `this.binded.transform` 同引用 */
transform: Transform;
/** 委托ticker的id */
private delegation: number;
/** 所有的动画id */
private animationIds: Set<number> = new Set();
/** 是否需要更新视角 */
private needUpdate: boolean = false;
/** 是否启用摄像机 */
private enabled: boolean = true;
/** 变换操作列表,因为矩阵乘法跟顺序有关,因此需要把各个操作拆分成列表进行 */
protected operation: CameraOperation[] = [];
/** 渲染元素到摄像机的映射 */
private static cameraMap: Map<RenderItem, Camera> = new Map();
/**
* 使`new Camera`
* @param item
*/
static for(item: RenderItem) {
const camera = this.cameraMap.get(item);
if (!camera) {
const ca = new Camera(item);
this.cameraMap.set(item, ca);
return ca;
} else {
return camera;
}
}
constructor(item: RenderItem) {
super();
this.binded = item;
this.delegation = item.delegateTicker(() => this.tick());
this.transform = item.transform;
item.on('destroy', () => {
this.destroy();
});
const ca = Camera.cameraMap.get(item);
if (ca && ca.enabled) {
logger.warn(22);
}
}
private tick = () => {
if (!this.needUpdate || !this.enabled) return;
const trans = this.transform;
trans.reset();
for (const o of this.operation) {
if (o.type === 'translate') {
trans.translate(-o.x, -o.y);
} else if (o.type === 'rotate') {
trans.rotate(o.angle);
} else {
trans.scale(o.x, o.y);
}
}
this.binded.update(this.binded);
this.needUpdate = false;
};
/**
*
*/
disable() {
this.enabled = false;
}
/**
*
*/
enable() {
this.enabled = true;
}
/**
*
*/
requestUpdate() {
this.needUpdate = true;
}
/**
*
* @param operation
*/
removeOperation(operation: CameraOperation) {
const index = this.operation.indexOf(operation);
if (index === -1) return;
this.operation.splice(index, 1);
}
/**
*
*/
clearOperation() {
this.operation.splice(0);
}
/**
*
* @returns
*/
addTranslate(): ICameraTranslate {
const item: ICameraTranslate = {
type: 'translate',
x: 0,
y: 0,
from: this
};
this.operation.push(item);
return item;
}
/**
*
* @returns
*/
addRotate(): ICameraRotate {
const item: ICameraRotate = {
type: 'rotate',
angle: 0,
from: this
};
this.operation.push(item);
return item;
}
/**
*
* @returns
*/
addScale(): ICameraScale {
const item: ICameraScale = {
type: 'scale',
x: 1,
y: 1,
from: this
};
this.operation.push(item);
return item;
}
/**
*
* @param time
* @param update
*/
applyAnimation(time: number, update: () => void) {
const delegation = this.binded.delegateTicker(
() => {
update();
this.needUpdate = true;
},
time,
() => {
update();
this.needUpdate = true;
this.animationIds.delete(delegation);
}
);
this.animationIds.add(delegation);
}
/**
*
* @param operation
* @param animate
* @param time
*/
applyTranslateAnimation(
operation: ICameraTranslate,
animate: Animation,
time: number
) {
if (operation.from !== this) {
logger.warn(20);
return;
}
const update = () => {
operation.x = animate.x;
operation.y = animate.y;
};
this.applyAnimation(time, update);
}
/**
*
* @param operation
* @param animate
* @param time
*/
applyRotateAnimation(
operation: ICameraRotate,
animate: Animation,
time: number
) {
if (operation.from !== this) {
logger.warn(20);
return;
}
const update = () => {
operation.angle = animate.angle;
};
this.applyAnimation(time, update);
}
/**
*
* @param operation
* @param animate
* @param time
*/
applyScaleAnimation(
operation: ICameraScale,
animate: Animation,
time: number
) {
if (operation.from !== this) {
logger.warn(20);
return;
}
const update = () => {
operation.x = animate.size;
operation.y = animate.size;
};
this.applyAnimation(time, update);
}
/**
* 使 x,y `transition.value.x``transition.value.y`
* @param operation
* @param animate
* @param time
*/
applyTranslateTransition(
operation: ICameraTranslate,
animate: Transition,
time: number
) {
if (operation.from !== this) {
logger.warn(21);
return;
}
const update = () => {
operation.x = animate.value.x;
operation.y = animate.value.y;
};
this.applyAnimation(time, update);
}
/**
* 使 angle `transition.value.angle`
* @param operation
* @param animate
* @param time
*/
applyRotateTransition(
operation: ICameraRotate,
animate: Transition,
time: number
) {
if (operation.from !== this) {
logger.warn(21);
return;
}
const update = () => {
operation.angle = animate.value.angle;
};
this.applyAnimation(time, update);
}
/**
* 使 size `transition.value.size`
* @param operation
* @param animate
* @param time
*/
applyScaleTransition(
operation: ICameraScale,
animate: Transition,
time: number
) {
if (operation.from !== this) {
logger.warn(21);
return;
}
const update = () => {
operation.x = animate.value.size;
operation.y = animate.value.size;
};
this.applyAnimation(time, update);
}
/**
*
*/
stopAllAnimates() {
this.animationIds.forEach(v => this.binded.removeTicker(v));
}
/**
* 使
*/
destroy() {
this.binded.removeTicker(this.delegation);
this.animationIds.forEach(v => this.binded.removeTicker(v));
Camera.cameraMap.delete(this.binded);
this.emit('destroy');
}
}
interface CameraAnimationBase {
type: string;
time: number;
start: number;
}
export interface TranslateAnimation extends CameraAnimationBase {
type: 'translate';
timing: TimingFn;
x: number;
y: number;
}
export interface TranslateAsAnimation extends CameraAnimationBase {
type: 'translateAs';
timing: TimingFn<2>;
time: number;
}
export interface RotateAnimation extends CameraAnimationBase {
type: 'rotate';
timing: TimingFn;
angle: number;
time: number;
}
export interface ScaleAnimation extends CameraAnimationBase {
type: 'scale';
timing: TimingFn;
scale: number;
time: number;
}
export type CameraAnimationData =
| TranslateAnimation
| TranslateAsAnimation
| RotateAnimation
| ScaleAnimation;
export interface CameraAnimationExecution {
data: CameraAnimationData[];
animation: Animation;
}
interface CameraAnimationEvent {
animate: [
operation: CameraOperation,
execution: CameraAnimationExecution,
item: CameraAnimationData
];
}
export class CameraAnimation extends EventEmitter<CameraAnimationEvent> {
camera: Camera;
/** 动画开始时刻 */
private startTime: number = 0;
/** 动画结束时刻 */
private endTime: number = 0;
/** 委托ticker的id */
private delegation: number;
/** 动画是否开始 */
private started: boolean = false;
/** 每个摄像机操作的动画映射 */
private animateMap: Map<CameraOperation, CameraAnimationExecution> =
new Map();
constructor(camera: Camera) {
super();
this.camera = camera;
this.delegation = camera.binded.delegateTicker(this.tick);
}
private tick = () => {
if (!this.started) return;
const now = Date.now();
const time = now - this.startTime;
if (now - this.startTime > this.endTime + 50) {
this.destroy();
return;
}
this.animateMap.forEach((exe, ope) => {
const data = exe.data;
if (data.length === 0) return;
const item = data[0];
if (item.start < time) {
this.executeAnimate(exe, item);
data.shift();
this.emit('animate', ope, exe, item);
}
});
this.camera.requestUpdate();
};
private executeAnimate(
execution: CameraAnimationExecution,
animate: CameraAnimationData
) {
if (animate.type === 'translateAs') {
const ani = this.ensureAnimate(execution);
ani.time(animate.time).moveAs(animate.timing);
} else if (animate.type === 'translate') {
const ani = this.ensureAnimate(execution);
const { x, y, time, timing } = animate;
ani.mode(timing).time(time).move(x, y);
} else if (animate.type === 'rotate') {
const ani = this.ensureAnimate(execution);
const { angle, time, timing } = animate;
ani.mode(timing).time(time).rotate(angle);
} else {
const ani = this.ensureAnimate(execution);
const { scale, time, timing } = animate;
ani.mode(timing).time(time).scale(scale);
}
}
private ensureAnimate(execution: CameraAnimationExecution) {
if (execution.animation) return execution.animation;
const ani = new Animation();
execution.animation = ani;
return ani;
}
private ensureOperation(operation: CameraOperation) {
if (!this.animateMap.has(operation)) {
const data: CameraAnimationExecution = {
data: [],
animation: new Animation()
};
this.animateMap.set(operation, data);
return data;
} else {
return this.animateMap.get(operation)!;
}
}
/**
*
* @param operation
* @param x
* @param y
* @param time
* @param start
* @param timing
*/
translate(
operation: ICameraTranslate,
x: number,
y: number,
time: number,
start: number,
timing: TimingFn
) {
const exe = this.ensureOperation(operation);
const data: TranslateAnimation = {
type: 'translate',
timing,
x: x * 32,
y: y * 32,
time,
start
};
exe.data.push(data);
}
/**
*
* @param operation
* @param angle
* @param time
* @param start
* @param timing
*/
rotate(
operation: ICameraRotate,
angle: number,
time: number,
start: number,
timing: TimingFn
) {
const exe = this.ensureOperation(operation);
const data: RotateAnimation = {
type: 'rotate',
timing,
angle,
time,
start
};
exe.data.push(data);
}
/**
*
* @param operation
* @param scale
* @param time
* @param start
* @param timing
*/
scale(
operation: ICameraScale,
scale: number,
time: number,
start: number,
timing: TimingFn
) {
const exe = this.ensureOperation(operation);
const data: ScaleAnimation = {
type: 'scale',
timing,
scale,
time,
start
};
exe.data.push(data);
}
/**
*
*/
start() {
if (this.started) return;
this.startTime = Date.now();
this.started = true;
let endTime = 0;
this.animateMap.forEach((exe, ope) => {
const data = exe.data;
data.sort((a, b) => a.start - b.start);
const end = data.at(-1);
if (!end) return;
const t = end.start + end.time;
if (t > endTime) endTime = t;
const cam = this.camera;
if (ope.type === 'translate') {
cam.applyTranslateAnimation(ope, exe.animation, t + 100);
} else if (ope.type === 'rotate') {
cam.applyRotateAnimation(ope, exe.animation, t + 100);
} else {
cam.applyScaleAnimation(ope, exe.animation, t + 100);
}
});
this.endTime = endTime + this.startTime;
this.tick();
}
destroy() {
this.camera.binded.removeTicker(this.delegation);
this.camera.stopAllAnimates();
this.animateMap.forEach(v => {
v.animation.ticker.destroy();
});
this.animateMap.clear();
}
}

View File

@ -0,0 +1,543 @@
import {
ERenderItemEvent,
RenderItem,
MotaOffscreenCanvas2D,
Transform,
transformCanvas
} from '@motajs/render-core';
import { logger } from '@motajs/common';
import EventEmitter from 'eventemitter3';
import { isNil } from 'lodash-es';
import { IDamageEnemy, IEnemyCollection, MapDamage } from '@motajs/types';
import { BlockCacher, ICanvasCacheItem, CanvasCacheItem } from './block';
import {
ILayerGroupRenderExtends,
LayerGroupFloorBinder,
LayerGroup,
Layer,
calNeedRenderOf
} from './layer';
import { MAP_BLOCK_WIDTH, MAP_HEIGHT, MAP_WIDTH } from '../shared';
/**
*
* @param damage
*/
export function getDamageColor(damage: number): string {
if (typeof damage !== 'number') return '#f00';
if (damage === 0) return '#2f2';
if (damage < 0) return '#7f7';
if (damage < core.status.hero.hp / 3) return '#fff';
if (damage < (core.status.hero.hp * 2) / 3) return '#ff4';
if (damage < core.status.hero.hp) return '#f93';
return '#f22';
}
interface EFloorDamageEvent {
update: [floor: FloorIds];
}
export class FloorDamageExtends
extends EventEmitter<EFloorDamageEvent>
implements ILayerGroupRenderExtends
{
id: string = 'floor-damage';
floorBinder!: LayerGroupFloorBinder;
group!: LayerGroup;
sprite!: Damage;
/**
*
*/
update(floor: FloorIds) {
if (!this.sprite || !floor) return;
const map = core.status.maps[floor];
this.sprite.setMapSize(map.width, map.height);
const { ensureFloorDamage } = Mota.require('@user/data-state');
ensureFloorDamage(floor);
const enemy = core.status.maps[floor].enemy;
this.sprite.updateCollection(enemy);
this.emit('update', floor);
}
/**
*
*/
private create() {
if (this.sprite) return;
const sprite = new Damage();
sprite.setZIndex(80);
this.group.appendChild(sprite);
this.sprite = sprite;
}
private onUpdate = (floor: FloorIds) => {
if (!this.floorBinder.bindThisFloor) {
const { ensureFloorDamage } = Mota.require('@user/data-state');
ensureFloorDamage(floor);
core.status.maps[floor].enemy.calRealAttribute();
}
this.update(floor);
};
// private onSetBlock = (x: number, y: number, floor: FloorIds) => {
// this.sprite.enemy?.once('extract', () => {
// if (floor !== this.sprite.enemy?.floorId) return;
// this.sprite.updateBlocks();
// });
// if (!this.floorBinder.bindThisFloor) {
// this.sprite.enemy?.extract();
// }
// };
/**
*
*/
private listen() {
this.floorBinder.on('update', this.onUpdate);
// this.floorBinder.on('setBlock', this.onSetBlock);
}
awake(group: LayerGroup): void {
const ex = group.getExtends('floor-binder');
if (ex instanceof LayerGroupFloorBinder) {
this.floorBinder = ex;
this.group = group;
this.create();
this.listen();
} else {
logger.warn(17);
group.removeExtends('floor-damage');
}
}
onDestroy(_group: LayerGroup): void {
this.floorBinder.off('update', this.onUpdate);
// this.floorBinder.off('setBlock', this.onSetBlock);
}
}
export interface DamageRenderable {
x: number;
y: number;
align: CanvasTextAlign;
baseline: CanvasTextBaseline;
text: string;
color: CanvasStyle;
font?: string;
stroke?: CanvasStyle;
strokeWidth?: number;
}
export interface EDamageEvent extends ERenderItemEvent {
setMapSize: [width: number, height: number];
beforeDamageRender: [need: Set<number>, transform: Transform];
updateBlocks: [blocks: Set<number>];
dirtyUpdate: [block: number];
}
export class Damage extends RenderItem<EDamageEvent> {
mapWidth: number = 0;
mapHeight: number = 0;
block: BlockCacher<ICanvasCacheItem>;
/** 键表示分块索引,值表示在这个分块上的渲染信息(当然实际渲染位置可以不在这个分块上) */
renderable: Map<number, Set<DamageRenderable>> = new Map();
/** 当前渲染怪物列表 */
enemy?: IEnemyCollection;
/** 每个分块中包含的怪物集合 */
blockData: Map<number, Map<number, IDamageEnemy>> = new Map();
/** 单元格大小 */
cellSize: number = 32;
/** 默认伤害字体 */
font: string = '300 9px Verdana';
/** 默认描边样式,当伤害文字不存在描边属性时会使用此属性 */
strokeStyle: CanvasStyle = '#000';
/** 默认描边宽度 */
strokeWidth: number = 2;
/** 要懒更新的所有分块 */
private dirtyBlocks: Set<number> = new Set();
constructor() {
super('absolute', false, true);
this.block = new BlockCacher(0, 0, MAP_BLOCK_WIDTH, 1);
this.type = 'absolute';
this.size(MAP_WIDTH, MAP_HEIGHT);
this.setHD(true);
this.setAntiAliasing(true);
}
protected render(
canvas: MotaOffscreenCanvas2D,
transform: Transform
): void {
this.renderDamage(canvas, transform);
}
private onExtract = () => {
if (this.enemy) this.updateCollection(this.enemy);
};
/**
*
*/
setMapSize(width: number, height: number) {
this.mapWidth = width;
this.mapHeight = height;
this.enemy = void 0;
this.blockData.clear();
this.renderable.clear();
this.block.size(width, height);
// 预留blockData
const w = this.block.blockData.width;
const h = this.block.blockData.height;
const num = w * h;
for (let i = 0; i < num; i++) {
this.blockData.set(i, new Map());
this.renderable.set(i, new Set());
this.dirtyBlocks.add(i);
}
this.emit('setMapSize', width, height);
}
/**
*
*/
setCellSize(size: number) {
this.cellSize = size;
this.update();
}
/**
* {@link Damage.enemy}
* @param enemy
*/
updateCollection(enemy: IEnemyCollection) {
if (this.enemy !== enemy) {
this.enemy?.off('calculated', this.onExtract);
enemy.on('calculated', this.onExtract);
}
this.enemy = enemy;
this.blockData.forEach(v => v.clear());
this.renderable.forEach(v => v.clear());
this.block.clearAllCache();
const w = this.block.blockData.width;
const h = this.block.blockData.height;
const num = w * h;
for (let i = 0; i < num; i++) {
this.dirtyBlocks.add(i);
}
enemy.list.forEach(v => {
if (isNil(v.x) || isNil(v.y)) return;
const index = this.block.getIndexByLoc(v.x, v.y);
this.blockData.get(index)?.set(v.y * this.mapWidth + v.x, v);
});
// this.updateBlocks();
this.update(this);
}
/**
*
* @param x
* @param y
* @param width
* @param height
*/
updateRenderable(x: number, y: number, width: number, height: number) {
this.updateBlocks(this.block.updateElementArea(x, y, width, height));
}
/**
*
* @param blocks
* @param map
*/
updateBlocks(blocks?: Set<number>) {
if (blocks) {
blocks.forEach(v => this.dirtyBlocks.add(v));
this.emit('updateBlocks', blocks);
} else {
this.blockData.forEach((_v, i) => {
this.dirtyBlocks.add(i);
});
this.emit('updateBlocks', new Set(this.blockData.keys()));
}
this.update(this);
}
/**
*
*/
updateEnemyOn(x: number, y: number) {
const enemy = this.enemy?.get(x, y);
const block = this.block.getIndexByLoc(x, y);
const data = this.blockData.get(block);
const index = x + y * this.mapWidth;
if (!data) return;
if (!enemy) {
data.delete(index);
} else {
data.set(index, enemy);
}
this.update(this);
// 渲染懒更新,优化性能表现
this.dirtyBlocks.add(block);
}
/**
*
* @param block
* @param map
*/
private updateBlock(block: number, map: boolean = true) {
const data = this.blockData.get(block);
if (!data) return;
this.block.clearCache(block, 1);
const renderable = this.renderable.get(block)!;
renderable.clear();
data.forEach(v => this.extract(v, renderable));
if (map) this.extractMapDamage(block, renderable);
}
/**
* renderable的伤害
* @param enemy
* @param block
*/
private extract(enemy: IDamageEnemy, block: Set<DamageRenderable>) {
if (enemy.progress !== 4) return;
const x = enemy.x!;
const y = enemy.y!;
const { damage } = enemy.calDamage();
const cri = enemy.calCritical(1)[0]?.atkDelta ?? Infinity;
const dam1: DamageRenderable = {
align: 'left',
baseline: 'alphabetic',
text: isFinite(damage) ? core.formatBigNumber(damage, true) : '???',
color: getDamageColor(damage),
x: x * this.cellSize + 1,
y: y * this.cellSize + this.cellSize - 1
};
const dam2: DamageRenderable = {
align: 'left',
baseline: 'alphabetic',
text: isFinite(cri) ? core.formatBigNumber(cri, true) : '?',
color: '#fff',
x: x * this.cellSize + 1,
y: y * this.cellSize + this.cellSize - 11
};
block.add(dam1).add(dam2);
}
/**
*
* @param block
*/
private extractMapDamage(block: number, renderable: Set<DamageRenderable>) {
if (!this.enemy) return;
const damage = this.enemy.mapDamage;
const [sx, sy, ex, ey] = this.block.getRectOfIndex(block);
for (let x = sx; x < ex; x++) {
for (let y = sy; y < ey; y++) {
const loc = `${x},${y}`;
const dam = damage[loc];
if (!dam) continue;
this.pushMapDamage(x, y, renderable, dam);
}
}
}
/**
*
*/
private extractAllMapDamage() {
// todo: 测试性能,这样真的会更快吗?或许能更好的优化?或者是根本不需要这个函数?
if (!this.enemy) return;
for (const [loc, enemy] of Object.entries(this.enemy.mapDamage)) {
const [sx, sy] = loc.split(',');
const x = Number(sx);
const y = Number(sy);
const block = this.renderable.get(this.block.getIndexByLoc(x, y))!;
this.pushMapDamage(x, y, block, enemy);
}
}
private pushMapDamage(
x: number,
y: number,
block: Set<DamageRenderable>,
dam: MapDamage
) {
// todo: 这个应当可以自定义,通过地图伤害注册实现
let text = '';
const color = '#fa3';
const font = '300 9px Verdana';
if (dam.damage > 0) {
text = core.formatBigNumber(dam.damage, true);
} else if (dam.ambush) {
text = `!`;
} else if (dam.repulse) {
text = '阻';
}
const mapDam: DamageRenderable = {
align: 'center',
baseline: 'middle',
text,
color,
font,
x: x * this.cellSize + this.cellSize / 2,
y: y * this.cellSize + this.cellSize / 2
};
block.add(mapDam);
}
/**
*
*/
calNeedRender(transform: Transform) {
if (this.parent instanceof LayerGroup) {
// 如果处于地图组中,每个地图的渲染区域应该是一样的,因此可以缓存优化
return this.parent.cacheNeedRender(transform, this.block);
} else if (this.parent instanceof Layer) {
// 如果是地图的子元素直接调用Layer的计算函数
return this.parent.calNeedRender(transform);
} else {
return calNeedRenderOf(transform, this.cellSize, this.block);
}
}
/**
*
* @param transform
*/
renderDamage(canvas: MotaOffscreenCanvas2D, transform: Transform) {
// console.time('damage');
const { ctx } = canvas;
ctx.save();
transformCanvas(canvas, transform);
const render = this.calNeedRender(transform);
const block = this.block;
const cell = this.cellSize;
const size = cell * block.blockSize;
this.emit('beforeDamageRender', render, transform);
render.forEach(v => {
const [x, y] = block.getBlockXYByIndex(v);
const bx = x * block.blockSize;
const by = y * block.blockSize;
const px = bx * cell;
const py = by * cell;
// todo: 是否真的需要缓存
// 检查有没有缓存
const cache = block.cache.get(v);
if (cache && cache.symbol === cache.canvas.symbol) {
ctx.drawImage(cache.canvas.canvas, px, py, size, size);
return;
}
if (this.dirtyBlocks.has(v)) {
this.updateBlock(v, true);
}
this.emit('dirtyUpdate', v);
// 否则依次渲染并写入缓存
const temp = block.cache.get(v)?.canvas ?? this.requireCanvas();
temp.clear();
temp.setHD(true);
temp.setAntiAliasing(true);
temp.size(size, size);
const { ctx: ct } = temp;
ct.translate(-px, -py);
ct.lineJoin = 'round';
ct.lineCap = 'round';
const render = this.renderable.get(v);
render?.forEach(v => {
if (!v) return;
ct.fillStyle = v.color;
ct.textAlign = v.align;
ct.textBaseline = v.baseline;
ct.font = v.font ?? this.font;
ct.strokeStyle = v.stroke ?? this.strokeStyle;
ct.lineWidth = v.strokeWidth ?? this.strokeWidth;
ct.strokeText(v.text, v.x, v.y);
ct.fillText(v.text, v.x, v.y);
});
ctx.drawImage(temp.canvas, px, py, size, size);
block.cache.set(v, new CanvasCacheItem(temp, temp.symbol, this));
});
ctx.restore();
// console.timeEnd('damage');
}
protected handleProps(
key: string,
_prevValue: any,
nextValue: any
): boolean {
switch (key) {
case 'mapWidth':
if (!this.assertType(nextValue, 'number', key)) return false;
this.setMapSize(nextValue, this.mapHeight);
return true;
case 'mapHeight':
if (!this.assertType(nextValue, 'number', key)) return false;
this.setMapSize(this.mapWidth, nextValue);
return true;
case 'cellSize':
if (!this.assertType(nextValue, 'number', key)) return false;
this.setCellSize(nextValue);
return true;
case 'enemy':
if (!this.assertType(nextValue, 'object', key)) return false;
this.updateCollection(nextValue);
return true;
case 'font':
if (!this.assertType(nextValue, 'string', key)) return false;
this.font = nextValue;
this.update();
return true;
case 'strokeStyle':
this.strokeStyle = nextValue;
this.update();
return true;
case 'strokeWidth':
if (!this.assertType(nextValue, 'number', key)) return false;
this.strokeWidth = nextValue;
this.update();
return true;
}
return false;
}
destroy(): void {
super.destroy();
this.block.destroy();
this.enemy?.off('extract', this.onExtract);
}
}
// const adapter = new RenderAdapter<Damage>('damage');

View File

@ -0,0 +1,54 @@
import EventEmitter from 'eventemitter3';
import { RenderItem } from '@motajs/render-core';
export interface IAnimateFrame {
updateFrameAnimate(frame: number, time: number): void;
}
interface RenderEvent {
animateFrame: [frame: number, time: number];
}
class RenderEmits extends EventEmitter<RenderEvent> {
private framer: Set<IAnimateFrame> = new Set();
/**
*
*/
addFramer(framer: IAnimateFrame) {
this.framer.add(framer);
}
/**
*
*/
removeFramer(framer: IAnimateFrame) {
this.framer.delete(framer);
}
/**
*
* @param frame
* @param time
*/
emitAnimateFrame(frame: number, time: number) {
this.framer.forEach(v => v.updateFrameAnimate(frame, time));
this.emit('animateFrame', frame, time);
}
}
export const renderEmits = new RenderEmits();
export function createFrame() {
Mota.require('@user/data-base').hook.once('reset', () => {
let lastTime = 0;
RenderItem.ticker.add(time => {
if (!core.isPlaying()) return;
if (time - lastTime > core.values.animateSpeed) {
RenderItem.animatedFrame++;
lastTime = time;
renderEmits.emitAnimateFrame(RenderItem.animatedFrame, time);
}
});
});
}

View File

@ -0,0 +1,481 @@
import { RenderAdapter } from '@motajs/render-core';
import { SizedCanvasImageSource } from '@motajs/render-assets';
import { logger } from '@motajs/common';
import { ILayerRenderExtends, Layer, LayerMovingRenderable } from './layer';
import EventEmitter from 'eventemitter3';
import { texture } from './cache';
import { TimingFn } from 'mutate-animate';
import { isNil } from 'lodash-es';
// type HeroMovingStatus = 'stop' | 'moving' | 'moving-as';
// export const enum HeroMovingStatus {}
interface HeroRenderEvent {
stepEnd: [];
moveTick: [x: number, y: number];
append: [renderable: LayerMovingRenderable[]];
}
export class HeroRenderer
extends EventEmitter<HeroRenderEvent>
implements ILayerRenderExtends
{
id: string = 'floor-hero';
/** 勇士的图片资源 */
image?: SizedCanvasImageSource;
cellWidth?: number;
cellHeight?: number;
/** 勇士的渲染信息 */
renderable?: LayerMovingRenderable;
layer!: Layer;
/** 当前移动帧数 */
movingFrame: number = 0;
/** 是否正在移动 */
moving: boolean = false;
/** 是否正在播放动画,与移动分开以实现原地踏步 */
animate: boolean = false;
/** 勇士移动速度 */
speed: number = 100;
/** 勇士移动的真正速度,经过录像修正 */
realSpeed: number = 100;
/** 当前的移动方向 */
moveDir: Dir2 = 'down';
/** 当前移动的勇士显示方向 */
showDir: Dir = 'down';
/** 帧动画是否反向播放例如后退时就应该设为true */
animateReverse: boolean = false;
/** 勇士移动定时器id */
private moveId: number = -1;
/** 上一次帧数切换的时间 */
private lastFrameTime: number = 0;
/** 上一步走到格子上的时间 */
private lastStepTime: number = 0;
/** 执行当前步移动的Promise */
private moveDetached?: Promise<void>;
/** endMove的Promise */
private moveEnding?: Promise<void>;
/**
* {@link moveDir}
* {@link moveDir}
*/
stepDir: Dir2 = 'down';
/** 每步的格子增量 */
private stepDelta: Loc = { x: 0, y: 1 };
/**
*
* @param image
*/
setImage(image: SizedCanvasImageSource) {
this.image = image;
this.split();
this.resetRenderable(true);
this.layer.requestUpdateMoving();
}
/**
*
* @param speed
*/
setMoveSpeed(speed: number) {
this.speed = speed;
this.fixMoveSpeed();
}
/**
* renderable信息
*/
split() {
this.cellWidth = this.image!.width / 4;
this.cellHeight = this.image!.height / 4;
this.generateRenderable();
}
/**
*
*/
generateRenderable() {
if (!this.image) return;
this.renderable = {
image: this.image,
frame: 4,
x: core.status.hero.loc.x,
y: core.status.hero.loc.y,
zIndex: core.status.hero.loc.y,
autotile: false,
bigImage: true,
render: this.getRenderFromDir(this.showDir),
animate: 0,
alpha: 1
};
}
/**
*
* @param dir
*/
getRenderFromDir(dir: Dir): [number, number, number, number][] {
if (!this.cellWidth || !this.cellHeight) return [];
const index = texture.characterDirection[dir];
const y = index * this.cellHeight;
const res: [number, number, number, number][] = [0, 1, 2, 3].map(v => {
return [v * this.cellWidth!, y, this.cellWidth!, this.cellHeight!];
});
if (this.animateReverse) return res.reverse();
else return res;
}
/**
*
*/
startAnimate() {
this.animate = true;
this.lastFrameTime = Date.now();
}
/**
*
*/
endAnimate() {
this.animate = false;
this.resetRenderable(false);
}
/**
*
* @param reverse
*/
setAnimateReversed(reverse: boolean) {
this.animateReverse = reverse;
this.resetRenderable(true);
}
/**
*
* @param dir
*/
setAnimateDir(dir: Dir) {
if (dir !== this.showDir) {
this.showDir = dir;
this.resetRenderable(true);
}
}
/**
* renderable状态
* @param getInfo
*/
resetRenderable(getInfo: boolean) {
this.movingFrame = 0;
if (this.renderable) {
this.renderable.animate = 0;
if (getInfo) {
this.renderable.render = this.getRenderFromDir(this.showDir);
}
}
this.layer.update(this.layer);
}
/**
*
*/
private animateTick(time: number) {
if (!this.animate) return;
if (time - this.lastFrameTime > this.speed) {
this.lastFrameTime = time;
this.movingFrame++;
this.movingFrame %= 4;
if (this.renderable) this.renderable.animate = this.movingFrame;
}
this.layer.update(this.layer);
}
/**
*
*/
private moveTick(time: number) {
if (!this.renderable) return;
if (this.moving) {
const progress = (time - this.lastStepTime) / this.realSpeed;
const { x: dx, y: dy } = this.stepDelta;
const { x, y } = core.status.hero.loc;
if (progress >= 1) {
this.renderable.x = x + dx;
this.renderable.y = y + dy;
this.fixMoveSpeed();
this.emit('stepEnd');
} else {
const rx = dx * progress + x;
const ry = dy * progress + y;
this.renderable.x = rx;
this.renderable.y = ry;
}
this.layer.update(this.layer);
}
this.emit('moveTick', this.renderable.x, this.renderable.y);
}
/**
*
*/
private step() {
this.stepDir = this.moveDir;
this.lastStepTime = Date.now();
this.stepDelta = core.utils.scan2[this.stepDir];
this.turn(this.stepDir);
}
private fixMoveSpeed() {
if (!core.isReplaying()) {
this.realSpeed = this.speed;
} else {
const replay = core.status.replay.speed;
this.realSpeed = replay === 24 ? 1 : this.speed / replay;
}
}
/**
*
*/
readyMove() {
this.moving = true;
this.fixMoveSpeed();
}
/**
*
*/
move(dir: Dir2): Promise<void> {
if (!this.moving) {
logger.error(12);
return Promise.reject(
'Cannot moving hero while hero not in moving!'
);
}
this.moveDir = dir;
if (this.moveDetached) {
return this.moveDetached;
} else {
this.step();
this.moveDetached = new Promise(res => {
this.once('stepEnd', () => {
this.moveDetached = void 0;
res();
});
});
return this.moveDetached;
}
}
/**
*
*/
endMove(): Promise<void> {
if (!this.moving) return Promise.resolve();
if (this.moveEnding) return this.moveEnding;
else {
const promise = new Promise<void>(resolve => {
this.once('stepEnd', () => {
this.moveEnding = void 0;
this.moving = false;
const { x, y } = core.status.hero.loc;
this.setHeroLoc(x, y);
this.render();
resolve();
});
});
return (this.moveEnding = promise);
}
}
/**
*
* @param dir
*/
turn(dir?: Dir2): void {
if (!dir) {
const index = texture.characterTurn2.indexOf(this.stepDir);
if (index === -1) {
const length = texture.characterTurn.length;
const index = texture.characterTurn.indexOf(
this.stepDir as Dir
);
const next = texture.characterTurn[index % length];
return this.turn(next);
} else {
return this.turn(texture.characterTurn[index]);
}
}
this.moveDir = dir;
this.stepDir = dir;
if (!this.renderable) return;
this.renderable.render = this.getRenderFromDir(this.showDir);
this.layer.update(this.layer);
}
/**
*
* @param x
* @param y
*/
setHeroLoc(x?: number, y?: number) {
if (!this.renderable) return;
if (!isNil(x)) {
this.renderable.x = x;
}
if (!isNil(y)) {
this.renderable.y = y;
}
this.emit('moveTick', this.renderable.x, this.renderable.y);
this.layer.update(this.layer);
}
/**
*
* @param x
* @param y
* @param time
* @param fn 0-1
*
*
*
*/
moveAs(x: number, y: number, time: number, fn: TimingFn<3>): Promise<void> {
if (!this.moving) return Promise.resolve();
if (!this.renderable) return Promise.resolve();
const nowZIndex = fn(0)[2];
const startTime = Date.now();
return new Promise(res => {
this.layer.delegateTicker(
() => {
if (!this.renderable) return;
const now = Date.now();
const progress = (now - startTime) / time;
const [nx, ny, nz] = fn(progress);
this.renderable.x = nx;
this.renderable.y = ny;
this.renderable.zIndex = nz;
if (nz !== nowZIndex) {
this.layer.sortMovingRenderable();
}
this.emit('moveTick', this.renderable.x, this.renderable.y);
this.layer.update(this.layer);
},
time,
() => {
this.moving = false;
if (!this.renderable) return res();
this.renderable.x = x;
this.renderable.y = y;
this.emit('moveTick', this.renderable.x, this.renderable.y);
this.layer.update(this.layer);
res();
}
);
});
}
/**
*
*/
render() {
if (!this.renderable) return;
if (!this.animate) {
this.renderable.animate = 0;
} else {
this.renderable.animate = this.movingFrame;
}
this.layer.update(this.layer);
}
awake(layer: Layer): void {
this.layer = layer;
adapter.add(this);
this.moveId = layer.delegateTicker(() => {
const time = Date.now();
this.animateTick(time);
this.moveTick(time);
});
if (core.status.hero) {
const image = core.status.hero.image;
this.setImage(core.material.images.images[image]);
}
}
onDestroy(layer: Layer): void {
adapter.remove(this);
layer.removeTicker(this.moveId);
}
onMovingUpdate(_layer: Layer, renderable: LayerMovingRenderable[]): void {
if (this.renderable) {
renderable.push(this.renderable);
this.emit('append', renderable);
}
}
}
const adapter = new RenderAdapter<HeroRenderer>('hero-adapter');
adapter.receive('readyMove', item => {
item.readyMove();
return Promise.resolve();
});
adapter.receive('move', (item, dir: Dir) => {
return item.move(dir);
});
adapter.receive('endMove', item => {
return item.endMove();
});
adapter.receive(
'moveAs',
(item, x: number, y: number, time: number, fn: TimingFn<3>) => {
return item.moveAs(x, y, time, fn);
}
);
adapter.receive('setHeroLoc', (item, x?: number, y?: number) => {
item.setHeroLoc(x, y);
return Promise.resolve();
});
adapter.receive('turn', (item, dir: Dir2) => {
item.turn(dir);
return Promise.resolve();
});
// 同步适配函数,这些函数用于同步设置信息等
adapter.receiveSync('setImage', (item, image: SizedCanvasImageSource) => {
item.setImage(image);
});
adapter.receiveSync('setMoveSpeed', (item, speed: number) => {
item.setMoveSpeed(speed);
});
adapter.receiveSync('setAnimateReversed', (item, reverse: boolean) => {
item.setAnimateReversed(reverse);
});
adapter.receiveSync('startAnimate', item => {
item.startAnimate();
});
adapter.receiveSync('endAnimate', item => {
item.endAnimate();
});
adapter.receiveSync('setAnimateDir', (item, dir: Dir) => {
item.setAnimateDir(dir);
});
// 不分同步fallback用于适配现在的样板之后会删除
adapter.receiveSync('setHeroLoc', (item, x?: number, y?: number) => {
item.setHeroLoc(x, y);
});
adapter.receiveSync('turn', (item, dir: Dir2) => {
item.turn(dir);
});

View File

@ -1,47 +1,115 @@
import { logger } from '@motajs/common'; import { standardElementNoCache, tagMap } from '@motajs/render-vue';
import { MapRenderItem } from '../map';
import { mainRenderer, tagManager } from '../renderer';
import { createCache } from './cache'; import { createCache } from './cache';
import { createFrame } from './frame';
import { createLayer, Layer, LayerGroup } from './layer';
import { createViewport } from './viewport';
import { Icon, Winskin } from './misc'; import { Icon, Winskin } from './misc';
import { Animate } from './animate';
import { createItemDetail } from './itemDetail';
import { logger } from '@motajs/common';
import { MapExtensionManager, MapRender, MapRenderer } from '../map';
import { state } from '@user/data-state';
import { materials } from '@user/client-base';
export function createElements() { export function createElements() {
createCache(); createCache();
createFrame();
createLayer();
createViewport();
createItemDetail();
// ----- 注册标签 // ----- 注册标签
mainRenderer.registerElement('icon', Icon);
mainRenderer.registerElement('winskin', Winskin);
mainRenderer.registerElement('map-render', MapRenderItem);
tagManager.registerTag( tagMap.register('winskin', (_0, _1, props) => {
'icon', if (!props)
tagManager.createStandardElement(false, Icon) return new Winskin(core.material.images.images['winskin.png']);
); else {
tagManager.registerTag( const {
'winskin', image = core.material.images.images['winskin.png'],
tagManager.createStandardElement(false, Winskin) type = 'static'
); } = props;
tagManager.registerTag('map-render', props => { return new Winskin(image, type);
}
});
tagMap.register('layer', (_0, _1, props) => {
if (!props) return new Layer();
else {
const { ex } = props;
const l = new Layer();
if (ex) {
(ex as any[]).forEach(v => {
l.extends(v);
});
}
return l;
}
});
tagMap.register('layer-group', (_0, _1, props) => {
if (!props) return new LayerGroup();
else {
const { ex, layers } = props;
const l = new LayerGroup();
if (ex) {
(ex as any[]).forEach(v => {
l.extends(v);
});
}
if (layers) {
(layers as any[]).forEach(v => {
l.addLayer(v);
});
}
return l;
}
});
tagMap.register('animation', (_0, _1, _props) => {
return new Animate();
});
tagMap.register('icon', standardElementNoCache(Icon));
tagMap.register('map-render', (_0, _1, props) => {
if (!props) { if (!props) {
logger.error(42, 'layerState'); logger.error(42, 'layerState, renderer, extenstion');
throw new Error(`Lack of map-render property.`); const renderer = new MapRenderer(materials, state.layer);
const manager = new MapExtensionManager(renderer);
return new MapRender(state.layer, renderer, manager);
} }
const { layerState, renderer, extension } = props; const { layerState, renderer, extension } = props;
if (!layerState) { if (!layerState) {
logger.error(42, 'layerState'); logger.error(42, 'layerState');
throw new Error(`Lack of map-render property.`); const renderer = new MapRenderer(materials, state.layer);
const manager = new MapExtensionManager(renderer);
return new MapRender(state.layer, renderer, manager);
} }
if (!renderer) { if (!renderer) {
logger.error(42, 'renderer'); logger.error(42, 'renderer');
throw new Error(`Lack of map-render property.`); const renderer = new MapRenderer(materials, state.layer);
const manager = new MapExtensionManager(renderer);
return new MapRender(state.layer, renderer, manager);
} }
if (!extension) { if (!extension) {
logger.error(42, 'extension'); logger.error(42, 'extension');
throw new Error(`Lack of map-render property.`); const renderer = new MapRenderer(materials, state.layer);
const manager = new MapExtensionManager(renderer);
return new MapRender(state.layer, renderer, manager);
} }
return new MapRenderItem(layerState, renderer, extension); return new MapRender(layerState, renderer, extension);
}); });
} }
export * from './animate';
export * from './block';
export * from './cache'; export * from './cache';
export * from './camera';
export * from './damage';
export * from './frame';
export * from './hero';
export * from './itemDetail';
export * from './layer';
export * from './misc'; export * from './misc';
export * from './props'; export * from './props';
export * from './utils';
export * from './viewport';

View File

@ -0,0 +1,270 @@
import { logger } from '@motajs/common';
import { mainSetting } from '@motajs/legacy-ui';
import { hook } from '@user/data-base';
import { ItemState } from '@user/data-state';
import { Damage, DamageRenderable, FloorDamageExtends } from './damage';
import {
ILayerGroupRenderExtends,
LayerGroup,
LayerGroupFloorBinder
} from './layer';
interface ItemDetailData {
x: number;
y: number;
diff: Record<string | symbol, number | undefined>;
}
interface ItemData {
id: AllIdsOf<'items'>;
x: number;
y: number;
}
export function createItemDetail() {
hook.on('setBlock', (x, y, _floorId, block) => {
FloorItemDetail.listened.forEach(v => {
v.setBlock(block, x, y);
});
});
}
export class FloorItemDetail implements ILayerGroupRenderExtends {
id: string = 'item-detail';
group!: LayerGroup;
floorBinder!: LayerGroupFloorBinder;
damage!: FloorDamageExtends;
sprite!: Damage;
/** 每个分块中包含的物品信息 */
blockData: Map<number, Map<number, ItemData>> = new Map();
/** 需要更新的分块 */
private dirtyBlock: Set<number> = new Set();
/** 道具详细信息 */
private detailData: Map<number, Map<number, ItemDetailData>> = new Map();
static detailColor: Record<string, CanvasStyle> = {
atk: '#FF7A7A',
atkper: '#FF7A7A',
def: '#00E6F1',
defper: '#00E6F1',
mdef: '#6EFF83',
mdefper: '#6EFF83',
hp: '#A4FF00',
hpmax: '#F9FF00',
hpmaxper: '#F9FF00',
mana: '#c66',
manaper: '#c66'
};
static listened: Set<FloorItemDetail> = new Set();
private onBeforeDamageRender = (block: number) => {
if (!mainSetting.getValue('screen.itemDetail')) return;
if (this.dirtyBlock.has(block)) {
this.sprite.block.clearCache(block, 1);
}
this.render(block);
};
private onUpdateMapSize = (width: number, height: number) => {
this.updateMapSize(width, height);
};
private onUpdate = () => {
this.updateItems();
};
private onUpdateBlocks = (blocks: Set<number>) => {
blocks.forEach(v => {
this.dirtyBlock.add(v);
});
};
private listen() {
this.sprite.on('dirtyUpdate', this.onBeforeDamageRender);
this.sprite.on('setMapSize', this.onUpdateMapSize);
this.sprite.on('updateBlocks', this.onUpdateBlocks);
this.damage.on('update', this.onUpdate);
}
/**
*
*/
updateMapSize(width: number, height: number) {
this.blockData.clear();
// 预留blockData
this.sprite.block.size(width, height);
const data = this.sprite.block.blockData;
const num = data.width * data.height;
for (let i = 0; i <= num; i++) {
this.blockData.set(i, new Map());
this.detailData.set(i, new Map());
this.dirtyBlock.add(i);
}
}
/**
*
*/
updateItems() {
const floor = this.floorBinder.getFloor();
if (!floor) return;
core.extractBlocks(floor);
core.status.maps[floor].blocks.forEach(v => {
if (v.event.cls !== 'items' || v.disable) return;
const id = v.event.id as AllIdsOf<'items'>;
const item = core.material.items[id];
if (item.cls === 'constants' || item.cls === 'tools') return;
const x = v.x;
const y = v.y;
const block = this.sprite.block.getIndexByLoc(x, y);
const index = x + y * this.sprite.mapWidth;
const blockData = this.blockData.get(block);
blockData?.set(index, { x, y, id });
});
}
/**
*
* @param block
* @param x
* @param y
*/
setBlock(block: AllNumbers, x: number, y: number) {
const map = maps_90f36752_8815_4be8_b32b_d7fad1d0542e;
const index = this.sprite.block.getIndexByLoc(x, y);
const itemIndex = x + y * this.sprite.mapWidth;
const blockData = this.blockData.get(index);
this.dirtyBlock.add(index);
if (block === 0) {
blockData?.delete(itemIndex);
return;
}
const cls = map[block].cls;
if (cls !== 'items') {
blockData?.delete(itemIndex);
return;
}
const id = map[block].id;
blockData?.set(itemIndex, { x, y, id });
}
/**
*
* @param block
*/
calAllItems(block: Set<number>) {
const enable = mainSetting.getValue('screen.itemDetail');
if (!core.status.thisMap || !enable) return;
if (this.dirtyBlock.size === 0 || block.size === 0) return;
let diff: Record<string | symbol, number | undefined> = {};
const before = core.status.hero;
const hero = structuredClone(core.status.hero);
const handler: ProxyHandler<any> = {
set(target, key, v) {
diff[key] = v - (target[key] || 0);
if (!diff[key]) diff[key] = void 0;
return true;
}
};
core.status.hero = new Proxy(hero, handler);
core.setFlag('__statistics__', true);
this.dirtyBlock.forEach(v => {
const data = this.blockData.get(v);
const detail = this.detailData.get(v);
detail?.clear();
if (!data) return;
data.forEach(v => {
const { id, x, y } = v;
const index = x + y * this.sprite.mapWidth;
diff = {};
const item = core.material.items[id];
if (item.cls === 'equips') {
// 装备也显示
const diff: Record<string, any> = {
...(item.equip.value ?? {})
};
const per = item.equip.percentage ?? {};
for (const name of Object.keys(per)) {
const n = name as SelectKey<HeroStatus, number>;
diff[name + 'per'] = per[n].toString() + '%';
}
detail?.set(index, { x, y, diff });
return;
}
ItemState.item(id)?.itemEffectFn?.();
detail?.set(index, { x, y, diff });
});
});
core.status.hero = before;
window.hero = before;
window.flags = before.flags;
}
/**
*
* @param block
*/
render(block: number) {
if (this.dirtyBlock.has(block)) {
this.calAllItems(new Set([block]));
}
const data = this.detailData;
this.dirtyBlock.delete(block);
const info = data.get(block);
if (!info) return;
info.forEach(({ x, y, diff }) => {
let n = 0;
for (const [key, value] of Object.entries(diff)) {
if (!value) continue;
const color = FloorItemDetail.detailColor[key] ?? '#fff';
const text = core.formatBigNumber(value, 4);
const renderable: DamageRenderable = {
x: x * this.sprite.cellSize + 2,
y: y * this.sprite.cellSize + 31 - n * 10,
text,
color,
align: 'left',
baseline: 'alphabetic'
};
this.sprite.renderable.get(block)?.add(renderable);
n++;
}
});
}
awake(group: LayerGroup): void {
this.group = group;
const binder = group.getExtends('floor-binder');
const damage = group.getExtends('floor-damage');
if (
binder instanceof LayerGroupFloorBinder &&
damage instanceof FloorDamageExtends
) {
this.floorBinder = binder;
this.damage = damage;
this.sprite = damage.sprite;
this.listen();
FloorItemDetail.listened.add(this);
} else {
logger.warn(1001);
group.removeExtends('item-detail');
}
}
onDestroy(_group: LayerGroup): void {
this.sprite.off('beforeDamageRender', this.onBeforeDamageRender);
this.sprite.off('setMapSize', this.onUpdateMapSize);
FloorItemDetail.listened.delete(this);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,49 +1,36 @@
import { logger } from '@motajs/common'; import { logger } from '@motajs/common';
import { import {
ERenderItemEvent,
RenderItem, RenderItem,
RenderItemPosition,
MotaOffscreenCanvas2D, MotaOffscreenCanvas2D,
Transform, Transform
SizedCanvasImageSource } from '@motajs/render-core';
} from '@motajs/render'; 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 { IExcitable } from '@motajs/animate'; import { IAnimateFrame, renderEmits } from './frame';
import { IMotaIcon, IMotaWinskin } from './types';
export class Icon extends RenderItem implements IMotaIcon, IExcitable<number> { export interface EIconEvent extends ERenderItemEvent {}
export class Icon extends RenderItem<EIconEvent> implements IAnimateFrame {
/** 图标id */ /** 图标id */
icon: AllNumbers = 0; icon: AllNumbers = 0;
/** 渲染动画的第几帧 */ /** */
frame: number = 0; frame: number = 0;
/** 是否启用动画 */ /** 是否启用动画 */
animate: boolean = false; animate: boolean = false;
/** 当前动画速度 */
frameSpeed: number = 300;
/** 当前帧率 */
nowFrame: number = 0;
/** 图标的渲染信息 */ /** 图标的渲染信息 */
private renderable?: RenderableData | AutotileRenderable; private renderable?: RenderableData | AutotileRenderable;
private pendingIcon?: AllNumbers; private pendingIcon?: AllNumbers;
/** 委托激励对象 id用于图标的动画展示 */ constructor(type: RenderItemPosition, cache?: boolean, fall?: boolean) {
private delegation: number = -1; super(type, cache, fall);
constructor(cache: boolean = false) {
super(cache);
this.setAntiAliasing(false); this.setAntiAliasing(false);
this.setHD(false); this.setHD(false);
} }
excited(payload: number): void {
if (!this.renderable) return;
const frame = Math.floor(payload / 300);
if (frame === this.nowFrame) return;
this.nowFrame = frame;
this.update();
}
protected render( protected render(
canvas: MotaOffscreenCanvas2D, canvas: MotaOffscreenCanvas2D,
_transform: Transform _transform: Transform
@ -55,7 +42,7 @@ export class Icon extends RenderItem implements IMotaIcon, IExcitable<number> {
const cw = this.width; const cw = this.width;
const ch = this.height; const ch = this.height;
const frame = this.animate const frame = this.animate
? this.nowFrame % renderable.frame ? RenderItem.animatedFrame % renderable.frame
: this.frame; : this.frame;
if (!this.animate) { if (!this.animate) {
@ -100,27 +87,6 @@ export class Icon extends RenderItem implements IMotaIcon, IExcitable<number> {
} }
} }
setFrameSpeed(speed: number): void {
this.frameSpeed = speed;
this.update();
}
setFrame(frame: number): void {
if (frame < 0) {
this.setAnimateStatus(true);
return;
}
this.frame = frame;
this.update();
}
setAnimateStatus(animate: boolean): void {
this.animate = animate;
if (!animate) this.removeExcitable(this.delegation);
else this.delegation = this.delegateExcitable(this);
this.update();
}
private setIconRenderable(num: AllNumbers) { private setIconRenderable(num: AllNumbers) {
const renderable = texture.getRenderable(num); const renderable = texture.getRenderable(num);
@ -135,7 +101,15 @@ export class Icon extends RenderItem implements IMotaIcon, IExcitable<number> {
this.update(); this.update();
} }
/**
*
*/
updateFrameAnimate(): void {
if (this.animate) this.update(this);
}
destroy(): void { destroy(): void {
renderEmits.removeFramer(this);
super.destroy(); super.destroy();
} }
@ -150,50 +124,142 @@ export class Icon extends RenderItem implements IMotaIcon, IExcitable<number> {
return true; return true;
case 'animate': case 'animate':
if (!this.assertType(nextValue, 'boolean', key)) return false; if (!this.assertType(nextValue, 'boolean', key)) return false;
this.setAnimateStatus(nextValue); this.animate = nextValue;
if (nextValue) renderEmits.addFramer(this);
else renderEmits.removeFramer(this);
this.update();
return true; return true;
case 'frame': case 'frame':
if (!this.assertType(nextValue, 'number', key)) return false; if (!this.assertType(nextValue, 'number', key)) return false;
this.setFrame(nextValue); this.frame = nextValue;
return true; this.update();
case 'speed':
if (!this.assertType(nextValue, 'number', key)) return false;
this.setFrameSpeed(nextValue);
return true; return true;
} }
return false; return false;
} }
} }
export class Winskin extends RenderItem implements IMotaWinskin { interface WinskinPatterns {
image: SizedCanvasImageSource | null = null; top: CanvasPattern;
left: CanvasPattern;
bottom: CanvasPattern;
right: CanvasPattern;
}
export interface EWinskinEvent extends ERenderItemEvent {}
export class Winskin extends RenderItem<EWinskinEvent> {
image: SizedCanvasImageSource;
/** 边框宽度32表示原始宽度 */ /** 边框宽度32表示原始宽度 */
borderSize: number = 32; borderSize: number = 32;
/** 图片名称 */ /** 图片名称 */
imageName: string = ''; imageName?: string;
private pendingImage?: ImageIds; private pendingImage?: ImageIds;
private patternCache?: WinskinPatterns;
private patternTransform: DOMMatrix;
constructor(enableCache: boolean = false) { // todo: 跨上下文可能是未定义行为,需要上下文无关化
super(enableCache); private static patternMap: Map<string, WinskinPatterns> = new Map();
constructor(
image: SizedCanvasImageSource,
type: RenderItemPosition = 'static'
) {
super(type, false, false);
this.image = image;
this.setAntiAliasing(false); this.setAntiAliasing(false);
if (window.DOMMatrix) {
this.patternTransform = new DOMMatrix();
} else if (window.WebKitCSSMatrix) {
this.patternTransform = new WebKitCSSMatrix();
} else {
this.patternTransform = new SVGMatrix();
}
} }
protected render(canvas: MotaOffscreenCanvas2D): void { private generatePattern() {
const pattern = this.requireCanvas(true, false);
pattern.setScale(1);
const img = this.image; const img = this.image;
if (!img) return; pattern.size(32, 16);
pattern.setHD(false);
pattern.setAntiAliasing(false);
const ctx = pattern.ctx;
ctx.drawImage(img, 144, 0, 32, 16, 0, 0, 32, 16);
const topPattern = ctx.createPattern(pattern.canvas, 'repeat');
ctx.clearRect(0, 0, 32, 16);
ctx.drawImage(img, 144, 48, 32, 16, 0, 0, 32, 16);
const bottomPattern = ctx.createPattern(pattern.canvas, 'repeat');
ctx.clearRect(0, 0, 32, 16);
pattern.size(16, 32);
ctx.drawImage(img, 128, 16, 16, 32, 0, 0, 16, 32);
const leftPattern = ctx.createPattern(pattern.canvas, 'repeat');
ctx.clearRect(0, 0, 16, 32);
ctx.drawImage(img, 176, 16, 16, 32, 0, 0, 16, 32);
const rightPattern = ctx.createPattern(pattern.canvas, 'repeat');
if (!topPattern || !bottomPattern || !leftPattern || !rightPattern) {
return null;
}
const winskinPattern: WinskinPatterns = {
top: topPattern,
bottom: bottomPattern,
left: leftPattern,
right: rightPattern
};
if (this.imageName) {
Winskin.patternMap.set(this.imageName, winskinPattern);
}
this.patternCache = winskinPattern;
this.deleteCanvas(pattern);
return winskinPattern;
}
private getPattern() {
if (!this.imageName) {
if (this.patternCache) return this.patternCache;
return this.generatePattern();
} else {
const pattern = Winskin.patternMap.get(this.imageName);
if (pattern) return pattern;
return this.generatePattern();
}
}
protected render(
canvas: MotaOffscreenCanvas2D,
_transform: Transform
): void {
const ctx = canvas.ctx; const ctx = canvas.ctx;
const img = this.image;
const w = this.width; const w = this.width;
const h = this.height; const h = this.height;
const pad = this.borderSize / 2; const pad = this.borderSize / 2;
// 背景 // 背景
ctx.drawImage(img, 0, 0, 128, 128, 2, 2, w - 4, h - 4); ctx.drawImage(img, 0, 0, 128, 128, 2, 2, w - 4, h - 4);
const pattern = this.getPattern();
if (!pattern) return;
const { top, left, right, bottom } = pattern;
top.setTransform(this.patternTransform);
left.setTransform(this.patternTransform);
right.setTransform(this.patternTransform);
bottom.setTransform(this.patternTransform);
// 上下左右边框 // 上下左右边框
ctx.save(); ctx.save();
ctx.drawImage(img, 144, 0, 32, 16, pad, 0, w - pad * 2, pad); ctx.fillStyle = top;
ctx.drawImage(img, 144, 48, 32, 16, pad, h - pad, w - pad * 2, pad); ctx.translate(pad, 0);
ctx.drawImage(img, 128, 16, 16, 32, 0, pad, pad, h - pad * 2); ctx.fillRect(0, 0, w - pad * 2, pad);
ctx.drawImage(img, 176, 16, 16, 32, w - pad, pad, pad, h - pad * 2); ctx.fillStyle = bottom;
ctx.translate(0, h - pad);
ctx.fillRect(0, 0, w - pad * 2, pad);
ctx.fillStyle = left;
ctx.translate(-pad, pad * 2 - h);
ctx.fillRect(0, 0, pad, h - pad * 2);
ctx.fillStyle = right;
ctx.translate(w - pad, 0);
ctx.fillRect(0, 0, pad, h - pad * 2);
ctx.restore();
// 四个角的边框 // 四个角的边框
ctx.drawImage(img, 128, 0, 16, 16, 0, 0, pad, pad); ctx.drawImage(img, 128, 0, 16, 16, 0, 0, pad, pad);
ctx.drawImage(img, 176, 0, 16, 16, w - pad, 0, pad, pad); ctx.drawImage(img, 176, 0, 16, 16, w - pad, 0, pad, pad);
@ -207,7 +273,7 @@ export class Winskin extends RenderItem implements IMotaWinskin {
*/ */
setImage(image: SizedCanvasImageSource) { setImage(image: SizedCanvasImageSource) {
this.image = image; this.image = image;
this.imageName = ''; this.patternCache = void 0;
this.update(); this.update();
} }
@ -241,6 +307,8 @@ export class Winskin extends RenderItem implements IMotaWinskin {
*/ */
setBorderSize(size: number) { setBorderSize(size: number) {
this.borderSize = size; this.borderSize = size;
this.patternTransform.a = size / 32;
this.patternTransform.d = size / 32;
this.update(); this.update();
} }
@ -251,9 +319,6 @@ export class Winskin extends RenderItem implements IMotaWinskin {
): boolean { ): boolean {
switch (key) { switch (key) {
case 'image': case 'image':
this.setImage(nextValue);
return true;
case 'imageName':
if (!this.assertType(nextValue, 'string', key)) return false; if (!this.assertType(nextValue, 'string', key)) return false;
this.setImageByName(nextValue); this.setImageByName(nextValue);
return true; return true;

View File

@ -1,7 +1,30 @@
import { BaseProps, TagDefine } from '@motajs/render-vue'; import { BaseProps, TagDefine } from '@motajs/render-vue';
import { ERenderItemEvent, SizedCanvasImageSource } from '@motajs/render'; import { ERenderItemEvent, Transform } from '@motajs/render-core';
import { CanvasStyle } from '@motajs/render-assets';
import {
ILayerGroupRenderExtends,
FloorLayer,
ILayerRenderExtends,
ELayerEvent,
ELayerGroupEvent
} from './layer';
import { EAnimateEvent } from './animate';
import { EIconEvent, EWinskinEvent } from './misc';
import { IEnemyCollection } from '@motajs/types';
import { ILayerState } from '@user/data-state'; import { ILayerState } from '@user/data-state';
import { IMapExtensionManager, IMapRenderer } from '../map'; import { IMapExtensionManager, IMapRenderer, IOnMapTextRenderer } from '../map';
export interface AnimateProps extends BaseProps {}
export interface DamageProps extends BaseProps {
mapWidth?: number;
mapHeight?: number;
cellSize?: number;
enemy?: IEnemyCollection;
font?: string;
strokeStyle?: CanvasStyle;
strokeWidth?: number;
}
export interface IconProps extends BaseProps { export interface IconProps extends BaseProps {
/** 图标 id 或数字 */ /** 图标 id 或数字 */
@ -10,30 +33,50 @@ export interface IconProps extends BaseProps {
frame?: number; frame?: number;
/** 是否开启动画,开启后 frame 参数无效 */ /** 是否开启动画,开启后 frame 参数无效 */
animate?: boolean; animate?: boolean;
/** 动画速度 */
speed?: number;
} }
export interface WinskinProps extends BaseProps { export interface WinskinProps extends BaseProps {
/** 直接设置 winskin 图片 */ /** winskin 的图片 id */
image?: SizedCanvasImageSource; image: ImageIds;
/** 根据图片名称设置 winskin 图片 */
imageName?: string;
/** 边框大小 */ /** 边框大小 */
borderSize?: number; borderSize?: number;
} }
export interface LayerGroupProps extends BaseProps {
cellSize?: number;
blockSize?: number;
floorId?: FloorIds;
bindThisFloor?: boolean;
camera?: Transform;
ex?: readonly ILayerGroupRenderExtends[];
layers?: readonly FloorLayer[];
}
export interface LayerProps extends BaseProps {
layer?: FloorLayer;
mapWidth?: number;
mapHeight?: number;
cellSize?: number;
background?: AllNumbers;
floorImage?: FloorAnimate[];
ex?: readonly ILayerRenderExtends[];
}
export interface MapRenderProps extends BaseProps { export interface MapRenderProps extends BaseProps {
layerState: ILayerState; layerState: ILayerState;
renderer: IMapRenderer; renderer: IMapRenderer;
extension: IMapExtensionManager; extension: IMapExtensionManager;
textExtension?: IOnMapTextRenderer | null;
} }
declare module 'vue/jsx-runtime' { declare module 'vue/jsx-runtime' {
namespace JSX { namespace JSX {
export interface IntrinsicElements { export interface IntrinsicElements {
icon: TagDefine<IconProps, ERenderItemEvent>; layer: TagDefine<LayerProps, ELayerEvent>;
winskin: TagDefine<WinskinProps, ERenderItemEvent>; 'layer-group': TagDefine<LayerGroupProps, ELayerGroupEvent>;
animation: TagDefine<AnimateProps, EAnimateEvent>;
icon: TagDefine<IconProps, EIconEvent>;
winskin: TagDefine<WinskinProps, EWinskinEvent>;
'map-render': TagDefine<MapRenderProps, ERenderItemEvent>; 'map-render': TagDefine<MapRenderProps, ERenderItemEvent>;
} }
} }

View File

@ -1,65 +0,0 @@
import { IRenderItem, SizedCanvasImageSource } from '@motajs/render';
export interface IMotaIcon extends IRenderItem {
/** 图标id */
readonly icon: AllNumbers;
/** 渲染动画的第几帧 */
readonly frame: number;
/** 是否启用动画 */
readonly animate: boolean;
/** 当前动画帧数,如果没有启用动画则为 -1 */
readonly nowFrame: number;
/** 当前图标的动画速度,每多长时间切换至下一帧,单位毫秒 */
readonly frameSpeed: number;
/**
*
* @param id id
*/
setIcon(id: AllIdsWithNone | AllNumbers): void;
/**
*
* @param speed
*/
setFrameSpeed(speed: number): void;
/**
*
* @param animate
*/
setAnimateStatus(animate: boolean): void;
/**
*
* @param frame
*/
setFrame(frame: number): void;
}
export interface IMotaWinskin extends IRenderItem {
/** winskin 图片源 */
readonly image: SizedCanvasImageSource | null;
/** 边框尺寸 */
readonly borderSize: number;
/** winskin 图片名称,如果不是使用名称设置的图片的话,此值为空字符串 */
readonly imageName: string;
/**
* winskin图片
* @param image winskin图片
*/
setImage(image: SizedCanvasImageSource): void;
/**
* winskin
* @param name
*/
setImageByName(name: ImageIds): void;
/**
*
* @param size
*/
setBorderSize(size: number): void;
}

View File

@ -0,0 +1,14 @@
import { RenderAdapter } from '@motajs/render-core';
import { FloorViewport } from './viewport';
export function disableViewport() {
const adapter = RenderAdapter.get<FloorViewport>('viewport');
if (!adapter) return;
adapter.sync('disable');
}
export function enableViewport() {
const adapter = RenderAdapter.get<FloorViewport>('viewport');
if (!adapter) return;
adapter.sync('enable');
}

View File

@ -0,0 +1,360 @@
import { RenderAdapter } from '@motajs/render-core';
import { HeroRenderer } from './hero';
import { ILayerGroupRenderExtends, LayerGroup } from './layer';
import { LayerGroupFloorBinder } from './layer';
import { hyper, TimingFn } from 'mutate-animate';
import {
MAP_BLOCK_HEIGHT,
MAP_BLOCK_WIDTH,
MAP_HEIGHT,
MAP_WIDTH
} from '../shared';
export class FloorViewport implements ILayerGroupRenderExtends {
id: string = 'viewport';
group!: LayerGroup;
hero?: HeroRenderer;
binder?: LayerGroupFloorBinder;
/** 是否启用视角控制拓展 */
enabled: boolean = true;
/** 是否自动限定视角范围至地图范围 */
boundX: boolean = true;
boundY: boolean = true;
/** 渐变的速率曲线 */
transitionFn: TimingFn = hyper('sin', 'out');
/** 瞬移的速率曲线 */
mutateFn: TimingFn = hyper('sin', 'out');
/** 突变时的渐变时长 */
transitionTime: number = 600;
/** 当前视角位置 */
private nx: number = 0;
private ny: number = 0;
/** 移动时的偏移位置 */
private ox: number = 0;
private oy: number = 0;
/** 移动时的偏移最大值 */
private maxOffset: number = 1;
/** 委托ticker */
private delegation: number = -1;
/** 渐变委托ticker */
private transition: number = -1;
/** 移动委托ticker */
private moving: number = -1;
/** 是否在渐变过程中 */
private inTransition: boolean = false;
/** 是否在移动过程中 */
private inMoving: boolean = false;
/** 移动的监听函数 */
private movingFramer?: () => void;
/**
*
*/
disable() {
this.enabled = false;
}
/**
*
*/
enable() {
this.enabled = true;
const { x, y } = core.status.hero.loc;
const { x: nx, y: ny } = this.group.camera;
const halfWidth = MAP_WIDTH / 2;
const halfHeight = MAP_HEIGHT / 2;
const cell = this.group.cellSize;
const half = cell / 2;
this.applyPosition(
-(nx - halfWidth + half) / this.group.cellSize,
-(ny - halfHeight + half) / this.group.cellSize
);
this.mutateTo(x, y);
}
/**
*
* @param boundX
* @param boundY
*/
setAutoBound(boundX: boolean = this.boundX, boundY: boolean = this.boundY) {
this.boundX = boundX;
this.boundY = boundY;
this.group.requestBeforeFrame(() => {
this.setPosition(this.nx, this.ny);
});
}
/**
*
* @param x
* @param y
*/
getBoundedPosition(x: number, y: number) {
if (!this.checkDependency()) return { x, y };
if (!this.boundX && !this.boundY) return { x, y };
const width = MAP_BLOCK_WIDTH;
const height = MAP_BLOCK_HEIGHT;
const minX = (width - 1) / 2;
const minY = (height - 1) / 2;
const floor = core.status.maps[this.binder!.getFloor()];
const maxX = floor.width - minX - 1;
const maxY = floor.height - minY - 1;
return {
x: this.boundX ? core.clamp(x, minX, maxX) : x,
y: this.boundY ? core.clamp(y, minY, maxY) : y
};
}
/**
*
* @param x
* @param y
*/
setPosition(x: number, y: number) {
if (!this.enabled) return;
const { x: nx, y: ny } = this.getBoundedPosition(x, y);
this.group.removeTicker(this.transition, false);
this.applyPosition(nx, ny);
}
/**
*
*/
startMove() {
if (this.inMoving) return;
this.inMoving = true;
this.createMoveTransition();
}
/**
*
*/
endMove() {
this.inMoving = false;
}
/**
*
* @param x
* @param y
*/
moveTo(x: number, y: number, time: number = 200) {
if (!this.enabled) return;
const { x: nx, y: ny } = this.getBoundedPosition(x, y);
if (this.inTransition) {
const distance = Math.hypot(this.nx - nx, this.ny - ny);
const t = core.clamp(distance * time, time, time * 3);
this.createTransition(nx, ny, t, this.transitionFn);
}
}
private createMoveTransition() {
if (!this.checkDependency()) return;
let xTarget: number = 0;
let yTarget: number = 0;
let xStart: number = this.ox;
let yStart: number = this.oy;
let xStartTime: number = Date.now();
let yStartTime: number = Date.now();
let ending: boolean = false;
// 这个数等于 sinh(2)用这个数的话可以正好在刚开始移动的时候达到1的斜率效果会比较好
const transitionTime = this.hero!.speed * 3.626860407847019;
const setTargetX = (x: number, time: number) => {
if (x === xTarget) return;
xTarget = x;
xStartTime = time;
xStart = this.ox;
};
const setTargetY = (y: number, time: number) => {
if (y === yTarget) return;
yTarget = y;
yStart = this.oy;
yStartTime = time;
};
if (this.movingFramer) {
this.hero!.off('moveTick', this.movingFramer);
}
this.movingFramer = () => {
if (this.inTransition) return;
const now = Date.now();
if (!this.inMoving && !ending) {
setTargetX(0, now);
setTargetY(0, now);
ending = true;
}
if (!ending) {
const dir = this.hero!.stepDir;
const { x, y } = core.utils.scan2[dir];
setTargetX(-x * this.maxOffset, now);
setTargetY(-y * this.maxOffset, now);
}
if (!this.hero!.renderable) return;
const { x, y } = this.hero!.renderable;
const { x: nx, y: ny } = this.getBoundedPosition(x, y);
this.applyPosition(nx, ny);
if (ending) {
if (this.ox === xTarget && this.oy === yTarget) {
this.hero!.off('moveTick', this.movingFramer);
return;
}
}
// todo: 效果太差了,需要优化
return;
if (this.ox !== xTarget) {
const time = transitionTime * Math.abs(xStart - xTarget);
const progress = (now - xStartTime) / time;
if (progress > 1) {
this.ox = xTarget;
} else {
const p = this.transitionFn(progress);
this.ox = (xTarget - xStart) * p + xStart;
}
}
if (this.oy !== yTarget) {
const time = transitionTime * Math.abs(yStart - yTarget);
const progress = (now - yStartTime) / time;
if (progress > 1) {
this.oy = yTarget;
} else {
const p = this.transitionFn(progress);
this.oy = (yTarget - yStart) * p + yStart;
}
}
};
this.hero!.on('moveTick', this.movingFramer);
}
/**
*
* @param x
* @param y
*/
mutateTo(x: number, y: number, time: number = this.transitionTime) {
if (!this.enabled) return;
const { x: nx, y: ny } = this.getBoundedPosition(x, y);
this.createTransition(nx, ny, time, this.mutateFn);
}
private createTransition(x: number, y: number, time: number, fn: TimingFn) {
const start = Date.now();
const end = start + time;
const sx = this.nx;
const sy = this.ny;
const dx = x - sx;
const dy = y - sy;
this.inTransition = true;
this.group.removeTicker(this.transition, false);
this.transition = this.group.delegateTicker(
() => {
const now = Date.now();
if (now >= end) {
this.group.removeTicker(this.transition, true);
return;
}
const progress = fn((now - start) / time);
const tx = dx * progress;
const ty = dy * progress;
this.applyPosition(tx + sx, ty + sy);
},
time,
() => {
this.applyPosition(x, y);
this.inTransition = false;
}
);
}
private applyPosition(x: number, y: number) {
if (!this.enabled) return;
if (x === this.nx && y === this.ny) return;
const halfWidth = MAP_WIDTH / 2;
const halfHeight = MAP_HEIGHT / 2;
const cell = this.group.cellSize;
const half = cell / 2;
this.nx = x;
this.ny = y;
const { x: bx, y: by } = this.getBoundedPosition(x, y);
const rx = bx * cell - halfWidth + half;
const ry = by * cell - halfHeight + half;
core.bigmap.offsetX = rx;
core.bigmap.offsetY = ry;
this.group.camera.setTranslate(-rx, -ry);
this.group.update(this.group);
}
private checkDependency() {
if (this.hero && this.binder) return true;
const group = this.group;
const ex1 = group.getLayer('event')?.getExtends('floor-hero');
const ex2 = group.getExtends('floor-binder');
if (
ex1 instanceof HeroRenderer &&
ex2 instanceof LayerGroupFloorBinder
) {
this.hero = ex1;
this.binder = ex2;
return true;
}
return false;
}
awake(group: LayerGroup): void {
this.group = group;
adapter.add(this);
}
onDestroy(group: LayerGroup): void {
group.removeTicker(this.delegation);
group.removeTicker(this.transition);
group.removeTicker(this.moving);
adapter.remove(this);
}
}
const adapter = new RenderAdapter<FloorViewport>('viewport');
adapter.receive('mutateTo', (item, x, y, time) => {
item.mutateTo(x, y, time);
return Promise.resolve();
});
adapter.receive('moveTo', (item, x, y, time) => {
item.moveTo(x, y, time);
return Promise.resolve();
});
adapter.receive('setPosition', (item, x, y) => {
item.setPosition(x, y);
return Promise.resolve();
});
adapter.receiveSync('disable', item => {
item.disable();
});
adapter.receiveSync('enable', item => {
item.enable();
});
adapter.receiveSync('startMove', item => {
item.startMove();
});
adapter.receiveSync('endMove', item => {
item.endMove();
});
export function createViewport() {
const { hook } = Mota.require('@user/data-base');
hook.on('changingFloor', (_, loc) => {
adapter.all('setPosition', loc.x, loc.y);
});
}

View File

@ -1,4 +1,4 @@
import { Shader, ShaderProgram } from '@motajs/render'; import { Shader, ShaderProgram } from '@motajs/render-core';
export abstract class EffectBase<T> { export abstract class EffectBase<T> {
/** 当前使用的程序 */ /** 当前使用的程序 */

View File

@ -2,7 +2,7 @@ import {
ITransformUpdatable, ITransformUpdatable,
ShaderProgram, ShaderProgram,
Transform3D Transform3D
} from '@motajs/render'; } from '@motajs/render-core';
import { EffectBase } from './base'; import { EffectBase } from './base';
export class Image3DEffect export class Image3DEffect

View File

@ -1,4 +1,4 @@
import { Font } from '@motajs/render'; import { createApp, Font } from '@motajs/render';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { DEFAULT_FONT, MAIN_HEIGHT, MAIN_WIDTH } from './shared'; import { DEFAULT_FONT, MAIN_HEIGHT, MAIN_WIDTH } from './shared';
import { hook, loading } from '@user/data-base'; import { hook, loading } from '@user/data-base';
@ -10,8 +10,6 @@ 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'; import { createMainExtension } from './commonIns';
import { createApp } from './renderer';
import { LoadSceneUI } from './ui/load';
export function createGameRenderer() { export function createGameRenderer() {
const App = defineComponent(_props => { const App = defineComponent(_props => {
@ -24,9 +22,6 @@ export function createGameRenderer() {
mainRenderer.hide(); mainRenderer.hide();
createApp(App).mount(mainRenderer); createApp(App).mount(mainRenderer);
sceneController.open(LoadSceneUI, {});
mainRenderer.show();
} }
export function createRender() { export function createRender() {
@ -35,6 +30,11 @@ export function createRender() {
createAction(); createAction();
createWeather(); createWeather();
loading.once('loaded', () => {
sceneController.open(GameTitleUI, {});
mainRenderer.show();
});
loading.once('assetBuilt', () => { loading.once('assetBuilt', () => {
createMainExtension(); createMainExtension();
}); });

View File

@ -0,0 +1,114 @@
import {
wrapInstancedComponent,
MotaOffscreenCanvas2D,
RenderItem,
RenderItemPosition,
Transform
} from '@motajs/render';
// 渲染端的向后兼容用,会充当两个版本间过渡的作用
class Change extends RenderItem {
private tips: string[] = [];
/** 当前小贴士 */
private usingTip: string = '';
/** 透明度 */
private backAlpha: number = 0;
private title: string = '';
constructor(type: RenderItemPosition) {
super(type, false);
}
/**
*
*/
setTips(tip: string[]) {
this.tips = tip;
}
/**
*
*/
setTitle(title: string) {
this.title = title;
}
/**
*
* @param time
*/
showChange(time: number) {
const length = this.tips.length;
const tip = this.tips[Math.floor(Math.random() * length)] ?? '';
this.usingTip = tip;
return new Promise<void>(res => {
const start = Date.now();
const id = this.delegateTicker(
() => {
const dt = Date.now() - start;
const progress = dt / time;
if (progress > 1) {
this.backAlpha = 1;
this.removeTicker(id);
} else {
this.backAlpha = progress;
}
this.update();
},
10000,
res
);
});
}
/**
*
* @param time
*/
async hideChange(time: number) {
return new Promise<void>(res => {
const start = Date.now();
const id = this.delegateTicker(
() => {
const dt = Date.now() - start;
const progress = dt / time;
if (progress > 1) {
this.removeTicker(id);
this.backAlpha = 0;
} else {
this.backAlpha = 1 - progress;
}
this.update();
},
10000,
res
);
});
}
protected render(
canvas: MotaOffscreenCanvas2D,
_transform: Transform
): void {
if (this.backAlpha === 0) return;
const ctx = canvas.ctx;
ctx.globalAlpha = this.backAlpha;
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.textAlign = 'center';
ctx.fillStyle = '#fff';
ctx.font = '32px "normal"';
ctx.fillText(this.title, canvas.width / 2, canvas.height * 0.4);
ctx.font = '16px "normal"';
if (this.usingTip.length > 0) {
ctx.fillText(
'小贴士:' + this.usingTip,
canvas.width / 2,
canvas.height * 0.75
);
}
}
}
export const FloorChange = wrapInstancedComponent(() => new Change('static'));

View File

@ -6,7 +6,6 @@ import {
IBlockSplitter, IBlockSplitter,
IBlockSplitterConfig IBlockSplitterConfig
} from './types'; } from './types';
import { ISearchable8Dir } from '@motajs/common';
export class BlockSplitter<T> implements IBlockSplitter<T> { export class BlockSplitter<T> implements IBlockSplitter<T> {
blockWidth: number = 0; blockWidth: number = 0;
@ -289,7 +288,7 @@ export class BlockSplitter<T> implements IBlockSplitter<T> {
} }
} }
class SplittedBlockData<T> implements IBlockData<T>, ISearchable8Dir { class SplittedBlockData<T> implements IBlockData<T> {
width: number; width: number;
height: number; height: number;
x: number; x: number;

View File

@ -1,11 +1,11 @@
import { MotaOffscreenCanvas2D, RenderItem } from '@motajs/render'; import { MotaOffscreenCanvas2D, RenderItem } from '@motajs/render-core';
import { ILayerState } from '@user/data-state'; import { ILayerState } from '@user/data-state';
import { IMapRenderer } from './types'; import { IMapRenderer } from './types';
import { ElementNamespace, ComponentInternalInstance } from 'vue'; import { ElementNamespace, ComponentInternalInstance } from 'vue';
import { CELL_HEIGHT, CELL_WIDTH, MAP_HEIGHT, MAP_WIDTH } from '../shared'; import { CELL_HEIGHT, CELL_WIDTH, MAP_HEIGHT, MAP_WIDTH } from '../shared';
import { IMapExtensionManager } from './extension'; import { IMapExtensionManager } from './extension';
export class MapRenderItem extends RenderItem { export class MapRender extends RenderItem {
/** /**
* @param layerState * @param layerState
* @param renderer * @param renderer
@ -15,14 +15,13 @@ export class MapRenderItem extends RenderItem {
readonly renderer: IMapRenderer, readonly renderer: IMapRenderer,
readonly exManager: IMapExtensionManager readonly exManager: IMapExtensionManager
) { ) {
super(false); super('static', false, false);
renderer.setLayerState(layerState); renderer.setLayerState(layerState);
renderer.setCellSize(CELL_WIDTH, CELL_HEIGHT); renderer.setCellSize(CELL_WIDTH, CELL_HEIGHT);
renderer.setRenderSize(MAP_WIDTH, MAP_HEIGHT); renderer.setRenderSize(MAP_WIDTH, MAP_HEIGHT);
// 元素被销毁时会自动删除所有的激励对象,所以不需要担心会内存泄漏 this.delegateTicker(time => {
this.delegateExcitable(time => {
this.renderer.tick(time); this.renderer.tick(time);
if (this.renderer.needUpdate()) { if (this.renderer.needUpdate()) {
this.update(); this.update();

View File

@ -13,7 +13,11 @@ import { IMapRenderer, IMapRendererTicker, IMovingBlock } from '../types';
import { isNil } from 'lodash-es'; import { isNil } from 'lodash-es';
import { IHookController, logger } from '@motajs/common'; import { IHookController, logger } from '@motajs/common';
import { BlockCls, IMaterialFramedData } from '@user/client-base'; import { BlockCls, IMaterialFramedData } from '@user/client-base';
import { ITexture, ITextureSplitter, TextureRowSplitter } from '@motajs/render'; import {
ITexture,
ITextureSplitter,
TextureRowSplitter
} from '@motajs/render-assets';
import { IMapHeroRenderer } from './types'; import { IMapHeroRenderer } from './types';
import { TimingFn } from 'mutate-animate'; import { TimingFn } from 'mutate-animate';

View File

@ -7,7 +7,7 @@ import {
IMapVertexBlock IMapVertexBlock
} from '../types'; } from '../types';
import { IMapTextArea, IMapTextRenderable, IOnMapTextRenderer } from './types'; import { IMapTextArea, IMapTextRenderable, IOnMapTextRenderer } from './types';
import { ITransformUpdatable, Transform } from '@motajs/render'; import { ITransformUpdatable, Transform } from '@motajs/render-core';
export class OnMapTextRenderer export class OnMapTextRenderer
implements IOnMapTextRenderer, ITransformUpdatable<Transform> implements IOnMapTextRenderer, ITransformUpdatable<Transform>
@ -116,7 +116,6 @@ export class OnMapTextRenderer
(e * renderWidth) / 2, (e * renderWidth) / 2,
(f * renderHeight) / 2 (f * renderHeight) / 2
); );
// 由于 WebGL 坐标系与 Canvas2D 坐标系不一样,所以还需要变换一下
ctx.scale(1, -1); ctx.scale(1, -1);
// draw text in each block // draw text in each block

View File

@ -1,11 +1,11 @@
import { ITexture, Font } from '@motajs/render'; import { ITexture } from '@motajs/render-assets';
import { import {
FaceDirection, FaceDirection,
HeroAnimateDirection, HeroAnimateDirection,
IHeroState, IHeroState,
IMapLayer IMapLayer
} from '@user/data-state'; } from '@user/data-state';
import { Font } from '@motajs/render-style';
import { IMapRenderResult } from '../types'; import { IMapRenderResult } from '../types';
export interface IMapExtensionManager { export interface IMapExtensionManager {

View File

@ -2,9 +2,8 @@ import {
ITextureAnimater, ITextureAnimater,
ITextureRenderable, ITextureRenderable,
SizedCanvasImageSource, SizedCanvasImageSource,
TextureColumnAnimater, TextureColumnAnimater
Transform } from '@motajs/render-assets';
} from '@motajs/render';
import { import {
AutotileProcessor, AutotileProcessor,
BlockCls, BlockCls,
@ -46,6 +45,7 @@ import {
DYNAMIC_RESERVE, DYNAMIC_RESERVE,
MOVING_TOLERANCE MOVING_TOLERANCE
} from '../shared'; } from '../shared';
import { Transform } from '@motajs/render-core';
import { MapViewport } from './viewport'; import { MapViewport } from './viewport';
import { INSTANCED_COUNT } from './constant'; import { INSTANCED_COUNT } from './constant';
import { StaticBlockStatus } from './status'; import { StaticBlockStatus } from './status';

View File

@ -1,9 +1,9 @@
import { IDirtyMark, IDirtyTracker } from '@motajs/common'; import { IDirtyMark, IDirtyTracker } from '@motajs/common';
import { import {
ITextureRenderable, ITextureRenderable,
SizedCanvasImageSource, SizedCanvasImageSource
Transform } from '@motajs/render-assets';
} from '@motajs/render'; import { Transform } from '@motajs/render-core';
import { import {
IAutotileProcessor, IAutotileProcessor,
IMaterialFramedData, IMaterialFramedData,
@ -997,7 +997,8 @@ export interface IMapVertexStatus {
* *
*/ */
export interface IMapVertexGenerator export interface IMapVertexGenerator
extends IDirtyTracker<boolean>, IMapVertexStatus { extends IDirtyTracker<boolean>,
IMapVertexStatus {
/** 地图渲染器 */ /** 地图渲染器 */
readonly renderer: IMapRenderer; readonly renderer: IMapRenderer;
/** 地图分块 */ /** 地图分块 */

View File

@ -21,7 +21,7 @@ import { DYNAMIC_RESERVE, MAP_BLOCK_HEIGHT, MAP_BLOCK_WIDTH } from '../shared';
import { BlockSplitter } from './block'; import { BlockSplitter } from './block';
import { clamp, isNil } from 'lodash-es'; import { clamp, isNil } from 'lodash-es';
import { BlockCls, IMaterialFramedData } from '@user/client-base'; import { BlockCls, IMaterialFramedData } from '@user/client-base';
import { IRect } from '@motajs/render'; import { IRect } from '@motajs/render-assets';
import { INSTANCED_COUNT } from './constant'; import { INSTANCED_COUNT } from './constant';
export interface IMapDataGetter { export interface IMapDataGetter {

View File

@ -1,4 +1,4 @@
import { Transform } from '@motajs/render'; import { Transform } from '@motajs/render-core';
import { import {
IBlockData, IBlockData,
IMapRenderArea, IMapRenderArea,

View File

@ -1,46 +1,8 @@
import { MotaRenderer } from '@motajs/render'; import { MotaRenderer } from '@motajs/render-core';
import { import { MAIN_WIDTH, MAIN_HEIGHT } from './shared';
MAIN_WIDTH,
MAIN_HEIGHT,
DEBUG_VARIATOR,
VARIATOR_DEBUG_SPEED,
DEBUG_DIVIDER,
DIVIDER_DEBUG_DIVIDER
} from './shared';
import { createRendererFor, RendererUsing } from '@motajs/render-vue';
import {
ExcitationDivider,
ExcitationVariator,
RafExcitation
} from '@motajs/animate';
/** 渲染激励源 */
export const rafExcitation = new RafExcitation();
/** 渲染分频器 */
export const excitationDivider = new ExcitationDivider<number>();
if (DEBUG_VARIATOR) {
const variator = new ExcitationVariator();
variator.bindExcitation(rafExcitation);
variator.setSpeed(VARIATOR_DEBUG_SPEED);
excitationDivider.bindExcitation(variator);
} else {
excitationDivider.bindExcitation(rafExcitation);
}
if (DEBUG_DIVIDER) {
excitationDivider.setDivider(DIVIDER_DEBUG_DIVIDER);
}
export const mainRenderer = new MotaRenderer({ export const mainRenderer = new MotaRenderer({
canvas: '#render-main', canvas: '#render-main',
width: MAIN_WIDTH, width: MAIN_WIDTH,
height: MAIN_HEIGHT, height: MAIN_HEIGHT
// 使用分频器,用户可以在设置中调整,如果设备性能较差调高分频有助于提高性能表现
excitaion: excitationDivider
}); });
export const using = new RendererUsing(mainRenderer);
export const { createApp, render, tagManager } =
createRendererFor(mainRenderer);

View File

@ -1,3 +1,3 @@
import { UIController } from '@motajs/system'; import { UIController } from '@motajs/system-ui';
export const sceneController = new UIController('main-scene'); export const sceneController = new UIController('main-scene');

View File

@ -1,26 +1,8 @@
import { ElementLocator, Font } from '@motajs/render'; import { ElementLocator } from '@motajs/render-core';
import { Font } from '@motajs/render-style';
// 本文件为 UI 配置文件,你可以修改下面的每个常量来控制 UI 的显示参数,每个常量都有注释说明 // 本文件为 UI 配置文件,你可以修改下面的每个常量来控制 UI 的显示参数,每个常量都有注释说明
//#region 调试用参数
/**
* 使使 {@link VARIATOR_DEBUG_SPEED}
* 便
*/
export const DEBUG_VARIATOR = false;
/** 当使用变速器作为激励源调试时,变速器的速度 */
export const VARIATOR_DEBUG_SPEED = 0.2;
/**
* 使使 {@link DIVIDER_DEBUG_DIVIDER}
* 便
*/
export const DEBUG_DIVIDER = false;
/** 当使用分频器调试时,分配比例 */
export const DIVIDER_DEBUG_DIVIDER = 60;
//#endregion
//#region 地图 //#region 地图
/** 每个格子的默认宽度,现阶段用处不大 */ /** 每个格子的默认宽度,现阶段用处不大 */
@ -54,8 +36,6 @@ export const MOVING_TOLERANCE = 60;
/** 开关门动画的动画时长 */ /** 开关门动画的动画时长 */
export const DOOR_ANIMATE_INTERVAL = 50; export const DOOR_ANIMATE_INTERVAL = 50;
//#endregion
//#region 状态栏 //#region 状态栏
/** 状态栏像素宽度 */ /** 状态栏像素宽度 */
@ -71,8 +51,6 @@ export const STATUS_BAR_COUNT = ENABLE_RIGHT_STATUS_BAR ? 2 : 1;
/** 状态栏宽度的一半 */ /** 状态栏宽度的一半 */
export const HALF_STATUS_WIDTH = STATUS_BAR_WIDTH / 2; export const HALF_STATUS_WIDTH = STATUS_BAR_WIDTH / 2;
//#endregion
//#region 游戏画面 //#region 游戏画面
/** 游戏画面像素宽度,宽=地图宽度+状态栏宽度*状态栏数量 */ /** 游戏画面像素宽度,宽=地图宽度+状态栏宽度*状态栏数量 */
@ -95,8 +73,6 @@ export const CENTER_LOC: ElementLocator = [
0.5 0.5
]; ];
//#endregion
//#region 通用配置 //#region 通用配置
/** 弹框的宽度,使用在内置 UI 与组件中,包括确认框、选择框、等待框等 */ /** 弹框的宽度,使用在内置 UI 与组件中,包括确认框、选择框、等待框等 */
@ -104,31 +80,6 @@ export const POP_BOX_WIDTH = MAP_WIDTH / 2;
/** 默认字体 */ /** 默认字体 */
export const DEFAULT_FONT = new Font('Verdana', 16); export const DEFAULT_FONT = new Font('Verdana', 16);
//#endregion
//#region 加载界面
/** 加载界面的任务进度条半径 */
export const LOAD_TASK_RADIUS = Math.min(MAIN_WIDTH, MAIN_HEIGHT) / 6;
/** 加载界面的字节进度条纵轴位置 */
export const LOAD_BYTE_HEIGHT = MAIN_HEIGHT / 2 + MAIN_HEIGHT / 4;
/** 加载界面任务进度条的纵轴位置 */
export const LOAD_TASK_CENTER_HEIGHT = MAIN_HEIGHT / 2 - MAIN_HEIGHT / 8;
/** 加载界面字节进度条的长度 */
export const LOAD_BYTE_LENGTH = MAIN_WIDTH - MAIN_WIDTH / 12;
/** 加载界面任务进度条的粗细 */
export const LOAD_TASK_LINE_WIDTH = 6;
/** 加载界面字节进度条的粗细 */
export const LOAD_BYTE_LINE_WIDTH = 6;
/** 已加载部分进度条的颜色 */
export const LOAD_LOADED_COLOR = '#57ff78';
/** 未加载部分进度条的颜色 */
export const LOAD_UNLOADED_COLOR = '#ccc';
/** 加载界面的文字颜色 */
export const LOAD_FONT_COLOR = '#fff';
//#endregion
//#region 存档界面 //#region 存档界面
/** 存档缩略图尺寸 */ /** 存档缩略图尺寸 */
@ -146,13 +97,8 @@ export const SAVE_DOWN_PAD = 30;
/** 存档页码数,调高并不会影响性能,但是如果玩家存档太多的话会导致存档体积很大 */ /** 存档页码数,调高并不会影响性能,但是如果玩家存档太多的话会导致存档体积很大 */
export const SAVE_PAGES = 1000; export const SAVE_PAGES = 1000;
//#endregion
//#region 标题界面 //#region 标题界面
/** 标题图 */
export const TITLE_BACKGROUND_IMAGE = 'bg.jpg';
/** 标题文字中心横坐标 */ /** 标题文字中心横坐标 */
export const TITLE_X = HALF_WIDTH; export const TITLE_X = HALF_WIDTH;
/** 标题文字中心纵坐标 */ /** 标题文字中心纵坐标 */
@ -172,5 +118,3 @@ export const BUTTONS_HEIGHT = 200;
export const BUTTONS_X = HALF_WIDTH; export const BUTTONS_X = HALF_WIDTH;
/** 标题界面按钮左上角纵坐标 */ /** 标题界面按钮左上角纵坐标 */
export const BUTTONS_Y = MAIN_HEIGHT - BUTTONS_HEIGHT; export const BUTTONS_Y = MAIN_HEIGHT - BUTTONS_HEIGHT;
//#endregion

View File

@ -4,7 +4,7 @@ import {
SetupComponentOptions, SetupComponentOptions,
UIComponentProps, UIComponentProps,
UIController UIController
} from '@motajs/system'; } from '@motajs/system-ui';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { MAIN_HEIGHT, MAIN_WIDTH } from '../shared'; import { MAIN_HEIGHT, MAIN_WIDTH } from '../shared';

View File

@ -1,165 +0,0 @@
import { DefaultProps } from '@motajs/render-vue';
import {
GameUI,
SetupComponentOptions,
UIComponentProps
} from '@motajs/system';
import { defineComponent } from 'vue';
import {
FULL_LOC,
LOAD_BYTE_HEIGHT,
LOAD_BYTE_LENGTH,
LOAD_BYTE_LINE_WIDTH,
LOAD_FONT_COLOR,
LOAD_LOADED_COLOR,
LOAD_TASK_CENTER_HEIGHT,
LOAD_TASK_LINE_WIDTH,
LOAD_TASK_RADIUS,
LOAD_UNLOADED_COLOR,
MAIN_WIDTH
} from '../shared';
import { ElementLocator, Font, MotaOffscreenCanvas2D } from '@motajs/render';
import { transitioned } from '../use';
import { cosh, CurveMode, linear } from '@motajs/animate';
import { loader } from '@user/client-base';
import { clamp } from 'lodash-es';
import { sleep } from '@motajs/common';
import { loading } from '@user/data-base';
import { GameTitleUI } from './title';
export interface ILoadProps extends UIComponentProps, DefaultProps {}
const loadSceneProps = {
props: ['controller', 'instance']
} satisfies SetupComponentOptions<ILoadProps>;
export const LoadScene = defineComponent<ILoadProps>(props => {
const taskFont = new Font('Verdana', 24);
const byteFont = new Font('Verdana', 12);
/** 当前加载进度 */
const taskProgress = transitioned(0, 500, cosh(2, CurveMode.EaseOut))!;
const byteProgress = transitioned(0, 500, cosh(2, CurveMode.EaseOut))!;
const alpha = transitioned(1, 400, linear())!;
// 两个进度条的位置
const taskLoc: ElementLocator = [
MAIN_WIDTH / 2,
LOAD_TASK_CENTER_HEIGHT,
LOAD_TASK_RADIUS * 2 + LOAD_TASK_LINE_WIDTH * 2,
LOAD_TASK_RADIUS * 2 + LOAD_TASK_LINE_WIDTH * 2,
0.5,
0.5
];
const byteLoc: ElementLocator = [
MAIN_WIDTH / 2,
LOAD_BYTE_HEIGHT,
LOAD_BYTE_LENGTH + LOAD_BYTE_LINE_WIDTH,
LOAD_BYTE_LINE_WIDTH * 2 + byteFont.size,
0.5,
0.5
];
const loadEnd = async () => {
loading.emit('loaded');
alpha.set(0);
await sleep(400);
props.controller.closeAll();
props.controller.open(GameTitleUI, {});
};
const startLoad = async () => {
loader.initSystemLoadTask();
loader.load().then(() => {
loadEnd();
});
for await (const _ of loader.progress) {
taskProgress.set(loader.progress.getLoadedTasks());
byteProgress.set(loader.progress.getLoadedByte());
}
};
// 开始加载
startLoad();
/** 渲染加载任务进度 */
const renderTaskList = (canvas: MotaOffscreenCanvas2D) => {
const ctx = canvas.ctx;
ctx.lineCap = 'round';
ctx.lineWidth = LOAD_TASK_LINE_WIDTH;
ctx.font = taskFont.string();
const loaded = loader.progress.getLoadedTasks();
const total = loader.progress.getAddedTasks();
// 这里使用渐变参数,因为要有动画效果
const progress = clamp(taskProgress.value / total, 0, 1);
const cx = taskLoc[2]! / 2;
const cy = taskLoc[3]! / 2;
ctx.beginPath();
ctx.arc(cx, cy, LOAD_TASK_RADIUS, 0, Math.PI * 2);
ctx.strokeStyle = LOAD_UNLOADED_COLOR;
ctx.stroke();
ctx.beginPath();
const end = progress * Math.PI * 2 - Math.PI / 2;
ctx.arc(cx, cy, LOAD_TASK_RADIUS, -Math.PI / 2, end);
ctx.strokeStyle = LOAD_LOADED_COLOR;
ctx.stroke();
ctx.fillStyle = LOAD_FONT_COLOR;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(`${loaded} / ${total}`, cx, cy + 3);
};
/** 渲染加载字节进度 */
const renderByteList = (canvas: MotaOffscreenCanvas2D) => {
const ctx = canvas.ctx;
ctx.lineCap = 'round';
ctx.lineWidth = LOAD_BYTE_LINE_WIDTH;
ctx.font = byteFont.string();
const total = loader.progress.getTotalByte();
const loaded = loader.progress.getLoadedByte();
// 这里使用渐变参数,因为要有动画效果
const progress = clamp(byteProgress.value / total, 0, 1);
const sx = LOAD_BYTE_LINE_WIDTH;
const sy = byteFont.size + LOAD_BYTE_LINE_WIDTH;
ctx.beginPath();
ctx.moveTo(sx, sy);
ctx.lineTo(sx + LOAD_BYTE_LENGTH, sy);
ctx.strokeStyle = LOAD_UNLOADED_COLOR;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(sx, sy);
ctx.lineTo(sx + progress * LOAD_BYTE_LENGTH, sy);
ctx.strokeStyle = LOAD_LOADED_COLOR;
ctx.stroke();
ctx.textAlign = 'right';
ctx.textBaseline = 'bottom';
ctx.fillStyle = LOAD_FONT_COLOR;
const loadedMB = (loaded / 2 ** 20).toFixed(2);
const totalMB = (total / 2 ** 20).toFixed(2);
const percent = loader.progress.getByteRatio() * 100;
ctx.fillText(
`${loadedMB}MB / ${totalMB}MB | ${percent.toFixed(2)}%`,
byteLoc[2]! - LOAD_BYTE_LINE_WIDTH,
byteLoc[3]! - LOAD_BYTE_LINE_WIDTH * 2
);
};
return () => (
<container loc={FULL_LOC} alpha={alpha.ref.value}>
<custom
loc={taskLoc}
render={renderTaskList}
bindings={[taskProgress.ref]}
nocache
/>
<custom
loc={byteLoc}
render={renderByteList}
bindings={[byteProgress.ref]}
nocache
/>
</container>
);
}, loadSceneProps);
export const LoadSceneUI = new GameUI('load-scene', LoadScene);

View File

@ -1,13 +1,15 @@
import { import {
Props,
Font, Font,
IActionEvent, IActionEvent,
MotaOffscreenCanvas2D, MotaOffscreenCanvas2D,
CustomRenderItem Sprite,
onTick
} from '@motajs/render'; } from '@motajs/render';
// import { WeatherController } from '../weather'; // import { WeatherController } from '../weather';
import { defineComponent, onUnmounted, reactive, ref } from 'vue'; import { defineComponent, onUnmounted, reactive, ref } from 'vue';
import { Textbox, TextboxProps, Tip } from '../components'; import { Textbox, Tip } from '../components';
import { GameUI } from '@motajs/system'; import { GameUI } from '@motajs/system-ui';
import { import {
ENABLE_RIGHT_STATUS_BAR, ENABLE_RIGHT_STATUS_BAR,
MAIN_HEIGHT, MAIN_HEIGHT,
@ -27,14 +29,14 @@ import {
import { ReplayingStatus } from './toolbar'; import { ReplayingStatus } from './toolbar';
import { getHeroStatusOn, state } from '@user/data-state'; import { getHeroStatusOn, state } from '@user/data-state';
import { hook } from '@user/data-base'; import { hook } from '@user/data-base';
import { FloorChange } from '../legacy/fallback';
import { mainUIController } from './controller'; import { mainUIController } from './controller';
import { isNil } from 'lodash-es'; import { isNil } from 'lodash-es';
import { mainMapExtension, mainMapRenderer } from '../commonIns'; import { mainMapExtension, mainMapRenderer } from '../commonIns';
import { using } from '../renderer';
const MainScene = defineComponent(() => { const MainScene = defineComponent(() => {
//#region 基本定义 //#region 基本定义
const mainTextboxProps: TextboxProps = { const mainTextboxProps: Props<typeof Textbox> = {
text: '', text: '',
hidden: true, hidden: true,
loc: [0, MAP_HEIGHT - 150, MAP_WIDTH, 150], loc: [0, MAP_HEIGHT - 150, MAP_WIDTH, 150],
@ -146,7 +148,7 @@ const MainScene = defineComponent(() => {
//#region sprite 渲染 //#region sprite 渲染
let lastLength = 0; let lastLength = 0;
using.onExcitedFunc(() => { onTick(() => {
const len = core.status.stepPostfix?.length ?? 0; const len = core.status.stepPostfix?.length ?? 0;
if (len !== lastLength) { if (len !== lastLength) {
mapMiscSprite.value?.update(); mapMiscSprite.value?.update();
@ -154,7 +156,7 @@ const MainScene = defineComponent(() => {
} }
}); });
const mapMiscSprite = ref<CustomRenderItem>(); const mapMiscSprite = ref<Sprite>();
const renderMapMisc = (canvas: MotaOffscreenCanvas2D) => { const renderMapMisc = (canvas: MotaOffscreenCanvas2D) => {
const step = core.status.stepPostfix; const step = core.status.stepPostfix;
@ -244,7 +246,7 @@ const MainScene = defineComponent(() => {
loc={[0, 0, MAP_WIDTH, MAP_HEIGHT]} loc={[0, 0, MAP_WIDTH, MAP_HEIGHT]}
/> />
<Textbox id="main-textbox" {...mainTextboxProps}></Textbox> <Textbox id="main-textbox" {...mainTextboxProps}></Textbox>
{/* <FloorChange id="floor-change" zIndex={50}></FloorChange> */} <FloorChange id="floor-change" zIndex={50}></FloorChange>
<Tip <Tip
id="main-tip" id="main-tip"
zIndex={80} zIndex={80}
@ -252,7 +254,7 @@ const MainScene = defineComponent(() => {
pad={[12, 6]} pad={[12, 6]}
corner={16} corner={16}
/> />
<custom <sprite
noevent noevent
loc={[0, 0, MAP_WIDTH, MAP_HEIGHT]} loc={[0, 0, MAP_WIDTH, MAP_HEIGHT]}
ref={mapMiscSprite} ref={mapMiscSprite}

View File

@ -1,11 +1,12 @@
import { ElementLocator, IWheelEvent, Font } from '@motajs/render'; import { ElementLocator, IWheelEvent } from '@motajs/render-core';
import { DefaultProps } from '@motajs/render-vue'; import { DefaultProps } from '@motajs/render-vue';
import { Font } from '@motajs/render';
import { import {
GameUI, GameUI,
IUIMountable, IUIMountable,
SetupComponentOptions, SetupComponentOptions,
UIComponentProps UIComponentProps
} from '@motajs/system'; } from '@motajs/system-ui';
import { import {
defineComponent, defineComponent,
ref, ref,

View File

@ -4,7 +4,7 @@ import {
IUIMountable, IUIMountable,
SetupComponentOptions, SetupComponentOptions,
UIComponentProps UIComponentProps
} from '@motajs/system'; } from '@motajs/system-ui';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { import {
ChoiceItem, ChoiceItem,
@ -15,7 +15,8 @@ import {
waitbox waitbox
} from '../components'; } from '../components';
import { mainUi } from '@motajs/legacy-ui'; import { mainUi } from '@motajs/legacy-ui';
import { gameKey, generateKeyboardEvent } from '@motajs/system'; import { gameKey } from '@motajs/system-action';
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';
@ -27,7 +28,8 @@ import { CENTER_LOC, FULL_LOC, MAIN_HEIGHT, POP_BOX_WIDTH } from '../shared';
import { useKey } from '../use'; import { useKey } from '../use';
export interface MainSettingsProps export interface MainSettingsProps
extends Partial<ChoicesProps>, UIComponentProps { extends Partial<ChoicesProps>,
UIComponentProps {
loc: ElementLocator; loc: ElementLocator;
} }

View File

@ -3,7 +3,7 @@ import {
IUIMountable, IUIMountable,
SetupComponentOptions, SetupComponentOptions,
UIComponentProps UIComponentProps
} from '@motajs/system'; } from '@motajs/system-ui';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { waitbox, ListPage, TextContent } from '../components'; import { waitbox, ListPage, TextContent } from '../components';
import { DefaultProps } from '@motajs/render-vue'; import { DefaultProps } from '@motajs/render-vue';

View File

@ -1,7 +1,12 @@
import { GameUI, SetupComponentOptions } from '@motajs/system'; import { GameUI, SetupComponentOptions } from '@motajs/system-ui';
import { computed, ComputedRef, defineComponent, shallowReactive } from 'vue'; import { computed, ComputedRef, defineComponent, shallowReactive } from 'vue';
import { TextContent } from '../components'; import { TextContent } from '../components';
import { ElementLocator, Font, ITexture } from '@motajs/render'; import {
DefaultProps,
ElementLocator,
Font,
SizedCanvasImageSource
} from '@motajs/render';
import { MixedToolbar, ReplayingStatus } from './toolbar'; import { MixedToolbar, ReplayingStatus } from './toolbar';
import { openViewMap } from './viewmap'; import { openViewMap } from './viewmap';
import { mainUIController } from './controller'; import { mainUIController } from './controller';
@ -11,8 +16,6 @@ import {
STATUS_BAR_HEIGHT, STATUS_BAR_HEIGHT,
STATUS_BAR_WIDTH STATUS_BAR_WIDTH
} from '../shared'; } from '../shared';
import { DefaultProps } from '@motajs/render-vue';
import { materials } from '@user/client-base';
export interface ILeftHeroStatus { export interface ILeftHeroStatus {
/** 楼层 id */ /** 楼层 id */
@ -70,27 +73,27 @@ export interface IRightHeroStatus {
interface StatusInfo { interface StatusInfo {
/** 图标 */ /** 图标 */
readonly icon: ITexture | null; icon: SizedCanvasImageSource;
/** 属性值,经过格式化 */ /** 属性值,经过格式化 */
readonly value: ComputedRef<string>; value: ComputedRef<string>;
/** 字体 */ /** 字体 */
readonly font: Font; font: Font;
/** 文字颜色 */ /** 文字颜色 */
readonly color: CanvasStyle; color: CanvasStyle;
} }
interface KeyLikeItem { interface KeyLikeItem {
/** 属性值,经过格式化 */ /** 属性值,经过格式化 */
readonly value: ComputedRef<string>; value: ComputedRef<string>;
/** 字体 */ /** 字体 */
readonly font: Font; font: Font;
/** 文字颜色 */ /** 文字颜色 */
readonly color: CanvasStyle; color: CanvasStyle;
} }
interface KeyLikeInfo { interface KeyLikeInfo {
/** 这一行包含的内容 */ /** 这一行包含的内容 */
readonly items: KeyLikeItem[]; items: KeyLikeItem[];
} }
interface StatusBarProps<T> extends DefaultProps { interface StatusBarProps<T> extends DefaultProps {
@ -116,15 +119,15 @@ export const LeftStatusBar = defineComponent<StatusBarProps<ILeftHeroStatus>>(
/** 状态属性的开始纵坐标 */ /** 状态属性的开始纵坐标 */
const STATUS_Y = TITLE_HEIGHT + STATUS_PAD; const STATUS_Y = TITLE_HEIGHT + STATUS_PAD;
// 可以换成 materials.getImageByAlias('xxx.png') 来使用全塔属性注册的图片 // 可以换成 core.material.images.images['xxx.png'] 来使用全塔属性注册的图片
const hpIcon = materials.getImageByAlias('icon-hp'); const hpIcon = core.statusBar.icons.hp;
const atkIcon = materials.getImageByAlias('icon-atk'); const atkIcon = core.statusBar.icons.atk;
const defIcon = materials.getImageByAlias('icon-def'); const defIcon = core.statusBar.icons.def;
const mdefIcon = materials.getImageByAlias('icon-mdef'); const mdefIcon = core.statusBar.icons.mdef;
const moneyIcon = materials.getImageByAlias('icon-money'); const moneyIcon = core.statusBar.icons.money;
const expIcon = materials.getImageByAlias('icon-exp'); const expIcon = core.statusBar.icons.exp;
const manaIcon = materials.getImageByAlias('icon-mana'); const manaIcon = core.statusBar.icons.mana;
const lvIcon = materials.getImageByAlias('icon-lv'); const lvIcon = core.statusBar.icons.lv;
const s = p.status; const s = p.status;

View File

@ -3,7 +3,7 @@ import {
GameUI, GameUI,
SetupComponentOptions, SetupComponentOptions,
UIComponentProps UIComponentProps
} from '@motajs/system'; } from '@motajs/system-ui';
import { defineComponent, nextTick, onMounted, ref } from 'vue'; import { defineComponent, nextTick, onMounted, ref } from 'vue';
import { import {
BUTTONS_HEIGHT, BUTTONS_HEIGHT,
@ -14,28 +14,26 @@ import {
HALF_WIDTH, HALF_WIDTH,
MAIN_HEIGHT, MAIN_HEIGHT,
MAIN_WIDTH, MAIN_WIDTH,
TITLE_BACKGROUND_IMAGE,
TITLE_FILL, TITLE_FILL,
TITLE_STROKE, TITLE_STROKE,
TITLE_STROKE_WIDTH, TITLE_STROKE_WIDTH,
TITLE_X, TITLE_X,
TITLE_Y TITLE_Y
} from '../shared'; } from '../shared';
import { ElementLocator, Font } from '@motajs/render'; import { ElementLocator } from '@motajs/render-core';
import { import {
ITransitionedController, ITransitionedController,
transitioned, transitioned,
transitionedColor, transitionedColor,
useKey useKey
} from '../use'; } from '../use';
import { hyper, linear, sleep } from 'mutate-animate';
import { Font } from '@motajs/render-style';
import { ExitFullscreen, Fullscreen, SoundVolume } from '../components'; import { ExitFullscreen, Fullscreen, SoundVolume } from '../components';
import { mainSetting, triggerFullscreen } from '@motajs/legacy-ui'; import { mainSetting, triggerFullscreen } from '@motajs/legacy-ui';
import { saveLoad } from './save'; import { saveLoad } from './save';
import { MainSceneUI } from './main'; import { MainSceneUI } from './main';
import { adjustCover } from '../utils'; import { adjustCover } from '../utils';
import { cosh, CurveMode, linear } from '@motajs/animate';
import { sleep } from '@motajs/common';
import { materials } from '@user/client-base';
const enum TitleButton { const enum TitleButton {
StartGame, StartGame,
@ -64,12 +62,12 @@ const gameTitleProps = {
} satisfies SetupComponentOptions<GameTitleProps>; } satisfies SetupComponentOptions<GameTitleProps>;
export const GameTitle = defineComponent<GameTitleProps>(props => { export const GameTitle = defineComponent<GameTitleProps>(props => {
const bg = materials.getImageByAlias(TITLE_BACKGROUND_IMAGE); const bg = core.material.images.images['bg.jpg'];
//#region 计算背景图 //#region 计算背景图
const [width, height] = adjustCover( const [width, height] = adjustCover(
bg?.width ?? MAIN_WIDTH, bg.width,
bg?.height ?? MAIN_HEIGHT, bg.height,
MAIN_WIDTH, MAIN_WIDTH,
MAIN_HEIGHT MAIN_HEIGHT
); );
@ -109,12 +107,8 @@ export const GameTitle = defineComponent<GameTitleProps>(props => {
color: v.color, color: v.color,
name: v.name, name: v.name,
hard: '', hard: '',
colorTrans: transitionedColor( colorTrans: transitionedColor('#fff', 400, hyper('sin', 'out'))!,
'#fff', scale: transitioned(1, 400, hyper('sin', 'out'))!
400,
cosh(2, CurveMode.EaseOut)
)!,
scale: transitioned(1, 400, cosh(2, CurveMode.EaseOut))!
}; };
}); });
@ -125,12 +119,8 @@ export const GameTitle = defineComponent<GameTitleProps>(props => {
color: core.arrayToRGBA(v.color!), color: core.arrayToRGBA(v.color!),
name: v.title, name: v.title,
hard: v.name, hard: v.name,
colorTrans: transitionedColor( colorTrans: transitionedColor('#fff', 400, hyper('sin', 'out'))!,
'#fff', scale: transitioned(1, 400, hyper('sin', 'out'))!
400,
cosh(2, CurveMode.EaseOut)
)!,
scale: transitioned(1, 400, cosh(2, CurveMode.EaseOut))!
}; };
}); });
// 返回按钮 // 返回按钮
@ -139,15 +129,11 @@ export const GameTitle = defineComponent<GameTitleProps>(props => {
color: '#aaa', color: '#aaa',
name: '返回', name: '返回',
hard: '', hard: '',
colorTrans: transitionedColor('#fff', 400, cosh(2, CurveMode.EaseOut))! colorTrans: transitionedColor('#fff', 400, hyper('sin', 'out'))!
}); });
/** 声音设置按钮的颜色 */ /** 声音设置按钮的颜色 */
const soundColor = transitionedColor( const soundColor = transitionedColor('#ddd', 400, hyper('sin', 'out'))!;
'#ddd',
400,
cosh(2, CurveMode.EaseOut)
)!;
/** 开始界面按钮的不透明度,选择难度界面的不透明度使用 `1-buttonsAlpha` 计算 */ /** 开始界面按钮的不透明度,选择难度界面的不透明度使用 `1-buttonsAlpha` 计算 */
const buttonsAlpha = transitioned(1, 300, linear())!; const buttonsAlpha = transitioned(1, 300, linear())!;
@ -167,11 +153,7 @@ export const GameTitle = defineComponent<GameTitleProps>(props => {
/** 选择难度界面按钮的高度 */ /** 选择难度界面按钮的高度 */
const hardHeight = (hard.length - 1) * 40 + 60; const hardHeight = (hard.length - 1) * 40 + 60;
/** 按钮的背景框高度 */ /** 按钮的背景框高度 */
const rectHeight = transitioned( const rectHeight = transitioned(buttonHeight, 600, hyper('sin', 'in-out'))!;
buttonHeight,
600,
cosh(2, CurveMode.EaseOut)
)!;
//#region 按钮功能 //#region 按钮功能

View File

@ -1,4 +1,4 @@
import { ElementLocator, Font } from '@motajs/render'; import { DefaultProps, ElementLocator, Font } from '@motajs/render';
import { computed, defineComponent, ref } from 'vue'; import { computed, defineComponent, ref } from 'vue';
import { import {
DoubleArrow, DoubleArrow,
@ -12,20 +12,19 @@ import {
ViewMapIcon ViewMapIcon
} from '../components/icons'; } from '../components/icons';
import { getVitualKeyOnce } from '@motajs/legacy-ui'; import { getVitualKeyOnce } from '@motajs/legacy-ui';
import { gameKey, generateKeyboardEvent } from '@motajs/system'; import { gameKey } from '@motajs/system-action';
import { generateKeyboardEvent } from '@motajs/system-action';
import { transitioned } from '../use'; import { transitioned } from '../use';
import { linear } from 'mutate-animate'; import { linear } from 'mutate-animate';
import { KeyCode } from '@motajs/client-base'; import { KeyCode } from '@motajs/client-base';
import { Progress } from '../components/misc'; import { Progress } from '../components/misc';
import { generateBinary } from '@motajs/legacy-common'; import { generateBinary } from '@motajs/legacy-common';
import { SetupComponentOptions } from '@motajs/system'; import { SetupComponentOptions } from '@motajs/system-ui';
import { saveSave, saveLoad } from './save'; import { saveSave, saveLoad } from './save';
import { mainUIController } from './controller'; import { mainUIController } from './controller';
import { MAIN_HEIGHT, FULL_LOC, POP_BOX_WIDTH, CENTER_LOC } from '../shared'; import { MAIN_HEIGHT, FULL_LOC, POP_BOX_WIDTH, CENTER_LOC } from '../shared';
import { openReplay, openSettings } from './settings'; import { openReplay, openSettings } from './settings';
import { openViewMap } from './viewmap'; import { openViewMap } from './viewmap';
import { DefaultProps } from '@motajs/render-vue';
import { materials } from '@user/client-base';
interface ToolbarProps extends DefaultProps { interface ToolbarProps extends DefaultProps {
loc?: ElementLocator; loc?: ElementLocator;
@ -74,15 +73,15 @@ export const PlayingToolbar = defineComponent<
ToolbarEmits, ToolbarEmits,
keyof ToolbarEmits keyof ToolbarEmits
>((props, { emit }) => { >((props, { emit }) => {
const bookIcon = materials.getImageByAlias('icon-book'); const bookIcon = core.statusBar.icons.book;
const flyIcon = materials.getImageByAlias('icon-fly'); const flyIcon = core.statusBar.icons.fly;
const toolIcon = materials.getImageByAlias('icon-toolbox'); const toolIcon = core.statusBar.icons.toolbox;
const equipIcon = materials.getImageByAlias('icon-equipbox'); const equipIcon = core.statusBar.icons.equipbox;
const keyIcon = materials.getImageByAlias('icon-keyboard'); const keyIcon = core.statusBar.icons.keyboard;
const shopIcon = materials.getImageByAlias('icon-shop'); const shopIcon = core.statusBar.icons.shop;
const saveIcon = materials.getImageByAlias('icon-save'); const saveIcon = core.statusBar.icons.save;
const loadIcon = materials.getImageByAlias('icon-load'); const loadIcon = core.statusBar.icons.load;
const setIcon = materials.getImageByAlias('icon-settings'); const setIcon = core.statusBar.icons.settings;
const iconFont = new Font('Verdana', 12); const iconFont = new Font('Verdana', 12);
@ -171,8 +170,8 @@ const replayingProps = {
export const ReplayingToolbar = defineComponent<ReplayingProps>(props => { export const ReplayingToolbar = defineComponent<ReplayingProps>(props => {
const status = props.status; const status = props.status;
const bookIcon = materials.getImageByAlias('icon-book'); const bookIcon = core.statusBar.icons.book;
const saveIcon = materials.getImageByAlias('icon-save'); const saveIcon = core.statusBar.icons.save;
const font1 = Font.defaults({ size: 16 }); const font1 = Font.defaults({ size: 16 });
const font2 = new Font('Verdana', 12); const font2 = new Font('Verdana', 12);

View File

@ -3,16 +3,15 @@ import {
IActionEvent, IActionEvent,
IActionEventBase, IActionEventBase,
IWheelEvent, IWheelEvent,
MotaOffscreenCanvas2D, MotaOffscreenCanvas2D
Font } from '@motajs/render-core';
} from '@motajs/render';
import { BaseProps } from '@motajs/render-vue'; import { BaseProps } from '@motajs/render-vue';
import { import {
GameUI, GameUI,
IUIMountable, IUIMountable,
SetupComponentOptions, SetupComponentOptions,
UIComponentProps UIComponentProps
} from '@motajs/system'; } from '@motajs/system-ui';
import { import {
computed, computed,
defineComponent, defineComponent,
@ -23,9 +22,18 @@ import {
shallowRef, shallowRef,
watch watch
} from 'vue'; } from 'vue';
import { FloorSelector } from '../components'; import { FloorSelector } from '../components/floorSelect';
import {
ILayerGroupRenderExtends,
FloorDamageExtends,
FloorItemDetail,
LayerGroupAnimate,
LayerGroup,
LayerGroupFloorBinder
} from '../elements';
import { Font } from '@motajs/render-style';
import { clamp, mean } from 'lodash-es'; import { clamp, mean } from 'lodash-es';
import { StatisticsDataOneFloor } from './statistics'; import { calculateStatisticsOne, StatisticsDataOneFloor } from './statistics';
import { Tip, TipExpose } from '../components'; import { Tip, TipExpose } from '../components';
import { useKey } from '../use'; import { useKey } from '../use';
import { import {
@ -53,11 +61,11 @@ const viewMapProps = {
export const ViewMap = defineComponent<ViewMapProps>(props => { export const ViewMap = defineComponent<ViewMapProps>(props => {
const nowFloorId = core.status.floorId; const nowFloorId = core.status.floorId;
// const layerGroupExtends: ILayerGroupRenderExtends[] = [ const layerGroupExtends: ILayerGroupRenderExtends[] = [
// new FloorDamageExtends(), new FloorDamageExtends(),
// new FloorItemDetail(), new FloorItemDetail(),
// new LayerGroupAnimate() new LayerGroupAnimate()
// ]; ];
const restHeight = STATUS_BAR_HEIGHT - 292; const restHeight = STATUS_BAR_HEIGHT - 292;
const col = restHeight / 4; const col = restHeight / 4;
@ -76,7 +84,7 @@ export const ViewMap = defineComponent<ViewMapProps>(props => {
}) })
); );
// const group = ref<LayerGroup>(); const group = ref<LayerGroup>();
const tip = ref<TipExpose>(); const tip = ref<TipExpose>();
const statistics = shallowRef<StatisticsDataOneFloor>(); const statistics = shallowRef<StatisticsDataOneFloor>();
@ -99,7 +107,7 @@ export const ViewMap = defineComponent<ViewMapProps>(props => {
.realize('@viewMap_down_ten', () => changeFloor(-10)) .realize('@viewMap_down_ten', () => changeFloor(-10))
.realize('@viewMap_book', () => openBook()) .realize('@viewMap_book', () => openBook())
.realize('@viewMap_fly', () => fly()) .realize('@viewMap_fly', () => fly())
// .realize('@viewMap_reset', () => resetCamera()) .realize('@viewMap_reset', () => resetCamera())
.realize('confirm', () => close()) .realize('confirm', () => close())
.realize('exit', (_, code, assist) => { .realize('exit', (_, code, assist) => {
// 如果按键不能触发怪物手册,则关闭界面,因为怪物手册和退出默认使用同一个按键,需要特判 // 如果按键不能触发怪物手册,则关闭界面,因为怪物手册和退出默认使用同一个按键,需要特判
@ -137,10 +145,10 @@ export const ViewMap = defineComponent<ViewMapProps>(props => {
else tip.value?.drawTip(`无法飞往${core.floors[id].title}`); else tip.value?.drawTip(`无法飞往${core.floors[id].title}`);
}; };
// const resetCamera = () => { const resetCamera = () => {
// group.value?.camera.reset(); group.value?.camera.reset();
// group.value?.update(); group.value?.update();
// }; };
//#region 渐变渲染 //#region 渐变渲染
@ -187,33 +195,33 @@ export const ViewMap = defineComponent<ViewMapProps>(props => {
//#region 地图渲染 //#region 地图渲染
// const renderLayer = (floorId: FloorIds) => { const renderLayer = (floorId: FloorIds) => {
// const binder = group.value?.getExtends( const binder = group.value?.getExtends(
// 'floor-binder' 'floor-binder'
// ) as LayerGroupFloorBinder; ) as LayerGroupFloorBinder;
// binder.bindFloor(floorId); binder.bindFloor(floorId);
// group.value?.camera.reset(); group.value?.camera.reset();
// core.status.floorId = floorId; core.status.floorId = floorId;
// core.status.thisMap = core.status.maps[floorId]; core.status.thisMap = core.status.maps[floorId];
// statistics.value = calculateStatisticsOne(floorId); statistics.value = calculateStatisticsOne(floorId);
// }; };
// const moveCamera = (dx: number, dy: number) => { const moveCamera = (dx: number, dy: number) => {
// const camera = group.value?.camera; const camera = group.value?.camera;
// if (!camera) return; if (!camera) return;
// camera.translate(dx / camera.scaleX, dy / camera.scaleX); camera.translate(dx / camera.scaleX, dy / camera.scaleX);
// group.value?.update(); group.value?.update();
// }; };
// const scaleCamera = (scale: number, x: number, y: number) => { const scaleCamera = (scale: number, x: number, y: number) => {
// const camera = group.value?.camera; const camera = group.value?.camera;
// if (!camera) return; if (!camera) return;
// const [cx, cy] = camera.untransformed(x, y); const [cx, cy] = camera.untransformed(x, y);
// camera.translate(cx, cy); camera.translate(cx, cy);
// camera.scale(scale); camera.scale(scale);
// camera.translate(-cx, -cy); camera.translate(-cx, -cy);
// group.value?.update(); group.value?.update();
// }; };
//#region 事件监听 //#region 事件监听
@ -222,7 +230,7 @@ export const ViewMap = defineComponent<ViewMapProps>(props => {
if (ev.offsetX < col * 2) { if (ev.offsetX < col * 2) {
changeFloor(1); changeFloor(1);
} else { } else {
// resetCamera(); resetCamera();
} }
}; };
@ -285,7 +293,7 @@ export const ViewMap = defineComponent<ViewMapProps>(props => {
const dx = ev.offsetX - lastMoveX; const dx = ev.offsetX - lastMoveX;
const dy = ev.offsetY - lastMoveY; const dy = ev.offsetY - lastMoveY;
movement += Math.hypot(dx, dy); movement += Math.hypot(dx, dy);
// moveCamera(dx, dy); moveCamera(dx, dy);
} }
moved = true; moved = true;
lastMoveX = ev.offsetX; lastMoveX = ev.offsetX;
@ -314,7 +322,7 @@ export const ViewMap = defineComponent<ViewMapProps>(props => {
return; return;
} }
if (!isFinite(scale) || scale === 0) return; if (!isFinite(scale) || scale === 0) return;
// scaleCamera(scale, cx, cy); scaleCamera(scale, cx, cy);
} }
} else { } else {
if (mouseDown) { if (mouseDown) {
@ -333,8 +341,8 @@ export const ViewMap = defineComponent<ViewMapProps>(props => {
const wheelMap = (ev: IWheelEvent) => { const wheelMap = (ev: IWheelEvent) => {
if (ev.altKey) { if (ev.altKey) {
// const scale = ev.wheelY < 0 ? 1.1 : 0.9; const scale = ev.wheelY < 0 ? 1.1 : 0.9;
// scaleCamera(scale, ev.offsetX, ev.offsetY); scaleCamera(scale, ev.offsetX, ev.offsetY);
} else if (ev.ctrlKey) { } else if (ev.ctrlKey) {
changeFloor(-Math.sign(ev.wheelY) * 10); changeFloor(-Math.sign(ev.wheelY) * 10);
} else { } else {
@ -354,7 +362,7 @@ export const ViewMap = defineComponent<ViewMapProps>(props => {
}; };
onMounted(() => { onMounted(() => {
// renderLayer(floorId.value); renderLayer(floorId.value);
}); });
onUnmounted(() => { onUnmounted(() => {
@ -363,7 +371,7 @@ export const ViewMap = defineComponent<ViewMapProps>(props => {
}); });
watch(floorId, value => { watch(floorId, value => {
// renderLayer(value); renderLayer(value);
}); });
//#region 组件树 //#region 组件树
@ -386,7 +394,7 @@ export const ViewMap = defineComponent<ViewMapProps>(props => {
v-model:now={now.value} v-model:now={now.value}
onClose={close} onClose={close}
/> />
{/* <layer-group <layer-group
ref={group} ref={group}
ex={layerGroupExtends} ex={layerGroupExtends}
loc={[STATUS_BAR_WIDTH, 0, MAP_WIDTH, MAP_HEIGHT]} loc={[STATUS_BAR_WIDTH, 0, MAP_WIDTH, MAP_HEIGHT]}
@ -402,7 +410,7 @@ export const ViewMap = defineComponent<ViewMapProps>(props => {
<layer layer="event" zIndex={30}></layer> <layer layer="event" zIndex={30}></layer>
<layer layer="fg" zIndex={40}></layer> <layer layer="fg" zIndex={40}></layer>
<layer layer="fg2" zIndex={50}></layer> <layer layer="fg2" zIndex={50}></layer>
</layer-group> */} </layer-group>
<Tip <Tip
ref={tip} ref={tip}
zIndex={40} zIndex={40}
@ -410,7 +418,7 @@ export const ViewMap = defineComponent<ViewMapProps>(props => {
pad={[12, 6]} pad={[12, 6]}
corner={16} corner={16}
/> />
<custom <sprite
loc={[STATUS_BAR_WIDTH, 0, MAP_WIDTH, 64]} loc={[STATUS_BAR_WIDTH, 0, MAP_WIDTH, 64]}
render={renderTop} render={renderTop}
alpha={topAlpha.value} alpha={topAlpha.value}
@ -420,7 +428,7 @@ export const ViewMap = defineComponent<ViewMapProps>(props => {
onLeave={leaveTop} onLeave={leaveTop}
onClick={clickTop} onClick={clickTop}
/> />
<custom <sprite
loc={[STATUS_BAR_WIDTH, MAP_HEIGHT - 64, MAP_WIDTH, 64]} loc={[STATUS_BAR_WIDTH, MAP_HEIGHT - 64, MAP_WIDTH, 64]}
render={renderBottom} render={renderBottom}
alpha={bottomAlpha.value} alpha={bottomAlpha.value}
@ -546,7 +554,7 @@ export const ViewMap = defineComponent<ViewMapProps>(props => {
loc={loc3} loc={loc3}
anc={[0.5, 0.5]} anc={[0.5, 0.5]}
cursor="pointer" cursor="pointer"
// onClick={resetCamera} onClick={resetCamera}
/> />
</container> </container>
</container> </container>

View File

@ -1,15 +1,6 @@
import { import { Hotkey, gameKey } from '@motajs/system-action';
ExcitationCurve,
excited,
IAnimatable,
IExcitableController,
ITransition,
Transition
} from '@motajs/animate';
import { logger } from '@motajs/common';
import { IRenderItem, IRenderTreeRoot } from '@motajs/render';
import { Hotkey, gameKey } from '@motajs/system';
import { loading } from '@user/data-base'; import { loading } from '@user/data-base';
import { TimingFn, Transition } from 'mutate-animate';
import { import {
ComponentInternalInstance, ComponentInternalInstance,
getCurrentInstance, getCurrentInstance,
@ -116,7 +107,7 @@ export interface ITransitionedController<T> {
* 线 * 线
* @param timing 线 * @param timing 线
*/ */
mode(timing: ExcitationCurve): void; mode(timing: TimingFn): void;
/** /**
* *
@ -126,29 +117,37 @@ export interface ITransitionedController<T> {
} }
class RenderTransition implements ITransitionedController<number> { class RenderTransition implements ITransitionedController<number> {
private static key: number = 0;
private readonly key: string = `$${RenderTransition.key++}`;
public readonly ref: Ref<number>; public readonly ref: Ref<number>;
set value(v: number) { set value(v: number) {
this.set(v); this.transition.transition(this.key, v);
} }
get value() { get value() {
return this.ref.value; return this.transition.value[this.key];
} }
constructor( constructor(
value: number, value: number,
public readonly transition: ITransition, public readonly transition: Transition,
public time: number, public time: number,
public curve: ExcitationCurve public curve: TimingFn
) { ) {
this.ref = ref(value); this.ref = ref(value);
transition.value[this.key] = value;
transition.ticker.add(() => {
this.ref.value = transition.value[this.key];
});
} }
set(value: number, time: number = this.time): void { set(value: number, time: number = this.time): void {
this.transition.curve(this.curve).transition(this.ref).to(value, time); this.transition.time(time).mode(this.curve).transition(this.key, value);
} }
mode(timing: ExcitationCurve): void { mode(timing: TimingFn): void {
this.curve = timing; this.curve = timing;
} }
@ -167,13 +166,6 @@ class RenderColorTransition implements ITransitionedController<string> {
private readonly keyB: string = `$colorB${RenderColorTransition.key++}`; private readonly keyB: string = `$colorB${RenderColorTransition.key++}`;
private readonly keyA: string = `$colorA${RenderColorTransition.key++}`; private readonly keyA: string = `$colorA${RenderColorTransition.key++}`;
private readonly rValue: IAnimatable;
private readonly gValue: IAnimatable;
private readonly bValue: IAnimatable;
private readonly aValue: IAnimatable;
private readonly controller: IExcitableController<number> | null = null;
public readonly ref: Ref<string>; public readonly ref: Ref<string>;
set value(v: string) { set value(v: string) {
@ -185,32 +177,26 @@ class RenderColorTransition implements ITransitionedController<string> {
constructor( constructor(
value: string, value: string,
public readonly transition: ITransition, public readonly transition: Transition,
public time: number, public time: number,
public curve: ExcitationCurve public curve: TimingFn
) { ) {
this.ref = ref(value); this.ref = ref(value);
const [r, g, b, a] = this.decodeColor(value); const [r, g, b, a] = this.decodeColor(value);
this.rValue = { value: r }; transition.value[this.keyR] = r;
this.gValue = { value: g }; transition.value[this.keyG] = g;
this.bValue = { value: b }; transition.value[this.keyB] = b;
this.aValue = { value: a }; transition.value[this.keyA] = a;
if (!transition.excitation) { transition.ticker.add(() => {
logger.warn(94, 'transitionedColor');
} else {
this.controller = transition.excitation.add(
excited(() => {
this.ref.value = this.encodeColor(); this.ref.value = this.encodeColor();
}) });
);
}
} }
set(value: string, time: number = this.time): void { set(value: string, time: number = this.time): void {
this.transitionColor(this.decodeColor(value), time); this.transitionColor(this.decodeColor(value), time);
} }
mode(timing: ExcitationCurve): void { mode(timing: TimingFn): void {
this.curve = timing; this.curve = timing;
} }
@ -220,15 +206,12 @@ class RenderColorTransition implements ITransitionedController<string> {
private transitionColor([r, g, b, a]: ColorRGBA, time: number) { private transitionColor([r, g, b, a]: ColorRGBA, time: number) {
this.transition this.transition
.curve(this.curve) .mode(this.curve)
.transition(this.rValue) .time(time)
.to(r, time) .transition(this.keyR, r)
.transition(this.gValue) .transition(this.keyG, g)
.to(g, time) .transition(this.keyB, b)
.transition(this.bValue) .transition(this.keyA, a);
.to(b, time)
.transition(this.aValue)
.to(a, time);
} }
private decodeColor(color: string): ColorRGBA { private decodeColor(color: string): ColorRGBA {
@ -289,37 +272,31 @@ class RenderColorTransition implements ITransitionedController<string> {
} }
private encodeColor() { private encodeColor() {
const r = this.rValue.value; const r = this.transition.value[this.keyR];
const g = this.gValue.value; const g = this.transition.value[this.keyG];
const b = this.bValue.value; const b = this.transition.value[this.keyB];
const a = this.aValue.value; const a = this.transition.value[this.keyA];
return `rgba(${r},${g},${b},${a})`; return `rgba(${r},${g},${b},${a})`;
} }
} }
const transitionMap = new Map<ComponentInternalInstance, ITransition>(); const transitionMap = new Map<ComponentInternalInstance, Transition>();
function checkTransition() { function checkTransition() {
const instance = getCurrentInstance(); const instance = getCurrentInstance();
if (!instance) return null; if (!instance) return null;
const root = instance.root;
if (!root) return null;
const el = root.vnode.el as IRenderItem;
const renderer = el.parent as IRenderTreeRoot;
if (!renderer) return null;
if (instance.isUnmounted) { if (instance.isUnmounted) {
const tran = transitionMap.get(instance); const tran = transitionMap.get(instance);
tran?.destroy(); tran?.ticker.destroy();
transitionMap.delete(instance); transitionMap.delete(instance);
return null; return null;
} }
if (!transitionMap.has(instance)) { if (!transitionMap.has(instance)) {
const tran = new Transition(); const tran = new Transition();
tran.bindExcitation(renderer.excitation);
transitionMap.set(instance, tran); transitionMap.set(instance, tran);
onUnmounted(() => { onUnmounted(() => {
transitionMap.delete(instance); transitionMap.delete(instance);
tran.destroy(); tran.ticker.destroy();
}); });
} }
const tran = transitionMap.get(instance); const tran = transitionMap.get(instance);
@ -343,7 +320,7 @@ function checkTransition() {
export function transitioned( export function transitioned(
value: number, value: number,
time: number, time: number,
curve: ExcitationCurve curve: TimingFn
): ITransitionedController<number> | null { ): ITransitionedController<number> | null {
const tran = checkTransition(); const tran = checkTransition();
if (!tran) return null; if (!tran) return null;
@ -367,7 +344,7 @@ export function transitioned(
export function transitionedColor( export function transitionedColor(
color: string, color: string,
time: number, time: number,
curve: ExcitationCurve curve: TimingFn
): ITransitionedController<string> | null { ): ITransitionedController<string> | null {
const tran = checkTransition(); const tran = checkTransition();
if (!tran) return null; if (!tran) return null;

View File

@ -1,4 +1,4 @@
import { ElementLocator } from '@motajs/render'; import { ElementLocator } from '@motajs/render-core';
export interface IGridLayoutData { export interface IGridLayoutData {
/** 有多少列 */ /** 有多少列 */

View File

@ -1,6 +1,6 @@
import { compressToBase64, decompressFromBase64 } from 'lz-string'; import { compressToBase64, decompressFromBase64 } from 'lz-string';
import { getConfirm, waitbox } from '../components'; import { getConfirm, waitbox } from '../components';
import { IUIMountable } from '@motajs/system'; import { IUIMountable } from '@motajs/system-ui';
import { SyncSaveFromServerResponse } from '@motajs/client-base'; import { SyncSaveFromServerResponse } from '@motajs/client-base';
import { CENTER_LOC, POP_BOX_WIDTH } from '../shared'; import { CENTER_LOC, POP_BOX_WIDTH } from '../shared';

View File

@ -1,9 +1,8 @@
import { onUnmounted } from 'vue'; import { onUnmounted } from 'vue';
import { WeatherController } from '../weather'; import { WeatherController } from '../weather';
import { IRenderTreeRoot } from '@motajs/render';
export function useWeather(renderer: IRenderTreeRoot): [WeatherController] { export function useWeather(): [WeatherController] {
const weather = new WeatherController(renderer); const weather = new WeatherController();
onUnmounted(() => { onUnmounted(() => {
weather.destroy(); weather.destroy();

View File

@ -1,21 +1,19 @@
import { IRenderTreeRoot, RenderItem } from '@motajs/render'; import { RenderItem } from '@motajs/render-core';
import { IWeather, IWeatherController, IWeatherInstance } from './types'; import { IWeather, IWeatherController, IWeatherInstance } from './types';
import { logger } from '@motajs/common'; import { logger } from '@motajs/common';
import { isNil } from 'lodash-es'; import { isNil } from 'lodash-es';
import { IExcitable } from '@motajs/animate'; import { Ticker } from 'mutate-animate';
type WeatherConstructor = new () => IWeather; type WeatherConstructor = new () => IWeather;
// todo: refactor? export class WeatherController implements IWeatherController {
export class WeatherController
implements IWeatherController, IExcitable<number>
{
/** 暴露到全局的控制器 */ /** 暴露到全局的控制器 */
static extern: Map<string, IWeatherController> = new Map(); static extern: Map<string, IWeatherController> = new Map();
/** 注册的天气 */ /** 注册的天气 */
static weathers: Map<string, WeatherConstructor> = new Map(); static weathers: Map<string, WeatherConstructor> = new Map();
private static ticker: Ticker = new Ticker();
/** 暴露至全局的 id */ /** 暴露至全局的 id */
private externId?: string; private externId?: string;
/** 天气元素纵深 */ /** 天气元素纵深 */
@ -25,13 +23,13 @@ export class WeatherController
container: RenderItem | null = null; container: RenderItem | null = null;
constructor(readonly renderer: IRenderTreeRoot) { constructor() {
renderer.delegateExcitable(this); WeatherController.ticker.add(this.tick);
} }
excited(payload: number): void { private tick = (time: number) => {
this.active.forEach(v => v.weather.tick(payload)); this.active.forEach(v => v.weather.tick(time));
} };
/** /**
* `zIndex` `zIndex+1` `zIndex+2` ... * `zIndex` `zIndex+1` `zIndex+2` ...
@ -113,6 +111,7 @@ export class WeatherController
destroy() { destroy() {
this.clearWeather(); this.clearWeather();
WeatherController.ticker.remove(this.tick);
if (!isNil(this.externId)) { if (!isNil(this.externId)) {
WeatherController.extern.delete(this.externId); WeatherController.extern.delete(this.externId);
} }
@ -139,7 +138,8 @@ export class WeatherController
export class WeatherInstance< export class WeatherInstance<
R extends RenderItem = RenderItem, R extends RenderItem = RenderItem,
T extends IWeather<R> = IWeather<R> T extends IWeather<R> = IWeather<R>
> implements IWeatherInstance<R, T> { > implements IWeatherInstance<R, T>
{
constructor( constructor(
readonly weather: T, readonly weather: T,
readonly element: R readonly element: R

View File

@ -1,5 +1,5 @@
import { CloudLike } from './cloudLike'; import { CloudLike } from './cloudLike';
import { SizedCanvasImageSource } from '@motajs/render'; 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,11 +1,8 @@
import { import { MotaOffscreenCanvas2D, Sprite } from '@motajs/render-core';
MotaOffscreenCanvas2D,
CustomRenderItem,
SizedCanvasImageSource
} from '@motajs/render';
import { Weather } from '../weather'; import { Weather } from '../weather';
import { SizedCanvasImageSource } from '@motajs/render-assets';
export abstract class CloudLike extends Weather<CustomRenderItem> { export abstract class CloudLike extends Weather<Sprite> {
/** 不透明度 */ /** 不透明度 */
private alpha: number = 0; private alpha: number = 0;
/** 水平速度 */ /** 水平速度 */
@ -78,8 +75,8 @@ export abstract class CloudLike extends Weather<CustomRenderItem> {
this.cy %= this.image.height; this.cy %= this.image.height;
} }
createElement(level: number): CustomRenderItem { createElement(level: number): Sprite {
const element = new CustomRenderItem(true); const element = new Sprite('static', true);
element.setRenderFn(canvas => this.drawImage(canvas)); element.setRenderFn(canvas => this.drawImage(canvas));
this.maxSpeed = Math.sqrt(level) * 100; this.maxSpeed = Math.sqrt(level) * 100;
this.vx = ((Math.random() - 0.5) * this.maxSpeed) / 2; this.vx = ((Math.random() - 0.5) * this.maxSpeed) / 2;

View File

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

View File

@ -1,10 +1,10 @@
import { MotaOffscreenCanvas2D, CustomRenderItem } from '@motajs/render'; import { MotaOffscreenCanvas2D, Sprite } from '@motajs/render-core';
import { Weather } from '../weather'; import { Weather } from '../weather';
import { clamp } from 'lodash-es'; import { clamp } from 'lodash-es';
export class SunWeather extends Weather<CustomRenderItem> { export class SunWeather extends Weather<Sprite> {
/** 阳光图片 */ /** 阳光图片 */
private image: ImageBitmap | null = null; private image: HTMLImageElement | null = null;
/** 阳光图片的不透明度 */ /** 阳光图片的不透明度 */
private alpha: number = 0; private alpha: number = 0;
/** 阳光的最大不透明度 */ /** 阳光的最大不透明度 */
@ -41,8 +41,8 @@ export class SunWeather extends Weather<CustomRenderItem> {
} }
} }
createElement(level: number): CustomRenderItem { createElement(level: number): Sprite {
const element = new CustomRenderItem(true); const element = new Sprite('static', true);
element.setRenderFn(canvas => this.drawSun(canvas)); element.setRenderFn(canvas => this.drawSun(canvas));
this.maxAlpha = level / 10; this.maxAlpha = level / 10;
this.minAlpha = level / 20; this.minAlpha = level / 20;

View File

@ -1,4 +1,4 @@
import { RenderItem } from '@motajs/render'; import { RenderItem } from '@motajs/render-core';
export interface IWeather<T extends RenderItem = RenderItem> { export interface IWeather<T extends RenderItem = RenderItem> {
/** 天气的等级,-1 表示未创建 */ /** 天气的等级,-1 表示未创建 */

View File

@ -1,4 +1,4 @@
import { RenderItem } from '@motajs/render'; import { RenderItem } from '@motajs/render-core';
import { IWeather } from './types'; import { IWeather } from './types';
export abstract class Weather<T extends RenderItem> implements IWeather<T> { export abstract class Weather<T extends RenderItem> implements IWeather<T> {

View File

@ -1,7 +1,6 @@
{ {
"name": "@user/data-base", "name": "@user/data-base",
"dependencies": { "dependencies": {
"@motajs/types": "workspace:*", "@motajs/types": "workspace:*"
"@motajs/loader": "workspace:*"
} }
} }

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