mirror of
https://github.com/motajs/template.git
synced 2026-04-15 00:51:11 +08:00
Compare commits
14 Commits
93fb788bef
...
fe67939a05
| Author | SHA1 | Date | |
|---|---|---|---|
| fe67939a05 | |||
| 4fc6db3e79 | |||
| fdeee99d40 | |||
| ee54148558 | |||
| 191ba8d1db | |||
| 9ab0db9465 | |||
| f6a065fa3e | |||
| 555cf96d76 | |||
| feb6abb5bb | |||
| 5d6f40a469 | |||
| 5550fac160 | |||
| 5a1a0fee42 | |||
| 03cf6a8917 | |||
| 744a21e042 |
@ -1,59 +1,64 @@
|
|||||||
import fs from 'fs-extra';
|
import { basename, join, resolve } from 'node:path';
|
||||||
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 = path.resolve('./docs/api');
|
const apiDir = resolve('./docs/api');
|
||||||
const sidebarConfigPath = path.resolve('./docs/.vitepress/apiSidebar.ts');
|
const sidebarConfigPath = resolve('./docs/.vitepress/apiSidebar.ts');
|
||||||
|
|
||||||
const weight: Record<string, number> = {
|
const weight: Record<string, number> = {
|
||||||
主页: 10,
|
主页: 10,
|
||||||
函数: 5
|
函数: 5
|
||||||
};
|
};
|
||||||
|
|
||||||
function generateSidebar(): void {
|
export async function generateSidebar(): Promise<void> {
|
||||||
const sidebar: DefaultTheme.SidebarItem[] = [
|
const sidebar: DefaultTheme.SidebarItem[] = [
|
||||||
{ text: '目录', link: '/api/' }
|
{ text: '目录', link: '/api/' }
|
||||||
];
|
];
|
||||||
|
|
||||||
// 遍历 api 目录,查找 package 目录
|
// 遍历 api 目录,查找 package 目录
|
||||||
const packages = fs
|
const dir = await readdir(apiDir);
|
||||||
.readdirSync(apiDir)
|
const packages = [];
|
||||||
.filter(pkg => fs.statSync(path.join(apiDir, pkg)).isDirectory());
|
for (const pkg of dir) {
|
||||||
|
const stats = await stat(join(apiDir, pkg));
|
||||||
|
if (stats.isDirectory()) {
|
||||||
|
packages.push(pkg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
packages.forEach(pkg => {
|
await Promise.all(
|
||||||
const pkgPath = path.join(apiDir, pkg);
|
packages.map(async pkg => {
|
||||||
const files = fs
|
const pkgPath = join(apiDir, pkg);
|
||||||
.readdirSync(pkgPath)
|
const dir = await readdir(pkgPath);
|
||||||
.filter(file => file.endsWith('.md'));
|
const files = dir.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 = path.basename(file, '.md');
|
const fileName = basename(file, '.md');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
text:
|
text:
|
||||||
fileName === 'index'
|
fileName === 'index'
|
||||||
? '主页'
|
? '主页'
|
||||||
: fileName === 'functions'
|
: fileName === 'functions'
|
||||||
? '函数'
|
? '函数'
|
||||||
: fileName,
|
: fileName,
|
||||||
link: `/${filePath.replace(/\\/g, '/')}` // 兼容 Windows 路径
|
link: `/${filePath.replace(/\\/g, '/')}` // 兼容 Windows 路径
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
items.sort((a, b) => {
|
items.sort((a, b) => {
|
||||||
const titleA = a.text ?? '';
|
const titleA = a.text ?? '';
|
||||||
const titleB = b.text ?? '';
|
const titleB = b.text ?? '';
|
||||||
return (weight[titleB] ?? 0) - (weight[titleA] ?? 0);
|
return (weight[titleB] ?? 0) - (weight[titleA] ?? 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
sidebar.push({
|
sidebar.push({
|
||||||
text: pkg,
|
text: pkg,
|
||||||
collapsed: true,
|
collapsed: true,
|
||||||
items
|
items
|
||||||
});
|
});
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// 生成 sidebar.ts
|
// 生成 sidebar.ts
|
||||||
const sidebarContent = `import { DefaultTheme } from 'vitepress';
|
const sidebarContent = `import { DefaultTheme } from 'vitepress';
|
||||||
@ -63,35 +68,6 @@ export default ${JSON.stringify(
|
|||||||
null,
|
null,
|
||||||
4
|
4
|
||||||
)} as DefaultTheme.SidebarItem[];`;
|
)} as DefaultTheme.SidebarItem[];`;
|
||||||
fs.writeFileSync(sidebarConfigPath, sidebarContent);
|
await writeFile(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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|||||||
@ -1,7 +1,39 @@
|
|||||||
import { defineConfig } from 'vitepress';
|
import { defineConfig, Plugin } 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({
|
||||||
@ -157,7 +189,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
vite: {
|
vite: {
|
||||||
// @ts-expect-error 类型错误
|
// @ts-expect-error 类型错误
|
||||||
plugins: [MermaidPlugin()],
|
plugins: [MermaidPlugin(), listenSidebar()],
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
include: ['mermaid']
|
include: ['mermaid']
|
||||||
},
|
},
|
||||||
|
|||||||
5
docs/.vitepress/init.ts
Normal file
5
docs/.vitepress/init.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { generateSidebar } from './api';
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
generateSidebar();
|
||||||
|
})();
|
||||||
82
package.json
82
package.json
@ -5,6 +5,7 @@
|
|||||||
"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",
|
||||||
@ -12,84 +13,83 @@
|
|||||||
"build:packages": "vue-tsc --noEmit && tsx script/build-packages.ts",
|
"build:packages": "vue-tsc --noEmit && tsx script/build-packages.ts",
|
||||||
"build:game": "tsx script/declare.ts && vue-tsc --noEmit && tsx script/build-game.ts",
|
"build:game": "tsx script/declare.ts && vue-tsc --noEmit && tsx script/build-game.ts",
|
||||||
"build:lib": "vue-tsc --noEmit && tsx script/build-lib.ts",
|
"build:lib": "vue-tsc --noEmit && tsx script/build-lib.ts",
|
||||||
"docs:dev": "concurrently -k -n SIDEBAR,VITEPRESS -c blue,green \"tsx docs/.vitepress/api.ts\" \"vitepress dev docs\"",
|
"docs:dev": "tsx docs/.vitepress/init.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.16",
|
"@wasm-audio-decoders/ogg-vorbis": "^0.1.20",
|
||||||
"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.8.4",
|
"axios": "^1.13.6",
|
||||||
"chart.js": "^4.4.8",
|
"chart.js": "^4.5.1",
|
||||||
"codec-parser": "^2.5.0",
|
"codec-parser": "^2.5.0",
|
||||||
"eventemitter3": "^5.0.1",
|
"eventemitter3": "^5.0.4",
|
||||||
"gl-matrix": "^3.4.3",
|
"gl-matrix": "^3.4.4",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.23",
|
||||||
"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.6.14",
|
"ogg-opus-decoder": "^1.7.3",
|
||||||
"opus-decoder": "^0.7.7",
|
"opus-decoder": "^0.7.11",
|
||||||
"vue": "^3.5.20"
|
"vue": "^3.5.29"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.26.4",
|
"@babel/cli": "^7.28.6",
|
||||||
"@babel/core": "^7.26.10",
|
"@babel/core": "^7.29.0",
|
||||||
"@babel/preset-env": "^7.26.9",
|
"@babel/preset-env": "^7.29.0",
|
||||||
"@eslint/js": "^9.24.0",
|
"@eslint/js": "^9.39.4",
|
||||||
"@rollup/plugin-babel": "^6.0.4",
|
"@rollup/plugin-babel": "^6.1.0",
|
||||||
"@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.3",
|
"@types/archiver": "^6.0.4",
|
||||||
"@types/babel__core": "^7.20.5",
|
"@types/babel__core": "^7.20.5",
|
||||||
"@types/express": "^5.0.3",
|
"@types/express": "^5.0.6",
|
||||||
"@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.18.0",
|
"@types/node": "^22.19.15",
|
||||||
"@types/ws": "^8.18.0",
|
"@types/ws": "^8.18.1",
|
||||||
"@vitejs/plugin-legacy": "^7.2.1",
|
"@vitejs/plugin-legacy": "^7.2.1",
|
||||||
"@vitejs/plugin-vue": "^6.0.1",
|
"@vitejs/plugin-vue": "^6.0.4",
|
||||||
"@vitejs/plugin-vue-jsx": "^5.1.1",
|
"@vitejs/plugin-vue-jsx": "^5.1.4",
|
||||||
"archiver": "^7.0.1",
|
"archiver": "^7.0.1",
|
||||||
"chokidar": "^3.6.0",
|
"chokidar": "^3.6.0",
|
||||||
"compressing": "^1.10.1",
|
"compressing": "^1.10.4",
|
||||||
"concurrently": "^9.1.2",
|
"eslint": "^9.39.4",
|
||||||
"eslint": "^9.22.0",
|
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-prettier": "^5.5.4",
|
"eslint-plugin-prettier": "^5.5.5",
|
||||||
"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.1.0",
|
"express": "^5.2.1",
|
||||||
"fontmin": "^2.0.3",
|
"fontmin": "^2.0.3",
|
||||||
"form-data": "^4.0.2",
|
"fs-extra": "^11.3.4",
|
||||||
"fs-extra": "^11.3.1",
|
"glob": "^11.1.0",
|
||||||
"glob": "^11.0.1",
|
|
||||||
"globals": "^15.15.0",
|
"globals": "^15.15.0",
|
||||||
"less": "^4.2.2",
|
"less": "^4.5.1",
|
||||||
"madge": "^8.0.0",
|
"madge": "^8.0.0",
|
||||||
"markdown-it-mathjax3": "^4.3.2",
|
"markdown-it-mathjax3": "^4.3.2",
|
||||||
"mermaid": "^11.5.0",
|
"mermaid": "^11.12.3",
|
||||||
"postcss-preset-env": "^9.6.0",
|
"postcss-preset-env": "^9.6.0",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.8.1",
|
||||||
"rollup": "^4.49.0",
|
"rollup": "^4.59.0",
|
||||||
"terser": "^5.39.0",
|
"terser": "^5.46.0",
|
||||||
"tsx": "^4.20.5",
|
"tsx": "^4.21.0",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "6.0.1-rc",
|
||||||
"typescript-eslint": "^8.27.0",
|
"typescript-eslint": "^8.57.0",
|
||||||
"vite": "^7.0.0",
|
"vite": "^7.3.1",
|
||||||
"vite-plugin-dts": "^4.5.4",
|
"vite-plugin-dts": "^4.5.4",
|
||||||
"vitepress": "^1.6.3",
|
"vitepress": "^1.6.4",
|
||||||
"vitepress-plugin-mermaid": "^2.0.17",
|
"vitepress-plugin-mermaid": "^2.0.17",
|
||||||
"vue-tsc": "^2.2.8",
|
"vitest": "^4.0.18",
|
||||||
"ws": "^8.18.1"
|
"vue-tsc": "^2.2.12",
|
||||||
|
"ws": "^8.19.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "@user/client-base",
|
"name": "@user/client-base",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@motajs/render-assets": "workspace:*",
|
"@motajs/audio": "workspace:*",
|
||||||
"@motajs/client-base": "workspace:*"
|
"@motajs/render": "workspace:*",
|
||||||
|
"@motajs/client-base": "workspace:*",
|
||||||
|
"@user/data-base": "workspace:*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,16 @@
|
|||||||
import { createMaterial } from './material';
|
import { loading } from '@user/data-base';
|
||||||
|
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';
|
||||||
|
|||||||
47
packages-user/client-base/src/ins.ts
Normal file
47
packages-user/client-base/src/ins.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
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
|
||||||
37
packages-user/client-base/src/load/data.ts
Normal file
37
packages-user/client-base/src/load/data.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
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'
|
||||||
|
];
|
||||||
1
packages-user/client-base/src/load/index.ts
Normal file
1
packages-user/client-base/src/load/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './loader';
|
||||||
505
packages-user/client-base/src/load/loader.ts
Normal file
505
packages-user/client-base/src/load/loader.ts
Normal file
@ -0,0 +1,505 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
84
packages-user/client-base/src/load/types.ts
Normal file
84
packages-user/client-base/src/load/types.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
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>;
|
||||||
|
}
|
||||||
@ -2,7 +2,7 @@ import {
|
|||||||
IRect,
|
IRect,
|
||||||
ITextureRenderable,
|
ITextureRenderable,
|
||||||
SizedCanvasImageSource
|
SizedCanvasImageSource
|
||||||
} from '@motajs/render-assets';
|
} from '@motajs/render';
|
||||||
import {
|
import {
|
||||||
AutotileConnection,
|
AutotileConnection,
|
||||||
AutotileType,
|
AutotileType,
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import {
|
|||||||
ITextureStreamComposer,
|
ITextureStreamComposer,
|
||||||
TextureMaxRectsStreamComposer,
|
TextureMaxRectsStreamComposer,
|
||||||
SizedCanvasImageSource
|
SizedCanvasImageSource
|
||||||
} from '@motajs/render-assets';
|
} from '@motajs/render';
|
||||||
import { IAssetBuilder, IMaterialGetter, ITrackedAssetData } from './types';
|
import { IAssetBuilder, IMaterialGetter, ITrackedAssetData } from './types';
|
||||||
import { logger, PrivateListDirtyTracker } from '@motajs/common';
|
import { logger, PrivateListDirtyTracker } from '@motajs/common';
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
import { ITexture } from '@motajs/render-assets';
|
import { ITexture } from '@motajs/render';
|
||||||
import { materials } from './ins';
|
import {
|
||||||
import { IBlockIdentifier, IIndexedIdentifier } from './types';
|
IBlockIdentifier,
|
||||||
|
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'>>(
|
||||||
@ -47,7 +50,7 @@ function addAutotile(set: Set<number>, map?: readonly (readonly number[])[]) {
|
|||||||
/**
|
/**
|
||||||
* 兼容旧版加载
|
* 兼容旧版加载
|
||||||
*/
|
*/
|
||||||
export function fallbackLoad() {
|
export function fallbackLoad(materials: IMaterialManager) {
|
||||||
// 基本素材
|
// 基本素材
|
||||||
const icons = core.icons.icons;
|
const icons = core.icons.icons;
|
||||||
const images = core.material.images;
|
const images = core.material.images;
|
||||||
@ -102,12 +105,6 @@ export function fallbackLoad() {
|
|||||||
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>();
|
||||||
|
|||||||
@ -1,19 +1,12 @@
|
|||||||
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';
|
||||||
|
|||||||
@ -1,5 +0,0 @@
|
|||||||
import { AutotileProcessor } from './autotile';
|
|
||||||
import { MaterialManager } from './manager';
|
|
||||||
|
|
||||||
export const materials = new MaterialManager();
|
|
||||||
export const autotile = new AutotileProcessor(materials);
|
|
||||||
@ -9,7 +9,7 @@ import {
|
|||||||
TextureGridSplitter,
|
TextureGridSplitter,
|
||||||
TextureRowSplitter,
|
TextureRowSplitter,
|
||||||
TextureStore
|
TextureStore
|
||||||
} from '@motajs/render-assets';
|
} from '@motajs/render';
|
||||||
import {
|
import {
|
||||||
IBlockIdentifier,
|
IBlockIdentifier,
|
||||||
IMaterialData,
|
IMaterialData,
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import {
|
|||||||
ITextureRenderable,
|
ITextureRenderable,
|
||||||
ITextureStore,
|
ITextureStore,
|
||||||
SizedCanvasImageSource
|
SizedCanvasImageSource
|
||||||
} from '@motajs/render-assets';
|
} from '@motajs/render';
|
||||||
|
|
||||||
export const enum BlockCls {
|
export const enum BlockCls {
|
||||||
Unknown,
|
Unknown,
|
||||||
@ -102,8 +102,7 @@ export interface IMaterialFramedData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IMaterialAsset
|
export interface IMaterialAsset
|
||||||
extends IDirtyTracker<boolean>,
|
extends IDirtyTracker<boolean>, IDirtyMarker<void> {
|
||||||
IDirtyMarker<void> {
|
|
||||||
/** 图集的贴图数据 */
|
/** 图集的贴图数据 */
|
||||||
readonly data: ITextureComposedData;
|
readonly data: ITextureComposedData;
|
||||||
}
|
}
|
||||||
@ -290,8 +289,7 @@ export interface IMaterialAliasGetter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IMaterialManager
|
export interface IMaterialManager
|
||||||
extends IMaterialGetter,
|
extends IMaterialGetter, IMaterialAliasGetter {
|
||||||
IMaterialAliasGetter {
|
|
||||||
/** 贴图存储,把 terrains 等内容单独分开存储 */
|
/** 贴图存储,把 terrains 等内容单独分开存储 */
|
||||||
readonly tileStore: ITextureStore;
|
readonly tileStore: ITextureStore;
|
||||||
/** tilesets 贴图存储,每个 tileset 是一个贴图对象 */
|
/** tilesets 贴图存储,每个 tileset 是一个贴图对象 */
|
||||||
@ -331,7 +329,6 @@ 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>;
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { ITexture } from '@motajs/render-assets';
|
import { ITexture } from '@motajs/render';
|
||||||
import { BlockCls } from './types';
|
import { BlockCls } from './types';
|
||||||
|
|
||||||
export function getClsByString(cls: Cls): BlockCls {
|
export function getClsByString(cls: Cls): BlockCls {
|
||||||
|
|||||||
@ -1,16 +1,15 @@
|
|||||||
{
|
{
|
||||||
"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-assets": "workspace:*",
|
"@motajs/render-vue": "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-action": "workspace:*",
|
"@motajs/system": "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:*"
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { KeyCode } from '@motajs/client-base';
|
import { KeyCode } from '@motajs/client-base';
|
||||||
import { gameKey, HotkeyJSON } from '@motajs/system-action';
|
import { gameKey, HotkeyJSON } from '@motajs/system';
|
||||||
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');
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { KeyCode } from '@motajs/client-base';
|
import { KeyCode } from '@motajs/client-base';
|
||||||
import { Hotkey, HotkeyData } from '@motajs/system-action';
|
import { Hotkey, HotkeyData } from '@motajs/system';
|
||||||
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';
|
||||||
|
|||||||
@ -1,268 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -1,203 +0,0 @@
|
|||||||
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>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
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';
|
|
||||||
@ -1,605 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { Patch, PatchClass } from '@motajs/legacy-common';
|
import { Patch, PatchClass } from '@motajs/legacy-common';
|
||||||
import { audioPlayer, bgmController, soundPlayer } from '../audio';
|
import { audioContext, bgmPlayer, soundPlayer } from '@user/client-base';
|
||||||
import { mainSetting } from '@motajs/legacy-ui';
|
import { mainSetting } from '@motajs/legacy-ui';
|
||||||
import { sleep } from 'mutate-animate';
|
import { sleep } from '@motajs/common';
|
||||||
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) => {
|
||||||
bgmController.play(bgm, when);
|
bgmPlayer.play(bgm, when);
|
||||||
};
|
};
|
||||||
const pause = () => {
|
const pause = () => {
|
||||||
bgmController.pause();
|
bgmPlayer.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 () {
|
||||||
bgmController.resume();
|
bgmPlayer.resume();
|
||||||
});
|
});
|
||||||
patch.add('checkBgm', function () {
|
patch.add('checkBgm', function () {
|
||||||
if (bgmController.playing) return;
|
if (bgmPlayer.playing) return;
|
||||||
if (mainSetting.getValue('audio.bgmEnabled')) {
|
if (mainSetting.getValue('audio.bgmEnabled')) {
|
||||||
if (bgmController.playingBgm) {
|
if (bgmPlayer.playingBgm) {
|
||||||
bgmController.play(bgmController.playingBgm);
|
bgmPlayer.play(bgmPlayer.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 (bgmController.playing) bgmController.pause();
|
if (bgmPlayer.playing) bgmPlayer.pause();
|
||||||
else bgmController.resume();
|
else bgmPlayer.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 = audioPlayer.getRoute(`sounds.${num}`);
|
const route = audioContext.getRoute(`sounds.${num}`);
|
||||||
if (!route) {
|
if (!route) {
|
||||||
callback?.();
|
callback?.();
|
||||||
return -1;
|
return -1;
|
||||||
|
|||||||
@ -1,11 +1,9 @@
|
|||||||
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();
|
||||||
@ -13,7 +11,5 @@ 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';
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
export * from './stream';
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { gameKey } from '@motajs/system-action';
|
import { gameKey } from '@motajs/system';
|
||||||
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-core';
|
import { ElementLocator } from '@motajs/render';
|
||||||
|
|
||||||
export function createAction() {
|
export function createAction() {
|
||||||
gameKey
|
gameKey
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
import { DefaultProps, ElementLocator, Font } from '@motajs/render';
|
import { 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-ui';
|
import { GameUI, IUIMountable, SetupComponentOptions } from '@motajs/system';
|
||||||
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 {
|
||||||
/** 确认框的提示文本内容 */
|
/** 确认框的提示文本内容 */
|
||||||
@ -138,11 +139,11 @@ export const ConfirmBox = defineComponent<
|
|||||||
height.value = textHeight + pad.value * 4;
|
height.value = textHeight + pad.value * 4;
|
||||||
};
|
};
|
||||||
|
|
||||||
const setYes = (_: string, width: number, height: number) => {
|
const setYes = (width: number, height: number) => {
|
||||||
yesSize.value = [width, height];
|
yesSize.value = [width, height];
|
||||||
};
|
};
|
||||||
|
|
||||||
const setNo = (_: string, width: number, height: number) => {
|
const setNo = (width: number, height: number) => {
|
||||||
noSize.value = [width, height];
|
noSize.value = [width, height];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -189,7 +190,7 @@ export const ConfirmBox = defineComponent<
|
|||||||
zIndex={15}
|
zIndex={15}
|
||||||
onClick={() => emit('yes')}
|
onClick={() => emit('yes')}
|
||||||
onEnter={() => (selected.value = true)}
|
onEnter={() => (selected.value = true)}
|
||||||
onSetText={setYes}
|
onResize={setYes}
|
||||||
/>
|
/>
|
||||||
<text
|
<text
|
||||||
loc={noLoc.value}
|
loc={noLoc.value}
|
||||||
@ -200,7 +201,7 @@ export const ConfirmBox = defineComponent<
|
|||||||
zIndex={15}
|
zIndex={15}
|
||||||
onClick={() => emit('no')}
|
onClick={() => emit('no')}
|
||||||
onEnter={() => (selected.value = false)}
|
onEnter={() => (selected.value = false)}
|
||||||
onSetText={setNo}
|
onResize={setNo}
|
||||||
/>
|
/>
|
||||||
</container>
|
</container>
|
||||||
);
|
);
|
||||||
@ -458,7 +459,7 @@ export const Choices = defineComponent<
|
|||||||
contentHeight.value = height;
|
contentHeight.value = height;
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateTitleHeight = (_0: string, _1: number, height: number) => {
|
const updateTitleHeight = (_: number, height: number) => {
|
||||||
titleHeight.value = height;
|
titleHeight.value = height;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -518,7 +519,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}
|
||||||
onSetText={updateTitleHeight}
|
onResize={updateTitleHeight}
|
||||||
/>
|
/>
|
||||||
<TextContent
|
<TextContent
|
||||||
{...attrs}
|
{...attrs}
|
||||||
@ -555,7 +556,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])}
|
||||||
onSetText={(_, width, height) =>
|
onResize={(width, height) =>
|
||||||
updateChoiceSize(i, width, height)
|
updateChoiceSize(i, width, height)
|
||||||
}
|
}
|
||||||
onEnter={() => (selected.value = i)}
|
onEnter={() => (selected.value = i)}
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
import { DefaultProps } from '@motajs/render-vue';
|
import { DefaultProps } from '@motajs/render-vue';
|
||||||
import { SetupComponentOptions } from '@motajs/system-ui';
|
import { SetupComponentOptions } from '@motajs/system';
|
||||||
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 { Font } from '@motajs/render-style';
|
import { MotaOffscreenCanvas2D, Font } from '@motajs/render';
|
||||||
import { MotaOffscreenCanvas2D } from '@motajs/render-core';
|
|
||||||
import {
|
import {
|
||||||
HALF_STATUS_WIDTH,
|
HALF_STATUS_WIDTH,
|
||||||
STATUS_BAR_HEIGHT,
|
STATUS_BAR_HEIGHT,
|
||||||
@ -220,7 +219,7 @@ export const FloorSelector = defineComponent<
|
|||||||
lineWidth={1}
|
lineWidth={1}
|
||||||
strokeStyle="#aaa"
|
strokeStyle="#aaa"
|
||||||
/>
|
/>
|
||||||
<sprite
|
<custom
|
||||||
zIndex={20}
|
zIndex={20}
|
||||||
loc={[0, 0, 144, SCROLL_HEIGHT]}
|
loc={[0, 0, 144, SCROLL_HEIGHT]}
|
||||||
nocache
|
nocache
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { DefaultProps, ElementLocator, GraphicPropsBase } from '@motajs/render';
|
import { ElementLocator } from '@motajs/render';
|
||||||
import { SetupComponentOptions } from '@motajs/system-ui';
|
import { DefaultProps, GraphicPropsBase } from '@motajs/render-vue';
|
||||||
|
import { SetupComponentOptions } from '@motajs/system';
|
||||||
import {
|
import {
|
||||||
computed,
|
computed,
|
||||||
defineComponent,
|
defineComponent,
|
||||||
|
|||||||
@ -1,19 +1,20 @@
|
|||||||
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,
|
||||||
RenderItem,
|
Transform,
|
||||||
Transform
|
Font,
|
||||||
} from '@motajs/render-core';
|
RectRCircleParams,
|
||||||
import { Font } from '@motajs/render-style';
|
IRenderItem,
|
||||||
|
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-ui';
|
import { GameUI, IUIMountable, SetupComponentOptions } from '@motajs/system';
|
||||||
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> {
|
||||||
@ -161,9 +162,9 @@ export const Input = defineComponent<InputProps, InputEmits, keyof InputEmits>(
|
|||||||
if (!ele) return;
|
if (!ele) return;
|
||||||
// 计算当前绝对位置
|
// 计算当前绝对位置
|
||||||
|
|
||||||
const chain: RenderItem[] = [];
|
const chain: IRenderItem[] = [];
|
||||||
let now: RenderItem | undefined = root.value;
|
let now: IRenderItem | null = root.value ?? null;
|
||||||
let renderer: MotaRenderer | undefined;
|
let renderer: IRenderTreeRoot | null = null;
|
||||||
if (!now) return;
|
if (!now) return;
|
||||||
while (now) {
|
while (now) {
|
||||||
chain.unshift(now);
|
chain.unshift(now);
|
||||||
@ -441,11 +442,11 @@ export const InputBox = defineComponent<
|
|||||||
emit('input', value);
|
emit('input', value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setYes = (_: string, width: number, height: number) => {
|
const setYes = (width: number, height: number) => {
|
||||||
yesSize.value = [width, height];
|
yesSize.value = [width, height];
|
||||||
};
|
};
|
||||||
|
|
||||||
const setNo = (_: string, width: number, height: number) => {
|
const setNo = (width: number, height: number) => {
|
||||||
noSize.value = [width, height];
|
noSize.value = [width, height];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -500,7 +501,7 @@ export const InputBox = defineComponent<
|
|||||||
zIndex={15}
|
zIndex={15}
|
||||||
onClick={confirm}
|
onClick={confirm}
|
||||||
onEnter={() => (selected.value = true)}
|
onEnter={() => (selected.value = true)}
|
||||||
onSetText={setYes}
|
onResize={setYes}
|
||||||
/>
|
/>
|
||||||
<text
|
<text
|
||||||
loc={noLoc.value}
|
loc={noLoc.value}
|
||||||
@ -511,7 +512,7 @@ export const InputBox = defineComponent<
|
|||||||
zIndex={15}
|
zIndex={15}
|
||||||
onClick={cancel}
|
onClick={cancel}
|
||||||
onEnter={() => (selected.value = false)}
|
onEnter={() => (selected.value = false)}
|
||||||
onSetText={setNo}
|
onResize={setNo}
|
||||||
/>
|
/>
|
||||||
</container>
|
</container>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
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 } from '@motajs/render-core';
|
import { ElementLocator, Font } from '@motajs/render';
|
||||||
import { Font } from '@motajs/render-style';
|
import { SetupComponentOptions } from '@motajs/system';
|
||||||
import { SetupComponentOptions } from '@motajs/system-ui';
|
|
||||||
import { Scroll } from './scroll';
|
import { Scroll } from './scroll';
|
||||||
|
|
||||||
export interface ListProps extends DefaultProps {
|
export interface ListProps extends DefaultProps {
|
||||||
|
|||||||
@ -1,19 +1,18 @@
|
|||||||
import {
|
import {
|
||||||
DefaultProps,
|
|
||||||
ElementLocator,
|
ElementLocator,
|
||||||
onTick,
|
CustomRenderItem,
|
||||||
PathProps,
|
MotaOffscreenCanvas2D
|
||||||
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-ui';
|
import { GameUI, IUIMountable, SetupComponentOptions } from '@motajs/system';
|
||||||
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 {
|
||||||
/** 进度条的位置 */
|
/** 进度条的位置 */
|
||||||
@ -43,7 +42,7 @@ const progressProps = {
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export const Progress = defineComponent<ProgressProps>(props => {
|
export const Progress = defineComponent<ProgressProps>(props => {
|
||||||
const element = ref<Sprite>();
|
const element = ref<CustomRenderItem>();
|
||||||
|
|
||||||
const render = (canvas: MotaOffscreenCanvas2D) => {
|
const render = (canvas: MotaOffscreenCanvas2D) => {
|
||||||
const { ctx } = canvas;
|
const { ctx } = canvas;
|
||||||
@ -73,7 +72,7 @@ export const Progress = defineComponent<ProgressProps>(props => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
return <sprite ref={element} loc={props.loc} render={render}></sprite>;
|
return <custom ref={element} loc={props.loc} render={render}></custom>;
|
||||||
};
|
};
|
||||||
}, progressProps);
|
}, progressProps);
|
||||||
|
|
||||||
@ -223,7 +222,7 @@ export const ScrollText = defineComponent<
|
|||||||
let paused = false;
|
let paused = false;
|
||||||
let nowScroll = 0;
|
let nowScroll = 0;
|
||||||
|
|
||||||
onTick(() => {
|
using.onExcitedFunc(() => {
|
||||||
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;
|
||||||
@ -302,7 +301,11 @@ 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(minAlpha.value, 2000, hyper('sin', 'in-out'))!;
|
const alpha = transitioned(
|
||||||
|
minAlpha.value,
|
||||||
|
2000,
|
||||||
|
cosh(2, CurveMode.EaseInOut)
|
||||||
|
)!;
|
||||||
|
|
||||||
const isWinskin = computed(() => !!props.winskin);
|
const isWinskin = computed(() => !!props.winskin);
|
||||||
const winskinImage = computed(() =>
|
const winskinImage = computed(() =>
|
||||||
@ -332,7 +335,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);
|
||||||
};
|
};
|
||||||
|
|
||||||
onTick(() => {
|
using.onExcitedFunc(() => {
|
||||||
if (alpha.value === maxAlpha.value) {
|
if (alpha.value === maxAlpha.value) {
|
||||||
alpha.set(minAlpha.value);
|
alpha.set(minAlpha.value);
|
||||||
}
|
}
|
||||||
@ -343,7 +346,7 @@ export const Selection = defineComponent<SelectionProps>(props => {
|
|||||||
|
|
||||||
return () =>
|
return () =>
|
||||||
isWinskin.value ? (
|
isWinskin.value ? (
|
||||||
<sprite
|
<custom
|
||||||
loc={props.loc}
|
loc={props.loc}
|
||||||
render={renderWinskin}
|
render={renderWinskin}
|
||||||
alpha={alpha.ref.value}
|
alpha={alpha.ref.value}
|
||||||
@ -392,7 +395,7 @@ export const Background = defineComponent<BackgroundProps>(props => {
|
|||||||
|
|
||||||
return () =>
|
return () =>
|
||||||
isWinskin.value ? (
|
isWinskin.value ? (
|
||||||
<winskin image={props.winskin!} loc={props.loc} noanti />
|
<winskin imageName={props.winskin!} loc={props.loc} noanti />
|
||||||
) : (
|
) : (
|
||||||
<g-rectr
|
<g-rectr
|
||||||
loc={fixedLoc.value}
|
loc={fixedLoc.value}
|
||||||
@ -407,8 +410,7 @@ export const Background = defineComponent<BackgroundProps>(props => {
|
|||||||
}, backgroundProps);
|
}, backgroundProps);
|
||||||
|
|
||||||
export interface WaitBoxProps<T>
|
export interface WaitBoxProps<T>
|
||||||
extends Partial<BackgroundProps>,
|
extends Partial<BackgroundProps>, Partial<TextContentProps> {
|
||||||
Partial<TextContentProps> {
|
|
||||||
loc: ElementLocator;
|
loc: ElementLocator;
|
||||||
width: number;
|
width: number;
|
||||||
promise?: Promise<T>;
|
promise?: Promise<T>;
|
||||||
|
|||||||
@ -9,8 +9,9 @@ import {
|
|||||||
watch
|
watch
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { clamp, isNil } from 'lodash-es';
|
import { clamp, isNil } from 'lodash-es';
|
||||||
import { DefaultProps, ElementLocator, Font } from '@motajs/render';
|
import { ElementLocator, Font } from '@motajs/render';
|
||||||
import { SetupComponentOptions } from '@motajs/system-ui';
|
import { SetupComponentOptions } from '@motajs/system';
|
||||||
|
import { DefaultProps } from '@motajs/render-vue';
|
||||||
|
|
||||||
/** 圆角矩形页码距离容器的边框大小,与 pageSize 相乘 */
|
/** 圆角矩形页码距离容器的边框大小,与 pageSize 相乘 */
|
||||||
const RECT_PAD = 0.1;
|
const RECT_PAD = 0.1;
|
||||||
|
|||||||
@ -12,10 +12,8 @@ import {
|
|||||||
} from 'vue';
|
} from 'vue';
|
||||||
import {
|
import {
|
||||||
Container,
|
Container,
|
||||||
DefaultProps,
|
|
||||||
ElementLocator,
|
ElementLocator,
|
||||||
RenderItem,
|
CustomRenderItem,
|
||||||
Sprite,
|
|
||||||
Transform,
|
Transform,
|
||||||
MotaOffscreenCanvas2D,
|
MotaOffscreenCanvas2D,
|
||||||
IActionEvent,
|
IActionEvent,
|
||||||
@ -23,14 +21,16 @@ 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-ui';
|
import { SetupComponentOptions } from '@motajs/system';
|
||||||
|
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<RenderItem> = new Set();
|
const listenedChild: Set<IRenderItem> = new Set();
|
||||||
const areaMap: Map<RenderItem, [number, number]> = new Map();
|
const areaMap: Map<IRenderItem, [number, number]> = new Map();
|
||||||
const content = ref<Container>();
|
const content = ref<Container>();
|
||||||
const scroll = ref<Sprite>();
|
const scroll = ref<CustomRenderItem>();
|
||||||
|
|
||||||
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: RenderItem, rect: DOMRectReadOnly) => {
|
const getArea = (item: IRenderItem, 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: RenderItem) => {
|
const checkItem = (item: IRenderItem) => {
|
||||||
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: RenderItem) => {
|
const onTransform = (item: IRenderItem) => {
|
||||||
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: RenderItem[],
|
children: IRenderItem[],
|
||||||
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],
|
||||||
_: ContainerCustom,
|
_: ICustomContainer,
|
||||||
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>
|
||||||
<sprite
|
<custom
|
||||||
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}
|
||||||
></sprite>
|
></custom>
|
||||||
</container>
|
</container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
ElementLocator,
|
ElementLocator,
|
||||||
Font,
|
Font,
|
||||||
Sprite,
|
CustomRenderItem,
|
||||||
DefaultProps,
|
|
||||||
Text,
|
Text,
|
||||||
MotaOffscreenCanvas2D
|
MotaOffscreenCanvas2D
|
||||||
} from '@motajs/render';
|
} from '@motajs/render';
|
||||||
@ -30,16 +29,16 @@ import {
|
|||||||
WordBreak,
|
WordBreak,
|
||||||
TextAlign
|
TextAlign
|
||||||
} from './textboxTyper';
|
} from './textboxTyper';
|
||||||
import { SetupComponentOptions } from '@motajs/system-ui';
|
import { SetupComponentOptions } from '@motajs/system';
|
||||||
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,
|
extends DefaultProps, Partial<ITextContentConfig> {
|
||||||
Partial<ITextContentConfig> {
|
|
||||||
/** 显示的文字 */
|
/** 显示的文字 */
|
||||||
text?: string;
|
text?: string;
|
||||||
/** 是否填充 */
|
/** 是否填充 */
|
||||||
@ -172,7 +171,7 @@ export const TextContent = defineComponent<
|
|||||||
|
|
||||||
expose<TextContentExpose>({ retype, showAll, getHeight });
|
expose<TextContentExpose>({ retype, showAll, getHeight });
|
||||||
|
|
||||||
const spriteElement = shallowRef<Sprite>();
|
const spriteElement = shallowRef<CustomRenderItem>();
|
||||||
const renderContent = (canvas: MotaOffscreenCanvas2D) => {
|
const renderContent = (canvas: MotaOffscreenCanvas2D) => {
|
||||||
const ctx = canvas.ctx;
|
const ctx = canvas.ctx;
|
||||||
ctx.textBaseline = 'top';
|
ctx.textBaseline = 'top';
|
||||||
@ -226,11 +225,11 @@ export const TextContent = defineComponent<
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
return (
|
return (
|
||||||
<sprite
|
<custom
|
||||||
loc={loc.value}
|
loc={loc.value}
|
||||||
ref={spriteElement}
|
ref={spriteElement}
|
||||||
render={renderContent}
|
render={renderContent}
|
||||||
></sprite>
|
></custom>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
}, textContentOptions);
|
}, textContentOptions);
|
||||||
@ -494,7 +493,7 @@ export const Textbox = defineComponent<
|
|||||||
slots.title(data)
|
slots.title(data)
|
||||||
) : props.winskin ? (
|
) : props.winskin ? (
|
||||||
<winskin
|
<winskin
|
||||||
image={props.winskin}
|
imageName={props.winskin}
|
||||||
loc={[0, 0, tw.value, th.value]}
|
loc={[0, 0, tw.value, th.value]}
|
||||||
></winskin>
|
></winskin>
|
||||||
) : (
|
) : (
|
||||||
@ -515,7 +514,7 @@ export const Textbox = defineComponent<
|
|||||||
slots.default(data)
|
slots.default(data)
|
||||||
) : props.winskin ? (
|
) : props.winskin ? (
|
||||||
<winskin
|
<winskin
|
||||||
image={props.winskin}
|
imageName={props.winskin}
|
||||||
loc={[0, contentY.value, data.width!, backHeight.value]}
|
loc={[0, contentY.value, data.width!, backHeight.value]}
|
||||||
></winskin>
|
></winskin>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import { logger } from '@motajs/common';
|
import { logger } from '@motajs/common';
|
||||||
import { Font, onTick, MotaOffscreenCanvas2D } from '@motajs/render';
|
import { Font, 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;
|
||||||
@ -150,8 +151,7 @@ interface ISizedTextContentBlock {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ITextContentTextBlock
|
export interface ITextContentTextBlock
|
||||||
extends ITextContentBlockBase,
|
extends ITextContentBlockBase, ISizedTextContentBlock {
|
||||||
ISizedTextContentBlock {
|
|
||||||
readonly type: TextContentType.Text;
|
readonly type: TextContentType.Text;
|
||||||
/** 文本 block 的文字内容 */
|
/** 文本 block 的文字内容 */
|
||||||
readonly text: string;
|
readonly text: string;
|
||||||
@ -164,8 +164,7 @@ export interface ITextContentTextBlock
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ITextContentIconBlock
|
export interface ITextContentIconBlock
|
||||||
extends ITextContentBlockBase,
|
extends ITextContentBlockBase, ISizedTextContentBlock {
|
||||||
ISizedTextContentBlock {
|
|
||||||
readonly type: TextContentType.Icon;
|
readonly type: TextContentType.Icon;
|
||||||
/** 图标 block 显示的图标 */
|
/** 图标 block 显示的图标 */
|
||||||
readonly icon: AllNumbers;
|
readonly icon: AllNumbers;
|
||||||
@ -199,8 +198,7 @@ export interface ITyperRenderableBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ITyperTextRenderable
|
export interface ITyperTextRenderable
|
||||||
extends ITextContentTextBlock,
|
extends ITextContentTextBlock, ITyperRenderableBase {
|
||||||
ITyperRenderableBase {
|
|
||||||
/** 文本左上角的横坐标 */
|
/** 文本左上角的横坐标 */
|
||||||
readonly x: number;
|
readonly x: number;
|
||||||
/** 文本左上角的纵坐标 */
|
/** 文本左上角的纵坐标 */
|
||||||
@ -210,8 +208,7 @@ export interface ITyperTextRenderable
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ITyperIconRenderable
|
export interface ITyperIconRenderable
|
||||||
extends ITextContentIconBlock,
|
extends ITextContentIconBlock, ITyperRenderableBase {
|
||||||
ITyperRenderableBase {
|
|
||||||
/** 图标左上角的横坐标 */
|
/** 图标左上角的横坐标 */
|
||||||
readonly x: number;
|
readonly x: number;
|
||||||
/** 图标左上角的纵坐标 */
|
/** 图标左上角的纵坐标 */
|
||||||
@ -219,8 +216,7 @@ export interface ITyperIconRenderable
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ITyperWaitRenderable
|
export interface ITyperWaitRenderable
|
||||||
extends ITextContentWaitBlock,
|
extends ITextContentWaitBlock, ITyperRenderableBase {
|
||||||
ITyperRenderableBase {
|
|
||||||
/** 当然是否已经等待了多少个字符 */
|
/** 当然是否已经等待了多少个字符 */
|
||||||
waited: number;
|
waited: number;
|
||||||
}
|
}
|
||||||
@ -313,7 +309,7 @@ export class TextContentTyper extends EventEmitter<TextContentTyperEvent> {
|
|||||||
this.config
|
this.config
|
||||||
);
|
);
|
||||||
|
|
||||||
onTick(() => this.tick());
|
using.onExcitedFunc(() => this.tick());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
ElementLocator,
|
ElementLocator,
|
||||||
MotaOffscreenCanvas2D,
|
MotaOffscreenCanvas2D,
|
||||||
Sprite
|
CustomRenderItem
|
||||||
} from '@motajs/render-core';
|
} from '@motajs/render';
|
||||||
import { SpriteProps } from '@motajs/render-vue';
|
import { CustomProps } from '@motajs/render-vue';
|
||||||
import { defineComponent, ref, watch } from 'vue';
|
import { defineComponent, ref, watch } from 'vue';
|
||||||
import { SetupComponentOptions } from '@motajs/system-ui';
|
import { SetupComponentOptions } from '@motajs/system';
|
||||||
|
|
||||||
export interface ThumbnailProps extends SpriteProps {
|
export interface ThumbnailProps extends CustomProps {
|
||||||
/** 缩略图的位置 */
|
/** 缩略图的位置 */
|
||||||
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<Sprite>();
|
const spriteRef = ref<CustomRenderItem>();
|
||||||
|
|
||||||
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 () => (
|
||||||
<sprite noanti ref={spriteRef} loc={props.loc} render={drawThumbnail} />
|
<custom noanti ref={spriteRef} loc={props.loc} render={drawThumbnail} />
|
||||||
);
|
);
|
||||||
}, thumbnailProps);
|
}, thumbnailProps);
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
import { DefaultProps, ElementLocator, Font } from '@motajs/render';
|
import { 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-ui';
|
import { SetupComponentOptions } from '@motajs/system';
|
||||||
|
import { DefaultProps } from '@motajs/render-vue';
|
||||||
|
|
||||||
export interface TipProps extends DefaultProps {
|
export interface TipProps extends DefaultProps {
|
||||||
/** 显示的位置 */
|
/** 显示的位置 */
|
||||||
@ -105,7 +106,7 @@ export const Tip = defineComponent<TipProps>((props, { expose }) => {
|
|||||||
hide();
|
hide();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSetText = (_: string, width: number) => {
|
const onSetText = (width: number) => {
|
||||||
textWidth.value = width;
|
textWidth.value = width;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -136,7 +137,7 @@ export const Tip = defineComponent<TipProps>((props, { expose }) => {
|
|||||||
<text
|
<text
|
||||||
loc={textLoc.value}
|
loc={textLoc.value}
|
||||||
text={text.value}
|
text={text.value}
|
||||||
onSetText={onSetText}
|
onResize={onSetText}
|
||||||
font={font}
|
font={font}
|
||||||
/>
|
/>
|
||||||
</container>
|
</container>
|
||||||
|
|||||||
@ -1,278 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
@ -1,319 +0,0 @@
|
|||||||
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 清除哪些深度的缓存,至多31位二进制数,例如填0b111就是清除前三层的索引
|
|
||||||
*/
|
|
||||||
clearCache(index: number, deep: number) {
|
|
||||||
const depth = this.cacheDepth;
|
|
||||||
for (let i = 0; i < depth; i++) {
|
|
||||||
if (deep & (1 << i)) {
|
|
||||||
const nowIndex = index * this.cacheDepth + i;
|
|
||||||
const item = this.cache.get(nowIndex);
|
|
||||||
item?.destroy();
|
|
||||||
this.cache.delete(nowIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清空指定索引的缓存,与 {@link clearCache} 不同的是,这里会直接清空对应索引的缓存,而不是指定分块的缓存(元素->void)
|
|
||||||
*/
|
|
||||||
clearCacheByIndex(index: number) {
|
|
||||||
const item = this.cache.get(index);
|
|
||||||
item?.destroy();
|
|
||||||
this.cache.delete(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清空所有缓存
|
|
||||||
*/
|
|
||||||
clearAllCache() {
|
|
||||||
this.cache.forEach(v => v.destroy());
|
|
||||||
this.cache.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据分块的横纵坐标获取其索引(分块->分块)
|
|
||||||
*/
|
|
||||||
getIndex(x: number, y: number) {
|
|
||||||
return x + y * this.blockData.width;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据元素位置获取分块索引(注意是单个元素的位置,而不是分块的位置)(元素->分块)
|
|
||||||
*/
|
|
||||||
getIndexByLoc(x: number, y: number) {
|
|
||||||
return this.getIndex(
|
|
||||||
Math.floor(x / this.blockSize),
|
|
||||||
Math.floor(y / this.blockSize)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据块的索引获取其位置(分块->分块)
|
|
||||||
*/
|
|
||||||
getBlockXYByIndex(index: number): LocArr {
|
|
||||||
const width = this.blockData.width;
|
|
||||||
return [index % width, Math.floor(index / width)];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取一个元素位置所在的分块位置(即使它不在任何分块内)(元素->分块)
|
|
||||||
*/
|
|
||||||
getBlockXY(x: number, y: number): LocArr {
|
|
||||||
return [Math.floor(x / this.blockSize), Math.floor(y / this.blockSize)];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据分块坐标与deep获取一个分块的精确索引(分块->分块)
|
|
||||||
*/
|
|
||||||
getPreciseIndex(x: number, y: number, deep: number) {
|
|
||||||
return (x + y * this.blockSize) * this.cacheDepth + deep;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据元素坐标及deep获取元素所在块的精确索引(元素->分块)
|
|
||||||
*/
|
|
||||||
getPreciseIndexByLoc(x: number, y: number, deep: number) {
|
|
||||||
return this.getPreciseIndex(...this.getBlockXY(x, y), deep);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新指定元素区域内的缓存(注意坐标是元素坐标,而非分块坐标)(元素->分块)
|
|
||||||
* @param deep 缓存清除深度,默认全部清空
|
|
||||||
* @returns 更新区域内的所有有关分块索引
|
|
||||||
*/
|
|
||||||
updateElementArea(
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
width: number,
|
|
||||||
height: number,
|
|
||||||
deep: number = 2 ** 31 - 1
|
|
||||||
) {
|
|
||||||
const [bx, by] = this.getBlockXY(x, y);
|
|
||||||
const [ex, ey] = this.getBlockXY(x + width - 1, y + height - 1);
|
|
||||||
|
|
||||||
return this.updateArea(bx, by, ex - bx, ey - by, deep);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新指定分块区域内的缓存(注意坐标是分块坐标,而非元素坐标)(分块->分块)
|
|
||||||
* @param deep 缓存清除深度,默认全部清空
|
|
||||||
* @returns 更新区域内的所有分块索引
|
|
||||||
*/
|
|
||||||
updateArea(
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
width: number,
|
|
||||||
height: number,
|
|
||||||
deep: number = 2 ** 31 - 1
|
|
||||||
) {
|
|
||||||
const blocks = this.getIndexOf(x, y, width, height);
|
|
||||||
|
|
||||||
blocks.forEach(v => {
|
|
||||||
this.clearCache(v, deep);
|
|
||||||
});
|
|
||||||
return blocks;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 传入分块坐标与范围,获取该区域内包含的所有分块索引(分块->分块)
|
|
||||||
*/
|
|
||||||
getIndexOf(x: number, y: number, width: number, height: number) {
|
|
||||||
const res = new Set<number>();
|
|
||||||
const sx = Math.max(x, 0);
|
|
||||||
const sy = Math.max(y, 0);
|
|
||||||
const ex = Math.min(x + width, this.blockData.width);
|
|
||||||
const ey = Math.min(y + height, this.blockData.height);
|
|
||||||
|
|
||||||
for (let nx = sx; nx <= ex; nx++) {
|
|
||||||
for (let ny = sy; ny <= ey; ny++) {
|
|
||||||
const index = this.getIndex(nx, ny);
|
|
||||||
res.add(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 传入元素坐标与范围,获取该区域内包含的所有分块索引(元素->分块)
|
|
||||||
*/
|
|
||||||
getIndexOfElement(x: number, y: number, width: number, height: number) {
|
|
||||||
const [bx, by] = this.getBlockXY(x, y);
|
|
||||||
const [ex, ey] = this.getBlockXY(x + width, y + height);
|
|
||||||
return this.getIndexOf(bx, by, ex - bx, ey - by);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据分块索引,获取这个分块所在区域的元素矩形范围(左上角横纵坐标及右下角横纵坐标)(分块->元素)
|
|
||||||
* @param block 分块索引
|
|
||||||
*/
|
|
||||||
getRectOfIndex(block: number) {
|
|
||||||
const [x, y] = this.getBlockXYByIndex(block);
|
|
||||||
return this.getRectOfBlockXY(x, y);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据分块坐标,获取这个分块所在区域的元素矩形范围(左上角横纵坐标及右下角横纵坐标)(分块->元素)
|
|
||||||
* @param x 分块横坐标
|
|
||||||
* @param y 分块纵坐标
|
|
||||||
*/
|
|
||||||
getRectOfBlockXY(x: number, y: number) {
|
|
||||||
return [
|
|
||||||
x * this.blockSize,
|
|
||||||
y * this.blockSize,
|
|
||||||
(x + 1) * this.blockSize,
|
|
||||||
(y + 1) * this.blockSize
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 摧毁这个块缓存
|
|
||||||
*/
|
|
||||||
destroy() {
|
|
||||||
this.clearAllCache();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ICanvasCacheItem extends IBlockCacheable {
|
|
||||||
readonly canvas: MotaOffscreenCanvas2D;
|
|
||||||
symbol: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class CanvasCacheItem implements ICanvasCacheItem {
|
|
||||||
constructor(
|
|
||||||
public readonly canvas: MotaOffscreenCanvas2D,
|
|
||||||
public readonly symbol: number,
|
|
||||||
public readonly element: RenderItem<any>
|
|
||||||
) {}
|
|
||||||
|
|
||||||
destroy(): void {
|
|
||||||
this.element.deleteCanvas(this.canvas);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,12 +1,11 @@
|
|||||||
import { logger } from '@motajs/common';
|
import { logger } from '@motajs/common';
|
||||||
import { MotaOffscreenCanvas2D } from '@motajs/render-core';
|
import { MotaOffscreenCanvas2D, SizedCanvasImageSource } from '@motajs/render';
|
||||||
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, HTMLImageElement>;
|
type ImageMap = Record<ImageMapKeys, ImageBitmap>;
|
||||||
|
|
||||||
const i = (img: ImageMapKeys) => {
|
const i = (img: ImageMapKeys) => {
|
||||||
return core.material.images[img];
|
return core.material.images[img];
|
||||||
@ -22,10 +21,10 @@ interface AutotileCache {
|
|||||||
type AutotileCaches = Record<AllNumbersOf<'autotile'>, AutotileCache>;
|
type AutotileCaches = Record<AllNumbersOf<'autotile'>, AutotileCache>;
|
||||||
|
|
||||||
interface TextureRequire {
|
interface TextureRequire {
|
||||||
tileset: Record<string, HTMLImageElement>;
|
tileset: Record<string, ImageBitmap>;
|
||||||
material: Record<ImageMapKeys, HTMLImageElement>;
|
material: Record<ImageMapKeys, ImageBitmap>;
|
||||||
autotile: AutotileCaches;
|
autotile: AutotileCaches;
|
||||||
images: Record<ImageIds, HTMLImageElement>;
|
images: Record<ImageIds, ImageBitmap>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RenderableDataBase {
|
interface RenderableDataBase {
|
||||||
@ -50,10 +49,10 @@ export interface AutotileRenderable extends RenderableDataBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class TextureCache {
|
class TextureCache {
|
||||||
tileset!: Record<string, HTMLImageElement>;
|
tileset!: Record<string, ImageBitmap>;
|
||||||
material: Record<ImageMapKeys, HTMLImageElement>;
|
material: Record<ImageMapKeys, ImageBitmap>;
|
||||||
autotile!: AutotileCaches;
|
autotile!: AutotileCaches;
|
||||||
images!: Record<ImageIds, HTMLImageElement>;
|
images!: Record<ImageIds, ImageBitmap>;
|
||||||
|
|
||||||
idNumberMap!: IdToNumber;
|
idNumberMap!: IdToNumber;
|
||||||
|
|
||||||
@ -77,7 +76,7 @@ class TextureCache {
|
|||||||
characterTurn2: Dir2[] = ['leftup', 'rightup', 'rightdown', 'leftdown'];
|
characterTurn2: Dir2[] = ['leftup', 'rightup', 'rightdown', 'leftdown'];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.material = imageMap as Record<ImageMapKeys, HTMLImageElement>;
|
this.material = imageMap as Record<ImageMapKeys, ImageBitmap>;
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
|||||||
@ -1,624 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,543 +0,0 @@
|
|||||||
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');
|
|
||||||
@ -1,54 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -1,481 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
@ -1,115 +1,47 @@
|
|||||||
import { standardElementNoCache, tagMap } from '@motajs/render-vue';
|
|
||||||
import { createCache } from './cache';
|
|
||||||
import { createFrame } from './frame';
|
|
||||||
import { createLayer, Layer, LayerGroup } from './layer';
|
|
||||||
import { createViewport } from './viewport';
|
|
||||||
import { Icon, Winskin } from './misc';
|
|
||||||
import { Animate } from './animate';
|
|
||||||
import { createItemDetail } from './itemDetail';
|
|
||||||
import { logger } from '@motajs/common';
|
import { logger } from '@motajs/common';
|
||||||
import { MapExtensionManager, MapRender, MapRenderer } from '../map';
|
import { MapRenderItem } from '../map';
|
||||||
import { state } from '@user/data-state';
|
import { mainRenderer, tagManager } from '../renderer';
|
||||||
import { materials } from '@user/client-base';
|
import { createCache } from './cache';
|
||||||
|
import { Icon, Winskin } from './misc';
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
tagMap.register('winskin', (_0, _1, props) => {
|
tagManager.registerTag(
|
||||||
if (!props)
|
'icon',
|
||||||
return new Winskin(core.material.images.images['winskin.png']);
|
tagManager.createStandardElement(false, Icon)
|
||||||
else {
|
);
|
||||||
const {
|
tagManager.registerTag(
|
||||||
image = core.material.images.images['winskin.png'],
|
'winskin',
|
||||||
type = 'static'
|
tagManager.createStandardElement(false, Winskin)
|
||||||
} = props;
|
);
|
||||||
return new Winskin(image, type);
|
tagManager.registerTag('map-render', props => {
|
||||||
}
|
|
||||||
});
|
|
||||||
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, renderer, extenstion');
|
logger.error(42, 'layerState');
|
||||||
const renderer = new MapRenderer(materials, state.layer);
|
throw new Error(`Lack of map-render property.`);
|
||||||
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');
|
||||||
const renderer = new MapRenderer(materials, state.layer);
|
throw new Error(`Lack of map-render property.`);
|
||||||
const manager = new MapExtensionManager(renderer);
|
|
||||||
return new MapRender(state.layer, renderer, manager);
|
|
||||||
}
|
}
|
||||||
if (!renderer) {
|
if (!renderer) {
|
||||||
logger.error(42, 'renderer');
|
logger.error(42, 'renderer');
|
||||||
const renderer = new MapRenderer(materials, state.layer);
|
throw new Error(`Lack of map-render property.`);
|
||||||
const manager = new MapExtensionManager(renderer);
|
|
||||||
return new MapRender(state.layer, renderer, manager);
|
|
||||||
}
|
}
|
||||||
if (!extension) {
|
if (!extension) {
|
||||||
logger.error(42, 'extension');
|
logger.error(42, 'extension');
|
||||||
const renderer = new MapRenderer(materials, state.layer);
|
throw new Error(`Lack of map-render property.`);
|
||||||
const manager = new MapExtensionManager(renderer);
|
|
||||||
return new MapRender(state.layer, renderer, manager);
|
|
||||||
}
|
}
|
||||||
return new MapRender(layerState, renderer, extension);
|
return new MapRenderItem(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';
|
|
||||||
|
|||||||
@ -1,270 +0,0 @@
|
|||||||
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
@ -1,36 +1,49 @@
|
|||||||
import { logger } from '@motajs/common';
|
import { logger } from '@motajs/common';
|
||||||
import {
|
import {
|
||||||
ERenderItemEvent,
|
|
||||||
RenderItem,
|
RenderItem,
|
||||||
RenderItemPosition,
|
|
||||||
MotaOffscreenCanvas2D,
|
MotaOffscreenCanvas2D,
|
||||||
Transform
|
Transform,
|
||||||
} from '@motajs/render-core';
|
SizedCanvasImageSource
|
||||||
import { SizedCanvasImageSource } from '@motajs/render-assets';
|
} from '@motajs/render';
|
||||||
import { isNil } from 'lodash-es';
|
import { isNil } from 'lodash-es';
|
||||||
import { RenderableData, AutotileRenderable, texture } from './cache';
|
import { RenderableData, AutotileRenderable, texture } from './cache';
|
||||||
import { IAnimateFrame, renderEmits } from './frame';
|
import { IExcitable } from '@motajs/animate';
|
||||||
|
import { IMotaIcon, IMotaWinskin } from './types';
|
||||||
|
|
||||||
export interface EIconEvent extends ERenderItemEvent {}
|
export class Icon extends RenderItem implements IMotaIcon, IExcitable<number> {
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
constructor(type: RenderItemPosition, cache?: boolean, fall?: boolean) {
|
/** 委托激励对象 id,用于图标的动画展示 */
|
||||||
super(type, cache, fall);
|
private delegation: number = -1;
|
||||||
|
|
||||||
|
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
|
||||||
@ -42,7 +55,7 @@ export class Icon extends RenderItem<EIconEvent> implements IAnimateFrame {
|
|||||||
const cw = this.width;
|
const cw = this.width;
|
||||||
const ch = this.height;
|
const ch = this.height;
|
||||||
const frame = this.animate
|
const frame = this.animate
|
||||||
? RenderItem.animatedFrame % renderable.frame
|
? this.nowFrame % renderable.frame
|
||||||
: this.frame;
|
: this.frame;
|
||||||
|
|
||||||
if (!this.animate) {
|
if (!this.animate) {
|
||||||
@ -87,6 +100,27 @@ export class Icon extends RenderItem<EIconEvent> implements IAnimateFrame {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
@ -101,15 +135,7 @@ export class Icon extends RenderItem<EIconEvent> implements IAnimateFrame {
|
|||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新动画帧
|
|
||||||
*/
|
|
||||||
updateFrameAnimate(): void {
|
|
||||||
if (this.animate) this.update(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy(): void {
|
destroy(): void {
|
||||||
renderEmits.removeFramer(this);
|
|
||||||
super.destroy();
|
super.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,142 +150,50 @@ export class Icon extends RenderItem<EIconEvent> implements IAnimateFrame {
|
|||||||
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.animate = nextValue;
|
this.setAnimateStatus(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.frame = nextValue;
|
this.setFrame(nextValue);
|
||||||
this.update();
|
return true;
|
||||||
|
case 'speed':
|
||||||
|
if (!this.assertType(nextValue, 'number', key)) return false;
|
||||||
|
this.setFrameSpeed(nextValue);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WinskinPatterns {
|
export class Winskin extends RenderItem implements IMotaWinskin {
|
||||||
top: CanvasPattern;
|
image: SizedCanvasImageSource | null = null;
|
||||||
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;
|
|
||||||
|
|
||||||
// todo: 跨上下文可能是未定义行为,需要上下文无关化
|
constructor(enableCache: boolean = false) {
|
||||||
private static patternMap: Map<string, WinskinPatterns> = new Map();
|
super(enableCache);
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private generatePattern() {
|
protected render(canvas: MotaOffscreenCanvas2D): void {
|
||||||
const pattern = this.requireCanvas(true, false);
|
|
||||||
pattern.setScale(1);
|
|
||||||
const img = this.image;
|
const img = this.image;
|
||||||
pattern.size(32, 16);
|
if (!img) return;
|
||||||
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.fillStyle = top;
|
ctx.drawImage(img, 144, 0, 32, 16, pad, 0, w - pad * 2, pad);
|
||||||
ctx.translate(pad, 0);
|
ctx.drawImage(img, 144, 48, 32, 16, pad, h - pad, w - pad * 2, pad);
|
||||||
ctx.fillRect(0, 0, w - pad * 2, pad);
|
ctx.drawImage(img, 128, 16, 16, 32, 0, pad, pad, h - pad * 2);
|
||||||
ctx.fillStyle = bottom;
|
ctx.drawImage(img, 176, 16, 16, 32, w - pad, pad, pad, h - pad * 2);
|
||||||
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);
|
||||||
@ -273,7 +207,7 @@ export class Winskin extends RenderItem<EWinskinEvent> {
|
|||||||
*/
|
*/
|
||||||
setImage(image: SizedCanvasImageSource) {
|
setImage(image: SizedCanvasImageSource) {
|
||||||
this.image = image;
|
this.image = image;
|
||||||
this.patternCache = void 0;
|
this.imageName = '';
|
||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -307,8 +241,6 @@ export class Winskin extends RenderItem<EWinskinEvent> {
|
|||||||
*/
|
*/
|
||||||
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -319,6 +251,9 @@ export class Winskin extends RenderItem<EWinskinEvent> {
|
|||||||
): 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;
|
||||||
|
|||||||
@ -1,30 +1,7 @@
|
|||||||
import { BaseProps, TagDefine } from '@motajs/render-vue';
|
import { BaseProps, TagDefine } from '@motajs/render-vue';
|
||||||
import { ERenderItemEvent, Transform } from '@motajs/render-core';
|
import { ERenderItemEvent, SizedCanvasImageSource } from '@motajs/render';
|
||||||
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, IOnMapTextRenderer } from '../map';
|
import { IMapExtensionManager, IMapRenderer } 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 或数字 */
|
||||||
@ -33,50 +10,30 @@ 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 的图片 id */
|
/** 直接设置 winskin 图片 */
|
||||||
image: ImageIds;
|
image?: SizedCanvasImageSource;
|
||||||
|
/** 根据图片名称设置 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 {
|
||||||
layer: TagDefine<LayerProps, ELayerEvent>;
|
icon: TagDefine<IconProps, ERenderItemEvent>;
|
||||||
'layer-group': TagDefine<LayerGroupProps, ELayerGroupEvent>;
|
winskin: TagDefine<WinskinProps, ERenderItemEvent>;
|
||||||
animation: TagDefine<AnimateProps, EAnimateEvent>;
|
|
||||||
icon: TagDefine<IconProps, EIconEvent>;
|
|
||||||
winskin: TagDefine<WinskinProps, EWinskinEvent>;
|
|
||||||
'map-render': TagDefine<MapRenderProps, ERenderItemEvent>;
|
'map-render': TagDefine<MapRenderProps, ERenderItemEvent>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
65
packages-user/client-modules/src/render/elements/types.ts
Normal file
65
packages-user/client-modules/src/render/elements/types.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
@ -1,14 +0,0 @@
|
|||||||
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');
|
|
||||||
}
|
|
||||||
@ -1,360 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { Shader, ShaderProgram } from '@motajs/render-core';
|
import { Shader, ShaderProgram } from '@motajs/render';
|
||||||
|
|
||||||
export abstract class EffectBase<T> {
|
export abstract class EffectBase<T> {
|
||||||
/** 当前使用的程序 */
|
/** 当前使用的程序 */
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import {
|
|||||||
ITransformUpdatable,
|
ITransformUpdatable,
|
||||||
ShaderProgram,
|
ShaderProgram,
|
||||||
Transform3D
|
Transform3D
|
||||||
} from '@motajs/render-core';
|
} from '@motajs/render';
|
||||||
import { EffectBase } from './base';
|
import { EffectBase } from './base';
|
||||||
|
|
||||||
export class Image3DEffect
|
export class Image3DEffect
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { createApp, Font } from '@motajs/render';
|
import { 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,6 +10,8 @@ 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 => {
|
||||||
@ -22,6 +24,9 @@ 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() {
|
||||||
@ -30,11 +35,6 @@ export function createRender() {
|
|||||||
createAction();
|
createAction();
|
||||||
createWeather();
|
createWeather();
|
||||||
|
|
||||||
loading.once('loaded', () => {
|
|
||||||
sceneController.open(GameTitleUI, {});
|
|
||||||
mainRenderer.show();
|
|
||||||
});
|
|
||||||
|
|
||||||
loading.once('assetBuilt', () => {
|
loading.once('assetBuilt', () => {
|
||||||
createMainExtension();
|
createMainExtension();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,114 +0,0 @@
|
|||||||
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'));
|
|
||||||
@ -6,6 +6,7 @@ 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;
|
||||||
@ -288,7 +289,7 @@ export class BlockSplitter<T> implements IBlockSplitter<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SplittedBlockData<T> implements IBlockData<T> {
|
class SplittedBlockData<T> implements IBlockData<T>, ISearchable8Dir {
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
x: number;
|
x: number;
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import { MotaOffscreenCanvas2D, RenderItem } from '@motajs/render-core';
|
import { MotaOffscreenCanvas2D, RenderItem } from '@motajs/render';
|
||||||
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 MapRender extends RenderItem {
|
export class MapRenderItem extends RenderItem {
|
||||||
/**
|
/**
|
||||||
* @param layerState 地图状态对象
|
* @param layerState 地图状态对象
|
||||||
* @param renderer 地图渲染器对象
|
* @param renderer 地图渲染器对象
|
||||||
@ -15,13 +15,14 @@ export class MapRender extends RenderItem {
|
|||||||
readonly renderer: IMapRenderer,
|
readonly renderer: IMapRenderer,
|
||||||
readonly exManager: IMapExtensionManager
|
readonly exManager: IMapExtensionManager
|
||||||
) {
|
) {
|
||||||
super('static', false, false);
|
super(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();
|
||||||
|
|||||||
@ -13,11 +13,7 @@ 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 {
|
import { ITexture, ITextureSplitter, TextureRowSplitter } from '@motajs/render';
|
||||||
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';
|
||||||
|
|
||||||
|
|||||||
@ -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-core';
|
import { ITransformUpdatable, Transform } from '@motajs/render';
|
||||||
|
|
||||||
export class OnMapTextRenderer
|
export class OnMapTextRenderer
|
||||||
implements IOnMapTextRenderer, ITransformUpdatable<Transform>
|
implements IOnMapTextRenderer, ITransformUpdatable<Transform>
|
||||||
@ -116,6 +116,7 @@ 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
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import { ITexture } from '@motajs/render-assets';
|
import { ITexture, Font } from '@motajs/render';
|
||||||
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 {
|
||||||
|
|||||||
@ -2,8 +2,9 @@ import {
|
|||||||
ITextureAnimater,
|
ITextureAnimater,
|
||||||
ITextureRenderable,
|
ITextureRenderable,
|
||||||
SizedCanvasImageSource,
|
SizedCanvasImageSource,
|
||||||
TextureColumnAnimater
|
TextureColumnAnimater,
|
||||||
} from '@motajs/render-assets';
|
Transform
|
||||||
|
} from '@motajs/render';
|
||||||
import {
|
import {
|
||||||
AutotileProcessor,
|
AutotileProcessor,
|
||||||
BlockCls,
|
BlockCls,
|
||||||
@ -45,7 +46,6 @@ 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';
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { IDirtyMark, IDirtyTracker } from '@motajs/common';
|
import { IDirtyMark, IDirtyTracker } from '@motajs/common';
|
||||||
import {
|
import {
|
||||||
ITextureRenderable,
|
ITextureRenderable,
|
||||||
SizedCanvasImageSource
|
SizedCanvasImageSource,
|
||||||
} from '@motajs/render-assets';
|
Transform
|
||||||
import { Transform } from '@motajs/render-core';
|
} from '@motajs/render';
|
||||||
import {
|
import {
|
||||||
IAutotileProcessor,
|
IAutotileProcessor,
|
||||||
IMaterialFramedData,
|
IMaterialFramedData,
|
||||||
@ -997,8 +997,7 @@ export interface IMapVertexStatus {
|
|||||||
* 脏标记表示顶点数组的长度是否发生变化
|
* 脏标记表示顶点数组的长度是否发生变化
|
||||||
*/
|
*/
|
||||||
export interface IMapVertexGenerator
|
export interface IMapVertexGenerator
|
||||||
extends IDirtyTracker<boolean>,
|
extends IDirtyTracker<boolean>, IMapVertexStatus {
|
||||||
IMapVertexStatus {
|
|
||||||
/** 地图渲染器 */
|
/** 地图渲染器 */
|
||||||
readonly renderer: IMapRenderer;
|
readonly renderer: IMapRenderer;
|
||||||
/** 地图分块 */
|
/** 地图分块 */
|
||||||
|
|||||||
@ -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-assets';
|
import { IRect } from '@motajs/render';
|
||||||
import { INSTANCED_COUNT } from './constant';
|
import { INSTANCED_COUNT } from './constant';
|
||||||
|
|
||||||
export interface IMapDataGetter {
|
export interface IMapDataGetter {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Transform } from '@motajs/render-core';
|
import { Transform } from '@motajs/render';
|
||||||
import {
|
import {
|
||||||
IBlockData,
|
IBlockData,
|
||||||
IMapRenderArea,
|
IMapRenderArea,
|
||||||
|
|||||||
@ -1,8 +1,46 @@
|
|||||||
import { MotaRenderer } from '@motajs/render-core';
|
import { MotaRenderer } from '@motajs/render';
|
||||||
import { MAIN_WIDTH, MAIN_HEIGHT } from './shared';
|
import {
|
||||||
|
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);
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
import { UIController } from '@motajs/system-ui';
|
import { UIController } from '@motajs/system';
|
||||||
|
|
||||||
export const sceneController = new UIController('main-scene');
|
export const sceneController = new UIController('main-scene');
|
||||||
|
|||||||
@ -1,8 +1,26 @@
|
|||||||
import { ElementLocator } from '@motajs/render-core';
|
import { ElementLocator, Font } from '@motajs/render';
|
||||||
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 地图
|
||||||
|
|
||||||
/** 每个格子的默认宽度,现阶段用处不大 */
|
/** 每个格子的默认宽度,现阶段用处不大 */
|
||||||
@ -36,6 +54,8 @@ export const MOVING_TOLERANCE = 60;
|
|||||||
/** 开关门动画的动画时长 */
|
/** 开关门动画的动画时长 */
|
||||||
export const DOOR_ANIMATE_INTERVAL = 50;
|
export const DOOR_ANIMATE_INTERVAL = 50;
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
//#region 状态栏
|
//#region 状态栏
|
||||||
|
|
||||||
/** 状态栏像素宽度 */
|
/** 状态栏像素宽度 */
|
||||||
@ -51,6 +71,8 @@ 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 游戏画面
|
||||||
|
|
||||||
/** 游戏画面像素宽度,宽=地图宽度+状态栏宽度*状态栏数量 */
|
/** 游戏画面像素宽度,宽=地图宽度+状态栏宽度*状态栏数量 */
|
||||||
@ -73,6 +95,8 @@ export const CENTER_LOC: ElementLocator = [
|
|||||||
0.5
|
0.5
|
||||||
];
|
];
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
//#region 通用配置
|
//#region 通用配置
|
||||||
|
|
||||||
/** 弹框的宽度,使用在内置 UI 与组件中,包括确认框、选择框、等待框等 */
|
/** 弹框的宽度,使用在内置 UI 与组件中,包括确认框、选择框、等待框等 */
|
||||||
@ -80,6 +104,31 @@ 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 存档界面
|
||||||
|
|
||||||
/** 存档缩略图尺寸 */
|
/** 存档缩略图尺寸 */
|
||||||
@ -97,8 +146,13 @@ 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;
|
||||||
/** 标题文字中心纵坐标 */
|
/** 标题文字中心纵坐标 */
|
||||||
@ -118,3 +172,5 @@ 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
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import {
|
|||||||
SetupComponentOptions,
|
SetupComponentOptions,
|
||||||
UIComponentProps,
|
UIComponentProps,
|
||||||
UIController
|
UIController
|
||||||
} from '@motajs/system-ui';
|
} from '@motajs/system';
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import { MAIN_HEIGHT, MAIN_WIDTH } from '../shared';
|
import { MAIN_HEIGHT, MAIN_WIDTH } from '../shared';
|
||||||
|
|
||||||
|
|||||||
165
packages-user/client-modules/src/render/ui/load.tsx
Normal file
165
packages-user/client-modules/src/render/ui/load.tsx
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
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);
|
||||||
@ -1,15 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
Props,
|
|
||||||
Font,
|
Font,
|
||||||
IActionEvent,
|
IActionEvent,
|
||||||
MotaOffscreenCanvas2D,
|
MotaOffscreenCanvas2D,
|
||||||
Sprite,
|
CustomRenderItem
|
||||||
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, Tip } from '../components';
|
import { Textbox, TextboxProps, Tip } from '../components';
|
||||||
import { GameUI } from '@motajs/system-ui';
|
import { GameUI } from '@motajs/system';
|
||||||
import {
|
import {
|
||||||
ENABLE_RIGHT_STATUS_BAR,
|
ENABLE_RIGHT_STATUS_BAR,
|
||||||
MAIN_HEIGHT,
|
MAIN_HEIGHT,
|
||||||
@ -29,14 +27,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: Props<typeof Textbox> = {
|
const mainTextboxProps: TextboxProps = {
|
||||||
text: '',
|
text: '',
|
||||||
hidden: true,
|
hidden: true,
|
||||||
loc: [0, MAP_HEIGHT - 150, MAP_WIDTH, 150],
|
loc: [0, MAP_HEIGHT - 150, MAP_WIDTH, 150],
|
||||||
@ -148,7 +146,7 @@ const MainScene = defineComponent(() => {
|
|||||||
//#region sprite 渲染
|
//#region sprite 渲染
|
||||||
|
|
||||||
let lastLength = 0;
|
let lastLength = 0;
|
||||||
onTick(() => {
|
using.onExcitedFunc(() => {
|
||||||
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();
|
||||||
@ -156,7 +154,7 @@ const MainScene = defineComponent(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapMiscSprite = ref<Sprite>();
|
const mapMiscSprite = ref<CustomRenderItem>();
|
||||||
|
|
||||||
const renderMapMisc = (canvas: MotaOffscreenCanvas2D) => {
|
const renderMapMisc = (canvas: MotaOffscreenCanvas2D) => {
|
||||||
const step = core.status.stepPostfix;
|
const step = core.status.stepPostfix;
|
||||||
@ -246,7 +244,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}
|
||||||
@ -254,7 +252,7 @@ const MainScene = defineComponent(() => {
|
|||||||
pad={[12, 6]}
|
pad={[12, 6]}
|
||||||
corner={16}
|
corner={16}
|
||||||
/>
|
/>
|
||||||
<sprite
|
<custom
|
||||||
noevent
|
noevent
|
||||||
loc={[0, 0, MAP_WIDTH, MAP_HEIGHT]}
|
loc={[0, 0, MAP_WIDTH, MAP_HEIGHT]}
|
||||||
ref={mapMiscSprite}
|
ref={mapMiscSprite}
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
import { ElementLocator, IWheelEvent } from '@motajs/render-core';
|
import { ElementLocator, IWheelEvent, Font } from '@motajs/render';
|
||||||
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-ui';
|
} from '@motajs/system';
|
||||||
import {
|
import {
|
||||||
defineComponent,
|
defineComponent,
|
||||||
ref,
|
ref,
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import {
|
|||||||
IUIMountable,
|
IUIMountable,
|
||||||
SetupComponentOptions,
|
SetupComponentOptions,
|
||||||
UIComponentProps
|
UIComponentProps
|
||||||
} from '@motajs/system-ui';
|
} from '@motajs/system';
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import {
|
import {
|
||||||
ChoiceItem,
|
ChoiceItem,
|
||||||
@ -15,8 +15,7 @@ import {
|
|||||||
waitbox
|
waitbox
|
||||||
} from '../components';
|
} from '../components';
|
||||||
import { mainUi } from '@motajs/legacy-ui';
|
import { mainUi } from '@motajs/legacy-ui';
|
||||||
import { gameKey } from '@motajs/system-action';
|
import { gameKey, generateKeyboardEvent } from '@motajs/system';
|
||||||
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';
|
||||||
@ -28,8 +27,7 @@ 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>,
|
extends Partial<ChoicesProps>, UIComponentProps {
|
||||||
UIComponentProps {
|
|
||||||
loc: ElementLocator;
|
loc: ElementLocator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import {
|
|||||||
IUIMountable,
|
IUIMountable,
|
||||||
SetupComponentOptions,
|
SetupComponentOptions,
|
||||||
UIComponentProps
|
UIComponentProps
|
||||||
} from '@motajs/system-ui';
|
} from '@motajs/system';
|
||||||
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';
|
||||||
|
|||||||
@ -1,12 +1,7 @@
|
|||||||
import { GameUI, SetupComponentOptions } from '@motajs/system-ui';
|
import { GameUI, SetupComponentOptions } from '@motajs/system';
|
||||||
import { computed, ComputedRef, defineComponent, shallowReactive } from 'vue';
|
import { computed, ComputedRef, defineComponent, shallowReactive } from 'vue';
|
||||||
import { TextContent } from '../components';
|
import { TextContent } from '../components';
|
||||||
import {
|
import { ElementLocator, Font, ITexture } from '@motajs/render';
|
||||||
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';
|
||||||
@ -16,6 +11,8 @@ 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 */
|
||||||
@ -73,27 +70,27 @@ export interface IRightHeroStatus {
|
|||||||
|
|
||||||
interface StatusInfo {
|
interface StatusInfo {
|
||||||
/** 图标 */
|
/** 图标 */
|
||||||
icon: SizedCanvasImageSource;
|
readonly icon: ITexture | null;
|
||||||
/** 属性值,经过格式化 */
|
/** 属性值,经过格式化 */
|
||||||
value: ComputedRef<string>;
|
readonly value: ComputedRef<string>;
|
||||||
/** 字体 */
|
/** 字体 */
|
||||||
font: Font;
|
readonly font: Font;
|
||||||
/** 文字颜色 */
|
/** 文字颜色 */
|
||||||
color: CanvasStyle;
|
readonly color: CanvasStyle;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface KeyLikeItem {
|
interface KeyLikeItem {
|
||||||
/** 属性值,经过格式化 */
|
/** 属性值,经过格式化 */
|
||||||
value: ComputedRef<string>;
|
readonly value: ComputedRef<string>;
|
||||||
/** 字体 */
|
/** 字体 */
|
||||||
font: Font;
|
readonly font: Font;
|
||||||
/** 文字颜色 */
|
/** 文字颜色 */
|
||||||
color: CanvasStyle;
|
readonly color: CanvasStyle;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface KeyLikeInfo {
|
interface KeyLikeInfo {
|
||||||
/** 这一行包含的内容 */
|
/** 这一行包含的内容 */
|
||||||
items: KeyLikeItem[];
|
readonly items: KeyLikeItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StatusBarProps<T> extends DefaultProps {
|
interface StatusBarProps<T> extends DefaultProps {
|
||||||
@ -119,15 +116,15 @@ export const LeftStatusBar = defineComponent<StatusBarProps<ILeftHeroStatus>>(
|
|||||||
/** 状态属性的开始纵坐标 */
|
/** 状态属性的开始纵坐标 */
|
||||||
const STATUS_Y = TITLE_HEIGHT + STATUS_PAD;
|
const STATUS_Y = TITLE_HEIGHT + STATUS_PAD;
|
||||||
|
|
||||||
// 可以换成 core.material.images.images['xxx.png'] 来使用全塔属性注册的图片
|
// 可以换成 materials.getImageByAlias('xxx.png') 来使用全塔属性注册的图片
|
||||||
const hpIcon = core.statusBar.icons.hp;
|
const hpIcon = materials.getImageByAlias('icon-hp');
|
||||||
const atkIcon = core.statusBar.icons.atk;
|
const atkIcon = materials.getImageByAlias('icon-atk');
|
||||||
const defIcon = core.statusBar.icons.def;
|
const defIcon = materials.getImageByAlias('icon-def');
|
||||||
const mdefIcon = core.statusBar.icons.mdef;
|
const mdefIcon = materials.getImageByAlias('icon-mdef');
|
||||||
const moneyIcon = core.statusBar.icons.money;
|
const moneyIcon = materials.getImageByAlias('icon-money');
|
||||||
const expIcon = core.statusBar.icons.exp;
|
const expIcon = materials.getImageByAlias('icon-exp');
|
||||||
const manaIcon = core.statusBar.icons.mana;
|
const manaIcon = materials.getImageByAlias('icon-mana');
|
||||||
const lvIcon = core.statusBar.icons.lv;
|
const lvIcon = materials.getImageByAlias('icon-lv');
|
||||||
|
|
||||||
const s = p.status;
|
const s = p.status;
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import {
|
|||||||
GameUI,
|
GameUI,
|
||||||
SetupComponentOptions,
|
SetupComponentOptions,
|
||||||
UIComponentProps
|
UIComponentProps
|
||||||
} from '@motajs/system-ui';
|
} from '@motajs/system';
|
||||||
import { defineComponent, nextTick, onMounted, ref } from 'vue';
|
import { defineComponent, nextTick, onMounted, ref } from 'vue';
|
||||||
import {
|
import {
|
||||||
BUTTONS_HEIGHT,
|
BUTTONS_HEIGHT,
|
||||||
@ -14,26 +14,28 @@ 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 } from '@motajs/render-core';
|
import { ElementLocator, Font } from '@motajs/render';
|
||||||
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,
|
||||||
@ -62,12 +64,12 @@ const gameTitleProps = {
|
|||||||
} satisfies SetupComponentOptions<GameTitleProps>;
|
} satisfies SetupComponentOptions<GameTitleProps>;
|
||||||
|
|
||||||
export const GameTitle = defineComponent<GameTitleProps>(props => {
|
export const GameTitle = defineComponent<GameTitleProps>(props => {
|
||||||
const bg = core.material.images.images['bg.jpg'];
|
const bg = materials.getImageByAlias(TITLE_BACKGROUND_IMAGE);
|
||||||
|
|
||||||
//#region 计算背景图
|
//#region 计算背景图
|
||||||
const [width, height] = adjustCover(
|
const [width, height] = adjustCover(
|
||||||
bg.width,
|
bg?.width ?? MAIN_WIDTH,
|
||||||
bg.height,
|
bg?.height ?? MAIN_HEIGHT,
|
||||||
MAIN_WIDTH,
|
MAIN_WIDTH,
|
||||||
MAIN_HEIGHT
|
MAIN_HEIGHT
|
||||||
);
|
);
|
||||||
@ -107,8 +109,12 @@ export const GameTitle = defineComponent<GameTitleProps>(props => {
|
|||||||
color: v.color,
|
color: v.color,
|
||||||
name: v.name,
|
name: v.name,
|
||||||
hard: '',
|
hard: '',
|
||||||
colorTrans: transitionedColor('#fff', 400, hyper('sin', 'out'))!,
|
colorTrans: transitionedColor(
|
||||||
scale: transitioned(1, 400, hyper('sin', 'out'))!
|
'#fff',
|
||||||
|
400,
|
||||||
|
cosh(2, CurveMode.EaseOut)
|
||||||
|
)!,
|
||||||
|
scale: transitioned(1, 400, cosh(2, CurveMode.EaseOut))!
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -119,8 +125,12 @@ 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('#fff', 400, hyper('sin', 'out'))!,
|
colorTrans: transitionedColor(
|
||||||
scale: transitioned(1, 400, hyper('sin', 'out'))!
|
'#fff',
|
||||||
|
400,
|
||||||
|
cosh(2, CurveMode.EaseOut)
|
||||||
|
)!,
|
||||||
|
scale: transitioned(1, 400, cosh(2, CurveMode.EaseOut))!
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
// 返回按钮
|
// 返回按钮
|
||||||
@ -129,11 +139,15 @@ export const GameTitle = defineComponent<GameTitleProps>(props => {
|
|||||||
color: '#aaa',
|
color: '#aaa',
|
||||||
name: '返回',
|
name: '返回',
|
||||||
hard: '',
|
hard: '',
|
||||||
colorTrans: transitionedColor('#fff', 400, hyper('sin', 'out'))!
|
colorTrans: transitionedColor('#fff', 400, cosh(2, CurveMode.EaseOut))!
|
||||||
});
|
});
|
||||||
|
|
||||||
/** 声音设置按钮的颜色 */
|
/** 声音设置按钮的颜色 */
|
||||||
const soundColor = transitionedColor('#ddd', 400, hyper('sin', 'out'))!;
|
const soundColor = transitionedColor(
|
||||||
|
'#ddd',
|
||||||
|
400,
|
||||||
|
cosh(2, CurveMode.EaseOut)
|
||||||
|
)!;
|
||||||
|
|
||||||
/** 开始界面按钮的不透明度,选择难度界面的不透明度使用 `1-buttonsAlpha` 计算 */
|
/** 开始界面按钮的不透明度,选择难度界面的不透明度使用 `1-buttonsAlpha` 计算 */
|
||||||
const buttonsAlpha = transitioned(1, 300, linear())!;
|
const buttonsAlpha = transitioned(1, 300, linear())!;
|
||||||
@ -153,7 +167,11 @@ export const GameTitle = defineComponent<GameTitleProps>(props => {
|
|||||||
/** 选择难度界面按钮的高度 */
|
/** 选择难度界面按钮的高度 */
|
||||||
const hardHeight = (hard.length - 1) * 40 + 60;
|
const hardHeight = (hard.length - 1) * 40 + 60;
|
||||||
/** 按钮的背景框高度 */
|
/** 按钮的背景框高度 */
|
||||||
const rectHeight = transitioned(buttonHeight, 600, hyper('sin', 'in-out'))!;
|
const rectHeight = transitioned(
|
||||||
|
buttonHeight,
|
||||||
|
600,
|
||||||
|
cosh(2, CurveMode.EaseOut)
|
||||||
|
)!;
|
||||||
|
|
||||||
//#region 按钮功能
|
//#region 按钮功能
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { DefaultProps, ElementLocator, Font } from '@motajs/render';
|
import { ElementLocator, Font } from '@motajs/render';
|
||||||
import { computed, defineComponent, ref } from 'vue';
|
import { computed, defineComponent, ref } from 'vue';
|
||||||
import {
|
import {
|
||||||
DoubleArrow,
|
DoubleArrow,
|
||||||
@ -12,19 +12,20 @@ import {
|
|||||||
ViewMapIcon
|
ViewMapIcon
|
||||||
} from '../components/icons';
|
} from '../components/icons';
|
||||||
import { getVitualKeyOnce } from '@motajs/legacy-ui';
|
import { getVitualKeyOnce } from '@motajs/legacy-ui';
|
||||||
import { gameKey } from '@motajs/system-action';
|
import { gameKey, generateKeyboardEvent } from '@motajs/system';
|
||||||
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-ui';
|
import { SetupComponentOptions } from '@motajs/system';
|
||||||
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;
|
||||||
@ -73,15 +74,15 @@ export const PlayingToolbar = defineComponent<
|
|||||||
ToolbarEmits,
|
ToolbarEmits,
|
||||||
keyof ToolbarEmits
|
keyof ToolbarEmits
|
||||||
>((props, { emit }) => {
|
>((props, { emit }) => {
|
||||||
const bookIcon = core.statusBar.icons.book;
|
const bookIcon = materials.getImageByAlias('icon-book');
|
||||||
const flyIcon = core.statusBar.icons.fly;
|
const flyIcon = materials.getImageByAlias('icon-fly');
|
||||||
const toolIcon = core.statusBar.icons.toolbox;
|
const toolIcon = materials.getImageByAlias('icon-toolbox');
|
||||||
const equipIcon = core.statusBar.icons.equipbox;
|
const equipIcon = materials.getImageByAlias('icon-equipbox');
|
||||||
const keyIcon = core.statusBar.icons.keyboard;
|
const keyIcon = materials.getImageByAlias('icon-keyboard');
|
||||||
const shopIcon = core.statusBar.icons.shop;
|
const shopIcon = materials.getImageByAlias('icon-shop');
|
||||||
const saveIcon = core.statusBar.icons.save;
|
const saveIcon = materials.getImageByAlias('icon-save');
|
||||||
const loadIcon = core.statusBar.icons.load;
|
const loadIcon = materials.getImageByAlias('icon-load');
|
||||||
const setIcon = core.statusBar.icons.settings;
|
const setIcon = materials.getImageByAlias('icon-settings');
|
||||||
|
|
||||||
const iconFont = new Font('Verdana', 12);
|
const iconFont = new Font('Verdana', 12);
|
||||||
|
|
||||||
@ -170,8 +171,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 = core.statusBar.icons.book;
|
const bookIcon = materials.getImageByAlias('icon-book');
|
||||||
const saveIcon = core.statusBar.icons.save;
|
const saveIcon = materials.getImageByAlias('icon-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);
|
||||||
|
|
||||||
|
|||||||
@ -3,15 +3,16 @@ import {
|
|||||||
IActionEvent,
|
IActionEvent,
|
||||||
IActionEventBase,
|
IActionEventBase,
|
||||||
IWheelEvent,
|
IWheelEvent,
|
||||||
MotaOffscreenCanvas2D
|
MotaOffscreenCanvas2D,
|
||||||
} from '@motajs/render-core';
|
Font
|
||||||
|
} 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-ui';
|
} from '@motajs/system';
|
||||||
import {
|
import {
|
||||||
computed,
|
computed,
|
||||||
defineComponent,
|
defineComponent,
|
||||||
@ -22,18 +23,9 @@ import {
|
|||||||
shallowRef,
|
shallowRef,
|
||||||
watch
|
watch
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { FloorSelector } from '../components/floorSelect';
|
import { FloorSelector } from '../components';
|
||||||
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 { calculateStatisticsOne, StatisticsDataOneFloor } from './statistics';
|
import { StatisticsDataOneFloor } from './statistics';
|
||||||
import { Tip, TipExpose } from '../components';
|
import { Tip, TipExpose } from '../components';
|
||||||
import { useKey } from '../use';
|
import { useKey } from '../use';
|
||||||
import {
|
import {
|
||||||
@ -61,11 +53,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;
|
||||||
@ -84,7 +76,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>();
|
||||||
|
|
||||||
@ -107,7 +99,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) => {
|
||||||
// 如果按键不能触发怪物手册,则关闭界面,因为怪物手册和退出默认使用同一个按键,需要特判
|
// 如果按键不能触发怪物手册,则关闭界面,因为怪物手册和退出默认使用同一个按键,需要特判
|
||||||
@ -145,10 +137,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 渐变渲染
|
||||||
|
|
||||||
@ -195,33 +187,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 事件监听
|
||||||
|
|
||||||
@ -230,7 +222,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();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -293,7 +285,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;
|
||||||
@ -322,7 +314,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) {
|
||||||
@ -341,8 +333,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 {
|
||||||
@ -362,7 +354,7 @@ export const ViewMap = defineComponent<ViewMapProps>(props => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
renderLayer(floorId.value);
|
// renderLayer(floorId.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
@ -371,7 +363,7 @@ export const ViewMap = defineComponent<ViewMapProps>(props => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
watch(floorId, value => {
|
watch(floorId, value => {
|
||||||
renderLayer(value);
|
// renderLayer(value);
|
||||||
});
|
});
|
||||||
|
|
||||||
//#region 组件树
|
//#region 组件树
|
||||||
@ -394,7 +386,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]}
|
||||||
@ -410,7 +402,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}
|
||||||
@ -418,7 +410,7 @@ export const ViewMap = defineComponent<ViewMapProps>(props => {
|
|||||||
pad={[12, 6]}
|
pad={[12, 6]}
|
||||||
corner={16}
|
corner={16}
|
||||||
/>
|
/>
|
||||||
<sprite
|
<custom
|
||||||
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}
|
||||||
@ -428,7 +420,7 @@ export const ViewMap = defineComponent<ViewMapProps>(props => {
|
|||||||
onLeave={leaveTop}
|
onLeave={leaveTop}
|
||||||
onClick={clickTop}
|
onClick={clickTop}
|
||||||
/>
|
/>
|
||||||
<sprite
|
<custom
|
||||||
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}
|
||||||
@ -554,7 +546,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>
|
||||||
|
|||||||
@ -1,6 +1,15 @@
|
|||||||
import { Hotkey, gameKey } from '@motajs/system-action';
|
import {
|
||||||
|
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,
|
||||||
@ -107,7 +116,7 @@ export interface ITransitionedController<T> {
|
|||||||
* 设置动画的速率曲线
|
* 设置动画的速率曲线
|
||||||
* @param timing 速率曲线
|
* @param timing 速率曲线
|
||||||
*/
|
*/
|
||||||
mode(timing: TimingFn): void;
|
mode(timing: ExcitationCurve): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置动画的动画时长
|
* 设置动画的动画时长
|
||||||
@ -117,37 +126,29 @@ 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.transition.transition(this.key, v);
|
this.set(v);
|
||||||
}
|
}
|
||||||
get value() {
|
get value() {
|
||||||
return this.transition.value[this.key];
|
return this.ref.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
value: number,
|
value: number,
|
||||||
public readonly transition: Transition,
|
public readonly transition: ITransition,
|
||||||
public time: number,
|
public time: number,
|
||||||
public curve: TimingFn
|
public curve: ExcitationCurve
|
||||||
) {
|
) {
|
||||||
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.time(time).mode(this.curve).transition(this.key, value);
|
this.transition.curve(this.curve).transition(this.ref).to(value, time);
|
||||||
}
|
}
|
||||||
|
|
||||||
mode(timing: TimingFn): void {
|
mode(timing: ExcitationCurve): void {
|
||||||
this.curve = timing;
|
this.curve = timing;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,6 +167,13 @@ 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) {
|
||||||
@ -177,26 +185,32 @@ class RenderColorTransition implements ITransitionedController<string> {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
value: string,
|
value: string,
|
||||||
public readonly transition: Transition,
|
public readonly transition: ITransition,
|
||||||
public time: number,
|
public time: number,
|
||||||
public curve: TimingFn
|
public curve: ExcitationCurve
|
||||||
) {
|
) {
|
||||||
this.ref = ref(value);
|
this.ref = ref(value);
|
||||||
const [r, g, b, a] = this.decodeColor(value);
|
const [r, g, b, a] = this.decodeColor(value);
|
||||||
transition.value[this.keyR] = r;
|
this.rValue = { value: r };
|
||||||
transition.value[this.keyG] = g;
|
this.gValue = { value: g };
|
||||||
transition.value[this.keyB] = b;
|
this.bValue = { value: b };
|
||||||
transition.value[this.keyA] = a;
|
this.aValue = { value: a };
|
||||||
transition.ticker.add(() => {
|
if (!transition.excitation) {
|
||||||
this.ref.value = this.encodeColor();
|
logger.warn(94, 'transitionedColor');
|
||||||
});
|
} else {
|
||||||
|
this.controller = transition.excitation.add(
|
||||||
|
excited(() => {
|
||||||
|
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: TimingFn): void {
|
mode(timing: ExcitationCurve): void {
|
||||||
this.curve = timing;
|
this.curve = timing;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -206,12 +220,15 @@ 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
|
||||||
.mode(this.curve)
|
.curve(this.curve)
|
||||||
.time(time)
|
.transition(this.rValue)
|
||||||
.transition(this.keyR, r)
|
.to(r, time)
|
||||||
.transition(this.keyG, g)
|
.transition(this.gValue)
|
||||||
.transition(this.keyB, b)
|
.to(g, time)
|
||||||
.transition(this.keyA, a);
|
.transition(this.bValue)
|
||||||
|
.to(b, time)
|
||||||
|
.transition(this.aValue)
|
||||||
|
.to(a, time);
|
||||||
}
|
}
|
||||||
|
|
||||||
private decodeColor(color: string): ColorRGBA {
|
private decodeColor(color: string): ColorRGBA {
|
||||||
@ -272,31 +289,37 @@ class RenderColorTransition implements ITransitionedController<string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private encodeColor() {
|
private encodeColor() {
|
||||||
const r = this.transition.value[this.keyR];
|
const r = this.rValue.value;
|
||||||
const g = this.transition.value[this.keyG];
|
const g = this.gValue.value;
|
||||||
const b = this.transition.value[this.keyB];
|
const b = this.bValue.value;
|
||||||
const a = this.transition.value[this.keyA];
|
const a = this.aValue.value;
|
||||||
return `rgba(${r},${g},${b},${a})`;
|
return `rgba(${r},${g},${b},${a})`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const transitionMap = new Map<ComponentInternalInstance, Transition>();
|
const transitionMap = new Map<ComponentInternalInstance, ITransition>();
|
||||||
|
|
||||||
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?.ticker.destroy();
|
tran?.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.ticker.destroy();
|
tran.destroy();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const tran = transitionMap.get(instance);
|
const tran = transitionMap.get(instance);
|
||||||
@ -320,7 +343,7 @@ function checkTransition() {
|
|||||||
export function transitioned(
|
export function transitioned(
|
||||||
value: number,
|
value: number,
|
||||||
time: number,
|
time: number,
|
||||||
curve: TimingFn
|
curve: ExcitationCurve
|
||||||
): ITransitionedController<number> | null {
|
): ITransitionedController<number> | null {
|
||||||
const tran = checkTransition();
|
const tran = checkTransition();
|
||||||
if (!tran) return null;
|
if (!tran) return null;
|
||||||
@ -344,7 +367,7 @@ export function transitioned(
|
|||||||
export function transitionedColor(
|
export function transitionedColor(
|
||||||
color: string,
|
color: string,
|
||||||
time: number,
|
time: number,
|
||||||
curve: TimingFn
|
curve: ExcitationCurve
|
||||||
): ITransitionedController<string> | null {
|
): ITransitionedController<string> | null {
|
||||||
const tran = checkTransition();
|
const tran = checkTransition();
|
||||||
if (!tran) return null;
|
if (!tran) return null;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { ElementLocator } from '@motajs/render-core';
|
import { ElementLocator } from '@motajs/render';
|
||||||
|
|
||||||
export interface IGridLayoutData {
|
export interface IGridLayoutData {
|
||||||
/** 有多少列 */
|
/** 有多少列 */
|
||||||
|
|||||||
@ -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-ui';
|
import { IUIMountable } from '@motajs/system';
|
||||||
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';
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import { onUnmounted } from 'vue';
|
import { onUnmounted } from 'vue';
|
||||||
import { WeatherController } from '../weather';
|
import { WeatherController } from '../weather';
|
||||||
|
import { IRenderTreeRoot } from '@motajs/render';
|
||||||
|
|
||||||
export function useWeather(): [WeatherController] {
|
export function useWeather(renderer: IRenderTreeRoot): [WeatherController] {
|
||||||
const weather = new WeatherController();
|
const weather = new WeatherController(renderer);
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
weather.destroy();
|
weather.destroy();
|
||||||
|
|||||||
@ -1,19 +1,21 @@
|
|||||||
import { RenderItem } from '@motajs/render-core';
|
import { IRenderTreeRoot, RenderItem } from '@motajs/render';
|
||||||
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 { Ticker } from 'mutate-animate';
|
import { IExcitable } from '@motajs/animate';
|
||||||
|
|
||||||
type WeatherConstructor = new () => IWeather;
|
type WeatherConstructor = new () => IWeather;
|
||||||
|
|
||||||
export class WeatherController implements IWeatherController {
|
// todo: refactor?
|
||||||
|
|
||||||
|
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;
|
||||||
/** 天气元素纵深 */
|
/** 天气元素纵深 */
|
||||||
@ -23,13 +25,13 @@ export class WeatherController implements IWeatherController {
|
|||||||
|
|
||||||
container: RenderItem | null = null;
|
container: RenderItem | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor(readonly renderer: IRenderTreeRoot) {
|
||||||
WeatherController.ticker.add(this.tick);
|
renderer.delegateExcitable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private tick = (time: number) => {
|
excited(payload: number): void {
|
||||||
this.active.forEach(v => v.weather.tick(time));
|
this.active.forEach(v => v.weather.tick(payload));
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置天气元素纵深,第一个天气会被设置为 `zIndex`,之后依次是 `zIndex+1` `zIndex+2` ...
|
* 设置天气元素纵深,第一个天气会被设置为 `zIndex`,之后依次是 `zIndex+1` `zIndex+2` ...
|
||||||
@ -111,7 +113,6 @@ export class WeatherController implements IWeatherController {
|
|||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
@ -138,8 +139,7 @@ export class WeatherController implements IWeatherController {
|
|||||||
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
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { CloudLike } from './cloudLike';
|
import { CloudLike } from './cloudLike';
|
||||||
import { SizedCanvasImageSource } from '@motajs/render-assets';
|
import { SizedCanvasImageSource } from '@motajs/render';
|
||||||
|
|
||||||
export class CloudWeather extends CloudLike {
|
export class CloudWeather extends CloudLike {
|
||||||
getImage(): SizedCanvasImageSource | null {
|
getImage(): SizedCanvasImageSource | null {
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
import { MotaOffscreenCanvas2D, Sprite } from '@motajs/render-core';
|
import {
|
||||||
|
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<Sprite> {
|
export abstract class CloudLike extends Weather<CustomRenderItem> {
|
||||||
/** 不透明度 */
|
/** 不透明度 */
|
||||||
private alpha: number = 0;
|
private alpha: number = 0;
|
||||||
/** 水平速度 */
|
/** 水平速度 */
|
||||||
@ -75,8 +78,8 @@ export abstract class CloudLike extends Weather<Sprite> {
|
|||||||
this.cy %= this.image.height;
|
this.cy %= this.image.height;
|
||||||
}
|
}
|
||||||
|
|
||||||
createElement(level: number): Sprite {
|
createElement(level: number): CustomRenderItem {
|
||||||
const element = new Sprite('static', true);
|
const element = new CustomRenderItem(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;
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { MotaOffscreenCanvas2D } from '@motajs/render-core';
|
import { MotaOffscreenCanvas2D, SizedCanvasImageSource } from '@motajs/render';
|
||||||
import { CloudLike } from './cloudLike';
|
import { CloudLike } from './cloudLike';
|
||||||
import { SizedCanvasImageSource } from '@motajs/render-assets';
|
|
||||||
|
|
||||||
export class FogWeather extends CloudLike {
|
export class FogWeather extends CloudLike {
|
||||||
/** 雾天气的图像比较小,因此将四个进行合并 */
|
/** 雾天气的图像比较小,因此将四个进行合并 */
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import { MotaOffscreenCanvas2D, Sprite } from '@motajs/render-core';
|
import { MotaOffscreenCanvas2D, CustomRenderItem } from '@motajs/render';
|
||||||
import { Weather } from '../weather';
|
import { Weather } from '../weather';
|
||||||
import { clamp } from 'lodash-es';
|
import { clamp } from 'lodash-es';
|
||||||
|
|
||||||
export class SunWeather extends Weather<Sprite> {
|
export class SunWeather extends Weather<CustomRenderItem> {
|
||||||
/** 阳光图片 */
|
/** 阳光图片 */
|
||||||
private image: HTMLImageElement | null = null;
|
private image: ImageBitmap | null = null;
|
||||||
/** 阳光图片的不透明度 */
|
/** 阳光图片的不透明度 */
|
||||||
private alpha: number = 0;
|
private alpha: number = 0;
|
||||||
/** 阳光的最大不透明度 */
|
/** 阳光的最大不透明度 */
|
||||||
@ -41,8 +41,8 @@ export class SunWeather extends Weather<Sprite> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createElement(level: number): Sprite {
|
createElement(level: number): CustomRenderItem {
|
||||||
const element = new Sprite('static', true);
|
const element = new CustomRenderItem(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;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { RenderItem } from '@motajs/render-core';
|
import { RenderItem } from '@motajs/render';
|
||||||
|
|
||||||
export interface IWeather<T extends RenderItem = RenderItem> {
|
export interface IWeather<T extends RenderItem = RenderItem> {
|
||||||
/** 天气的等级,-1 表示未创建 */
|
/** 天气的等级,-1 表示未创建 */
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { RenderItem } from '@motajs/render-core';
|
import { RenderItem } from '@motajs/render';
|
||||||
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> {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@user/data-base",
|
"name": "@user/data-base",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@motajs/types": "workspace:*"
|
"@motajs/types": "workspace:*",
|
||||||
|
"@motajs/loader": "workspace:*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -49,7 +49,7 @@ class GameLoading extends EventEmitter<GameLoadEvent> {
|
|||||||
* @param autotiles 自动元件数组
|
* @param autotiles 自动元件数组
|
||||||
*/
|
*/
|
||||||
onAutotileLoaded(
|
onAutotileLoaded(
|
||||||
autotiles: Partial<Record<AllIdsOf<'autotile'>, HTMLImageElement>>
|
autotiles: Partial<Record<AllIdsOf<'autotile'>, ImageBitmap>>
|
||||||
) {
|
) {
|
||||||
if (this.autotileListened) return;
|
if (this.autotileListened) return;
|
||||||
this.autotileListened = true;
|
this.autotileListened = true;
|
||||||
|
|||||||
@ -1 +1,4 @@
|
|||||||
|
export * from './load';
|
||||||
|
|
||||||
export * from './game';
|
export * from './game';
|
||||||
|
export * from './ins';
|
||||||
|
|||||||
11
packages-user/data-base/src/ins.ts
Normal file
11
packages-user/data-base/src/ins.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { LoadProgressTotal } from '@motajs/loader';
|
||||||
|
import { MotaDataLoader } from './load';
|
||||||
|
|
||||||
|
//#region 加载实例
|
||||||
|
|
||||||
|
/** 加载进度 */
|
||||||
|
export const loadProgress = new LoadProgressTotal();
|
||||||
|
/** 数据端加载对象 */
|
||||||
|
export const dataLoader = new MotaDataLoader(loadProgress);
|
||||||
|
|
||||||
|
//#endregion
|
||||||
3
packages-user/data-base/src/load/index.ts
Normal file
3
packages-user/data-base/src/load/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './loader';
|
||||||
|
export * from './processor';
|
||||||
|
export * from './types';
|
||||||
63
packages-user/data-base/src/load/loader.ts
Normal file
63
packages-user/data-base/src/load/loader.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import {
|
||||||
|
ILoadProgressTotal,
|
||||||
|
LoadDataType,
|
||||||
|
ILoadTask,
|
||||||
|
ILoadTaskProcessor
|
||||||
|
} from '@motajs/loader';
|
||||||
|
import { CustomLoadFunc, IMotaDataLoader } from './types';
|
||||||
|
import { LoadJSONProcessor } from './processor';
|
||||||
|
|
||||||
|
interface LoadTaskStore<T extends LoadDataType = LoadDataType, R = any> {
|
||||||
|
/** 加载任务对象 */
|
||||||
|
readonly task: ILoadTask<T, R>;
|
||||||
|
/** 当 `onLoaded` 兑现后兑现的 `Promise` */
|
||||||
|
readonly loadPromise: Promise<R>;
|
||||||
|
/** 兑现 `loadPromise` */
|
||||||
|
readonly loadResolve: (data: R) => void;
|
||||||
|
/** 当加载任务完成时执行的函数 */
|
||||||
|
readonly onLoaded: CustomLoadFunc<R>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MotaDataLoader implements IMotaDataLoader {
|
||||||
|
/** 当前已添加的加载任务 */
|
||||||
|
private readonly tasks: Set<LoadTaskStore> = new Set();
|
||||||
|
|
||||||
|
readonly jsonProcessor: ILoadTaskProcessor<LoadDataType.JSON, any>;
|
||||||
|
|
||||||
|
constructor(readonly progress: ILoadProgressTotal) {
|
||||||
|
this.jsonProcessor = new LoadJSONProcessor();
|
||||||
|
}
|
||||||
|
|
||||||
|
//#region 对外接口
|
||||||
|
|
||||||
|
initSystemLoadTask(): void {}
|
||||||
|
|
||||||
|
addCustomLoadTask<R>(
|
||||||
|
task: ILoadTask<LoadDataType, R>,
|
||||||
|
onLoaded: CustomLoadFunc<R>
|
||||||
|
): Promise<R> {
|
||||||
|
this.progress.addTask(task);
|
||||||
|
const { promise, resolve } = Promise.withResolvers<R>();
|
||||||
|
const store: LoadTaskStore<LoadDataType, R> = {
|
||||||
|
task,
|
||||||
|
onLoaded,
|
||||||
|
loadPromise: promise,
|
||||||
|
loadResolve: resolve
|
||||||
|
};
|
||||||
|
this.tasks.add(store);
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
async load(): Promise<void> {
|
||||||
|
const tasks = [...this.tasks].map(async task => {
|
||||||
|
task.task.start();
|
||||||
|
const data = await task.task.loaded();
|
||||||
|
await task.onLoaded(data);
|
||||||
|
task.loadResolve(data);
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
await Promise.all(tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user