mirror of
https://github.com/unanmed/HumanBreak.git
synced 2026-11-15 11:24:31 +08:00
refactor: 把渲染系统迁移到 monorepo
This commit is contained in:
1 parent
8cc0c76846
commit
1556e2c3ea
108 files changed
+756
-411
No files matched your search
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { logger } from '@/core/common/logger';
|
||||
import { logger } from '@motajs/common';
|
||||
|
||||
export const enum PatchClass {
|
||||
Actions,
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
import { debounce } from 'lodash-es';
|
||||
import logInfo from '../../data/logger.json';
|
||||
|
||||
export const enum LogLevel {
|
||||
/** 输出所有,包括日志 */
|
||||
LOG,
|
||||
/** 报错、严重警告和警告 */
|
||||
WARNING,
|
||||
/** 仅报错 */
|
||||
ERROR
|
||||
}
|
||||
|
||||
interface LoggerCatchInfo {
|
||||
code?: number;
|
||||
level: LogLevel;
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface LoggerCatchReturns<T> {
|
||||
ret: T;
|
||||
info: LoggerCatchInfo[];
|
||||
}
|
||||
|
||||
let logTip: HTMLSpanElement;
|
||||
if (!main.replayChecking) {
|
||||
const tip = document.createElement('span');
|
||||
logTip = tip;
|
||||
tip.style.position = 'fixed';
|
||||
tip.style.right = '0';
|
||||
tip.style.bottom = '0';
|
||||
tip.style.height = '20px';
|
||||
tip.style.width = 'auto';
|
||||
tip.style.textAlign = 'right';
|
||||
tip.style.padding = '0 5px';
|
||||
tip.style.fontSize = '16px';
|
||||
tip.style.fontFamily = 'Arial';
|
||||
tip.style.display = 'none';
|
||||
tip.style.margin = '2px';
|
||||
document.body.appendChild(tip);
|
||||
}
|
||||
|
||||
const hideTipText = debounce(() => {
|
||||
if (main.replayChecking) return;
|
||||
logTip.style.display = 'none';
|
||||
}, 5000);
|
||||
|
||||
const nums = new Set(['1', '2', '3', '4', '5', '6', '7', '8', '9', '0']);
|
||||
|
||||
const logError = logInfo.error as Record<number, string>;
|
||||
const logWarn = logInfo.warn as Record<number, string>;
|
||||
|
||||
export class Logger {
|
||||
level: LogLevel = LogLevel.LOG;
|
||||
enabled: boolean = true;
|
||||
|
||||
private catching: boolean = false;
|
||||
private catchedInfo: LoggerCatchInfo[] = [];
|
||||
|
||||
private catchStack: LoggerCatchInfo[][] = [];
|
||||
|
||||
constructor(logLevel: LogLevel) {
|
||||
this.level = logLevel;
|
||||
}
|
||||
|
||||
private parseInfo(text: string, ...params: string[]) {
|
||||
let pointer = -1;
|
||||
let str = '';
|
||||
|
||||
let inParam = false;
|
||||
let paramNum = '';
|
||||
while (++pointer < text.length) {
|
||||
const char = text[pointer];
|
||||
|
||||
if (char === '$' && text[pointer - 1] !== '\\') {
|
||||
inParam = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inParam) {
|
||||
if (nums.has(char)) {
|
||||
paramNum += char;
|
||||
}
|
||||
if (!nums.has(text[pointer + 1])) {
|
||||
inParam = false;
|
||||
const num = Number(paramNum);
|
||||
paramNum = '';
|
||||
str += params[num - 1] ?? '[not delivered]';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
str += char;
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置该日志类的输出等级
|
||||
* @param level 要设置为的输出等级
|
||||
*/
|
||||
setLogLevel(level: LogLevel) {
|
||||
this.level = level;
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出报错信息
|
||||
* @param code 错误代码,每个错误都应当使用唯一的错误代码
|
||||
* @param params 参数
|
||||
*/
|
||||
error(code: number, ...params: string[]) {
|
||||
const info = logError[code];
|
||||
if (!info) {
|
||||
logger.error(16, 'error', code.toString());
|
||||
return;
|
||||
}
|
||||
const text = this.parseInfo(logError[code], ...params);
|
||||
if (this.catching) {
|
||||
this.catchedInfo.push({
|
||||
level: LogLevel.ERROR,
|
||||
message: text,
|
||||
code
|
||||
});
|
||||
}
|
||||
if (this.level <= LogLevel.ERROR && this.enabled) {
|
||||
if (!main.replayChecking) {
|
||||
logTip.style.color = 'lightcoral';
|
||||
logTip.style.display = 'block';
|
||||
logTip.textContent = `Error thrown, please check in console.`;
|
||||
hideTipText();
|
||||
}
|
||||
console.error(`[ERROR Code ${code}] ${text}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出警告信息
|
||||
* @param code 警告代码
|
||||
* @param text 警告信息
|
||||
*/
|
||||
warn(code: number, ...params: string[]) {
|
||||
const info = logWarn[code];
|
||||
if (!info) {
|
||||
logger.error(16, 'warn', code.toString());
|
||||
return;
|
||||
}
|
||||
const text = this.parseInfo(logWarn[code], ...params);
|
||||
if (this.catching) {
|
||||
this.catchedInfo.push({
|
||||
level: LogLevel.ERROR,
|
||||
message: text,
|
||||
code
|
||||
});
|
||||
}
|
||||
if (this.level <= LogLevel.WARNING && this.enabled) {
|
||||
console.warn(`[WARNING Code ${code}] ${text}`);
|
||||
if (!main.replayChecking) {
|
||||
logTip.style.color = 'gold';
|
||||
logTip.style.display = 'block';
|
||||
logTip.textContent = `Warning thrown, please check in console.`;
|
||||
hideTipText();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出日志
|
||||
* @param text 日志信息
|
||||
*/
|
||||
log(text: string) {
|
||||
if (this.catching) {
|
||||
this.catchedInfo.push({
|
||||
level: LogLevel.ERROR,
|
||||
message: text
|
||||
});
|
||||
}
|
||||
if (this.level <= LogLevel.LOG && this.enabled) {
|
||||
console.log(`[LOG] ${text}`);
|
||||
}
|
||||
}
|
||||
|
||||
catch<T>(fn: () => T): LoggerCatchReturns<T> {
|
||||
const before = this.enabled;
|
||||
this.catchedInfo = [];
|
||||
this.catchStack.push(this.catchedInfo);
|
||||
this.disable();
|
||||
this.catching = true;
|
||||
const ret = fn();
|
||||
this.catching = false;
|
||||
if (before) this.enable();
|
||||
|
||||
this.catchStack.pop();
|
||||
const last = this.catchStack?.at(-1);
|
||||
const info = this.catchedInfo;
|
||||
this.catchedInfo = last ?? [];
|
||||
|
||||
return { ret, info };
|
||||
}
|
||||
|
||||
disable() {
|
||||
this.enabled = false;
|
||||
}
|
||||
|
||||
enable() {
|
||||
this.enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
export const logger = new Logger(LogLevel.LOG);
|
||||
@@ -1,6 +1,6 @@
|
||||
import axios, { AxiosRequestConfig, ResponseType } from 'axios';
|
||||
import { Disposable } from './disposable';
|
||||
import { logger } from './logger';
|
||||
import { logger } from '@motajs/common';
|
||||
import JSZip from 'jszip';
|
||||
import { EventEmitter } from './eventEmitter';
|
||||
|
||||
|
||||
@@ -1,214 +0,0 @@
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import { logger } from '../common/logger';
|
||||
|
||||
interface OffscreenCanvasEvent {
|
||||
/** 当被动触发resize时(例如core.domStyle.scale变化、窗口大小变化)时触发,使用size函数并不会触发 */
|
||||
resize: [];
|
||||
}
|
||||
|
||||
export class MotaOffscreenCanvas2D extends EventEmitter<OffscreenCanvasEvent> {
|
||||
static list: Set<MotaOffscreenCanvas2D> = new Set();
|
||||
|
||||
canvas: HTMLCanvasElement;
|
||||
ctx: CanvasRenderingContext2D;
|
||||
|
||||
width: number;
|
||||
height: number;
|
||||
|
||||
/** 是否自动跟随样板的core.domStyle.scale进行缩放 */
|
||||
autoScale: boolean = false;
|
||||
/** 是否是高清画布 */
|
||||
highResolution: boolean = true;
|
||||
/** 是否启用抗锯齿 */
|
||||
antiAliasing: boolean = true;
|
||||
|
||||
scale: number = 1;
|
||||
|
||||
/** 更新标识符,如果发生变化则说明画布被动清空 */
|
||||
symbol: number = 0;
|
||||
|
||||
private _freezed: boolean = false;
|
||||
/** 当前画布是否被冻结 */
|
||||
get freezed() {
|
||||
return this._freezed;
|
||||
}
|
||||
|
||||
private _active: boolean = true;
|
||||
get active() {
|
||||
return this._active;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个新的离屏画布\
|
||||
* **注意**:如果你在自定义渲染元素中使用,请避免使用此构造函数,而应该使用 `RenderItem.requireCanvas`
|
||||
* @param alpha 是否启用透明度通道
|
||||
* @param canvas 指定画布,不指定时会自动创建一个新画布
|
||||
*/
|
||||
constructor(alpha: boolean = true, canvas?: HTMLCanvasElement) {
|
||||
super();
|
||||
// console.trace();
|
||||
|
||||
this.canvas = canvas ?? document.createElement('canvas');
|
||||
this.ctx = this.canvas.getContext('2d', { alpha })!;
|
||||
this.width = this.canvas.width / devicePixelRatio;
|
||||
this.height = this.canvas.height / devicePixelRatio;
|
||||
|
||||
MotaOffscreenCanvas2D.list.add(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置画布的大小
|
||||
*/
|
||||
size(width: number, height: number) {
|
||||
if (this._freezed) {
|
||||
logger.warn(33);
|
||||
return;
|
||||
}
|
||||
const w = Math.max(width, 1);
|
||||
const h = Math.max(height, 1);
|
||||
let ratio = this.highResolution ? devicePixelRatio : 1;
|
||||
const scale = core.domStyle.scale;
|
||||
if (this.autoScale) {
|
||||
ratio *= scale;
|
||||
}
|
||||
this.scale = ratio;
|
||||
this.canvas.width = w * ratio;
|
||||
this.canvas.height = h * ratio;
|
||||
this.width = w;
|
||||
this.height = height;
|
||||
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
this.ctx.scale(ratio, ratio);
|
||||
this.ctx.imageSmoothingEnabled = this.antiAliasing;
|
||||
if (this.canvas.isConnected) {
|
||||
this.canvas.style.width = `${w * scale}px`;
|
||||
this.canvas.style.height = `${h * scale}px`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前画布是否跟随样板的 core.domStyle.scale 一同进行缩放
|
||||
*/
|
||||
withGameScale(auto: boolean) {
|
||||
if (this._freezed) {
|
||||
logger.warn(33);
|
||||
return;
|
||||
}
|
||||
this.autoScale = auto;
|
||||
this.size(this.width, this.height);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前画布是否为高清画布
|
||||
*/
|
||||
setHD(hd: boolean) {
|
||||
if (this._freezed) {
|
||||
logger.warn(33);
|
||||
return;
|
||||
}
|
||||
this.highResolution = hd;
|
||||
this.size(this.width, this.height);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前画布的抗锯齿设置
|
||||
*/
|
||||
setAntiAliasing(anti: boolean) {
|
||||
if (this._freezed) {
|
||||
logger.warn(33);
|
||||
return;
|
||||
}
|
||||
this.antiAliasing = anti;
|
||||
this.ctx.imageSmoothingEnabled = anti;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空画布
|
||||
*/
|
||||
clear() {
|
||||
if (this._freezed) {
|
||||
logger.warn(33);
|
||||
return;
|
||||
}
|
||||
this.ctx.save();
|
||||
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
this.ctx.restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除这个画布
|
||||
*/
|
||||
delete() {
|
||||
this.canvas.remove();
|
||||
this.ctx.reset();
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
this._freezed = true;
|
||||
MotaOffscreenCanvas2D.list.delete(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 冻结这个画布的属性,之后便不能被修改,同时会从画布列表中删去。
|
||||
*/
|
||||
freeze() {
|
||||
this._freezed = true;
|
||||
MotaOffscreenCanvas2D.list.delete(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使此画布生效,使用前请务必调用此函数,生效后会跟随游戏的放缩比例更改大小,但会导致不会被垃圾回收
|
||||
*/
|
||||
activate() {
|
||||
if (this._active || this._freezed) return;
|
||||
MotaOffscreenCanvas2D.list.add(this);
|
||||
if (this.autoScale) {
|
||||
this.size(this.width, this.height);
|
||||
this.symbol++;
|
||||
this.emit('resize');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使此画布失效,当这个画布暂时不会被使用时请务必调用此函数,失效后若没有对此画布的引用,那么会自动垃圾回收
|
||||
*/
|
||||
deactivate() {
|
||||
if (!this._active || this._freezed) return;
|
||||
MotaOffscreenCanvas2D.list.delete(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制一个离屏Canvas2D对象,一般用于缓存等操作
|
||||
* @param canvas 被复制的MotaOffscreenCanvas2D对象
|
||||
* @returns 复制结果,注意复制结果是被冻结的,无法进行大小等的修改,但是可以继续绘制
|
||||
*/
|
||||
static clone(canvas: MotaOffscreenCanvas2D): MotaOffscreenCanvas2D {
|
||||
const newCanvas = new MotaOffscreenCanvas2D();
|
||||
newCanvas.setHD(canvas.highResolution);
|
||||
newCanvas.withGameScale(canvas.autoScale);
|
||||
newCanvas.size(canvas.width, canvas.height);
|
||||
newCanvas.ctx.drawImage(
|
||||
canvas.canvas,
|
||||
0,
|
||||
0,
|
||||
canvas.width,
|
||||
canvas.height
|
||||
);
|
||||
newCanvas.freeze();
|
||||
return newCanvas;
|
||||
}
|
||||
|
||||
static refreshAll(force: boolean = false) {
|
||||
this.list.forEach(v => {
|
||||
if (force || v.autoScale) {
|
||||
v.size(v.width, v.height);
|
||||
v.symbol++;
|
||||
v.emit('resize');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
requestAnimationFrame(() => {
|
||||
MotaOffscreenCanvas2D.refreshAll();
|
||||
});
|
||||
});
|
||||
@@ -1,21 +1,17 @@
|
||||
import { mat4 } from 'gl-matrix';
|
||||
import { logger } from '../common/logger';
|
||||
import { logger } from '@motajs/common';
|
||||
import {
|
||||
WebGLColorArray,
|
||||
createProgram,
|
||||
isWebGL2Supported
|
||||
} from './webgl';
|
||||
import { ILayerRenderExtends, Layer } from '../render/preset/layer';
|
||||
import { HeroRenderer } from '../render/preset/hero';
|
||||
import { Sprite } from '../render/sprite';
|
||||
import { ILayerRenderExtends, Layer, HeroRenderer, Sprite } from '@motajs/render';
|
||||
|
||||
/**
|
||||
* 最大光源数量,必须设置,且光源数不能超过这个值,这个值决定了会预留多少的缓冲区,因此最好尽可能小,同时游戏过程中不可修改
|
||||
* 这个值越大,对显卡尤其是显存的要求会越大,不过考虑到各种设备的性能差异,不建议超过10
|
||||
*/
|
||||
const MAX_LIGHT_NUM = 5;
|
||||
/** 阴影层的Z值 */
|
||||
const Z_INDEX = 55;
|
||||
// 我也不知道这个数怎么来的,试出来是这个,别动就行
|
||||
const FOVY = Math.PI / 2;
|
||||
const ignore: Set<AllNumbers> = new Set([660, 661]);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ensureArray, tip } from '@/plugin/utils';
|
||||
import { sleep } from 'mutate-animate';
|
||||
import { logger } from '../common/logger';
|
||||
import { logger } from '@motajs/common';
|
||||
|
||||
const { gl, gl2 } = checkSupport();
|
||||
|
||||
|
||||
+3
-29
@@ -56,25 +56,13 @@ import EnemyTarget from '@/panel/enemyTarget.vue';
|
||||
import KeyboardPanel from '@/panel/keyboard.vue';
|
||||
import { MCGenerator } from './main/layout';
|
||||
import { ResourceController } from './loader/controller';
|
||||
import { logger } from './common/logger';
|
||||
import { logger } from '@motajs/common';
|
||||
import { Danmaku } from './main/custom/danmaku';
|
||||
import * as Shadow from './fx/shadow';
|
||||
import { MotaRenderer } from './render/render';
|
||||
import { Container } from './render/container';
|
||||
import { Sprite } from './render/sprite';
|
||||
import { Image, Text } from './render/preset/misc';
|
||||
import { RenderItem } from './render/item';
|
||||
import { texture } from './render/cache';
|
||||
import { RenderAdapter } from './render/adapter';
|
||||
import { Layer } from './render/preset/layer';
|
||||
import { LayerGroupFloorBinder } from './render/preset/floor';
|
||||
import { Render } from '@motajs/client';
|
||||
import { HeroKeyMover } from './main/action/move';
|
||||
import { Camera } from './render/camera';
|
||||
import * as Animation from 'mutate-animate';
|
||||
import './render/index';
|
||||
import * as RenderUtils from './render/utils';
|
||||
import '@/module';
|
||||
import { MotaOffscreenCanvas2D } from './fx/canvas2d';
|
||||
|
||||
// ----- 类注册
|
||||
Mota.register('class', 'CustomToolbar', CustomToolbar);
|
||||
@@ -143,21 +131,7 @@ Mota.register('module', 'UIComponents', {
|
||||
Mota.register('module', 'MCGenerator', MCGenerator);
|
||||
Mota.register('module', 'Shadow', Shadow);
|
||||
Mota.register('module', 'Effect', {});
|
||||
Mota.register('module', 'Render', {
|
||||
texture,
|
||||
MotaRenderer,
|
||||
Container,
|
||||
Sprite,
|
||||
Text,
|
||||
Image,
|
||||
RenderItem,
|
||||
RenderAdapter,
|
||||
Layer,
|
||||
LayerGroupFloorBinder,
|
||||
Camera,
|
||||
MotaOffscreenCanvas2D,
|
||||
Utils: RenderUtils
|
||||
});
|
||||
Mota.register('module', 'Render', Render);
|
||||
Mota.register('module', 'Action', {
|
||||
HeroKeyMover
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import BoxAnimate from '@/components/boxAnimate.vue';
|
||||
import { EventEmitter } from '@/core/common/eventEmitter';
|
||||
import { logger } from '@/core/common/logger';
|
||||
import { logger } from '@motajs/common';
|
||||
import { ResponseBase } from '@/core/interface';
|
||||
import {
|
||||
deleteWith,
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
type AdapterFunction<T> = (item: T, ...params: any[]) => Promise<any>;
|
||||
type SyncAdapterFunction<T> = (item: T, ...params: any[]) => any;
|
||||
type GlobalAdapterFunction = (...params: any[]) => Promise<any>;
|
||||
|
||||
/**
|
||||
* 渲染适配器,用作渲染层与数据层沟通的桥梁,用于在数据层等待渲染层执行,常用与动画等。
|
||||
* 例如移动图块,移动图块操作先由数据层执行,删除原有图块,然后通知渲染层进行移动动画,
|
||||
* 而移动动画是耗时的,因此需要通过本类进行适配,实现等待移动动画执行完毕,然后进行后续操作
|
||||
*/
|
||||
export class RenderAdapter<T> {
|
||||
static adapters: Map<string, RenderAdapter<any>> = new Map();
|
||||
|
||||
/** 所有元素的集合 */
|
||||
items: Set<T> = new Set();
|
||||
/** 适配器的id */
|
||||
id: string;
|
||||
|
||||
private execute: Map<string, AdapterFunction<T>> = new Map();
|
||||
private syncExecutes: Map<string, SyncAdapterFunction<T>> = new Map();
|
||||
private globalExecutes: Map<string, GlobalAdapterFunction> = new Map();
|
||||
|
||||
constructor(id: string) {
|
||||
this.id = id;
|
||||
RenderAdapter.adapters.set(id, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加一个元素
|
||||
*/
|
||||
add(item: T) {
|
||||
this.items.add(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除一个元素
|
||||
*/
|
||||
remove(item: T) {
|
||||
this.items.delete(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义一个类似于静态函数的函数,只会调用一次,不会对每个元素执行
|
||||
* @param id 函数名称
|
||||
* @param fn 执行的函数
|
||||
*/
|
||||
receiveGlobal(id: string, fn: GlobalAdapterFunction) {
|
||||
this.globalExecutes.set(id, fn);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置执行函数
|
||||
* @param fn 对于每个元素执行的函数
|
||||
*/
|
||||
receive(id: string, fn: AdapterFunction<T>): void {
|
||||
this.execute.set(id, fn);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置同步执行函数
|
||||
* @param fn 对于每个元素执行的函数
|
||||
*/
|
||||
receiveSync(id: string, fn: SyncAdapterFunction<T>): void {
|
||||
this.syncExecutes.set(id, fn);
|
||||
}
|
||||
|
||||
/**
|
||||
* 对所有元素执行函数,当所有元素都运行完毕后兑现,类似于Promise.all
|
||||
* @returns 包含每个元素运行结果的数组
|
||||
*/
|
||||
all<R = any>(fn: string, ...params: any[]): Promise<R[]> {
|
||||
const execute = this.execute.get(fn);
|
||||
|
||||
if (!execute) {
|
||||
return Promise.reject();
|
||||
} else {
|
||||
return Promise.all(
|
||||
[...this.items].map(v => execute!(v, ...params))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对所有元素执行函数,当任意一个元素运行完毕后兑现,类似于Promise.any
|
||||
* @returns 最先运行完毕的元素的结果
|
||||
*/
|
||||
any<R = any>(fn: string, ...params: any[]): Promise<R> {
|
||||
const execute = this.execute.get(fn);
|
||||
if (!execute) {
|
||||
return Promise.reject();
|
||||
} else {
|
||||
return Promise.any(
|
||||
[...this.items].map(v => execute!(v, ...params))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对所有元素执行同步函数
|
||||
* @returns 包含每个元素运行结果的数组
|
||||
*/
|
||||
sync<R = any>(fn: string, ...params: any[]): R[] {
|
||||
const execute = this.syncExecutes.get(fn);
|
||||
if (!execute) {
|
||||
return [];
|
||||
} else {
|
||||
return [...this.items].map(v => execute!(v, ...params));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用一个全局函数
|
||||
*/
|
||||
global<R = any>(id: string, ...params: any[]): Promise<R> {
|
||||
const execute = this.globalExecutes.get(id);
|
||||
if (!execute) {
|
||||
return Promise.reject();
|
||||
} else {
|
||||
return execute(...params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁这个adapter
|
||||
*/
|
||||
destroy() {
|
||||
RenderAdapter.adapters.delete(this.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取适配器
|
||||
*/
|
||||
static get<T>(id: string): RenderAdapter<T> | undefined {
|
||||
return this.adapters.get(id);
|
||||
}
|
||||
}
|
||||
@@ -1,560 +0,0 @@
|
||||
import { EventEmitter } from '../common/eventEmitter';
|
||||
import { logger } from '../common/logger';
|
||||
import { MotaOffscreenCanvas2D } from '../fx/canvas2d';
|
||||
import { SizedCanvasImageSource } from './preset/misc';
|
||||
|
||||
// 经过测试(https://www.measurethat.net/Benchmarks/Show/30741/1/drawimage-img-vs-canvas-vs-bitmap-cropping-fix-loading)
|
||||
// 得出结论,ImageBitmap和Canvas的绘制性能不如Image,于是直接画Image就行,所以缓存基本上就是存Image
|
||||
|
||||
type ImageMapKeys = Exclude<Cls, 'tileset' | 'autotile'>;
|
||||
type ImageMap = Record<ImageMapKeys, HTMLImageElement>;
|
||||
|
||||
const i = (img: ImageMapKeys) => {
|
||||
return core.material.images[img];
|
||||
};
|
||||
|
||||
const imageMap: Partial<ImageMap> = {};
|
||||
|
||||
Mota.require('var', 'loading').once('loaded', () => {
|
||||
[
|
||||
'enemys',
|
||||
'enemy48',
|
||||
'npcs',
|
||||
'npc48',
|
||||
'terrains',
|
||||
'items',
|
||||
'animates'
|
||||
].forEach(v => (imageMap[v as ImageMapKeys] = i(v as ImageMapKeys)));
|
||||
});
|
||||
|
||||
interface AutotileCache {
|
||||
parent?: Set<AllNumbersOf<'autotile'>>;
|
||||
frame: number;
|
||||
cache: Record<string, HTMLCanvasElement>;
|
||||
}
|
||||
type AutotileCaches = Record<AllNumbersOf<'autotile'>, AutotileCache>;
|
||||
|
||||
interface TextureRequire {
|
||||
tileset: Record<string, HTMLImageElement>;
|
||||
material: Record<ImageMapKeys, HTMLImageElement>;
|
||||
autotile: AutotileCaches;
|
||||
images: Record<ImageIds, HTMLImageElement>;
|
||||
}
|
||||
|
||||
interface RenderableDataBase {
|
||||
/** 图块的总帧数 */
|
||||
frame: number;
|
||||
/** 对应图块属性的动画帧数,-1表示没有设定,0表示第一帧 */
|
||||
animate: number;
|
||||
/** 是否是大怪物 */
|
||||
bigImage: boolean;
|
||||
render: [x: number, y: number, width: number, height: number][];
|
||||
}
|
||||
|
||||
export interface RenderableData extends RenderableDataBase {
|
||||
image: SizedCanvasImageSource;
|
||||
autotile: false;
|
||||
}
|
||||
|
||||
export interface AutotileRenderable extends RenderableDataBase {
|
||||
image: Record<string, SizedCanvasImageSource>;
|
||||
autotile: true;
|
||||
bigImage: false;
|
||||
}
|
||||
|
||||
interface TextureCacheEvent {}
|
||||
|
||||
class TextureCache extends EventEmitter<TextureCacheEvent> {
|
||||
tileset!: Record<string, HTMLImageElement>;
|
||||
material: Record<ImageMapKeys, HTMLImageElement>;
|
||||
autotile!: AutotileCaches;
|
||||
images!: Record<ImageIds, HTMLImageElement>;
|
||||
|
||||
idNumberMap!: IdToNumber;
|
||||
|
||||
/** 渲染信息 */
|
||||
renderable: Map<number, RenderableData | AutotileRenderable> = new Map();
|
||||
/** 自动元件额外连接信息,用于对非自身图块进行连接 */
|
||||
autoConn: Map<number, Set<number>> = new Map();
|
||||
/** 行走图朝向绑定 */
|
||||
characterDirection: Record<Dir2, number> = {
|
||||
down: 0,
|
||||
left: 1,
|
||||
right: 2,
|
||||
up: 3,
|
||||
leftup: 1,
|
||||
leftdown: 1,
|
||||
rightup: 2,
|
||||
rightdown: 2
|
||||
};
|
||||
/** 行走图转向顺序 */
|
||||
characterTurn: Dir[] = ['up', 'right', 'down', 'left'];
|
||||
characterTurn2: Dir2[] = ['leftup', 'rightup', 'rightdown', 'leftdown'];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.material = imageMap as Record<ImageMapKeys, HTMLImageElement>;
|
||||
|
||||
Mota.require('var', 'loading').once('loaded', () => {
|
||||
const map = maps_90f36752_8815_4be8_b32b_d7fad1d0542e;
|
||||
// @ts-expect-error 无法推导
|
||||
this.idNumberMap = {};
|
||||
for (const [key, { id }] of Object.entries(map)) {
|
||||
// @ts-expect-error 无法推导
|
||||
this.idNumberMap[id] = parseInt(key) as AllNumbers;
|
||||
}
|
||||
this.tileset = core.material.images.tilesets;
|
||||
this.autotile = splitAutotiles(this.idNumberMap);
|
||||
this.images = core.material.images.images;
|
||||
this.calRenderable();
|
||||
this.calAutotileConnections();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取纹理
|
||||
* @param type 纹理类型
|
||||
* @param key 纹理名称
|
||||
*/
|
||||
require<T extends keyof TextureRequire, K extends keyof TextureRequire[T]>(
|
||||
type: T,
|
||||
key: K
|
||||
): TextureRequire[T][K] {
|
||||
return this[type][key];
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算每个图块的可渲染信息
|
||||
*/
|
||||
private calRenderable() {
|
||||
const map = maps_90f36752_8815_4be8_b32b_d7fad1d0542e;
|
||||
for (const key of Object.keys(map)) {
|
||||
this.calRenderableByNum(parseInt(key));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据图块数字计算出它的渲染信息
|
||||
* @param num 图块数字
|
||||
*/
|
||||
calRenderableByNum(
|
||||
num: number
|
||||
): RenderableData | AutotileRenderable | null {
|
||||
const map = maps_90f36752_8815_4be8_b32b_d7fad1d0542e;
|
||||
const enemys = enemys_fcae963b_31c9_42b4_b48c_bb48d09f3f80;
|
||||
const icons = icons_4665ee12_3a1f_44a4_bea3_0fccba634dc1;
|
||||
|
||||
/** 特判空图块与空气墙 */
|
||||
if (num === 0 || num === 17) return null;
|
||||
|
||||
// 额外素材
|
||||
if (num >= 10000) {
|
||||
const offset = core.getTilesetOffset(num);
|
||||
if (!offset) return null;
|
||||
const { image, x, y } = offset;
|
||||
const data: RenderableData = {
|
||||
image: this.tileset[image],
|
||||
frame: 1,
|
||||
render: [[x * 32, y * 32, 32, 32]],
|
||||
animate: 0,
|
||||
autotile: false,
|
||||
bigImage: false
|
||||
};
|
||||
this.renderable.set(num, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
const data = map[num as Exclude<AllNumbers, 0>];
|
||||
// 地狱般的分支if
|
||||
if (data) {
|
||||
let { faceIds, bigImage } = data;
|
||||
const { cls, id, animate } = data;
|
||||
if (cls === 'enemys' || cls === 'enemy48') {
|
||||
// 怪物需要特殊处理,因为它的大怪物信息不在 maps 里面
|
||||
({ bigImage, faceIds } = enemys[id as EnemyIds]);
|
||||
}
|
||||
if (bigImage) {
|
||||
const image = core.material.images.images[bigImage];
|
||||
if (!image) {
|
||||
logger.warn(10, id);
|
||||
return null;
|
||||
}
|
||||
let line = 0;
|
||||
if (faceIds) {
|
||||
const arr = ['down', 'left', 'right', 'up'];
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
if (faceIds[arr[i] as Dir] === id) {
|
||||
line = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const totalLines = image.width / image.height >= 2 ? 1 : 4;
|
||||
const w = Math.round(image.width / 4);
|
||||
const h = Math.round(image.height / totalLines);
|
||||
const y = h * line;
|
||||
const data: RenderableData = {
|
||||
image,
|
||||
frame: 4,
|
||||
render: [
|
||||
[0, y, w, h],
|
||||
[w, y, w, h],
|
||||
[w * 2, y, w, h],
|
||||
[w * 3, y, w, h]
|
||||
],
|
||||
animate: (animate ?? 0) - 1,
|
||||
autotile: false,
|
||||
bigImage: true
|
||||
};
|
||||
this.renderable.set(num, data);
|
||||
return data;
|
||||
}
|
||||
// enemy48和npc48都应该视为大怪物
|
||||
if (cls === 'enemy48' || cls === 'npc48') {
|
||||
const img = core.material.images[cls];
|
||||
if (!img) return null;
|
||||
// @ts-expect-error 无法推导
|
||||
const line = icons[cls][id];
|
||||
const w = 32;
|
||||
const h = 48;
|
||||
const y = h * line;
|
||||
const data: RenderableData = {
|
||||
image: img,
|
||||
frame: 4,
|
||||
render: [
|
||||
[0, y, w, h],
|
||||
[w, y, w, h],
|
||||
[w * 2, y, w, h],
|
||||
[w * 3, y, w, h]
|
||||
],
|
||||
animate: (animate ?? 0) - 1,
|
||||
autotile: false,
|
||||
bigImage: true
|
||||
};
|
||||
this.renderable.set(num, data);
|
||||
return data;
|
||||
}
|
||||
// 自动元件
|
||||
if (cls === 'autotile') {
|
||||
if (!this.autotile) return null;
|
||||
const auto = this.autotile[num as AllNumbersOf<'autotile'>];
|
||||
const cell = 32;
|
||||
const render: [number, number, number, number][] = [];
|
||||
if (auto.frame >= 1) {
|
||||
render.push([0, 0, cell, cell]);
|
||||
}
|
||||
if (auto.frame >= 3) {
|
||||
render.push(
|
||||
[cell, 0, cell, cell],
|
||||
[cell * 2, 0, cell, cell]
|
||||
);
|
||||
}
|
||||
if (auto.frame >= 4) {
|
||||
render.push([cell * 3, 0, cell, cell]);
|
||||
}
|
||||
const data: AutotileRenderable = {
|
||||
image: auto.cache,
|
||||
frame: auto.frame,
|
||||
render,
|
||||
autotile: true,
|
||||
bigImage: false,
|
||||
animate: (animate ?? 0) - 1
|
||||
};
|
||||
this.renderable.set(num, data);
|
||||
return data;
|
||||
} else {
|
||||
const image =
|
||||
core.material.images[
|
||||
cls as Exclude<Cls, 'tileset' | 'autotile'>
|
||||
];
|
||||
if (!image) return null;
|
||||
const frame = core.getAnimateFrames(cls);
|
||||
const cell = 32;
|
||||
// @ts-expect-error 无法推导
|
||||
const offset = (icons[cls][id] as number) * cell;
|
||||
const render: [number, number, number, number][] = [
|
||||
[0, offset, cell, cell]
|
||||
];
|
||||
if (frame === 2) {
|
||||
render.push([cell, offset, cell, cell]);
|
||||
}
|
||||
if (frame === 4) {
|
||||
render.push(
|
||||
[cell, offset, cell, cell],
|
||||
[cell * 2, offset, cell, cell],
|
||||
[cell * 3, offset, cell, cell]
|
||||
);
|
||||
}
|
||||
const data: RenderableData = {
|
||||
image,
|
||||
frame: frame,
|
||||
render,
|
||||
autotile: false,
|
||||
bigImage: false,
|
||||
animate: (animate ?? 0) - 1
|
||||
};
|
||||
this.renderable.set(num, data);
|
||||
return data;
|
||||
}
|
||||
} else {
|
||||
logger.warn(11, num.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一个图块的渲染信息,自动元件会特别标明autotile属性
|
||||
* @param num 图块数字
|
||||
*/
|
||||
getRenderable(num: number) {
|
||||
return this.renderable.get(num) ?? this.calRenderableByNum(num);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算自动元件的额外连接
|
||||
*/
|
||||
private calAutotileConnections() {
|
||||
const icons = icons_4665ee12_3a1f_44a4_bea3_0fccba634dc1;
|
||||
const maps = maps_90f36752_8815_4be8_b32b_d7fad1d0542e;
|
||||
|
||||
Object.keys(icons.autotile).forEach(v => {
|
||||
const num = this.idNumberMap[v as AllIdsOf<'autotile'>];
|
||||
const data = maps[num];
|
||||
const { autotileConnection } = data;
|
||||
if (!autotileConnection) return;
|
||||
const list = new Set<AllNumbers>();
|
||||
autotileConnection.forEach(v => {
|
||||
if (typeof v === 'number') {
|
||||
list.add(v);
|
||||
} else {
|
||||
list.add(this.idNumberMap[v]);
|
||||
}
|
||||
});
|
||||
this.autoConn.set(num, list);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取自动元件的额外连接情况
|
||||
* @param num 自动元件的图块数字
|
||||
*/
|
||||
getAutotileConnections(num: number) {
|
||||
return this.autoConn.get(num);
|
||||
}
|
||||
}
|
||||
|
||||
export const texture = new TextureCache();
|
||||
|
||||
// 3x4 与 2x3 的自动元件信息
|
||||
// 将自动元件按 16x16 切分后,数组分别表示 左上 右上 右下 左下 所在图块位置
|
||||
const bigAutotile: Record<number, [number, number, number, number]> = {};
|
||||
const smallAutotile: Record<number, [number, number, number, number]> = {};
|
||||
|
||||
function getAutotileIndices() {
|
||||
// 应当从 0 - 255 进行枚举
|
||||
// 二进制从高位到低位依次是 左上 上 右上 右 右下 下 左下 左
|
||||
// 有兴趣可以研究下这个算法
|
||||
const get = (
|
||||
target: Record<number, [number, number, number, number]>,
|
||||
mode: 1 | 2
|
||||
) => {
|
||||
const h = mode === 1 ? 4 : 2;
|
||||
const v = mode === 1 ? 24 : 8;
|
||||
const luo = mode === 1 ? 12 : 8; // leftup origin
|
||||
const ruo = mode === 1 ? 17 : 11; // rightup origin
|
||||
const ldo = mode === 1 ? 42 : 20; // leftdown origin
|
||||
const rdo = mode === 1 ? 47 : 23; // rightdown origin
|
||||
const luc = mode === 1 ? 4 : 2; // leftup corner
|
||||
const ruc = mode === 1 ? 5 : 3; // rightup corner
|
||||
const rdc = mode === 1 ? 11 : 7; // rightdown corner
|
||||
const ldc = mode === 1 ? 10 : 6; // leftdown corner
|
||||
|
||||
for (let i = 0; i <= 0b11111111; i++) {
|
||||
let lu = luo; // leftup
|
||||
let ru = ruo; // rightup
|
||||
let ld = ldo; // leftdown
|
||||
let rd = rdo; // rightdown
|
||||
|
||||
// 先看四个方向,最后看斜角方向
|
||||
if (i & 0b00000001) {
|
||||
lu += h;
|
||||
ld += h;
|
||||
if (i & 0b00010000) {
|
||||
ru += h / 2;
|
||||
rd += h / 2;
|
||||
}
|
||||
}
|
||||
if (i & 0b00000100) {
|
||||
ld -= v;
|
||||
rd -= v;
|
||||
if (i & 0b01000000) {
|
||||
lu -= v / 2;
|
||||
ru -= v / 2;
|
||||
}
|
||||
}
|
||||
if (i & 0b00010000) {
|
||||
ru -= h;
|
||||
rd -= h;
|
||||
if (i & 0b00000001) {
|
||||
lu -= h / 2;
|
||||
ld -= h / 2;
|
||||
}
|
||||
}
|
||||
if (i & 0b01000000) {
|
||||
lu += v;
|
||||
ru += v;
|
||||
if (i & 0b00000100) {
|
||||
ld += v / 2;
|
||||
rd += v / 2;
|
||||
}
|
||||
}
|
||||
// 斜角
|
||||
if ((i & 0b11000001) === 0b01000001) {
|
||||
lu = luc;
|
||||
}
|
||||
if ((i & 0b01110000) === 0b01010000) {
|
||||
ru = ruc;
|
||||
}
|
||||
if ((i & 0b00011100) === 0b00010100) {
|
||||
rd = rdc;
|
||||
}
|
||||
if ((i & 0b00000111) === 0b00000101) {
|
||||
ld = ldc;
|
||||
}
|
||||
target[i] = [lu, ru, rd, ld];
|
||||
}
|
||||
};
|
||||
|
||||
get(bigAutotile, 1);
|
||||
get(smallAutotile, 2);
|
||||
}
|
||||
getAutotileIndices();
|
||||
|
||||
function getRepeatMap() {
|
||||
// 实际上3x4与2x3的重复映射是一致的,因此只需要计算一个就行了
|
||||
// 这里使用2x3的进行计算
|
||||
const calculated: Record<string, number> = {};
|
||||
const repeatMap: Record<number, number> = {};
|
||||
for (const [num, [lu, ru, rd, ld]] of Object.entries(smallAutotile)) {
|
||||
const n = lu + ru * 24 + rd * 24 ** 2 + ld * 24 ** 3;
|
||||
calculated[num] = n;
|
||||
for (const [num2, n2] of Object.entries(calculated)) {
|
||||
if (n2 === n) {
|
||||
repeatMap[Number(num)] = Number(num2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return repeatMap;
|
||||
}
|
||||
|
||||
function splitAutotiles(map: IdToNumber): AutotileCaches {
|
||||
const cache: Partial<AutotileCaches> = {};
|
||||
/** 重复映射,由于自动元件只有48种,其余的208种是重复的,因此需要获取重复映射 */
|
||||
const repeatMap: Record<number, number> = getRepeatMap();
|
||||
/** 每个自动元件左上角32*32的内容,用于判断父子关系 */
|
||||
const masterMap: Partial<Record<AllNumbersOf<'autotile'>, string>> = {};
|
||||
|
||||
for (const [key, img] of Object.entries(core.material.images.autotile)) {
|
||||
const auto = map[key as AllIdsOf<'autotile'>];
|
||||
|
||||
// 判断自动元件类型
|
||||
let mode: 1 | 2 = 1;
|
||||
let frame = 1;
|
||||
if (img.width === 384) {
|
||||
frame = 4;
|
||||
} else if (img.width === 192) {
|
||||
mode = 2;
|
||||
frame = 3;
|
||||
} else if (img.width === 64) {
|
||||
mode = 2;
|
||||
}
|
||||
|
||||
cache[auto] = {
|
||||
frame,
|
||||
cache: {}
|
||||
};
|
||||
|
||||
// 父子关系,截取本图块的左上角存入map
|
||||
const master = new MotaOffscreenCanvas2D();
|
||||
master.setHD(false);
|
||||
master.setAntiAliasing(false);
|
||||
master.withGameScale(false);
|
||||
master.size(32, 32);
|
||||
master.ctx.drawImage(img, 0, 0, 32, 32, 0, 0, 32, 32);
|
||||
masterMap[auto] = master.canvas.toDataURL('image/png');
|
||||
master.delete();
|
||||
|
||||
// 自动图块的绘制信息
|
||||
for (let i = 0; i <= 0b11111111; i++) {
|
||||
const re = repeatMap[i];
|
||||
if (re) {
|
||||
const cached = cache[auto]!.cache[re];
|
||||
if (cached) {
|
||||
cache[auto]!.cache[i] = cached;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const data = (mode === 1 ? bigAutotile : smallAutotile)[i];
|
||||
const row = mode === 1 ? 6 : 4;
|
||||
const info: [number, number][] = data.map(v => [
|
||||
(v % row) * 16,
|
||||
Math.floor(v / row) * 16
|
||||
]);
|
||||
|
||||
const canvas = new MotaOffscreenCanvas2D();
|
||||
canvas.setHD(false);
|
||||
canvas.setAntiAliasing(false);
|
||||
canvas.withGameScale(false);
|
||||
canvas.size(32 * frame, 32);
|
||||
canvas.freeze();
|
||||
const ctx = canvas.ctx;
|
||||
for (let i = 0; i < frame; i++) {
|
||||
const dx = 32 * i;
|
||||
const sx1 = info[0][0] + (i * row * 32) / 2;
|
||||
const sx2 = info[1][0] + (i * row * 32) / 2;
|
||||
const sx3 = info[2][0] + (i * row * 32) / 2;
|
||||
const sx4 = info[3][0] + (i * row * 32) / 2;
|
||||
const sy1 = info[0][1];
|
||||
const sy2 = info[1][1];
|
||||
const sy3 = info[2][1];
|
||||
const sy4 = info[3][1];
|
||||
|
||||
ctx.drawImage(img, sx1, sy1, 16, 16, dx, 0, 16, 16);
|
||||
ctx.drawImage(img, sx2, sy2, 16, 16, dx + 16, 0, 16, 16);
|
||||
ctx.drawImage(img, sx3, sy3, 16, 16, dx + 16, 16, 16, 16);
|
||||
ctx.drawImage(img, sx4, sy4, 16, 16, dx, 16, 16, 16);
|
||||
}
|
||||
cache[auto]!.cache[i] = canvas.canvas;
|
||||
}
|
||||
}
|
||||
|
||||
const judge = new MotaOffscreenCanvas2D();
|
||||
judge.setHD(false);
|
||||
judge.setAntiAliasing(false);
|
||||
judge.withGameScale(false);
|
||||
judge.size(32, 32);
|
||||
// 进行父子关系判断
|
||||
for (const [key, img] of Object.entries(core.material.images.autotile)) {
|
||||
const auto = map[key as AllIdsOf<'autotile'>];
|
||||
|
||||
// 只针对3*4的图块进行,截取第一行中间的,然后判断
|
||||
judge.clear();
|
||||
judge.ctx.drawImage(img, 32, 0, 32, 32, 0, 0, 32, 32);
|
||||
const data = judge.canvas.toDataURL('image/png');
|
||||
|
||||
for (const [key, data2] of Object.entries(masterMap)) {
|
||||
const auto2 = map[key as AllIdsOf<'autotile'>];
|
||||
|
||||
if (data === data2) {
|
||||
cache[auto]!.parent ??= new Set();
|
||||
cache[auto]!.parent!.add(auto2);
|
||||
}
|
||||
}
|
||||
}
|
||||
judge.delete();
|
||||
|
||||
return cache as AutotileCaches;
|
||||
}
|
||||
@@ -1,625 +0,0 @@
|
||||
import { Animation, TimingFn, Transition } from 'mutate-animate';
|
||||
import { RenderItem } from './item';
|
||||
import { logger } from '../common/logger';
|
||||
import { Transform } from './transform';
|
||||
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;
|
||||
}
|
||||
|
||||
interface TranslateAnimation extends CameraAnimationBase {
|
||||
type: 'translate';
|
||||
timing: TimingFn;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
interface TranslateAsAnimation extends CameraAnimationBase {
|
||||
type: 'translateAs';
|
||||
timing: TimingFn<2>;
|
||||
time: number;
|
||||
}
|
||||
|
||||
interface RotateAnimation extends CameraAnimationBase {
|
||||
type: 'rotate';
|
||||
timing: TimingFn;
|
||||
angle: number;
|
||||
time: number;
|
||||
}
|
||||
|
||||
interface ScaleAnimation extends CameraAnimationBase {
|
||||
type: 'scale';
|
||||
timing: TimingFn;
|
||||
scale: number;
|
||||
time: number;
|
||||
}
|
||||
|
||||
type CameraAnimationData =
|
||||
| TranslateAnimation
|
||||
| TranslateAsAnimation
|
||||
| RotateAnimation
|
||||
| ScaleAnimation;
|
||||
|
||||
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,185 +0,0 @@
|
||||
import { MotaOffscreenCanvas2D } from '../fx/canvas2d';
|
||||
import { ActionType, EventProgress, ActionEventMap } from './event';
|
||||
import {
|
||||
ERenderItemEvent,
|
||||
IRenderChildable,
|
||||
RenderItem,
|
||||
RenderItemPosition
|
||||
} from './item';
|
||||
import { Transform } from './transform';
|
||||
|
||||
export interface EContainerEvent extends ERenderItemEvent {}
|
||||
|
||||
export class Container<E extends EContainerEvent = EContainerEvent>
|
||||
extends RenderItem<E | EContainerEvent>
|
||||
implements IRenderChildable
|
||||
{
|
||||
sortedChildren: RenderItem[] = [];
|
||||
|
||||
private needSort: boolean = false;
|
||||
|
||||
/**
|
||||
* 创建一个容器,容器中可以包含其他渲染对象
|
||||
* @param type 渲染模式,absolute表示绝对位置,static表示跟随摄像机移动
|
||||
* @param cache 是否启用缓存机制
|
||||
*/
|
||||
constructor(
|
||||
type: RenderItemPosition = 'static',
|
||||
cache: boolean = true,
|
||||
fall: boolean = false
|
||||
) {
|
||||
super(type, cache, fall);
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
protected render(
|
||||
canvas: MotaOffscreenCanvas2D,
|
||||
transform: Transform
|
||||
): void {
|
||||
this.sortedChildren.forEach(v => {
|
||||
if (v.hidden) return;
|
||||
v.renderContent(canvas, transform);
|
||||
});
|
||||
}
|
||||
|
||||
requestSort() {
|
||||
if (!this.needSort) {
|
||||
this.needSort = true;
|
||||
this.requestBeforeFrame(() => {
|
||||
this.needSort = false;
|
||||
this.sortChildren();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加子元素到这个容器上,然后在下一个tick执行更新
|
||||
* @param children 要添加的子元素
|
||||
*/
|
||||
appendChild(...children: RenderItem<any>[]) {
|
||||
children.forEach(v => {
|
||||
v.append(this);
|
||||
});
|
||||
this.requestSort();
|
||||
this.update(this);
|
||||
}
|
||||
|
||||
removeChild(...child: RenderItem<any>[]): void {
|
||||
let changed = false;
|
||||
child.forEach(v => {
|
||||
if (v.parent !== this) return;
|
||||
const success = v.remove();
|
||||
if (success) {
|
||||
changed = true;
|
||||
}
|
||||
});
|
||||
if (changed) this.requestSort();
|
||||
this.update(this);
|
||||
}
|
||||
|
||||
append(parent: RenderItem): void {
|
||||
super.append(parent);
|
||||
if (this.root) {
|
||||
const root = this.root;
|
||||
this.forEachChild(ele => {
|
||||
ele.setRoot(root);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 遍历这个元素中的每个子元素,并执行传入的函数
|
||||
* @param fn 对每个元素执行的函数
|
||||
*/
|
||||
forEachChild(fn: (ele: RenderItem) => void) {
|
||||
const stack: RenderItem[] = [this];
|
||||
while (stack.length > 0) {
|
||||
const ele = stack.pop()!;
|
||||
stack.push(...ele.children);
|
||||
fn(ele);
|
||||
}
|
||||
}
|
||||
|
||||
private sortChildren() {
|
||||
this.sortedChildren = [...this.children]
|
||||
.filter(v => !v.isComment)
|
||||
.sort((a, b) => a.zIndex - b.zIndex);
|
||||
this.update();
|
||||
}
|
||||
|
||||
protected propagateEvent<T extends ActionType>(
|
||||
type: T,
|
||||
progress: EventProgress,
|
||||
event: ActionEventMap[T]
|
||||
): void {
|
||||
const len = this.sortedChildren.length;
|
||||
if (progress === EventProgress.Capture) {
|
||||
let success = false;
|
||||
for (let i = len - 1; i >= 0; i--) {
|
||||
const child = this.sortedChildren[i];
|
||||
if (child.hidden) continue;
|
||||
if (child.captureEvent(type, event)) {
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 如果没有子元素能够触发,那么自身触发冒泡
|
||||
if (!success) {
|
||||
this.bubbleEvent(type, event);
|
||||
}
|
||||
} else {
|
||||
this.parent?.bubbleEvent(type, event);
|
||||
}
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
super.destroy();
|
||||
this.children.forEach(v => {
|
||||
v.remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export type CustomContainerRenderFn = (
|
||||
canvas: MotaOffscreenCanvas2D,
|
||||
children: RenderItem[],
|
||||
transform: Transform
|
||||
) => void;
|
||||
|
||||
export class ContainerCustom extends Container {
|
||||
private renderFn?: CustomContainerRenderFn;
|
||||
|
||||
protected render(
|
||||
canvas: MotaOffscreenCanvas2D,
|
||||
transform: Transform
|
||||
): void {
|
||||
if (!this.renderFn) {
|
||||
super.render(canvas, transform);
|
||||
} else {
|
||||
this.renderFn(canvas, this.sortedChildren, transform);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置这个自定义容器的渲染函数
|
||||
* @param render 渲染函数
|
||||
*/
|
||||
setRenderFn(render?: CustomContainerRenderFn) {
|
||||
this.renderFn = render;
|
||||
}
|
||||
|
||||
protected handleProps(
|
||||
key: string,
|
||||
prevValue: any,
|
||||
nextValue: any
|
||||
): boolean {
|
||||
switch (key) {
|
||||
case 'render': {
|
||||
if (!this.assertType(nextValue, 'function', key)) return false;
|
||||
this.setRenderFn(nextValue);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return super.handleProps(key, prevValue, nextValue);
|
||||
}
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
import type { RenderItem } from './item';
|
||||
|
||||
export const enum MouseType {
|
||||
/** 没有按键按下 */
|
||||
None = 0,
|
||||
/** 左键 */
|
||||
Left = 1 << 0,
|
||||
/** 中键,即按下滚轮 */
|
||||
Middle = 1 << 1,
|
||||
/** 右键 */
|
||||
Right = 1 << 2,
|
||||
/** 侧键后退 */
|
||||
Back = 1 << 3,
|
||||
/** 侧键前进 */
|
||||
Forward = 1 << 4
|
||||
}
|
||||
|
||||
export const enum WheelType {
|
||||
None,
|
||||
/** 以像素为单位 */
|
||||
Pixel,
|
||||
/** 以行为单位,每行长度视浏览器设置而定,约为 1rem */
|
||||
Line,
|
||||
/** 以页为单位,一般为一个屏幕高度 */
|
||||
Page
|
||||
}
|
||||
|
||||
export const enum ActionType {
|
||||
/** 点击事件,即按下与抬起都在该元素上时触发 */
|
||||
Click,
|
||||
/** 鼠标或手指按下事件 */
|
||||
Down,
|
||||
/** 鼠标或手指移动事件 */
|
||||
Move,
|
||||
/** 鼠标或手指抬起事件 */
|
||||
Up,
|
||||
/** 鼠标或手指移动入该元素时触发的事件 */
|
||||
Enter,
|
||||
/** 鼠标或手指移出该元素时触发的事件 */
|
||||
Leave,
|
||||
/** 鼠标在该元素上滚轮时触发的事件 */
|
||||
Wheel
|
||||
}
|
||||
|
||||
export const enum EventProgress {
|
||||
/** 捕获阶段 */
|
||||
Capture,
|
||||
/** 冒泡阶段 */
|
||||
Bubble
|
||||
}
|
||||
|
||||
export interface IActionEventBase {
|
||||
/** 当前事件是监听的哪个元素 */
|
||||
target: RenderItem;
|
||||
/** 是触摸操作还是鼠标操作 */
|
||||
touch: boolean;
|
||||
/**
|
||||
* 触发的按键种类,会出现在点击、按下、抬起三个事件中,而其他的如移动等该值只会是 {@link MouseType.None},
|
||||
* 电脑端可以有左键、中键、右键等,手机只会触发左键,每一项的值参考 {@link MouseType}
|
||||
*/
|
||||
type: MouseType;
|
||||
/**
|
||||
* 当前按下了哪些按键。该值是一个数字,可以通过位运算判断是否按下了某个按键。
|
||||
* 例如通过 `buttons & MouseType.Left` 来判断是否按下了左键。
|
||||
* 注意在鼠标抬起或鼠标点击事件中,并不会包含触发的那个按键
|
||||
*/
|
||||
buttons: number;
|
||||
/** 触发时是否按下了 alt 键 */
|
||||
altKey: boolean;
|
||||
/** 触发时是否按下了 shift 键 */
|
||||
shiftKey: boolean;
|
||||
/** 触发时是否按下了 ctrl 键 */
|
||||
ctrlKey: boolean;
|
||||
/** 触发时是否按下了 Windows(Windows) / Command(Mac) 键 */
|
||||
metaKey: boolean;
|
||||
}
|
||||
|
||||
export interface IActionEvent extends IActionEventBase {
|
||||
/** 这次操作的标识符,在按下、移动、抬起阶段中保持不变 */
|
||||
identifier: number;
|
||||
/** 相对于触发元素左上角的横坐标 */
|
||||
offsetX: number;
|
||||
/** 相对于触发元素左上角的纵坐标 */
|
||||
offsetY: number;
|
||||
/** 相对于整个画布左上角的横坐标 */
|
||||
absoluteX: number;
|
||||
/** 相对于整个画布左上角的纵坐标 */
|
||||
absoluteY: number;
|
||||
|
||||
/**
|
||||
* 调用后将停止事件的继续传播。
|
||||
* 在捕获阶段,将会阻止捕获的进一步进行,在冒泡阶段,将会阻止冒泡的进一步进行。
|
||||
* 如果当前元素有很多监听器,该方法并不会阻止其他监听器的执行。
|
||||
*/
|
||||
stopPropagation(): void;
|
||||
}
|
||||
|
||||
export interface IWheelEvent extends IActionEvent {
|
||||
/** 滚轮事件的鼠标横向滚动量 */
|
||||
wheelX: number;
|
||||
/** 滚轮事件的鼠标纵向滚动量 */
|
||||
wheelY: number;
|
||||
/** 滚轮事件的鼠标垂直屏幕的滚动量 */
|
||||
wheelZ: number;
|
||||
/** 滚轮事件的滚轮类型,表示了对应值的单位 */
|
||||
wheelType: WheelType;
|
||||
}
|
||||
|
||||
export interface ERenderItemActionEvent {
|
||||
/** 当这个元素被点击时的捕获阶段触发 */
|
||||
clickCapture: [ev: Readonly<IActionEvent>];
|
||||
/** 当这个元素被点击时的冒泡阶段触发 */
|
||||
click: [ev: Readonly<IActionEvent>];
|
||||
/** 当鼠标或手指在该元素上按下的捕获阶段触发 */
|
||||
downCapture: [ev: Readonly<IActionEvent>];
|
||||
/** 当鼠标或手指在该元素上按下的冒泡阶段触发 */
|
||||
down: [ev: Readonly<IActionEvent>];
|
||||
/** 当鼠标或手指在该元素上移动的捕获阶段触发 */
|
||||
moveCapture: [ev: Readonly<IActionEvent>];
|
||||
/** 当鼠标或手指在该元素上移动的冒泡阶段触发 */
|
||||
move: [ev: Readonly<IActionEvent>];
|
||||
/** 当鼠标或手指在该元素上抬起的捕获阶段触发 */
|
||||
upCapture: [ev: Readonly<IActionEvent>];
|
||||
/** 当鼠标或手指在该元素上抬起的冒泡阶段触发 */
|
||||
up: [ev: Readonly<IActionEvent>];
|
||||
/** 当鼠标或手指进入该元素时触发 */
|
||||
enter: [ev: Readonly<IActionEventBase>];
|
||||
/** 当鼠标或手指离开该元素时触发 */
|
||||
leave: [ev: Readonly<IActionEventBase>];
|
||||
/** 当鼠标滚轮时的捕获阶段触发 */
|
||||
wheelCapture: [ev: Readonly<IWheelEvent>];
|
||||
/** 当鼠标滚轮时的冒泡阶段触发 */
|
||||
wheel: [ev: Readonly<IWheelEvent>];
|
||||
}
|
||||
|
||||
export interface ActionEventMap {
|
||||
[ActionType.Click]: IActionEvent;
|
||||
[ActionType.Down]: IActionEvent;
|
||||
[ActionType.Enter]: IActionEvent;
|
||||
[ActionType.Leave]: IActionEvent;
|
||||
[ActionType.Move]: IActionEvent;
|
||||
[ActionType.Up]: IActionEvent;
|
||||
[ActionType.Wheel]: IWheelEvent;
|
||||
}
|
||||
|
||||
export const eventNameMap: Record<ActionType, keyof ERenderItemActionEvent> = {
|
||||
[ActionType.Click]: 'click',
|
||||
[ActionType.Down]: 'down',
|
||||
[ActionType.Move]: 'move',
|
||||
[ActionType.Up]: 'up',
|
||||
[ActionType.Enter]: 'enter',
|
||||
[ActionType.Leave]: 'leave',
|
||||
[ActionType.Wheel]: 'wheel'
|
||||
};
|
||||
@@ -1,52 +0,0 @@
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import { RenderItem } from './item';
|
||||
|
||||
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();
|
||||
|
||||
Mota.require('var', '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);
|
||||
}
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,15 +0,0 @@
|
||||
export * from './preset';
|
||||
export * from './renderer';
|
||||
export * from './style';
|
||||
export * from './adapter';
|
||||
export * from './cache';
|
||||
export * from './camera';
|
||||
export * from './container';
|
||||
export * from './frame';
|
||||
export * from './gl2';
|
||||
export * from './item';
|
||||
export * from './render';
|
||||
export * from './shader';
|
||||
export * from './sprite';
|
||||
export * from './transform';
|
||||
export * from './utils';
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,275 +0,0 @@
|
||||
import { RenderAdapter } from '../adapter';
|
||||
import { HeroRenderer } from './hero';
|
||||
import { ILayerGroupRenderExtends, LayerGroup } from './layer';
|
||||
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
|
||||
import { ERenderItemEvent, RenderItem } from '../item';
|
||||
import { Transform } from '../transform';
|
||||
import { transformCanvas } from '../utils';
|
||||
|
||||
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,311 +0,0 @@
|
||||
import { EventEmitter } from '@/core/common/eventEmitter';
|
||||
import { logger } from '@/core/common/logger';
|
||||
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
|
||||
|
||||
interface BlockCacherEvent {
|
||||
split: () => void;
|
||||
}
|
||||
|
||||
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;
|
||||
/** 分块大小 */
|
||||
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.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 canvas: MotaOffscreenCanvas2D, public symbol: number) {}
|
||||
|
||||
destroy(): void {
|
||||
this.canvas.delete();
|
||||
}
|
||||
}
|
||||
@@ -1,578 +0,0 @@
|
||||
import { logger } from '@/core/common/logger';
|
||||
import { LayerGroupFloorBinder } from './floor';
|
||||
import {
|
||||
calNeedRenderOf,
|
||||
ILayerGroupRenderExtends,
|
||||
Layer,
|
||||
LayerGroup
|
||||
} from './layer';
|
||||
import { BlockCacher, CanvasCacheItem, ICanvasCacheItem } from './block';
|
||||
import type {
|
||||
DamageEnemy,
|
||||
EnemyCollection,
|
||||
MapDamage
|
||||
} from '@/game/enemy/damage';
|
||||
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
|
||||
import { isNil } from 'lodash-es';
|
||||
import { getDamageColor } from '@/plugin/utils';
|
||||
import { ERenderItemEvent, RenderItem } from '../item';
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import { Transform } from '../transform';
|
||||
import { transformCanvas } from '../utils';
|
||||
|
||||
const ensureFloorDamage = Mota.require('fn', 'ensureFloorDamage');
|
||||
|
||||
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);
|
||||
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) {
|
||||
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?: EnemyCollection;
|
||||
/** 每个分块中包含的怪物集合 */
|
||||
blockData: Map<number, Map<number, DamageEnemy>> = 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, core._WIDTH_, 1);
|
||||
this.type = 'absolute';
|
||||
this.size(core._PX_, core._PY_);
|
||||
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: EnemyCollection) {
|
||||
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: DamageEnemy, 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 real = enemy.getRealInfo();
|
||||
|
||||
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);
|
||||
|
||||
const hasHorn = real.special.has(33);
|
||||
if (real.special.has(8) && real.togetherNum && !hasHorn) {
|
||||
const dam3: DamageRenderable = {
|
||||
align: 'right',
|
||||
baseline: 'top',
|
||||
text: real.togetherNum.toString(),
|
||||
color: '#09FF5B',
|
||||
x: x * this.cellSize + this.cellSize - 1,
|
||||
y: y * this.cellSize + 2,
|
||||
strokeWidth: 3
|
||||
};
|
||||
block.add(dam3);
|
||||
}
|
||||
if (real.special.has(30)) {
|
||||
const dam4: DamageRenderable = {
|
||||
align: 'left',
|
||||
baseline: 'top',
|
||||
text: '乾',
|
||||
color: '#FDCD0B',
|
||||
x: x * this.cellSize + 1,
|
||||
y: y * this.cellSize + 2,
|
||||
strokeWidth: 2,
|
||||
font: '500 10px Verdana'
|
||||
};
|
||||
block.add(dam4);
|
||||
}
|
||||
if (enemy.col && hasHorn) {
|
||||
const dam5: DamageRenderable = {
|
||||
align: 'right',
|
||||
baseline: 'top',
|
||||
text: enemy.col.list.size.toString(),
|
||||
color: '#fff866',
|
||||
x: x * this.cellSize + this.cellSize - 1,
|
||||
y: y * this.cellSize + 2,
|
||||
strokeWidth: 3
|
||||
};
|
||||
block.add(dam5);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析指定分块的地图伤害
|
||||
* @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 = '';
|
||||
let color = '#fa3';
|
||||
let font = '300 9px Verdana';
|
||||
if (dam.damage > 0) {
|
||||
text = core.formatBigNumber(dam.damage, true);
|
||||
} else if (dam.mockery) {
|
||||
dam.mockery.sort((a, b) =>
|
||||
a[0] === b[0] ? a[1] - b[1] : a[0] - b[0]
|
||||
);
|
||||
const [tx, ty] = dam.mockery[0];
|
||||
const dir = x > tx ? '←' : x < tx ? '→' : y > ty ? '↑' : '↓';
|
||||
text = '嘲' + dir;
|
||||
color = '#fd4';
|
||||
font = '500 11px Verdana';
|
||||
} else if (dam.hunt) {
|
||||
text = '猎';
|
||||
color = '#fd4';
|
||||
font = '500 11px Verdana';
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
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.withGameScale(true);
|
||||
temp.size(size, size);
|
||||
const { ctx: ct } = temp;
|
||||
|
||||
ct.translate(-px, -py);
|
||||
|
||||
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));
|
||||
});
|
||||
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,415 +0,0 @@
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import {
|
||||
FloorLayer,
|
||||
ILayerGroupRenderExtends,
|
||||
ILayerRenderExtends,
|
||||
Layer,
|
||||
LayerGroup,
|
||||
LayerMovingRenderable
|
||||
} from './layer';
|
||||
import { texture } from '../cache';
|
||||
import { sleep } from 'mutate-animate';
|
||||
import { RenderAdapter } from '../adapter';
|
||||
|
||||
const hook = Mota.require('var', 'hook');
|
||||
|
||||
hook.on('setBlock', (x, y, floor, block) => {
|
||||
const isNow = floor === core.status.floorId;
|
||||
LayerGroupFloorBinder.activedBinder.forEach(v => {
|
||||
if (floor === v.floor || (isNow && v.bindThisFloor)) {
|
||||
v.setBlock('event', block, x, y);
|
||||
}
|
||||
});
|
||||
LayerFloorBinder.listenedBinder.forEach(v => {
|
||||
if (v.layer.layer === 'event') {
|
||||
if (v.floor === floor || (isNow && v.bindThisFloor)) {
|
||||
v.setBlock(block, x, y);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
hook.on('changingFloor', floor => {
|
||||
// 潜在隐患:如果putRenderData改成异步,那么会变成两帧后才能真正刷新并渲染
|
||||
// 考虑到楼层转换一般不会同时执行很多次,因此这里改为立刻更新
|
||||
LayerGroupFloorBinder.activedBinder.forEach(v => {
|
||||
if (v.bindThisFloor) v.updateBindData();
|
||||
v.emit('floorChange', floor);
|
||||
});
|
||||
LayerFloorBinder.listenedBinder.forEach(v => {
|
||||
if (v.bindThisFloor) v.updateBindData();
|
||||
});
|
||||
});
|
||||
hook.on('setBgFgBlock', (name, number, x, y, floor) => {
|
||||
const isNow = floor === core.status.floorId;
|
||||
LayerGroupFloorBinder.activedBinder.forEach(v => {
|
||||
if (floor === v.floor || (isNow && v.bindThisFloor)) {
|
||||
v.setBlock(name, number, x, y);
|
||||
}
|
||||
});
|
||||
LayerFloorBinder.listenedBinder.forEach(v => {
|
||||
if (v.layer.layer === name) {
|
||||
if (v.floor === floor || (isNow && v.bindThisFloor)) {
|
||||
v.setBlock(number, x, y);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
interface LayerGroupBinderEvent {
|
||||
update: [floor: FloorIds];
|
||||
setBlock: [x: number, y: number, floor: FloorIds, block: AllNumbers];
|
||||
floorChange: [floor: FloorIds];
|
||||
}
|
||||
|
||||
/**
|
||||
* 楼层绑定拓展,用于LayerGroup,将楼层数据传输到渲染系统。
|
||||
* 添加后,会自动在LayerGroup包含的子Layer上添加LayerFloorBinder拓展,用于后续处理。
|
||||
* 当移除这个拓展时,其附属的所有子拓展也会一并被移除。
|
||||
*/
|
||||
export class LayerGroupFloorBinder
|
||||
extends EventEmitter<LayerGroupBinderEvent>
|
||||
implements ILayerGroupRenderExtends
|
||||
{
|
||||
id: string = 'floor-binder';
|
||||
|
||||
bindThisFloor: boolean = true;
|
||||
floor?: FloorIds;
|
||||
group!: LayerGroup;
|
||||
|
||||
/** 附属的子LayerFloorBinder拓展 */
|
||||
layerBinders: Set<LayerFloorBinder> = new Set();
|
||||
|
||||
private needUpdate: boolean = false;
|
||||
|
||||
static activedBinder: Set<LayerGroupFloorBinder> = new Set();
|
||||
|
||||
/**
|
||||
* 绑定楼层为当前楼层,并跟随变化
|
||||
*/
|
||||
bindThis() {
|
||||
this.floor = void 0;
|
||||
this.bindThisFloor = true;
|
||||
this.layerBinders.forEach(v => v.bindThis());
|
||||
this.updateBind();
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定楼层为指定楼层
|
||||
* @param floorId 楼层id
|
||||
*/
|
||||
bindFloor(floorId: FloorIds) {
|
||||
this.bindThisFloor = false;
|
||||
this.floor = floorId;
|
||||
this.layerBinders.forEach(v => v.bindFloor(floorId));
|
||||
this.updateBind();
|
||||
}
|
||||
|
||||
/**
|
||||
* 在下一帧进行绑定数据更新
|
||||
*/
|
||||
updateBind() {
|
||||
if (this.needUpdate || !this.group) return;
|
||||
this.needUpdate = true;
|
||||
this.group.requestBeforeFrame(() => {
|
||||
this.needUpdate = false;
|
||||
this.updateBindData();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 立刻进行数据绑定更新
|
||||
*/
|
||||
updateBindData() {
|
||||
this.layerBinders.forEach(v => {
|
||||
v.updateBindData();
|
||||
});
|
||||
|
||||
const floor = this.getFloor();
|
||||
this.emit('update', floor);
|
||||
}
|
||||
|
||||
getFloor() {
|
||||
return this.bindThisFloor ? core.status.floorId : this.floor!;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置图块
|
||||
*/
|
||||
setBlock(layer: FloorLayer, block: AllNumbers, x: number, y: number) {
|
||||
const ex = this.group
|
||||
.getLayer(layer)
|
||||
?.getExtends('floor-binder') as LayerFloorBinder;
|
||||
if (!ex) return;
|
||||
ex.setBlock(block, x, y);
|
||||
|
||||
const floor = this.bindThisFloor ? core.status.floorId : this.floor!;
|
||||
this.emit('setBlock', x, y, floor, block);
|
||||
}
|
||||
|
||||
checkLayerExtends(layer: Layer) {
|
||||
const ex = layer.getExtends('floor-binder');
|
||||
|
||||
if (!ex) {
|
||||
const extend = new LayerFloorBinder(this);
|
||||
layer.extends(extend);
|
||||
this.layerBinders.add(extend);
|
||||
} else {
|
||||
if (ex instanceof LayerFloorBinder) {
|
||||
ex.setParent(this);
|
||||
this.layerBinders.add(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
awake(group: LayerGroup) {
|
||||
this.group = group;
|
||||
|
||||
for (const layer of group.layers.values()) {
|
||||
this.checkLayerExtends(layer);
|
||||
}
|
||||
LayerGroupFloorBinder.activedBinder.add(this);
|
||||
}
|
||||
|
||||
onLayerAdd(_group: LayerGroup, layer: Layer): void {
|
||||
this.checkLayerExtends(layer);
|
||||
}
|
||||
|
||||
onDestroy(group: LayerGroup) {
|
||||
LayerGroupFloorBinder.activedBinder.delete(this);
|
||||
group.layers.forEach(v => {
|
||||
v.removeExtends('floor-binder');
|
||||
});
|
||||
this.removeAllListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 楼层绑定拓展,用于Layer的楼层渲染。
|
||||
* 注意,如果目标Layer是LayerGroup的子元素,那么会自动检测父元素是否包含LayerGroupFloorBinder拓展,
|
||||
* 如果包含,那么会自动将此拓展附加至父元素的拓展。当父元素的拓展被移除时,此拓展也会一并被移除。
|
||||
*/
|
||||
export class LayerFloorBinder implements ILayerRenderExtends {
|
||||
id: string = 'floor-binder';
|
||||
|
||||
parent?: LayerGroupFloorBinder;
|
||||
layer!: Layer;
|
||||
bindThisFloor: boolean = true;
|
||||
floor?: FloorIds;
|
||||
|
||||
static listenedBinder: Set<LayerFloorBinder> = new Set();
|
||||
|
||||
private needUpdate: boolean = false;
|
||||
|
||||
constructor(parent?: LayerGroupFloorBinder) {
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定楼层为当前楼层,并跟随变化
|
||||
*/
|
||||
bindThis() {
|
||||
this.floor = void 0;
|
||||
this.bindThisFloor = true;
|
||||
this.updateBind();
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定楼层为指定楼层
|
||||
* @param floorId 楼层id
|
||||
*/
|
||||
bindFloor(floorId: FloorIds) {
|
||||
this.bindThisFloor = false;
|
||||
this.floor = floorId;
|
||||
this.updateBind();
|
||||
}
|
||||
|
||||
getFloor() {
|
||||
return this.bindThisFloor ? core.status.floorId : this.floor!;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置这个拓展附属至的父拓展(LayerGroupFloorBinder拓展)
|
||||
* @param parent 父拓展
|
||||
*/
|
||||
setParent(parent?: LayerGroupFloorBinder) {
|
||||
this.parent = parent;
|
||||
this.checkListen();
|
||||
}
|
||||
|
||||
private checkListen() {
|
||||
if (this.parent) LayerFloorBinder.listenedBinder.delete(this);
|
||||
else LayerFloorBinder.listenedBinder.add(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在下一帧进行绑定数据更新
|
||||
*/
|
||||
updateBind() {
|
||||
if (this.needUpdate) return;
|
||||
this.needUpdate = true;
|
||||
this.layer.requestBeforeFrame(() => {
|
||||
this.needUpdate = false;
|
||||
this.updateBindData();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置图块
|
||||
*/
|
||||
setBlock(block: AllNumbers, x: number, y: number) {
|
||||
this.layer.putRenderData([block], 1, x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* 立刻更新绑定数据,而非下一帧
|
||||
*/
|
||||
updateBindData() {
|
||||
const floor = this.getFloor();
|
||||
if (!floor) return;
|
||||
core.extractBlocks(floor);
|
||||
const map = core.status.maps[floor];
|
||||
this.layer.setMapSize(map.width, map.height);
|
||||
const image = core.status.maps[this.getFloor()].images;
|
||||
if (this.layer.layer === 'event') {
|
||||
const m = map.map;
|
||||
this.layer.putRenderData(m.flat(), map.width, 0, 0);
|
||||
} else {
|
||||
const m = core.maps._getBgFgMapArray(this.layer.layer!, floor);
|
||||
this.layer.putRenderData(m.flat(), map.width, 0, 0);
|
||||
}
|
||||
if (this.layer.layer === 'bg') {
|
||||
// 别忘了背景图块
|
||||
this.layer.setBackground(texture.idNumberMap[map.defaultGround]);
|
||||
}
|
||||
const toDraw = image?.filter(v => v.canvas === this.layer.layer);
|
||||
this.layer.setFloorImage(toDraw ?? []);
|
||||
}
|
||||
|
||||
awake(layer: Layer) {
|
||||
this.layer = layer;
|
||||
if (!this.parent) {
|
||||
const group = layer.parent;
|
||||
if (group instanceof LayerGroup) {
|
||||
const ex = group.getExtends('floor-binder');
|
||||
if (ex instanceof LayerGroupFloorBinder) {
|
||||
ex.checkLayerExtends(layer);
|
||||
this.parent = ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.checkListen();
|
||||
}
|
||||
|
||||
onDestroy(_layer: Layer) {
|
||||
LayerFloorBinder.listenedBinder.delete(this);
|
||||
this.parent?.layerBinders.delete(this);
|
||||
}
|
||||
}
|
||||
|
||||
interface DoorAnimateRenderable {
|
||||
renderable: LayerMovingRenderable;
|
||||
count: number;
|
||||
perTime: number;
|
||||
}
|
||||
|
||||
export class LayerDoorAnimate implements ILayerRenderExtends {
|
||||
id: string = 'door-animate';
|
||||
|
||||
layer!: Layer;
|
||||
|
||||
private moving: Set<LayerMovingRenderable> = new Set();
|
||||
|
||||
private getRenderable(block: Block): DoorAnimateRenderable | null {
|
||||
const { x, y, id } = block;
|
||||
const renderable = texture.getRenderable(id);
|
||||
if (!renderable) return null;
|
||||
const image = renderable.autotile
|
||||
? renderable.image[0]
|
||||
: renderable.image;
|
||||
const time = block.event.doorInfo?.time ?? 160;
|
||||
const frame = renderable.render.length;
|
||||
const perTime = time / frame;
|
||||
|
||||
const data: LayerMovingRenderable = {
|
||||
x,
|
||||
y,
|
||||
zIndex: y,
|
||||
image,
|
||||
autotile: false,
|
||||
animate: 0,
|
||||
frame,
|
||||
bigImage: false,
|
||||
render: renderable.render,
|
||||
alpha: 1
|
||||
};
|
||||
return { renderable: data, count: frame, perTime };
|
||||
}
|
||||
|
||||
/**
|
||||
* 开门
|
||||
* @param block 图块信息
|
||||
*/
|
||||
async openDoor(block: Block) {
|
||||
const renderable = this.getRenderable(block);
|
||||
if (!renderable) return Promise.reject();
|
||||
const { renderable: data, count: frame, perTime } = renderable;
|
||||
data.animate = 0;
|
||||
this.moving.add(data);
|
||||
this.layer.requestUpdateMoving();
|
||||
|
||||
let now = 0;
|
||||
while (now < frame) {
|
||||
await sleep(perTime);
|
||||
data.animate = ++now;
|
||||
this.layer.update(this.layer);
|
||||
}
|
||||
|
||||
this.moving.delete(data);
|
||||
this.layer.requestUpdateMoving();
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* 关门
|
||||
* @param block 图块信息
|
||||
*/
|
||||
async closeDoor(block: Block) {
|
||||
const renderable = this.getRenderable(block);
|
||||
if (!renderable) return Promise.reject();
|
||||
const { renderable: data, count: frame, perTime } = renderable;
|
||||
data.animate = frame - 1;
|
||||
this.moving.add(data);
|
||||
this.layer.requestUpdateMoving();
|
||||
|
||||
let now = 0;
|
||||
while (now >= 0) {
|
||||
await sleep(perTime);
|
||||
data.animate = --now;
|
||||
this.layer.update(this.layer);
|
||||
}
|
||||
this.moving.delete(data);
|
||||
this.layer.requestUpdateMoving();
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
awake(layer: Layer) {
|
||||
this.layer = layer;
|
||||
doorAdapter.add(this);
|
||||
}
|
||||
|
||||
onMovingUpdate(_layer: Layer, renderable: LayerMovingRenderable[]): void {
|
||||
renderable.push(...this.moving);
|
||||
}
|
||||
|
||||
onDestroy(_layer: Layer): void {
|
||||
doorAdapter.remove(this);
|
||||
}
|
||||
}
|
||||
|
||||
const doorAdapter = new RenderAdapter<LayerDoorAnimate>('door-animate');
|
||||
doorAdapter.receive('openDoor', (item, block: Block) => {
|
||||
return item.openDoor(block);
|
||||
});
|
||||
doorAdapter.receive('closeDoor', (item, block: Block) => {
|
||||
return item.closeDoor(block);
|
||||
});
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,477 +0,0 @@
|
||||
import { ILayerRenderExtends, Layer, LayerMovingRenderable } from './layer';
|
||||
import { SizedCanvasImageSource } from './misc';
|
||||
import { RenderAdapter } from '../adapter';
|
||||
import { logger } from '@/core/common/logger';
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
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,9 +0,0 @@
|
||||
export * from './animate';
|
||||
export * from './block';
|
||||
export * from './damage';
|
||||
export * from './floor';
|
||||
export * from './hero';
|
||||
export * from './layer';
|
||||
export * from './misc';
|
||||
export * from './viewport';
|
||||
export * from './graphics';
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,551 +0,0 @@
|
||||
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
|
||||
import { ERenderItemEvent, RenderItem, RenderItemPosition } from '../item';
|
||||
import { Transform } from '../transform';
|
||||
import { AutotileRenderable, RenderableData } from '../cache';
|
||||
import { texture } from '../cache';
|
||||
import { isNil } from 'lodash-es';
|
||||
import { logger } from '@/core/common/logger';
|
||||
import { IAnimateFrame, renderEmits } from '../frame';
|
||||
import { Font } from '../style/font';
|
||||
|
||||
/** 文字的安全填充,会填充在文字的上侧和下侧,防止削顶和削底 */
|
||||
const SAFE_PAD = 1;
|
||||
|
||||
type CanvasStyle = string | CanvasGradient | CanvasPattern;
|
||||
|
||||
export interface ETextEvent extends ERenderItemEvent {
|
||||
setText: [text: string, width: number, height: number];
|
||||
}
|
||||
|
||||
export class Text extends RenderItem<ETextEvent> {
|
||||
text: string;
|
||||
|
||||
fillStyle?: CanvasStyle = '#fff';
|
||||
strokeStyle?: CanvasStyle;
|
||||
font: Font = new Font();
|
||||
strokeWidth: number = 1;
|
||||
|
||||
private length: number = 0;
|
||||
private descent: number = 0;
|
||||
|
||||
private static measureCanvas = new MotaOffscreenCanvas2D();
|
||||
|
||||
constructor(text: string = '', type: RenderItemPosition = 'static') {
|
||||
super(type, false);
|
||||
|
||||
this.text = text;
|
||||
if (text.length > 0) {
|
||||
this.requestBeforeFrame(() => {
|
||||
this.calBox();
|
||||
this.emit('setText', text, this.width, this.height);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected render(
|
||||
canvas: MotaOffscreenCanvas2D,
|
||||
_transform: Transform
|
||||
): void {
|
||||
const ctx = canvas.ctx;
|
||||
const stroke = this.strokeWidth;
|
||||
ctx.textBaseline = 'bottom';
|
||||
ctx.fillStyle = this.fillStyle ?? 'transparent';
|
||||
ctx.strokeStyle = this.strokeStyle ?? 'transparent';
|
||||
ctx.font = this.font.string();
|
||||
ctx.lineWidth = this.strokeWidth;
|
||||
|
||||
if (this.strokeStyle) {
|
||||
ctx.strokeText(this.text, stroke, this.descent + stroke + SAFE_PAD);
|
||||
}
|
||||
if (this.fillStyle) {
|
||||
ctx.fillText(this.text, stroke, this.descent + stroke + SAFE_PAD);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文字的长度
|
||||
*/
|
||||
measure() {
|
||||
const ctx = Text.measureCanvas.ctx;
|
||||
ctx.textBaseline = 'bottom';
|
||||
ctx.font = this.font.string();
|
||||
const res = ctx.measureText(this.text);
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置显示文字
|
||||
* @param text 显示的文字
|
||||
*/
|
||||
setText(text: string) {
|
||||
this.text = text;
|
||||
this.calBox();
|
||||
this.update(this);
|
||||
this.emit('setText', text, this.width, this.height);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置使用的字体
|
||||
* @param font 字体
|
||||
*/
|
||||
setFont(font: Font) {
|
||||
this.font = font;
|
||||
this.calBox();
|
||||
this.update(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置字体样式
|
||||
* @param fill 填充样式
|
||||
* @param stroke 描边样式
|
||||
*/
|
||||
setStyle(fill?: CanvasStyle, stroke?: CanvasStyle) {
|
||||
this.fillStyle = fill;
|
||||
this.strokeStyle = stroke;
|
||||
this.update();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置描边宽度
|
||||
* @param width 宽度
|
||||
*/
|
||||
setStrokeWidth(width: number) {
|
||||
const before = this.strokeWidth;
|
||||
this.strokeWidth = width;
|
||||
const dw = width - before;
|
||||
this.size(this.width + dw * 2, this.height + dw * 2);
|
||||
this.update();
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算字体所占空间,从而确定这个元素的大小
|
||||
*/
|
||||
calBox() {
|
||||
const { width, actualBoundingBoxAscent, actualBoundingBoxDescent } =
|
||||
this.measure();
|
||||
this.length = width;
|
||||
this.descent = actualBoundingBoxAscent;
|
||||
const height = actualBoundingBoxAscent + actualBoundingBoxDescent;
|
||||
const stroke = this.strokeWidth * 2;
|
||||
this.size(width + stroke, height + stroke + SAFE_PAD * 2);
|
||||
}
|
||||
|
||||
protected handleProps(
|
||||
key: string,
|
||||
_prevValue: any,
|
||||
nextValue: any
|
||||
): boolean {
|
||||
switch (key) {
|
||||
case 'text':
|
||||
if (!this.assertType(nextValue, 'string', key)) return false;
|
||||
this.setText(nextValue);
|
||||
return true;
|
||||
case 'fillStyle':
|
||||
this.setStyle(nextValue, this.strokeStyle);
|
||||
return true;
|
||||
case 'strokeStyle':
|
||||
this.setStyle(this.fillStyle, nextValue);
|
||||
return true;
|
||||
case 'font':
|
||||
if (!this.assertType(nextValue, Font, key)) return false;
|
||||
this.setFont(nextValue);
|
||||
return true;
|
||||
case 'strokeWidth':
|
||||
this.setStrokeWidth(nextValue);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export type SizedCanvasImageSource = Exclude<
|
||||
CanvasImageSource,
|
||||
VideoFrame | SVGElement
|
||||
>;
|
||||
|
||||
export interface EImageEvent extends ERenderItemEvent {}
|
||||
|
||||
export class Image extends RenderItem<EImageEvent> {
|
||||
image: CanvasImageSource;
|
||||
|
||||
constructor(image: CanvasImageSource, type: RenderItemPosition = 'static') {
|
||||
super(type);
|
||||
this.image = image;
|
||||
if (image instanceof VideoFrame || image instanceof SVGElement) {
|
||||
this.size(200, 200);
|
||||
} else {
|
||||
this.size(image.width, image.height);
|
||||
}
|
||||
}
|
||||
|
||||
protected render(
|
||||
canvas: MotaOffscreenCanvas2D,
|
||||
_transform: Transform
|
||||
): void {
|
||||
const ctx = canvas.ctx;
|
||||
ctx.drawImage(this.image, 0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置图片资源
|
||||
* @param image 图片资源
|
||||
*/
|
||||
setImage(image: CanvasImageSource) {
|
||||
this.image = image;
|
||||
this.update();
|
||||
}
|
||||
|
||||
protected handleProps(
|
||||
key: string,
|
||||
_prevValue: any,
|
||||
nextValue: any
|
||||
): boolean {
|
||||
switch (key) {
|
||||
case 'image':
|
||||
this.setImage(nextValue);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class Comment extends RenderItem {
|
||||
readonly isComment: boolean = true;
|
||||
|
||||
constructor(public text: string = '') {
|
||||
super('static', false, false);
|
||||
this.hide();
|
||||
}
|
||||
|
||||
getBoundingRect(): DOMRectReadOnly {
|
||||
return new DOMRectReadOnly(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
protected render(
|
||||
_canvas: MotaOffscreenCanvas2D,
|
||||
_transform: Transform
|
||||
): void {}
|
||||
|
||||
protected handleProps(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export interface EIconEvent extends ERenderItemEvent {}
|
||||
|
||||
export class Icon extends RenderItem<EIconEvent> implements IAnimateFrame {
|
||||
/** 图标id */
|
||||
icon: AllNumbers = 0;
|
||||
/** 帧数 */
|
||||
frame?: number = 0;
|
||||
/** 是否启用动画 */
|
||||
animate?: boolean = false;
|
||||
/** 图标的渲染信息 */
|
||||
private renderable?: RenderableData | AutotileRenderable;
|
||||
|
||||
private pendingIcon?: AllNumbers;
|
||||
|
||||
constructor(type: RenderItemPosition, cache?: boolean, fall?: boolean) {
|
||||
super(type, cache, fall);
|
||||
this.setAntiAliasing(false);
|
||||
this.setHD(false);
|
||||
}
|
||||
|
||||
protected render(
|
||||
canvas: MotaOffscreenCanvas2D,
|
||||
_transform: Transform
|
||||
): void {
|
||||
const ctx = canvas.ctx;
|
||||
const renderable = this.renderable;
|
||||
if (!renderable) return;
|
||||
const [x, y, w, h] = renderable.render[0];
|
||||
const cw = this.width;
|
||||
const ch = this.height;
|
||||
const frame = this.animate
|
||||
? RenderItem.animatedFrame % renderable.frame
|
||||
: 0;
|
||||
|
||||
if (!this.animate) {
|
||||
if (renderable.autotile) {
|
||||
ctx.drawImage(renderable.image[0], x, y, w, h, 0, 0, cw, ch);
|
||||
} else {
|
||||
ctx.drawImage(renderable.image, x, y, w, h, 0, 0, cw, ch);
|
||||
}
|
||||
} else {
|
||||
const [x1, y1, w1, h1] = renderable.render[frame];
|
||||
if (renderable.autotile) {
|
||||
const img = renderable.image[0];
|
||||
ctx.drawImage(img, x1, y1, w1, h1, 0, 0, cw, ch);
|
||||
} else {
|
||||
ctx.drawImage(renderable.image, x1, y1, w1, h1, 0, 0, cw, ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置图标
|
||||
* @param id 图标id
|
||||
*/
|
||||
setIcon(id: AllIds | AllNumbers) {
|
||||
if (id === 0) {
|
||||
this.renderable = void 0;
|
||||
return;
|
||||
}
|
||||
const num = typeof id === 'number' ? id : texture.idNumberMap[id];
|
||||
|
||||
const loading = Mota.require('var', 'loading');
|
||||
if (loading.loaded) {
|
||||
this.setIconRenderable(num);
|
||||
} else {
|
||||
if (isNil(this.pendingIcon)) {
|
||||
loading.once('loaded', () => {
|
||||
this.setIconRenderable(this.pendingIcon ?? 0);
|
||||
delete this.pendingIcon;
|
||||
});
|
||||
}
|
||||
this.pendingIcon = num;
|
||||
}
|
||||
}
|
||||
|
||||
private setIconRenderable(num: AllNumbers) {
|
||||
const renderable = texture.getRenderable(num);
|
||||
|
||||
if (!renderable) {
|
||||
logger.warn(43, num.toString());
|
||||
return;
|
||||
} else {
|
||||
this.icon = num;
|
||||
this.renderable = renderable;
|
||||
this.frame = renderable.frame;
|
||||
}
|
||||
this.update();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新动画帧
|
||||
*/
|
||||
updateFrameAnimate(): void {
|
||||
this.update(this);
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
renderEmits.removeFramer(this);
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
protected handleProps(
|
||||
key: string,
|
||||
_prevValue: any,
|
||||
nextValue: any
|
||||
): boolean {
|
||||
switch (key) {
|
||||
case 'icon':
|
||||
this.setIcon(nextValue);
|
||||
return true;
|
||||
case 'animate':
|
||||
if (!this.assertType(nextValue, 'boolean', key)) return false;
|
||||
this.animate = nextValue;
|
||||
if (nextValue) renderEmits.addFramer(this);
|
||||
else renderEmits.removeFramer(this);
|
||||
this.update();
|
||||
return true;
|
||||
case 'frame':
|
||||
if (!this.assertType(nextValue, 'number', key)) return false;
|
||||
this.frame = nextValue;
|
||||
this.update();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
interface WinskinPatterns {
|
||||
top: CanvasPattern;
|
||||
left: CanvasPattern;
|
||||
bottom: CanvasPattern;
|
||||
right: CanvasPattern;
|
||||
}
|
||||
|
||||
export interface EWinskinEvent extends ERenderItemEvent {}
|
||||
|
||||
export class Winskin extends RenderItem<EWinskinEvent> {
|
||||
image: SizedCanvasImageSource;
|
||||
/** 边框宽度,32表示原始宽度 */
|
||||
borderSize: number = 32;
|
||||
/** 图片名称 */
|
||||
imageName?: string;
|
||||
|
||||
private pendingImage?: ImageIds;
|
||||
private patternCache?: WinskinPatterns;
|
||||
private patternTransform: DOMMatrix;
|
||||
|
||||
private static patternMap: Map<string, WinskinPatterns> = new Map();
|
||||
|
||||
constructor(
|
||||
image: SizedCanvasImageSource,
|
||||
type: RenderItemPosition = 'static'
|
||||
) {
|
||||
super(type, false, false);
|
||||
this.image = image;
|
||||
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() {
|
||||
const pattern = this.requireCanvas();
|
||||
const img = this.image;
|
||||
pattern.size(32, 16);
|
||||
pattern.withGameScale(false);
|
||||
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 img = this.image;
|
||||
const w = this.width;
|
||||
const h = this.height;
|
||||
const pad = this.borderSize / 2;
|
||||
// 背景
|
||||
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.fillStyle = top;
|
||||
ctx.translate(pad, 0);
|
||||
ctx.fillRect(0, 0, w - pad * 2, pad);
|
||||
ctx.fillStyle = bottom;
|
||||
ctx.translate(0, h - pad);
|
||||
ctx.fillRect(0, 0, w - pad * 2, pad);
|
||||
ctx.fillStyle = left;
|
||||
ctx.translate(-pad, pad * 2 - h);
|
||||
ctx.fillRect(0, 0, pad, h - pad * 2);
|
||||
ctx.fillStyle = right;
|
||||
ctx.translate(w - pad, 0);
|
||||
ctx.fillRect(0, 0, pad, h - pad * 2);
|
||||
ctx.restore();
|
||||
// 四个角的边框
|
||||
ctx.drawImage(img, 128, 0, 16, 16, 0, 0, pad, pad);
|
||||
ctx.drawImage(img, 176, 0, 16, 16, w - pad, 0, pad, pad);
|
||||
ctx.drawImage(img, 128, 48, 16, 16, 0, h - pad, pad, pad);
|
||||
ctx.drawImage(img, 176, 48, 16, 16, w - pad, h - pad, pad, pad);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置winskin图片
|
||||
* @param image winskin图片
|
||||
*/
|
||||
setImage(image: SizedCanvasImageSource) {
|
||||
this.image = image;
|
||||
this.patternCache = void 0;
|
||||
this.update();
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过图片名称设置winskin
|
||||
* @param name 图片名称
|
||||
*/
|
||||
setImageByName(name: ImageIds) {
|
||||
const loading = Mota.require('var', 'loading');
|
||||
if (loading.loaded) {
|
||||
const image = core.material.images.images[name];
|
||||
this.setImage(image);
|
||||
} else {
|
||||
if (isNil(this.pendingImage)) {
|
||||
loading.once('loaded', () => {
|
||||
const id = this.pendingImage;
|
||||
if (!id) return;
|
||||
const image = core.material.images.images[id];
|
||||
this.setImage(image);
|
||||
delete this.pendingImage;
|
||||
});
|
||||
}
|
||||
this.pendingImage = name;
|
||||
}
|
||||
this.imageName = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置边框大小
|
||||
* @param size 边框大小
|
||||
*/
|
||||
setBorderSize(size: number) {
|
||||
this.borderSize = size;
|
||||
this.patternTransform.a = size / 32;
|
||||
this.patternTransform.d = size / 32;
|
||||
this.update();
|
||||
}
|
||||
|
||||
protected handleProps(
|
||||
key: string,
|
||||
_prevValue: any,
|
||||
nextValue: any
|
||||
): boolean {
|
||||
switch (key) {
|
||||
case 'image':
|
||||
if (!this.assertType(nextValue, 'string', key)) return false;
|
||||
this.setImageByName(nextValue);
|
||||
return true;
|
||||
case 'borderSize':
|
||||
if (!this.assertType(nextValue, 'number', key)) return false;
|
||||
this.setBorderSize(nextValue);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,352 +0,0 @@
|
||||
import { HeroRenderer } from './hero';
|
||||
import { ILayerGroupRenderExtends, LayerGroup } from './layer';
|
||||
import { LayerGroupFloorBinder } from './floor';
|
||||
import { hyper, TimingFn } from 'mutate-animate';
|
||||
import { RenderAdapter } from '../adapter';
|
||||
|
||||
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 = core._PX_ / 2;
|
||||
const halfHeight = core._PY_ / 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 = core._WIDTH_;
|
||||
const height = core._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 = core._PX_ / 2;
|
||||
const halfHeight = core._PY_ / 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();
|
||||
});
|
||||
|
||||
const hook = Mota.require('var', 'hook');
|
||||
hook.on('changingFloor', (_, loc) => {
|
||||
adapter.all('setPosition', loc.x, loc.y);
|
||||
});
|
||||
@@ -1,604 +0,0 @@
|
||||
import { logger } from '../common/logger';
|
||||
import { MotaOffscreenCanvas2D } from '../fx/canvas2d';
|
||||
import { Container } from './container';
|
||||
import {
|
||||
ActionType,
|
||||
IActionEvent,
|
||||
IActionEventBase,
|
||||
IWheelEvent,
|
||||
MouseType,
|
||||
WheelType
|
||||
} from './event';
|
||||
import { IRenderTreeRoot, RenderItem } from './item';
|
||||
import { Transform } from './transform';
|
||||
|
||||
interface TouchInfo {
|
||||
/** 这次触摸在渲染系统的标识符 */
|
||||
identifier: number;
|
||||
/** 浏览器的 clientX,用于判断这个触点有没有移动 */
|
||||
clientX: number;
|
||||
/** 浏览器的 clientY,用于判断这个触点有没有移动 */
|
||||
clientY: number;
|
||||
/** 是否覆盖在了当前元素上 */
|
||||
hovered: boolean;
|
||||
}
|
||||
|
||||
interface MouseInfo {
|
||||
/** 这个鼠标按键的标识符 */
|
||||
identifier: number;
|
||||
}
|
||||
|
||||
export class MotaRenderer extends Container implements IRenderTreeRoot {
|
||||
static list: Map<string, MotaRenderer> = new Map();
|
||||
|
||||
/** 所有连接到此根元素的渲染元素的 id 到元素自身的映射 */
|
||||
protected idMap: Map<string, RenderItem> = new Map();
|
||||
|
||||
/** 最后一次按下的鼠标按键,用于处理鼠标移动 */
|
||||
private lastMouse: MouseType = MouseType.None;
|
||||
/** 每个触点的信息 */
|
||||
private touchInfo: Map<number, TouchInfo> = new Map();
|
||||
/** 触点列表 */
|
||||
private touchList: Map<number, Touch> = new Map();
|
||||
/** 每个鼠标按键的信息 */
|
||||
private mouseInfo: Map<MouseType, MouseInfo> = new Map();
|
||||
/** 操作的标识符 */
|
||||
private actionIdentifier: number = 0;
|
||||
|
||||
/** 用于终止 document 上的监听 */
|
||||
private abort?: AbortController;
|
||||
/** 根据捕获行为判断光标样式 */
|
||||
private targetCursor: string = 'auto';
|
||||
/** 当前鼠标覆盖的元素 */
|
||||
private hoveredElement: Set<RenderItem> = new Set();
|
||||
/** 本次交互前鼠标覆盖的元素 */
|
||||
private beforeHovered: Set<RenderItem> = new Set();
|
||||
|
||||
target!: MotaOffscreenCanvas2D;
|
||||
|
||||
readonly isRoot = true;
|
||||
|
||||
constructor(id: string = 'render-main') {
|
||||
super('static', false);
|
||||
|
||||
const canvas = document.getElementById(id) as HTMLCanvasElement;
|
||||
if (!canvas) {
|
||||
logger.error(19);
|
||||
return;
|
||||
}
|
||||
this.target = new MotaOffscreenCanvas2D(true, canvas);
|
||||
this.size(core._PX_, core._PY_);
|
||||
this.target.withGameScale(true);
|
||||
this.target.setAntiAliasing(false);
|
||||
|
||||
this.setAnchor(0.5, 0.5);
|
||||
this.transform.translate(240, 240);
|
||||
|
||||
MotaRenderer.list.set(id, this);
|
||||
|
||||
const update = () => {
|
||||
this.requestRenderFrame(() => {
|
||||
this.refresh();
|
||||
update();
|
||||
});
|
||||
};
|
||||
|
||||
update();
|
||||
this.listen();
|
||||
}
|
||||
|
||||
size(width: number, height: number): void {
|
||||
super.size(width, height);
|
||||
this.target.size(width, height);
|
||||
this.transform.setTranslate(width / 2, height / 2);
|
||||
}
|
||||
|
||||
private listen() {
|
||||
// 画布监听
|
||||
const canvas = this.target.canvas;
|
||||
canvas.addEventListener('mousedown', ev => {
|
||||
ev.preventDefault();
|
||||
const mouse = this.getMouseType(ev);
|
||||
this.lastMouse = mouse;
|
||||
this.captureEvent(
|
||||
ActionType.Down,
|
||||
this.createMouseAction(ev, ActionType.Down, mouse)
|
||||
);
|
||||
});
|
||||
canvas.addEventListener('mouseup', ev => {
|
||||
ev.preventDefault();
|
||||
const event = this.createMouseAction(ev, ActionType.Up);
|
||||
this.captureEvent(ActionType.Up, event);
|
||||
this.captureEvent(ActionType.Click, event);
|
||||
});
|
||||
canvas.addEventListener('mousemove', ev => {
|
||||
ev.preventDefault();
|
||||
const event = this.createMouseAction(
|
||||
ev,
|
||||
ActionType.Move,
|
||||
this.lastMouse
|
||||
);
|
||||
this.targetCursor = 'auto';
|
||||
const temp = this.beforeHovered;
|
||||
temp.clear();
|
||||
this.beforeHovered = this.hoveredElement;
|
||||
this.hoveredElement = temp;
|
||||
this.captureEvent(ActionType.Move, event);
|
||||
if (this.targetCursor !== this.target.canvas.style.cursor) {
|
||||
this.target.canvas.style.cursor = this.targetCursor;
|
||||
}
|
||||
this.checkMouseEnterLeave(
|
||||
ev,
|
||||
this.beforeHovered,
|
||||
this.hoveredElement
|
||||
);
|
||||
});
|
||||
canvas.addEventListener('mouseleave', ev => {
|
||||
ev.preventDefault();
|
||||
this.hoveredElement.forEach(v => {
|
||||
v.emit('leave', this.createMouseActionBase(ev, v));
|
||||
});
|
||||
this.hoveredElement.clear();
|
||||
this.beforeHovered.clear();
|
||||
});
|
||||
document.addEventListener('touchstart', ev => {
|
||||
ev.preventDefault();
|
||||
this.createTouchAction(ev, ActionType.Down).forEach(v => {
|
||||
this.captureEvent(ActionType.Down, v);
|
||||
});
|
||||
});
|
||||
document.addEventListener('touchend', ev => {
|
||||
ev.preventDefault();
|
||||
this.createTouchAction(ev, ActionType.Up).forEach(v => {
|
||||
this.captureEvent(ActionType.Up, v);
|
||||
this.captureEvent(ActionType.Click, v);
|
||||
this.touchInfo.delete(v.identifier);
|
||||
});
|
||||
});
|
||||
document.addEventListener('touchcancel', ev => {
|
||||
ev.preventDefault();
|
||||
this.createTouchAction(ev, ActionType.Up).forEach(v => {
|
||||
this.captureEvent(ActionType.Up, v);
|
||||
this.touchInfo.delete(v.identifier);
|
||||
});
|
||||
});
|
||||
document.addEventListener('touchmove', ev => {
|
||||
ev.preventDefault();
|
||||
this.createTouchAction(ev, ActionType.Move).forEach(v => {
|
||||
const touch = this.touchInfo.get(v.identifier);
|
||||
if (!touch) return;
|
||||
const temp = this.beforeHovered;
|
||||
temp.clear();
|
||||
this.beforeHovered = this.hoveredElement;
|
||||
this.hoveredElement = temp;
|
||||
this.captureEvent(ActionType.Move, v);
|
||||
this.checkTouchEnterLeave(
|
||||
ev,
|
||||
this.beforeHovered,
|
||||
this.hoveredElement
|
||||
);
|
||||
});
|
||||
});
|
||||
canvas.addEventListener('wheel', ev => {
|
||||
ev.preventDefault();
|
||||
this.captureEvent(
|
||||
ActionType.Wheel,
|
||||
this.createWheelAction(ev, ActionType.Wheel)
|
||||
);
|
||||
});
|
||||
// 文档监听
|
||||
const abort = new AbortController();
|
||||
const signal = abort.signal;
|
||||
this.abort = abort;
|
||||
const clear = (ev: MouseEvent) => {
|
||||
const mouse = this.getMouseButtons(ev);
|
||||
for (const button of this.mouseInfo.keys()) {
|
||||
if (!(mouse & button)) {
|
||||
this.mouseInfo.delete(button);
|
||||
}
|
||||
}
|
||||
};
|
||||
document.addEventListener('click', clear, { signal });
|
||||
document.addEventListener('mouseenter', clear, { signal });
|
||||
document.addEventListener('mouseleave', clear, { signal });
|
||||
}
|
||||
|
||||
private isTouchInCanvas(clientX: number, clientY: number) {
|
||||
const rect = this.target.canvas.getBoundingClientRect();
|
||||
const { left, right, top, bottom } = rect;
|
||||
const x = clientX;
|
||||
const y = clientY;
|
||||
return x >= left && x <= right && y >= top && y <= bottom;
|
||||
}
|
||||
|
||||
private getMouseType(ev: MouseEvent): MouseType {
|
||||
switch (ev.button) {
|
||||
case 0:
|
||||
return MouseType.Left;
|
||||
case 1:
|
||||
return MouseType.Middle;
|
||||
case 2:
|
||||
return MouseType.Right;
|
||||
case 3:
|
||||
return MouseType.Back;
|
||||
case 4:
|
||||
return MouseType.Forward;
|
||||
}
|
||||
return MouseType.None;
|
||||
}
|
||||
|
||||
private getActiveMouseIdentifier(mouse: MouseType) {
|
||||
if (this.lastMouse === MouseType.None) {
|
||||
return -1;
|
||||
} else {
|
||||
const info = this.mouseInfo.get(mouse);
|
||||
if (!info) return -1;
|
||||
else return info.identifier;
|
||||
}
|
||||
}
|
||||
|
||||
private getMouseIdentifier(type: ActionType, mouse: MouseType): number {
|
||||
switch (type) {
|
||||
case ActionType.Down: {
|
||||
const id = this.actionIdentifier++;
|
||||
this.mouseInfo.set(mouse, { identifier: id });
|
||||
return id;
|
||||
}
|
||||
case ActionType.Move:
|
||||
case ActionType.Enter:
|
||||
case ActionType.Leave:
|
||||
case ActionType.Wheel: {
|
||||
return this.getActiveMouseIdentifier(mouse);
|
||||
}
|
||||
case ActionType.Up:
|
||||
case ActionType.Click: {
|
||||
const id = this.getActiveMouseIdentifier(mouse);
|
||||
this.mouseInfo.delete(mouse);
|
||||
return id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getMouseButtons(event: MouseEvent): number {
|
||||
if (event.buttons === 0) return MouseType.None;
|
||||
let buttons = 0;
|
||||
if (event.buttons & 0b1) buttons |= MouseType.Left;
|
||||
if (event.buttons & 0b10) buttons |= MouseType.Right;
|
||||
if (event.buttons & 0b100) buttons |= MouseType.Middle;
|
||||
if (event.buttons & 0b1000) buttons |= MouseType.Back;
|
||||
if (event.buttons & 0b10000) buttons |= MouseType.Forward;
|
||||
return buttons;
|
||||
}
|
||||
|
||||
private createMouseActionBase(
|
||||
event: MouseEvent,
|
||||
target: RenderItem = this,
|
||||
mouse: MouseType = this.getMouseType(event)
|
||||
): IActionEventBase {
|
||||
return {
|
||||
target: target,
|
||||
touch: false,
|
||||
type: mouse,
|
||||
buttons: this.getMouseButtons(event),
|
||||
altKey: event.altKey,
|
||||
ctrlKey: event.ctrlKey,
|
||||
shiftKey: event.shiftKey,
|
||||
metaKey: event.metaKey
|
||||
};
|
||||
}
|
||||
|
||||
private createTouchActionBase(
|
||||
event: TouchEvent,
|
||||
target: RenderItem
|
||||
): IActionEventBase {
|
||||
return {
|
||||
target: target,
|
||||
touch: false,
|
||||
type: MouseType.Left,
|
||||
buttons: MouseType.Left,
|
||||
altKey: event.altKey,
|
||||
ctrlKey: event.ctrlKey,
|
||||
shiftKey: event.shiftKey,
|
||||
metaKey: event.metaKey
|
||||
};
|
||||
}
|
||||
|
||||
private createMouseAction(
|
||||
event: MouseEvent,
|
||||
type: ActionType,
|
||||
mouse: MouseType = this.getMouseType(event)
|
||||
): IActionEvent {
|
||||
const id = this.getMouseIdentifier(type, mouse);
|
||||
const x = event.offsetX / core.domStyle.scale;
|
||||
const y = event.offsetY / core.domStyle.scale;
|
||||
|
||||
return {
|
||||
target: this,
|
||||
identifier: id,
|
||||
touch: false,
|
||||
offsetX: x,
|
||||
offsetY: y,
|
||||
absoluteX: x,
|
||||
absoluteY: y,
|
||||
type: mouse,
|
||||
buttons: this.getMouseButtons(event),
|
||||
altKey: event.altKey,
|
||||
ctrlKey: event.ctrlKey,
|
||||
shiftKey: event.shiftKey,
|
||||
metaKey: event.metaKey,
|
||||
stopPropagation: () => {
|
||||
this.propagationStoped.set(type, true);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private createWheelAction(
|
||||
event: WheelEvent,
|
||||
type: ActionType,
|
||||
mouse: MouseType = this.getMouseType(event)
|
||||
): IWheelEvent {
|
||||
const ev = this.createMouseAction(event, type, mouse) as IWheelEvent;
|
||||
ev.wheelX = event.deltaX;
|
||||
ev.wheelY = event.deltaY;
|
||||
ev.wheelZ = event.deltaZ;
|
||||
switch (event.deltaMode) {
|
||||
case 0x00:
|
||||
ev.wheelType = WheelType.Pixel;
|
||||
break;
|
||||
case 0x01:
|
||||
ev.wheelType = WheelType.Line;
|
||||
break;
|
||||
case 0x02:
|
||||
ev.wheelType = WheelType.Page;
|
||||
break;
|
||||
default:
|
||||
ev.wheelType = WheelType.None;
|
||||
break;
|
||||
}
|
||||
return ev;
|
||||
}
|
||||
|
||||
private getTouchIdentifier(touch: Touch, type: ActionType) {
|
||||
if (type === ActionType.Down) {
|
||||
const id = this.actionIdentifier++;
|
||||
this.touchInfo.set(touch.identifier, {
|
||||
identifier: id,
|
||||
clientX: touch.clientX,
|
||||
clientY: touch.clientY,
|
||||
hovered: this.isTouchInCanvas(touch.clientX, touch.clientY)
|
||||
});
|
||||
return id;
|
||||
}
|
||||
const info = this.touchInfo.get(touch.identifier);
|
||||
if (!info) return -1;
|
||||
return info.identifier;
|
||||
}
|
||||
|
||||
private createTouch(
|
||||
touch: Touch,
|
||||
type: ActionType,
|
||||
event: TouchEvent,
|
||||
rect: DOMRect
|
||||
): IActionEvent {
|
||||
const x = (touch.clientX - rect.left) / core.domStyle.scale;
|
||||
const y = (touch.clientY - rect.top) / core.domStyle.scale;
|
||||
return {
|
||||
target: this,
|
||||
identifier: this.getTouchIdentifier(touch, type),
|
||||
touch: true,
|
||||
offsetX: x,
|
||||
offsetY: y,
|
||||
absoluteX: x,
|
||||
absoluteY: y,
|
||||
type: MouseType.Left,
|
||||
buttons: MouseType.Left,
|
||||
altKey: event.altKey,
|
||||
ctrlKey: event.ctrlKey,
|
||||
shiftKey: event.shiftKey,
|
||||
metaKey: event.metaKey,
|
||||
stopPropagation: () => {
|
||||
this.propagationStoped.set(type, true);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private createTouchAction(
|
||||
event: TouchEvent,
|
||||
type: ActionType
|
||||
): IActionEvent[] {
|
||||
const list: IActionEvent[] = [];
|
||||
const rect = this.target.canvas.getBoundingClientRect();
|
||||
if (type === ActionType.Up) {
|
||||
// 抬起是一个需要特殊处理的东西,因为 touches 不会包含这个内容,所以需要特殊处理
|
||||
const touches = Array.from(event.touches).map(v => v.identifier);
|
||||
for (const [id, touch] of this.touchList) {
|
||||
if (!touches.includes(id)) {
|
||||
// 如果不包含,才需要触发
|
||||
if (this.isTouchInCanvas(touch.clientX, touch.clientY)) {
|
||||
const ev = this.createTouch(touch, type, event, rect);
|
||||
list.push(ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Array.from(event.touches).forEach(v => {
|
||||
const ev = this.createTouch(v, type, event, rect);
|
||||
if (type === ActionType.Move) {
|
||||
const touch = this.touchInfo.get(v.identifier);
|
||||
if (!touch) return;
|
||||
const moveX = touch.clientX - v.clientX;
|
||||
const moveY = touch.clientY - v.clientY;
|
||||
if (moveX !== 0 || moveY !== 0) {
|
||||
list.push(ev);
|
||||
}
|
||||
} else if (type === ActionType.Down) {
|
||||
this.touchList.set(v.identifier, v);
|
||||
if (this.isTouchInCanvas(v.clientX, v.clientY)) {
|
||||
list.push(ev);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private checkMouseEnterLeave(
|
||||
event: MouseEvent,
|
||||
before: Set<RenderItem>,
|
||||
now: Set<RenderItem>
|
||||
) {
|
||||
// 先 leave,再 enter
|
||||
before.forEach(v => {
|
||||
if (!now.has(v)) {
|
||||
v.emit('leave', this.createMouseActionBase(event, v));
|
||||
}
|
||||
});
|
||||
now.forEach(v => {
|
||||
if (!before.has(v)) {
|
||||
v.emit('enter', this.createMouseActionBase(event, v));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private checkTouchEnterLeave(
|
||||
event: TouchEvent,
|
||||
before: Set<RenderItem>,
|
||||
now: Set<RenderItem>
|
||||
) {
|
||||
// 先 leave,再 enter
|
||||
before.forEach(v => {
|
||||
if (!now.has(v)) {
|
||||
v.emit('leave', this.createTouchActionBase(event, v));
|
||||
}
|
||||
});
|
||||
now.forEach(v => {
|
||||
if (!before.has(v)) {
|
||||
v.emit('enter', this.createTouchActionBase(event, v));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
update(_item: RenderItem = this) {
|
||||
this.cacheDirty = true;
|
||||
}
|
||||
|
||||
protected refresh(): void {
|
||||
if (!this.cacheDirty) return;
|
||||
this.target.clear();
|
||||
this.renderContent(this.target, Transform.identity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据渲染元素的id获取一个渲染元素
|
||||
* @param id 要获取的渲染元素id
|
||||
* @returns
|
||||
*/
|
||||
getElementById(id: string): RenderItem | null {
|
||||
if (id.length === 0) return null;
|
||||
const item = this.idMap.get(id);
|
||||
if (item) return item;
|
||||
else {
|
||||
const item = this.searchElement(this, id);
|
||||
if (item) {
|
||||
this.idMap.set(id, item);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
private searchElement(ele: RenderItem, id: string): RenderItem | null {
|
||||
for (const child of ele.children) {
|
||||
if (child.id === id) return child;
|
||||
else {
|
||||
const ele = this.searchElement(child, id);
|
||||
if (ele) return ele;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
connect(item: RenderItem): void {
|
||||
if (item.id.length === 0) return;
|
||||
const existed = this.idMap.get(item.id);
|
||||
if (existed) {
|
||||
if (existed === item) return;
|
||||
else logger.warn(23, item.id);
|
||||
} else {
|
||||
this.idMap.set(item.id, item);
|
||||
}
|
||||
}
|
||||
|
||||
disconnect(item: RenderItem): void {
|
||||
this.idMap.delete(item.id);
|
||||
}
|
||||
|
||||
modifyId(item: RenderItem, previous: string, current: string): void {
|
||||
this.idMap.delete(previous);
|
||||
if (current.length !== 0) {
|
||||
if (this.idMap.has(item.id)) {
|
||||
logger.warn(23, item.id);
|
||||
} else {
|
||||
this.idMap.set(item.id, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getCanvas(): HTMLCanvasElement {
|
||||
return this.target.canvas;
|
||||
}
|
||||
|
||||
hoverElement(element: RenderItem): void {
|
||||
if (element.cursor !== 'inherit') {
|
||||
this.targetCursor = element.cursor;
|
||||
}
|
||||
this.hoveredElement.add(element);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
super.destroy();
|
||||
MotaRenderer.list.delete(this.id);
|
||||
this.target.delete();
|
||||
this.abort?.abort();
|
||||
}
|
||||
|
||||
private toTagString(item: RenderItem, space: number, deep: number): string {
|
||||
if (item.isComment) return '';
|
||||
const name = item.constructor.name;
|
||||
if (item.children.size === 0) {
|
||||
return `${' '.repeat(deep * space)}<${name} ${item.id ? `id="${item.id}" ` : ''}uid="${item.uid}"${item.hidden ? ' hidden' : ''} />\n`;
|
||||
} else {
|
||||
return (
|
||||
`${' '.repeat(deep * space)}<${name} ${item.id ? `${item.id} ` : ''}uid="${item.uid}" ${item.hidden ? 'hidden' : ''}>\n` +
|
||||
`${[...item.children]
|
||||
.filter(v => !v.isComment)
|
||||
.map(v => this.toTagString(v, space, deep + 1))
|
||||
.join('')}` +
|
||||
`${' '.repeat(deep * space)}</${name}>\n`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调试功能,将渲染树输出为 XML 标签形式,只包含渲染元素类名,以及元素 id 等基础属性,不包含属性值等
|
||||
* @param space 缩进空格数
|
||||
*/
|
||||
toTagTree(space: number = 4) {
|
||||
if (!import.meta.env.DEV) return '';
|
||||
return this.toTagString(this, space, 0);
|
||||
}
|
||||
|
||||
static get(id: string) {
|
||||
return this.list.get(id);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
MotaRenderer.list.forEach(v =>
|
||||
v.requestAfterFrame(() => v.refreshAllChildren())
|
||||
);
|
||||
});
|
||||
|
||||
// @ts-expect-error debug
|
||||
window.logTagTree = () => {
|
||||
console.log(MotaRenderer.get('render-main')?.toTagTree());
|
||||
};
|
||||
@@ -1,135 +0,0 @@
|
||||
import {
|
||||
ComponentOptionsMixin,
|
||||
defineComponent,
|
||||
DefineComponent,
|
||||
h,
|
||||
ReservedProps,
|
||||
VNodeProps
|
||||
} from 'vue';
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import {
|
||||
AnimateProps,
|
||||
BaseProps,
|
||||
BezierProps,
|
||||
CirclesProps,
|
||||
CommentProps,
|
||||
ConatinerCustomProps,
|
||||
ContainerProps,
|
||||
CustomProps,
|
||||
DamageProps,
|
||||
EllipseProps,
|
||||
IconProps,
|
||||
ImageProps,
|
||||
LayerGroupProps,
|
||||
LayerProps,
|
||||
LineProps,
|
||||
PathProps,
|
||||
QuadraticProps,
|
||||
RectProps,
|
||||
RectRProps,
|
||||
ShaderProps,
|
||||
SpriteProps,
|
||||
TextProps,
|
||||
WinskinProps
|
||||
} from './props';
|
||||
import { ERenderItemEvent, RenderItem } from '../item';
|
||||
import { ESpriteEvent } from '../sprite';
|
||||
import { EContainerEvent } from '../container';
|
||||
import {
|
||||
EIconEvent,
|
||||
EImageEvent,
|
||||
ETextEvent,
|
||||
EWinskinEvent
|
||||
} from '../preset/misc';
|
||||
import { ELayerEvent, ELayerGroupEvent } from '../preset/layer';
|
||||
import { EAnimateEvent } from '../preset/animate';
|
||||
import { EDamageEvent } from '../preset/damage';
|
||||
import { EShaderEvent } from '../shader';
|
||||
import { EGraphicItemEvent } from '../preset/graphics';
|
||||
|
||||
export type WrapEventEmitterEvents<T extends EventEmitter.ValidEventTypes> =
|
||||
T extends string | symbol
|
||||
? T
|
||||
: {
|
||||
[P in keyof T]: T[P] extends any[]
|
||||
? (...args: T[P]) => void
|
||||
: (...args: any[]) => void;
|
||||
};
|
||||
|
||||
type MappingEvent<E extends ERenderItemEvent> = {
|
||||
[P in keyof WrapEventEmitterEvents<E> as P extends string
|
||||
? `on${Capitalize<P>}`
|
||||
: never]?: WrapEventEmitterEvents<E>[P];
|
||||
};
|
||||
|
||||
type _Define<P extends BaseProps, E extends ERenderItemEvent> = DefineComponent<
|
||||
P,
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
ComponentOptionsMixin,
|
||||
ComponentOptionsMixin,
|
||||
WrapEventEmitterEvents<E>,
|
||||
Exclude<keyof WrapEventEmitterEvents<E>, number | symbol>,
|
||||
VNodeProps,
|
||||
Readonly<P & MappingEvent<E>>
|
||||
>;
|
||||
|
||||
export type TagDefine<T extends object, E extends ERenderItemEvent> = T &
|
||||
MappingEvent<E> &
|
||||
ReservedProps;
|
||||
|
||||
declare module 'vue/jsx-runtime' {
|
||||
namespace JSX {
|
||||
export interface IntrinsicElements {
|
||||
sprite: TagDefine<SpriteProps, ESpriteEvent>;
|
||||
container: TagDefine<ContainerProps, EContainerEvent>;
|
||||
'container-custom': TagDefine<
|
||||
ConatinerCustomProps,
|
||||
EContainerEvent
|
||||
>;
|
||||
shader: TagDefine<ShaderProps, EShaderEvent>;
|
||||
text: TagDefine<TextProps, ETextEvent>;
|
||||
image: TagDefine<ImageProps, EImageEvent>;
|
||||
comment: TagDefine<CommentProps, ERenderItemEvent>;
|
||||
custom: TagDefine<CustomProps, ERenderItemEvent>;
|
||||
layer: TagDefine<LayerProps, ELayerEvent>;
|
||||
'layer-group': TagDefine<LayerGroupProps, ELayerGroupEvent>;
|
||||
damage: TagDefine<DamageProps, EDamageEvent>;
|
||||
animation: TagDefine<AnimateProps, EAnimateEvent>;
|
||||
'g-rect': TagDefine<RectProps, EGraphicItemEvent>;
|
||||
'g-circle': TagDefine<CirclesProps, EGraphicItemEvent>;
|
||||
'g-ellipse': TagDefine<EllipseProps, EGraphicItemEvent>;
|
||||
'g-line': TagDefine<LineProps, EGraphicItemEvent>;
|
||||
'g-bezier': TagDefine<BezierProps, EGraphicItemEvent>;
|
||||
'g-quad': TagDefine<QuadraticProps, EGraphicItemEvent>;
|
||||
'g-path': TagDefine<PathProps, EGraphicItemEvent>;
|
||||
'g-rectr': TagDefine<RectRProps, EGraphicItemEvent>;
|
||||
icon: TagDefine<IconProps, EIconEvent>;
|
||||
winskin: TagDefine<WinskinProps, EWinskinEvent>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface InstancedElementProp {
|
||||
item: RenderItem;
|
||||
}
|
||||
|
||||
export function wrapInstancedComponent<
|
||||
P extends BaseProps = BaseProps,
|
||||
E extends ERenderItemEvent = ERenderItemEvent,
|
||||
C extends RenderItem = RenderItem
|
||||
>(onCreate: (props: P) => C): _Define<P, E> {
|
||||
const Com = defineComponent((props, ctx) => {
|
||||
return () => {
|
||||
const p = {
|
||||
...props,
|
||||
...ctx.attrs,
|
||||
_item: onCreate
|
||||
};
|
||||
return h('custom', p, ctx.slots);
|
||||
};
|
||||
});
|
||||
return Com as _Define<P, E>;
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
import {
|
||||
ComponentInternalInstance,
|
||||
createRenderer,
|
||||
ElementNamespace,
|
||||
VNodeProps
|
||||
} from 'vue';
|
||||
import { ERenderItemEvent, RenderItem } from '../item';
|
||||
import { tagMap } from './map';
|
||||
import { logger } from '@/core/common/logger';
|
||||
import { Comment, ETextEvent, Text } from '../preset/misc';
|
||||
import { BaseProps } from './props';
|
||||
import { TagDefine } from './elements';
|
||||
|
||||
export type DefaultProps<
|
||||
P extends BaseProps = BaseProps,
|
||||
E extends ERenderItemEvent = ERenderItemEvent
|
||||
> = TagDefine<P, E>;
|
||||
|
||||
export const { createApp, render } = createRenderer<RenderItem, RenderItem>({
|
||||
patchProp: function (
|
||||
el: RenderItem,
|
||||
key: string,
|
||||
prevValue: any,
|
||||
nextValue: any,
|
||||
namespace?: ElementNamespace,
|
||||
parentComponent?: ComponentInternalInstance | null
|
||||
): void {
|
||||
el.patchProp(key, prevValue, nextValue, namespace, parentComponent);
|
||||
},
|
||||
|
||||
insert: function (
|
||||
el: RenderItem<ERenderItemEvent>,
|
||||
parent: RenderItem,
|
||||
_anchor?: RenderItem<ERenderItemEvent> | null
|
||||
): void {
|
||||
parent.appendChild(el);
|
||||
},
|
||||
|
||||
remove: function (el: RenderItem<ERenderItemEvent>): void {
|
||||
el.remove();
|
||||
},
|
||||
|
||||
createElement: function (
|
||||
type: string,
|
||||
namespace?: ElementNamespace,
|
||||
isCustomizedBuiltIn?: string,
|
||||
vnodeProps?: (VNodeProps & { [key: string]: any }) | null
|
||||
): RenderItem {
|
||||
const onCreate = tagMap.get(type);
|
||||
if (!onCreate) {
|
||||
logger.error(20, type);
|
||||
throw new Error(`Cannot create element '${type}'`);
|
||||
}
|
||||
return onCreate(namespace, isCustomizedBuiltIn, vnodeProps);
|
||||
},
|
||||
|
||||
createText: function (text: string): RenderItem<ETextEvent> {
|
||||
if (/^\s*$/.test(text)) {
|
||||
return new Comment();
|
||||
} else {
|
||||
logger.warn(38);
|
||||
}
|
||||
return new Text(text);
|
||||
},
|
||||
|
||||
createComment: function (text: string): RenderItem<ERenderItemEvent> {
|
||||
return new Comment(text);
|
||||
},
|
||||
|
||||
setText: function (node: RenderItem<ERenderItemEvent>, text: string): void {
|
||||
if (node instanceof Text) {
|
||||
node.setText(text);
|
||||
} else {
|
||||
logger.warn(39);
|
||||
}
|
||||
},
|
||||
|
||||
setElementText: function (node: RenderItem, text: string): void {
|
||||
if (node instanceof Text) {
|
||||
node.setText(text);
|
||||
} else {
|
||||
logger.warn(39);
|
||||
}
|
||||
},
|
||||
|
||||
parentNode: function (
|
||||
node: RenderItem<ERenderItemEvent>
|
||||
): RenderItem | null {
|
||||
return node.parent ?? null;
|
||||
},
|
||||
|
||||
nextSibling: function (
|
||||
node: RenderItem<ERenderItemEvent>
|
||||
): RenderItem<ERenderItemEvent> | null {
|
||||
if (!node) return null;
|
||||
if (!node.parent) {
|
||||
return null;
|
||||
} else {
|
||||
const parent = node.parent;
|
||||
const list = [...parent.children];
|
||||
const index = list.indexOf(node);
|
||||
return list[index] ?? null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export * from './elements';
|
||||
export * from './map';
|
||||
export * from './props';
|
||||
export * from './use';
|
||||
@@ -1,256 +0,0 @@
|
||||
import { logger } from '@/core/common/logger';
|
||||
import { ERenderItemEvent, RenderItem, RenderItemPosition } from '../item';
|
||||
import { ElementNamespace, VNodeProps } from 'vue';
|
||||
import { Container, ContainerCustom } from '../container';
|
||||
import { MotaRenderer } from '../render';
|
||||
import { Sprite } from '../sprite';
|
||||
import {
|
||||
Comment,
|
||||
ETextEvent,
|
||||
Icon,
|
||||
Image,
|
||||
Text,
|
||||
Winskin
|
||||
} from '../preset/misc';
|
||||
import { Shader } from '../shader';
|
||||
import { Animate, Damage, EDamageEvent, Layer, LayerGroup } from '../preset';
|
||||
import {
|
||||
BezierCurve,
|
||||
Circle,
|
||||
Ellipse,
|
||||
Line,
|
||||
Path,
|
||||
QuadraticCurve,
|
||||
Rect,
|
||||
RectR
|
||||
} from '../preset/graphics';
|
||||
|
||||
type OnItemCreate<
|
||||
E extends ERenderItemEvent = ERenderItemEvent,
|
||||
T extends RenderItem<E> = RenderItem<E>
|
||||
> = (
|
||||
namespace?: ElementNamespace,
|
||||
isCustomizedBuiltIn?: string,
|
||||
vnodeProps?: (VNodeProps & { [key: string]: any }) | null
|
||||
) => T;
|
||||
|
||||
class RenderTagMap {
|
||||
private map: Map<string, OnItemCreate> = new Map();
|
||||
|
||||
/**
|
||||
* 注册一个标签,每个标签对应一类元素,重复注册会覆盖之前的
|
||||
* @param tag 标签名称
|
||||
* @param ele 对应的元素类或其构造器
|
||||
*/
|
||||
register<E extends ERenderItemEvent, T extends RenderItem<E>>(
|
||||
tag: string,
|
||||
onCreate: OnItemCreate<E, T>
|
||||
) {
|
||||
if (this.map.has(tag)) {
|
||||
logger.warn(34, tag);
|
||||
}
|
||||
this.map.set(tag, onCreate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一个标签对应的元素构造器
|
||||
* @param tag 标签名
|
||||
*/
|
||||
get<E extends ERenderItemEvent, T extends RenderItem<E>>(
|
||||
tag: string
|
||||
): OnItemCreate<E, T> | undefined {
|
||||
return this.map.get(tag) as OnItemCreate<E, T>;
|
||||
}
|
||||
}
|
||||
|
||||
export const tagMap = new RenderTagMap();
|
||||
|
||||
const standardElement = (
|
||||
Item: new (
|
||||
type: RenderItemPosition,
|
||||
cache?: boolean,
|
||||
fall?: boolean
|
||||
) => RenderItem
|
||||
) => {
|
||||
return (_0: any, _1: any, props?: any) => {
|
||||
if (!props) return new Item('static');
|
||||
else {
|
||||
const {
|
||||
type = 'static',
|
||||
cache = true,
|
||||
fall = false,
|
||||
nocache = false
|
||||
} = props;
|
||||
return new Item(type, cache && !nocache, fall);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const standardElementNoCache = (
|
||||
Item: new (
|
||||
type: RenderItemPosition,
|
||||
cache?: boolean,
|
||||
fall?: boolean
|
||||
) => RenderItem
|
||||
) => {
|
||||
return (_0: any, _1: any, props?: any) => {
|
||||
if (!props) return new Item('static');
|
||||
else {
|
||||
const {
|
||||
type = 'static',
|
||||
cache = false,
|
||||
fall = false,
|
||||
nocache = true
|
||||
} = props;
|
||||
return new Item(type, cache && !nocache, fall);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const enum ElementState {
|
||||
None = 0,
|
||||
Cache = 1,
|
||||
Fall = 2
|
||||
}
|
||||
|
||||
/**
|
||||
* standardElementFor
|
||||
*/
|
||||
const _se = (
|
||||
Item: new (
|
||||
type: RenderItemPosition,
|
||||
cache?: boolean,
|
||||
fall?: boolean
|
||||
) => RenderItem,
|
||||
position: RenderItemPosition,
|
||||
state: ElementState
|
||||
) => {
|
||||
const defaultCache = !!(state & ElementState.Cache);
|
||||
const defautFall = !!(state & ElementState.Fall);
|
||||
|
||||
return (_0: any, _1: any, props?: any) => {
|
||||
if (!props) return new Item('absolute');
|
||||
else {
|
||||
const {
|
||||
type = position,
|
||||
cache = defaultCache,
|
||||
fall = defautFall,
|
||||
nocache = !defaultCache
|
||||
} = props;
|
||||
return new Item(type, cache && !nocache, fall);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Default elements
|
||||
tagMap.register('container', standardElement(Container));
|
||||
tagMap.register('container-custom', standardElement(ContainerCustom));
|
||||
tagMap.register('template', standardElement(Container));
|
||||
tagMap.register('mota-renderer', (_0, _1, props) => {
|
||||
return new MotaRenderer(props?.id);
|
||||
});
|
||||
tagMap.register('sprite', standardElement(Sprite));
|
||||
tagMap.register<ETextEvent, Text>('text', (_0, _1, props) => {
|
||||
if (!props) return new Text();
|
||||
else {
|
||||
const { type = 'static', text = '' } = props;
|
||||
return new Text(text, type);
|
||||
}
|
||||
});
|
||||
tagMap.register('image', (_0, _1, props) => {
|
||||
if (!props) return new Image(core.material.images.images['bg.webp']);
|
||||
else {
|
||||
const {
|
||||
image = core.material.images.images['bg.webp'],
|
||||
type = 'static'
|
||||
} = props;
|
||||
return new Image(image, type);
|
||||
}
|
||||
});
|
||||
tagMap.register('comment', (_0, _1, props) => {
|
||||
if (!props) return new Comment();
|
||||
else {
|
||||
const { text = '' } = props;
|
||||
return new Comment(text);
|
||||
}
|
||||
});
|
||||
tagMap.register('shader', (_0, _1, props) => {
|
||||
if (!props) return new Shader();
|
||||
else {
|
||||
const { type = 'static' } = props;
|
||||
return new Shader(type);
|
||||
}
|
||||
});
|
||||
tagMap.register('custom', (_0, _1, props) => {
|
||||
if (!props) {
|
||||
logger.error(22);
|
||||
throw new Error('Cannot create custom element.');
|
||||
} else {
|
||||
const item = props._item;
|
||||
if (!item) {
|
||||
logger.error(22);
|
||||
throw new Error('Cannot create custom element.');
|
||||
}
|
||||
return item(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<EDamageEvent, Damage>('damage', (_0, _1, _props) => {
|
||||
return new Damage();
|
||||
});
|
||||
tagMap.register('animation', (_0, _1, _props) => {
|
||||
return new Animate();
|
||||
});
|
||||
tagMap.register('g-rect', standardElementNoCache(Rect));
|
||||
tagMap.register('g-circle', standardElementNoCache(Circle));
|
||||
tagMap.register('g-ellipse', standardElementNoCache(Ellipse));
|
||||
tagMap.register('g-line', standardElementNoCache(Line));
|
||||
tagMap.register('g-bezier', standardElementNoCache(BezierCurve));
|
||||
tagMap.register('g-quad', standardElementNoCache(QuadraticCurve));
|
||||
tagMap.register('g-path', standardElementNoCache(Path));
|
||||
tagMap.register('g-rectr', standardElementNoCache(RectR));
|
||||
tagMap.register('icon', standardElementNoCache(Icon));
|
||||
tagMap.register('winskin', (_0, _1, props) => {
|
||||
if (!props) return new Winskin(core.material.images.images['winskin.png']);
|
||||
else {
|
||||
const {
|
||||
image = core.material.images.images['winskin.png'],
|
||||
type = 'static'
|
||||
} = props;
|
||||
return new Winskin(image, type);
|
||||
}
|
||||
});
|
||||
@@ -1,256 +0,0 @@
|
||||
import { RenderFunction, RenderItem, RenderItemPosition } from '../item';
|
||||
import { Transform } from '../transform';
|
||||
import {
|
||||
FloorLayer,
|
||||
ILayerGroupRenderExtends,
|
||||
ILayerRenderExtends
|
||||
} from '../preset/layer';
|
||||
import type { EnemyCollection } from '@/game/enemy/damage';
|
||||
import {
|
||||
BezierParams,
|
||||
CircleParams,
|
||||
EllipseParams,
|
||||
ILineProperty,
|
||||
LineParams,
|
||||
QuadParams,
|
||||
RectRCircleParams,
|
||||
RectREllipseParams
|
||||
} from '../preset/graphics';
|
||||
import { ElementAnchor, ElementLocator, ElementScale } from '../utils';
|
||||
import { CustomContainerRenderFn } from '../container';
|
||||
import { Font } from '../style/font';
|
||||
|
||||
export interface CustomProps {
|
||||
_item: (props: BaseProps) => RenderItem;
|
||||
}
|
||||
|
||||
export interface BaseProps {
|
||||
/** 元素的横坐标 */
|
||||
x?: number;
|
||||
/** 元素的纵坐标 */
|
||||
y?: number;
|
||||
/** 元素的横向锚点位置 */
|
||||
anchorX?: number;
|
||||
/** 元素的纵向锚点位置 */
|
||||
anchorY?: number;
|
||||
/** 元素的纵深,值越大越靠上 */
|
||||
zIndex?: number;
|
||||
/** 元素的宽度 */
|
||||
width?: number;
|
||||
/** 元素的高度 */
|
||||
height?: number;
|
||||
/** 元素的滤镜 */
|
||||
filter?: string;
|
||||
/** 是否启用高清画布 */
|
||||
hd?: boolean;
|
||||
/** 是否启用抗锯齿 */
|
||||
anti?: boolean;
|
||||
/** 是否不启用抗锯齿,优先级大于 anti,主要用于像素图片渲染 */
|
||||
noanti?: boolean;
|
||||
/** 元素是否隐藏,可以用于一些画面效果,也可以用于调试 */
|
||||
hidden?: boolean;
|
||||
/** 元素的变换矩阵 */
|
||||
transform?: Transform;
|
||||
/** 元素的定位模式,static 表示常规定位,absolute 定位模式下元素位置始终处于左上角 */
|
||||
type?: RenderItemPosition;
|
||||
/** 是否启用缓存,用处较少,主要用于一些默认不启用缓存的元素的特殊优化 */
|
||||
cache?: boolean;
|
||||
/** 是否不启用缓存,优先级大于 cache,用处较少,主要用于一些特殊优化 */
|
||||
nocache?: boolean;
|
||||
/** 是否启用变换矩阵下穿,下穿模式下,当前元素会使用由父元素传递过来的变换矩阵,而非元素自身的 */
|
||||
fall?: boolean;
|
||||
/** 这个元素的唯一标识符,不可重复 */
|
||||
id?: string;
|
||||
/** 这个元素的不透明度 */
|
||||
alpha?: number;
|
||||
/** 这个元素与已渲染内容的混合模式,默认为 source-over */
|
||||
composite?: GlobalCompositeOperation;
|
||||
/** 鼠标放在这个元素上时的光标样式 */
|
||||
cursor?: string;
|
||||
/**
|
||||
* 定位属性,可以填 `[横坐标,纵坐标,宽度,高度,x锚点,y锚点]`,
|
||||
* 对于横坐标与纵坐标、宽度与高度、x锚点与y锚点,两两一组要么都填,要么都不填
|
||||
* 是 x, y, width, height, anchorX, anchorY 的简写属性
|
||||
*/
|
||||
loc?: ElementLocator;
|
||||
/** 锚点属性,可以填 `[x锚点,y锚点]`,是 anchorX, anchorY 的简写属性 */
|
||||
anc?: ElementAnchor;
|
||||
/** 放缩属性,可以填 `[x比例,y比例]`,是 transform 的简写属性之一 */
|
||||
scale?: ElementScale;
|
||||
/** 旋转属性,单位弧度,是 transform 的简写属性之一 */
|
||||
rotate?: number;
|
||||
/** 这个元素是否不会触发任何交互事件(cursor 属性也会无效),当执行到此元素时,会下穿至下一个元素 */
|
||||
noevent?: boolean;
|
||||
}
|
||||
|
||||
export interface SpriteProps extends BaseProps {
|
||||
render?: RenderFunction;
|
||||
}
|
||||
|
||||
export interface ContainerProps extends BaseProps {}
|
||||
|
||||
export interface ConatinerCustomProps extends ContainerProps {
|
||||
render?: CustomContainerRenderFn;
|
||||
}
|
||||
|
||||
export interface GL2Props extends BaseProps {}
|
||||
|
||||
export interface ShaderProps extends BaseProps {}
|
||||
|
||||
export interface TextProps extends BaseProps {
|
||||
text?: string;
|
||||
fillStyle?: CanvasStyle;
|
||||
strokeStyle?: CanvasStyle;
|
||||
font?: Font;
|
||||
strokeWidth?: number;
|
||||
}
|
||||
|
||||
export interface ImageProps extends BaseProps {
|
||||
image: CanvasImageSource;
|
||||
}
|
||||
|
||||
export interface CommentProps extends BaseProps {
|
||||
text?: string;
|
||||
}
|
||||
|
||||
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 AnimateProps extends BaseProps {}
|
||||
|
||||
export interface DamageProps extends BaseProps {
|
||||
mapWidth?: number;
|
||||
mapHeight?: number;
|
||||
cellSize?: number;
|
||||
enemy?: EnemyCollection;
|
||||
font?: string;
|
||||
strokeStyle?: CanvasStyle;
|
||||
strokeWidth?: number;
|
||||
}
|
||||
|
||||
export interface GraphicPropsBase extends BaseProps, Partial<ILineProperty> {
|
||||
/** 是否填充,若填写 {@link stroke},那么表现为先填充后描边 */
|
||||
fill?: boolean;
|
||||
/** 是否描边,若填写 {@link fill},那么表现为先填充后描边 */
|
||||
stroke?: boolean;
|
||||
/** 是否先描边后填充,优先级最高,若设置,则 {@link fill} 与 {@link stroke} 无效。 */
|
||||
strokeAndFill?: boolean;
|
||||
/** 填充原则,比如 `nonzero` 表示非零环绕原则,默认为奇偶环绕原则 `evenodd` */
|
||||
fillRule?: CanvasFillRule;
|
||||
/** 填充样式 */
|
||||
fillStyle?: CanvasStyle;
|
||||
/** 描边样式 */
|
||||
strokeStyle?: CanvasStyle;
|
||||
/** 在交互时,是否只检查交互位置只在描边上,对 fill, stroke, strokeAndFill 均有效 */
|
||||
actionStroke?: boolean;
|
||||
}
|
||||
|
||||
export interface RectProps extends GraphicPropsBase {}
|
||||
|
||||
export interface CirclesProps extends GraphicPropsBase {
|
||||
radius?: number;
|
||||
start?: number;
|
||||
end?: number;
|
||||
/**
|
||||
* 圆属性参数,可以填 `[半径,起始角度,终止角度]`,是 radius, start, end 的简写,
|
||||
* 其中半径可选,后两项要么都填,要么都不填
|
||||
*/
|
||||
circle?: CircleParams;
|
||||
}
|
||||
|
||||
export interface EllipseProps extends GraphicPropsBase {
|
||||
radiusX?: number;
|
||||
radiusY?: number;
|
||||
start?: number;
|
||||
end?: number;
|
||||
/**
|
||||
* 椭圆属性参数,可以填 `[x半径,y半径,起始角度,终止角度]`,是 radiusX, radiusY, start, end 的简写,
|
||||
* 其中前两项和后两项要么都填,要么都不填
|
||||
*/
|
||||
ellipse?: EllipseParams;
|
||||
}
|
||||
|
||||
export interface LineProps extends GraphicPropsBase {
|
||||
x1?: number;
|
||||
y1?: number;
|
||||
x2?: number;
|
||||
y2?: number;
|
||||
/** 直线属性参数,可以填 `[x1, y1, x2, y2]`,都是必填 */
|
||||
line?: LineParams;
|
||||
}
|
||||
|
||||
export interface BezierProps extends GraphicPropsBase {
|
||||
sx?: number;
|
||||
sy?: number;
|
||||
cp1x?: number;
|
||||
cp1y?: number;
|
||||
cp2x?: number;
|
||||
cp2y?: number;
|
||||
ex?: number;
|
||||
ey?: number;
|
||||
/** 三次贝塞尔曲线参数,可以填 `[sx, sy, cp1x, cp1y, cp2x, cp2y, ex, ey]`,都是必填 */
|
||||
curve?: BezierParams;
|
||||
}
|
||||
|
||||
export interface QuadraticProps extends GraphicPropsBase {
|
||||
sx?: number;
|
||||
sy?: number;
|
||||
cpx?: number;
|
||||
cpy?: number;
|
||||
ex?: number;
|
||||
ey?: number;
|
||||
/** 二次贝塞尔曲线参数,可以填 `[sx, sy, cpx, cpy, ex, ey]`,都是必填 */
|
||||
curve?: QuadParams;
|
||||
}
|
||||
|
||||
export interface PathProps extends GraphicPropsBase {
|
||||
path?: Path2D;
|
||||
}
|
||||
|
||||
export interface RectRProps extends GraphicPropsBase {
|
||||
/**
|
||||
* 圆形圆角参数,可以填 `[r1, r2, r3, r4]`,后三项可选。填写不同数量下的表现:
|
||||
* - 1个:每个角都是 `r1` 半径的圆
|
||||
* - 2个:左上和右下是 `r1` 半径的圆,右上和左下是 `r2` 半径的圆
|
||||
* - 3个:左上是 `r1` 半径的圆,右上和左下是 `r2` 半径的圆,右下是 `r3` 半径的圆
|
||||
* - 4个:左上、右上、左下、右下 分别是 `r1, r2, r3, r4` 半径的圆
|
||||
*/
|
||||
circle?: RectRCircleParams;
|
||||
/**
|
||||
* 椭圆圆角参数,可以填 `[rx1, ry1, rx2, ry2, rx3, ry3, rx4, ry4]`,
|
||||
* 两两一组,后三组可选,填写不同数量下的表现:
|
||||
* - 1组:每个角都是 `[rx1, ry1]` 半径的椭圆
|
||||
* - 2组:左上和右下是 `[rx1, ry1]` 半径的椭圆,右上和左下是 `[rx2, ry2]` 半径的椭圆
|
||||
* - 3组:左上是 `[rx1, ry1]` 半径的椭圆,右上和左下是 `[rx2, ey2]` 半径的椭圆,右下是 `[rx3, ry3]` 半径的椭圆
|
||||
* - 4组:左上、右上、左下、右下 分别是 `[rx1, ry1], [rx2, ry2], [rx3, ry3], [rx4, ry4]` 半径的椭圆
|
||||
*/
|
||||
ellipse?: RectREllipseParams;
|
||||
}
|
||||
|
||||
export interface IconProps extends BaseProps {
|
||||
icon: AllNumbers;
|
||||
frame?: number;
|
||||
animate?: boolean;
|
||||
}
|
||||
|
||||
export interface WinskinProps extends BaseProps {
|
||||
image: ImageIds;
|
||||
borderSize?: number;
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
import { gameKey, Hotkey } from '@/core/main/custom/hotkey';
|
||||
import { Animation, Ticker, Transition } from 'mutate-animate';
|
||||
import { onMounted, onUnmounted } from 'vue';
|
||||
import { ERenderItemEvent, RenderItem } from '../item';
|
||||
import EventEmitter from 'eventemitter3';
|
||||
|
||||
const ticker = new Ticker();
|
||||
|
||||
/**
|
||||
* 在组件中每帧执行一次函数
|
||||
* @param fn 每帧执行的函数
|
||||
*/
|
||||
export function onTick(fn: (time: number) => void) {
|
||||
onMounted(() => {
|
||||
ticker.add(fn);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
ticker.remove(fn);
|
||||
});
|
||||
}
|
||||
|
||||
type AnimationUsing = [Animation];
|
||||
type TransitionUsing = [Transition];
|
||||
|
||||
/**
|
||||
* 在组件中创建一个动画实例
|
||||
*/
|
||||
export function useAnimation(): AnimationUsing {
|
||||
const ani = new Animation();
|
||||
onUnmounted(() => {
|
||||
ani.ticker.destroy();
|
||||
});
|
||||
return [ani];
|
||||
}
|
||||
|
||||
/**
|
||||
* 在组件中创建一个渐变实例
|
||||
*/
|
||||
export function useTransition(): TransitionUsing {
|
||||
const tran = new Transition();
|
||||
onUnmounted(() => {
|
||||
tran.ticker.destroy();
|
||||
});
|
||||
return [tran];
|
||||
}
|
||||
|
||||
type KeyUsing = [Hotkey, symbol];
|
||||
|
||||
/**
|
||||
* 在组件中定义按键操作
|
||||
* @param noScope 是否不创建新作用域
|
||||
*/
|
||||
export function useKey(noScope: boolean = false): KeyUsing {
|
||||
if (noScope) {
|
||||
return [gameKey, gameKey.scope];
|
||||
} else {
|
||||
const sym = Symbol();
|
||||
gameKey.use(sym);
|
||||
onUnmounted(() => {
|
||||
gameKey.dispose();
|
||||
});
|
||||
return [gameKey, sym];
|
||||
}
|
||||
}
|
||||
|
||||
export function onEvent<
|
||||
T extends ERenderItemEvent,
|
||||
K extends EventEmitter.EventNames<T>
|
||||
>(item: RenderItem<T>, key: K, listener: EventEmitter.EventListener<T, K>) {
|
||||
item.on(key, listener);
|
||||
onUnmounted(() => {
|
||||
item.off(key, listener);
|
||||
});
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
import { MotaOffscreenCanvas2D } from '../fx/canvas2d';
|
||||
import { EGL2Event, GL2, GL2Program, IGL2ProgramPrefix } from './gl2';
|
||||
|
||||
const SHADER_PREFIX: IGL2ProgramPrefix = {
|
||||
VERTEX: /* glsl */ `#version 300 es
|
||||
precision highp float;
|
||||
|
||||
in vec4 a_position;
|
||||
in vec2 a_texCoord;
|
||||
|
||||
out vec2 v_texCoord;
|
||||
`,
|
||||
FRAGMENT: /* glsl */ `#version 300 es
|
||||
precision highp float;
|
||||
|
||||
in vec2 v_texCoord;
|
||||
|
||||
uniform sampler2D u_sampler;
|
||||
`
|
||||
};
|
||||
|
||||
const DEFAULT_VS = /* glsl */ `
|
||||
void main() {
|
||||
v_texCoord = a_texCoord;
|
||||
gl_Position = a_position;
|
||||
}
|
||||
`;
|
||||
const DEFAULT_FS = /* glsl */ `
|
||||
void main() {
|
||||
gl_FragColor = texture2D(u_sampler, v_texCoord);
|
||||
}
|
||||
`;
|
||||
|
||||
export interface EShaderEvent extends EGL2Event {}
|
||||
|
||||
export class Shader<E extends EShaderEvent = EShaderEvent> extends GL2<
|
||||
EShaderEvent | E
|
||||
> {
|
||||
protected drawScene(
|
||||
canvas: MotaOffscreenCanvas2D,
|
||||
gl: WebGL2RenderingContext,
|
||||
program: GL2Program
|
||||
): void {
|
||||
if (!program.modified) return;
|
||||
const tex = program.getTexture('u_sampler');
|
||||
if (!tex) return;
|
||||
const c = canvas.canvas;
|
||||
if (tex.width === c.width && tex.height === c.height) {
|
||||
tex.sub(c, 0, 0, c.width, c.height);
|
||||
} else {
|
||||
tex.set(c);
|
||||
}
|
||||
this.draw(gl, program);
|
||||
}
|
||||
}
|
||||
|
||||
export class ShaderProgram extends GL2Program {
|
||||
protected override readonly prefix: IGL2ProgramPrefix = SHADER_PREFIX;
|
||||
|
||||
constructor(gl2: GL2, vs?: string, fs?: string) {
|
||||
super(gl2, vs, fs);
|
||||
if (!vs) this.vs(DEFAULT_VS);
|
||||
if (!fs) this.fs(DEFAULT_FS);
|
||||
if (!vs && !fs) {
|
||||
this.modified = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected override compile() {
|
||||
const success = super.compile();
|
||||
if (!success) return false;
|
||||
const shader = this.element;
|
||||
const gl = shader.gl;
|
||||
if (!gl) return false;
|
||||
|
||||
const tex = this.defineAttribArray('a_texCoord');
|
||||
const position = this.defineAttribArray('a_position');
|
||||
const sampler = this.defineTexture('u_sampler', 0);
|
||||
const indices = this.defineIndices('defalutIndices');
|
||||
if (!tex || !position || !sampler || !indices) {
|
||||
return true;
|
||||
}
|
||||
position.buffer(
|
||||
new Float32Array([1, -1, -1, -1, 1, 1, -1, 1]),
|
||||
gl.STATIC_DRAW
|
||||
);
|
||||
position.pointer(2, gl.FLOAT, false, 0, 0);
|
||||
position.enable();
|
||||
tex.buffer(new Float32Array([1, 1, 0, 1, 1, 0, 0, 0]), gl.STATIC_DRAW);
|
||||
tex.pointer(2, gl.FLOAT, false, 0, 0);
|
||||
tex.enable();
|
||||
indices.buffer(new Uint16Array([0, 1, 2, 2, 3, 1]), gl.STATIC_DRAW);
|
||||
this.useIndices(indices);
|
||||
this.paramElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import {
|
||||
ERenderItemEvent,
|
||||
RenderFunction,
|
||||
RenderItem,
|
||||
RenderItemPosition
|
||||
} from './item';
|
||||
import { MotaOffscreenCanvas2D } from '../fx/canvas2d';
|
||||
import { Transform } from './transform';
|
||||
|
||||
export interface ESpriteEvent extends ERenderItemEvent {}
|
||||
|
||||
export class Sprite<
|
||||
E extends ESpriteEvent = ESpriteEvent
|
||||
> extends RenderItem<E> {
|
||||
renderFn: RenderFunction;
|
||||
|
||||
/**
|
||||
* 创建一个精灵,可以自由在上面渲染内容
|
||||
* @param type 渲染模式,absolute表示绝对位置,不会跟随自身的Transform改变
|
||||
* @param cache 是否启用缓存机制
|
||||
*/
|
||||
constructor(
|
||||
type: RenderItemPosition = 'static',
|
||||
cache: boolean = true,
|
||||
fall: boolean = false
|
||||
) {
|
||||
super(type, cache, fall);
|
||||
this.type = type;
|
||||
this.renderFn = () => {};
|
||||
}
|
||||
|
||||
protected render(
|
||||
canvas: MotaOffscreenCanvas2D,
|
||||
transform: Transform
|
||||
): void {
|
||||
this.renderFn(canvas, transform);
|
||||
}
|
||||
|
||||
setRenderFn(fn: RenderFunction) {
|
||||
this.renderFn = fn;
|
||||
this.update(this);
|
||||
}
|
||||
|
||||
protected handleProps(
|
||||
key: string,
|
||||
_prevValue: any,
|
||||
nextValue: any
|
||||
): boolean {
|
||||
switch (key) {
|
||||
case 'render':
|
||||
if (!this.assertType(nextValue, 'function', key)) return false;
|
||||
this.setRenderFn(nextValue);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
import { logger } from '@/core/common/logger';
|
||||
|
||||
export interface IFontConfig {
|
||||
/** 字体类型 */
|
||||
readonly family: string;
|
||||
/** 字体大小的值 */
|
||||
readonly size: number;
|
||||
/** 字体大小单位,推荐使用 px */
|
||||
readonly sizeUnit: string;
|
||||
/** 字体粗细,范围 0-1000 */
|
||||
readonly weight: number;
|
||||
/** 是否斜体 */
|
||||
readonly italic: boolean;
|
||||
}
|
||||
|
||||
export const enum FontWeight {
|
||||
Light = 300,
|
||||
Normal = 400,
|
||||
Bold = 700
|
||||
}
|
||||
|
||||
type _FontStretch =
|
||||
| 'ultra-condensed'
|
||||
| 'extra-condensed'
|
||||
| 'condensed'
|
||||
| 'semi-condensed'
|
||||
| 'normal'
|
||||
| 'semi-expanded'
|
||||
| 'expanded'
|
||||
| 'extra-expanded'
|
||||
| 'ultra-expanded';
|
||||
|
||||
type _FontVariant = 'normal' | 'small-caps';
|
||||
|
||||
export class Font implements IFontConfig {
|
||||
static defaultFamily: string = 'Verdana';
|
||||
static defaultSize: number = 16;
|
||||
static defaultSizeUnit: string = 'px';
|
||||
static defaultWeight: number = 400;
|
||||
static defaultItalic: boolean = false;
|
||||
|
||||
private readonly fallbacks: Font[] = [];
|
||||
|
||||
private fontString: string = '';
|
||||
|
||||
constructor(
|
||||
public readonly family: string = Font.defaultFamily,
|
||||
public readonly size: number = Font.defaultSize,
|
||||
public readonly sizeUnit: string = Font.defaultSizeUnit,
|
||||
public readonly weight: number = Font.defaultWeight,
|
||||
public readonly italic: boolean = Font.defaultItalic
|
||||
) {
|
||||
this.fontString = this.getFont();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加补充字体,若当前字体不可用,那么会使用补充字体,补充字体也可以添加补充字体,但是请避免递归添加
|
||||
* @param fallback 补充字体
|
||||
*/
|
||||
addFallback(...fallback: Font[]) {
|
||||
this.fallbacks.push(...fallback);
|
||||
this.fontString = this.getFont();
|
||||
}
|
||||
|
||||
private build() {
|
||||
return `${
|
||||
this.italic ? 'italic ' : ''
|
||||
} ${this.weight} ${this.size}${this.sizeUnit} ${this.family}`;
|
||||
}
|
||||
|
||||
private getFallbackFont(used: Set<Font>) {
|
||||
let font = '';
|
||||
this.fallbacks.forEach(v => {
|
||||
if (used.has(v)) {
|
||||
logger.warn(62, this.build());
|
||||
return;
|
||||
}
|
||||
used.add(v);
|
||||
font += `, ${v.getFallbackFont(used)}`;
|
||||
});
|
||||
return font;
|
||||
}
|
||||
|
||||
private getFont() {
|
||||
if (this.fallbacks.length === 0) {
|
||||
return this.build();
|
||||
} else {
|
||||
const usedFont = new Set<Font>();
|
||||
return this.build() + this.getFallbackFont(usedFont);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字体的 CSS 字符串
|
||||
*/
|
||||
string() {
|
||||
return this.fontString;
|
||||
}
|
||||
|
||||
private static parseOne(str: string) {
|
||||
if (!str) return new Font();
|
||||
let italic = this.defaultItalic;
|
||||
let weight = this.defaultWeight;
|
||||
let size = this.defaultSize;
|
||||
let unit = this.defaultSizeUnit;
|
||||
let family = this.defaultFamily;
|
||||
const tokens = str.split(/\s+/);
|
||||
tokens.forEach(v => {
|
||||
// font-italic
|
||||
if (v === 'italic') {
|
||||
italic = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// font-weight
|
||||
const num = Number(v);
|
||||
if (!isNaN(num)) {
|
||||
weight = num;
|
||||
return;
|
||||
}
|
||||
|
||||
// font-size
|
||||
const parse = parseFloat(v);
|
||||
if (!isNaN(parse)) {
|
||||
size = parse;
|
||||
unit = v.slice(parse.toString().length);
|
||||
return;
|
||||
}
|
||||
});
|
||||
family = tokens.at(-1) ?? 'Verdana';
|
||||
return new Font(family, size, unit, weight, italic);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 CSS 字体字符串解析出 Font 实例,不支持的属性将被忽略
|
||||
*/
|
||||
static parse(str: string) {
|
||||
const fonts = str.split(',');
|
||||
const main = this.parseOne(fonts[0]);
|
||||
for (let i = 1; i < fonts.length; i++) {
|
||||
main.addFallback(this.parseOne(fonts[i]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置默认字体
|
||||
*/
|
||||
static setDefaults(font: Font) {
|
||||
this.defaultFamily = font.family;
|
||||
this.defaultItalic = font.italic;
|
||||
this.defaultSize = font.size;
|
||||
this.defaultSizeUnit = font.sizeUnit;
|
||||
this.defaultWeight = font.weight;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认字体
|
||||
*/
|
||||
static defaults() {
|
||||
return new Font();
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制一个字体,同时修改字体的一部分属性
|
||||
* @param font 要复制的字体
|
||||
*/
|
||||
static clone(
|
||||
font: Font,
|
||||
{
|
||||
family = font.family,
|
||||
size = font.size,
|
||||
sizeUnit = font.sizeUnit,
|
||||
weight = font.weight,
|
||||
italic = font.italic
|
||||
}: Partial<IFontConfig>
|
||||
) {
|
||||
return new Font(family, size, sizeUnit, weight, italic);
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from './font';
|
||||
@@ -1,272 +0,0 @@
|
||||
import { mat3, ReadonlyMat3, ReadonlyVec3, vec2, vec3 } from 'gl-matrix';
|
||||
|
||||
export interface ITransformUpdatable {
|
||||
updateTransform?(): void;
|
||||
}
|
||||
|
||||
export class Transform {
|
||||
mat: mat3 = mat3.create();
|
||||
|
||||
x: number = 0;
|
||||
y: number = 0;
|
||||
scaleX: number = 1;
|
||||
scaleY: number = 1;
|
||||
rad: number = 0;
|
||||
|
||||
/** 有没有对这个Transform进行过修改,用于优化常规表现 */
|
||||
private modified: boolean = false;
|
||||
|
||||
/** 绑定的可更新元素 */
|
||||
bindedObject?: ITransformUpdatable;
|
||||
|
||||
/**
|
||||
* 对这个变换实例添加绑定对象,当矩阵变换时,自动调用其 update 函数
|
||||
* @param obj 要绑定的对象
|
||||
*/
|
||||
bind(obj?: ITransformUpdatable) {
|
||||
this.bindedObject = obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重设所有参数
|
||||
*/
|
||||
reset() {
|
||||
mat3.identity(this.mat);
|
||||
this.x = 0;
|
||||
this.y = 0;
|
||||
this.scaleX = 1;
|
||||
this.scaleY = 1;
|
||||
this.rad = 0;
|
||||
this.modified = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改缩放,叠加关系
|
||||
*/
|
||||
scale(x: number, y: number = x) {
|
||||
mat3.scale(this.mat, this.mat, [x, y]);
|
||||
this.scaleX *= x;
|
||||
this.scaleY *= y;
|
||||
this.modified = true;
|
||||
this.bindedObject?.updateTransform?.();
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动,叠加关系
|
||||
*/
|
||||
translate(x: number, y: number) {
|
||||
mat3.translate(this.mat, this.mat, [x, y]);
|
||||
this.x += x;
|
||||
this.y += y;
|
||||
this.modified = true;
|
||||
this.bindedObject?.updateTransform?.();
|
||||
}
|
||||
|
||||
/**
|
||||
* 旋转,叠加关系
|
||||
*/
|
||||
rotate(rad: number) {
|
||||
mat3.rotate(this.mat, this.mat, rad);
|
||||
this.rad += rad;
|
||||
if (this.rad >= Math.PI * 2) {
|
||||
const n = Math.floor(this.rad / Math.PI / 2);
|
||||
this.rad -= n * Math.PI * 2;
|
||||
}
|
||||
this.modified = true;
|
||||
this.bindedObject?.updateTransform?.();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置缩放,非叠加关系
|
||||
*/
|
||||
setScale(x: number, y: number = x) {
|
||||
mat3.scale(this.mat, this.mat, [x / this.scaleX, y / this.scaleY]);
|
||||
this.scaleX = x;
|
||||
this.scaleY = y;
|
||||
this.modified = true;
|
||||
this.bindedObject?.updateTransform?.();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置位置,非叠加关系
|
||||
*/
|
||||
setTranslate(x: number, y: number) {
|
||||
mat3.translate(this.mat, this.mat, [x - this.x, y - this.y]);
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.modified = true;
|
||||
this.bindedObject?.updateTransform?.();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置旋转,非叠加关系
|
||||
*/
|
||||
setRotate(rad: number) {
|
||||
mat3.rotate(this.mat, this.mat, rad - this.rad);
|
||||
this.rad = rad;
|
||||
this.modified = true;
|
||||
this.bindedObject?.updateTransform?.();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置变换矩阵,叠加模式
|
||||
* @param a 水平缩放
|
||||
* @param b 垂直倾斜
|
||||
* @param c 水平倾斜
|
||||
* @param d 垂直缩放
|
||||
* @param e 水平移动
|
||||
* @param f 垂直移动
|
||||
*/
|
||||
transform(
|
||||
a: number,
|
||||
b: number,
|
||||
c: number,
|
||||
d: number,
|
||||
e: number,
|
||||
f: number
|
||||
) {
|
||||
mat3.multiply(
|
||||
this.mat,
|
||||
this.mat,
|
||||
mat3.fromValues(a, b, 0, c, d, 0, e, f, 1)
|
||||
);
|
||||
this.calAttributes();
|
||||
this.bindedObject?.updateTransform?.();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置变换矩阵,非叠加模式
|
||||
* @param a 水平缩放
|
||||
* @param b 垂直倾斜
|
||||
* @param c 水平倾斜
|
||||
* @param d 垂直缩放
|
||||
* @param e 水平移动
|
||||
* @param f 垂直移动
|
||||
*/
|
||||
setTransform(
|
||||
a: number,
|
||||
b: number,
|
||||
c: number,
|
||||
d: number,
|
||||
e: number,
|
||||
f: number
|
||||
) {
|
||||
mat3.set(this.mat, a, b, 0, c, d, 0, e, f, 1);
|
||||
this.calAttributes();
|
||||
this.bindedObject?.updateTransform?.();
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新计算 translation scaling rotation
|
||||
*/
|
||||
calAttributes() {
|
||||
const [x, y] = getTranslation(this.mat);
|
||||
const [scaleX, scaleY] = getScaling(this.mat);
|
||||
const rad = getRotation(this.mat);
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.scaleX = scaleX;
|
||||
this.scaleY = scaleY;
|
||||
this.rad = rad;
|
||||
if (x === 0 && y === 0 && scaleX === 1 && scaleY === 1 && rad === 0) {
|
||||
this.modified = false;
|
||||
} else {
|
||||
this.modified = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 与一个矩阵相乘,返回其计算结果(不改变原矩阵)
|
||||
* @param transform 变换矩阵
|
||||
*/
|
||||
multiply(transform: Transform): Transform {
|
||||
if (this.modified) {
|
||||
const res = new Transform();
|
||||
const mat = mat3.clone(this.mat);
|
||||
mat3.multiply(mat, mat, transform.mat);
|
||||
res.mat = mat;
|
||||
return res;
|
||||
} else {
|
||||
return transform.clone();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制这个变换矩阵
|
||||
*/
|
||||
clone() {
|
||||
const transform = new Transform();
|
||||
transform.mat = mat3.clone(this.mat);
|
||||
return transform;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据变换矩阵的信息,将一个点转换为计算后的位置
|
||||
* @param x 横坐标
|
||||
* @param y 纵坐标
|
||||
*/
|
||||
transformed(x: number, y: number): vec3 {
|
||||
if (!this.modified) return [x, y, 1];
|
||||
return multiplyVec3(this.mat, [x, y, 1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据变换矩阵的信息,将一个计算后的位置逆转换为原位置
|
||||
* @param x 横坐标
|
||||
* @param y 纵坐标
|
||||
*/
|
||||
untransformed(x: number, y: number): vec3 {
|
||||
if (!this.modified) return [x, y, 1];
|
||||
const invert = mat3.create();
|
||||
mat3.invert(invert, this.mat);
|
||||
return multiplyVec3(invert, [x, y, 1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据变换矩阵的信息,将一个点转换为计算后的位置
|
||||
* @param transform 变换矩阵
|
||||
* @param x 横坐标
|
||||
* @param y 纵坐标
|
||||
*/
|
||||
static transformed(transform: Transform, x: number, y: number) {
|
||||
return multiplyVec3(transform.mat, [x, y, 1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据变换矩阵的信息,将一个计算后的位置逆转换为原位置
|
||||
* @param transform 变换矩阵
|
||||
* @param x 横坐标
|
||||
* @param y 纵坐标
|
||||
*/
|
||||
static untransformed(transform: Transform, x: number, y: number) {
|
||||
const invert = mat3.create();
|
||||
mat3.invert(invert, transform.mat);
|
||||
return multiplyVec3(invert, [x, y, 1]);
|
||||
}
|
||||
|
||||
/** 单位矩阵 */
|
||||
static readonly identity = new Transform();
|
||||
}
|
||||
|
||||
function multiplyVec3(mat: ReadonlyMat3, vec: ReadonlyVec3): vec3 {
|
||||
const v0 = vec[0];
|
||||
const v1 = vec[1];
|
||||
const v2 = vec[2];
|
||||
return [
|
||||
mat[0] * v0 + mat[3] * v1 + mat[6] * v2,
|
||||
mat[1] * v0 + mat[4] * v1 + mat[7] * v2,
|
||||
mat[2] * v0 + mat[5] * v1 + mat[8] * v2
|
||||
];
|
||||
}
|
||||
|
||||
function getTranslation(mat: ReadonlyMat3): vec2 {
|
||||
return [mat[6], mat[7]];
|
||||
}
|
||||
|
||||
function getScaling(mat: ReadonlyMat3): vec2 {
|
||||
return [Math.hypot(mat[0], mat[3]), Math.hypot(mat[1], mat[4])];
|
||||
}
|
||||
|
||||
function getRotation(mat: ReadonlyMat3): number {
|
||||
return Math.atan2(mat[3], mat[0]);
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
import { TimingFn } from 'mutate-animate';
|
||||
import { RenderAdapter } from './adapter';
|
||||
import { FloorViewport } from './preset/viewport';
|
||||
import { JSX } from 'vue/jsx-runtime';
|
||||
import { DefineComponent, DefineSetupFnComponent } from 'vue';
|
||||
import { MotaOffscreenCanvas2D } from '../fx/canvas2d';
|
||||
import { Transform } from './transform';
|
||||
|
||||
export type Props<
|
||||
T extends
|
||||
| keyof JSX.IntrinsicElements
|
||||
| DefineSetupFnComponent<any>
|
||||
| DefineComponent
|
||||
> = T extends keyof JSX.IntrinsicElements
|
||||
? JSX.IntrinsicElements[T]
|
||||
: T extends DefineSetupFnComponent<any>
|
||||
? InstanceType<T>['$props'] & InstanceType<T>['$emits']
|
||||
: T extends DefineComponent
|
||||
? InstanceType<T>['$props'] & InstanceType<T>['$emits']
|
||||
: unknown;
|
||||
|
||||
export type ElementLocator = [
|
||||
x?: number,
|
||||
y?: number,
|
||||
width?: number,
|
||||
height?: number,
|
||||
anchorX?: number,
|
||||
anchorY?: number
|
||||
];
|
||||
|
||||
export type ElementAnchor = [x: number, y: number];
|
||||
export type ElementScale = [x: number, y: number];
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
/**
|
||||
* 将两个缓动函数做加法
|
||||
*/
|
||||
export function addTiming(timing1: TimingFn, timing2: TimingFn): TimingFn {
|
||||
return (p: number) => timing1(p) + timing2(p);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将两个缓动函数做乘法
|
||||
*/
|
||||
export function multiplyTiming(timing1: TimingFn, timing2: TimingFn): TimingFn {
|
||||
return (p: number) => timing1(p) * timing2(p);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断两个集合是否相等
|
||||
*/
|
||||
export function isSetEqual<T>(set1: Set<T>, set2: Set<T>) {
|
||||
if (set1 === set2) return true;
|
||||
else return set1.size === set2.size && set1.isSubsetOf(set2);
|
||||
}
|
||||
|
||||
export function transformCanvas(
|
||||
canvas: MotaOffscreenCanvas2D,
|
||||
transform: Transform
|
||||
) {
|
||||
const { ctx } = canvas;
|
||||
const mat = transform.mat;
|
||||
const [a, b, , c, d, , e, f] = mat;
|
||||
ctx.transform(a, b, c, d, e, f);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { logger } from '@/core/common/logger';
|
||||
import { logger } from '@motajs/common';
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import {
|
||||
IGameUI,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Props } from '@/core/render';
|
||||
import { Props } from '@motajs/render';
|
||||
import { IGameUI, IUIInstance, UIComponent, UIProps } from './shared';
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import { markRaw, mergeProps } from 'vue';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Props } from '@/core/render';
|
||||
import { Props } from '@motajs/render';
|
||||
import { DefineComponent, DefineSetupFnComponent, Ref, ShallowRef } from 'vue';
|
||||
|
||||
export type UIComponent = DefineSetupFnComponent<any> | DefineComponent;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { backDir, has } from '@/plugin/game/utils';
|
||||
import { loading } from '../game';
|
||||
import type { LayerDoorAnimate } from '@/core/render/preset/floor';
|
||||
import type { LayerDoorAnimate } from '@motajs/render';
|
||||
import { getSkillLevel } from '@/plugin/game/skillTree';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { logger } from '@/core/common/logger';
|
||||
import { logger } from '@motajs/common';
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import { cloneDeep, isNil } from 'lodash-es';
|
||||
import { ItemState } from './item';
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import { backDir, checkCanMoveExtended, toDir } from './utils';
|
||||
import { loading } from '../game';
|
||||
import type { RenderAdapter } from '@/core/render/adapter';
|
||||
import type { HeroRenderer } from '@/core/render/preset/hero';
|
||||
import type { FloorViewport } from '@/core/render/preset/viewport';
|
||||
import type { HeroKeyMover } from '@/core/main/action/move';
|
||||
import type {
|
||||
RenderAdapter,
|
||||
HeroRenderer,
|
||||
FloorViewport,
|
||||
FloorLayer,
|
||||
Layer,
|
||||
LayerGroup,
|
||||
LayerMovingRenderable
|
||||
} from '@/core/render/preset/layer';
|
||||
import type { LayerFloorBinder } from '@/core/render/preset/floor';
|
||||
LayerMovingRenderable,
|
||||
LayerFloorBinder
|
||||
} from '@motajs/render';
|
||||
import type { HeroKeyMover } from '@/core/main/action/move';
|
||||
import { BluePalace, MiscData } from '../mechanism/misc';
|
||||
import { sleep } from 'mutate-animate';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Undoable } from '@/core/interface';
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import { logger } from '@/core/common/logger';
|
||||
import { logger } from '@motajs/common';
|
||||
|
||||
type ToJSONFunction<T> = (data: T) => string;
|
||||
type FromJSONFunction<T> = (data: string) => T;
|
||||
|
||||
+3
-28
@@ -18,26 +18,15 @@ import type { Ref } from 'vue';
|
||||
import type * as battle from './enemy/battle';
|
||||
import type * as hero from './state/hero';
|
||||
import type * as damage from './enemy/damage';
|
||||
import type { Logger } from '@/core/common/logger';
|
||||
import type { Logger } from '@motajs/common';
|
||||
import type { Danmaku } from '@/core/main/custom/danmaku';
|
||||
import type * as misc from './mechanism/misc';
|
||||
import type { texture } from '@/core/render/cache';
|
||||
import type { MotaRenderer } from '@/core/render/render';
|
||||
import type { Container } from '@/core/render/container';
|
||||
import type { Sprite } from '@/core/render/sprite';
|
||||
import type { Image, Text } from '@/core/render/preset/misc';
|
||||
import type { RenderItem } from '@/core/render/item';
|
||||
import type { RenderAdapter } from '@/core/render/adapter';
|
||||
import type { Render } from '@motajs/client';
|
||||
import type { ItemState } from './state/item';
|
||||
import type { Layer } from '@/core/render/preset/layer';
|
||||
import type { LayerGroupFloorBinder } from '@/core/render/preset/floor';
|
||||
import type { HeroKeyMover } from '@/core/main/action/move';
|
||||
import type { BlockMover, HeroMover, ObjectMoverBase } from './state/move';
|
||||
import type { Camera } from '@/core/render/camera';
|
||||
import type * as Animation from 'mutate-animate';
|
||||
import type * as RenderUtils from '@/core/render/utils';
|
||||
import type { WeatherController } from '@/module/weather/weather';
|
||||
import type { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
|
||||
|
||||
interface ClassInterface {
|
||||
// 渲染进程与游戏进程通用
|
||||
@@ -100,21 +89,7 @@ interface ModuleInterface {
|
||||
HeroSkill: typeof misc.HeroSkill;
|
||||
};
|
||||
Effect: {};
|
||||
Render: {
|
||||
texture: typeof texture;
|
||||
MotaRenderer: typeof MotaRenderer;
|
||||
Container: typeof Container;
|
||||
Sprite: typeof Sprite;
|
||||
Text: typeof Text;
|
||||
Image: typeof Image;
|
||||
RenderItem: typeof RenderItem;
|
||||
RenderAdapter: typeof RenderAdapter;
|
||||
Layer: typeof Layer;
|
||||
LayerGroupFloorBinder: typeof LayerGroupFloorBinder;
|
||||
Camera: typeof Camera;
|
||||
MotaOffscreenCanvas2D: typeof MotaOffscreenCanvas2D;
|
||||
Utils: typeof RenderUtils;
|
||||
};
|
||||
Render: typeof Render;
|
||||
State: {
|
||||
ItemState: typeof ItemState;
|
||||
HeroMover: typeof HeroMover;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import { audioPlayer, AudioPlayer, AudioRoute, AudioStatus } from './player';
|
||||
import { guessTypeByExt, isAudioSupport } from './support';
|
||||
import { logger } from '@/core/common/logger';
|
||||
import { logger } from '@motajs/common';
|
||||
import { StreamLoader } from '../loader';
|
||||
import { linear, sleep, Transition } from 'mutate-animate';
|
||||
import { VolumeEffect } from './effect';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { logger } from '@/core/common/logger';
|
||||
import { logger } from '@motajs/common';
|
||||
import { OggVorbisDecoderWebWorker } from '@wasm-audio-decoders/ogg-vorbis';
|
||||
import { OggOpusDecoderWebWorker } from 'ogg-opus-decoder';
|
||||
import { AudioType, isAudioSupport } from './support';
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
VolumeEffect
|
||||
} from './effect';
|
||||
import { isNil } from 'lodash-es';
|
||||
import { logger } from '@/core/common/logger';
|
||||
import { logger } from '@motajs/common';
|
||||
import { sleep } from 'mutate-animate';
|
||||
import { AudioDecoder } from './decoder';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import { audioPlayer, AudioPlayer } from './player';
|
||||
import { logger } from '@/core/common/logger';
|
||||
import { logger } from '@motajs/common';
|
||||
import { VolumeEffect } from './effect';
|
||||
|
||||
type LocationArray = [number, number, number];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import { IStreamController, IStreamReader } from '../loader';
|
||||
import { IAudioInput, IAudioOutput } from './effect';
|
||||
import { logger } from '@/core/common/logger';
|
||||
import { logger } from '@motajs/common';
|
||||
import { AudioType } from './support';
|
||||
import CodecParser, { CodecFrame, MimeType, OggPage } from 'codec-parser';
|
||||
import { isNil } from 'lodash-es';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { logger } from '@/core/common/logger';
|
||||
import { logger } from '@motajs/common';
|
||||
import EventEmitter from 'eventemitter3';
|
||||
|
||||
export interface IStreamController<T = void> {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DefaultProps, ElementLocator, Font, useKey } from '@/core/render';
|
||||
import { DefaultProps, ElementLocator, Font, useKey } from '@motajs/render';
|
||||
import { computed, defineComponent, reactive, ref } from 'vue';
|
||||
import { Background, Selection } from './misc';
|
||||
import { TextContent, TextContentExpose, TextContentProps } from './textbox';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DefaultProps, ElementLocator, GraphicPropsBase } from '@/core/render';
|
||||
import { DefaultProps, ElementLocator, GraphicPropsBase } from '@motajs/render';
|
||||
import { computed, defineComponent, onMounted, Ref, ref, watch } from 'vue';
|
||||
import { SetupComponentOptions } from './types';
|
||||
|
||||
|
||||
@@ -4,15 +4,15 @@ import {
|
||||
onTick,
|
||||
PathProps,
|
||||
Sprite
|
||||
} from '@/core/render';
|
||||
} from '@motajs/render';
|
||||
import { computed, defineComponent, ref, SetupContext, watch } from 'vue';
|
||||
import { SetupComponentOptions } from './types';
|
||||
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
|
||||
import { MotaOffscreenCanvas2D } from '@motajs/render';
|
||||
import { TextboxProps, TextContent, TextContentProps } from './textbox';
|
||||
import { Scroll, ScrollExpose, ScrollProps } from './scroll';
|
||||
import { transitioned } from '../use';
|
||||
import { hyper } from 'mutate-animate';
|
||||
import { logger } from '@/core/common/logger';
|
||||
import { logger } from '@motajs/common';
|
||||
import { GameUI, IUIMountable } from '@/core/system';
|
||||
|
||||
interface ProgressProps extends DefaultProps {
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from 'vue';
|
||||
import { SetupComponentOptions } from './types';
|
||||
import { clamp } from 'lodash-es';
|
||||
import { DefaultProps, ElementLocator, Font } from '@/core/render';
|
||||
import { DefaultProps, ElementLocator, Font } from '@motajs/render';
|
||||
|
||||
/** 圆角矩形页码距离容器的边框大小,与 pageSize 相乘 */
|
||||
const RECT_PAD = 0.1;
|
||||
|
||||
@@ -17,12 +17,14 @@ import {
|
||||
ElementLocator,
|
||||
RenderItem,
|
||||
Sprite,
|
||||
Transform
|
||||
} from '@/core/render';
|
||||
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
|
||||
Transform,
|
||||
MotaOffscreenCanvas2D,
|
||||
IActionEvent,
|
||||
IWheelEvent,
|
||||
MouseType
|
||||
} from '@motajs/render';
|
||||
import { hyper, linear, Transition } from 'mutate-animate';
|
||||
import { clamp } from 'lodash-es';
|
||||
import { IActionEvent, IWheelEvent, MouseType } from '@/core/render/event';
|
||||
import { transitioned } from '../use';
|
||||
|
||||
export const enum ScrollDirection {
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
|
||||
import {
|
||||
ElementLocator,
|
||||
Font,
|
||||
Sprite,
|
||||
DefaultProps,
|
||||
Text,
|
||||
MotaOffscreenCanvas2D
|
||||
} from '@motajs/render';
|
||||
import {
|
||||
computed,
|
||||
defineComponent,
|
||||
@@ -12,13 +19,10 @@ import {
|
||||
VNode,
|
||||
watch
|
||||
} from 'vue';
|
||||
import { logger } from '@/core/common/logger';
|
||||
import { Sprite } from '@/core/render/sprite';
|
||||
import { DefaultProps } from '@/core/render/renderer';
|
||||
import { logger } from '@motajs/common';
|
||||
import { isNil } from 'lodash-es';
|
||||
import { SetupComponentOptions } from './types';
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import { Text } from '@/core/render/preset';
|
||||
import {
|
||||
ITextContentConfig,
|
||||
TextContentTyper,
|
||||
@@ -27,7 +31,6 @@ import {
|
||||
WordBreak,
|
||||
TextAlign
|
||||
} from './textboxTyper';
|
||||
import { ElementLocator, Font } from '@/core/render';
|
||||
|
||||
export interface TextContentProps
|
||||
extends DefaultProps,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { logger } from '@/core/common/logger';
|
||||
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
|
||||
import { logger } from '@motajs/common';
|
||||
import {
|
||||
AutotileRenderable,
|
||||
Font,
|
||||
onTick,
|
||||
RenderableData,
|
||||
texture
|
||||
} from '@/core/render';
|
||||
texture,
|
||||
MotaOffscreenCanvas2D
|
||||
} from '@motajs/render';
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import { isNil } from 'lodash-es';
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { DefaultProps, ElementLocator, Font, texture } from '@/core/render';
|
||||
import { DefaultProps, ElementLocator, Font, texture } from '@motajs/render';
|
||||
import { logger } from '@motajs/common';
|
||||
import { computed, defineComponent, onUnmounted, ref } from 'vue';
|
||||
import { SetupComponentOptions } from './types';
|
||||
import { transitioned } from '../use';
|
||||
import { hyper } from 'mutate-animate';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { logger } from '@/core/common/logger';
|
||||
|
||||
export interface TipProps extends DefaultProps {
|
||||
loc: ElementLocator;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { MotaRenderer } from '@/core/render';
|
||||
import { createApp } from '@/core/render';
|
||||
import { MotaRenderer, createApp } from '@motajs/render';
|
||||
import { defineComponent } from 'vue';
|
||||
import { UIController } from '@/core/system';
|
||||
import { mainSceneUI } from './ui/main';
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
Props,
|
||||
LayerGroup,
|
||||
Font
|
||||
} from '@/core/render';
|
||||
} from '@motajs/render';
|
||||
import { WeatherController } from '@/module/weather';
|
||||
import { FloorChange } from '@/plugin/fallback';
|
||||
import { LayerGroupFilter } from '@/plugin/fx/gameCanvas';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ElementLocator } from '@/core/render';
|
||||
import { ElementLocator } from '@motajs/render';
|
||||
import { GameUI, UIComponentProps } from '@/core/system';
|
||||
import { defineComponent } from 'vue';
|
||||
import {
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { GameUI, UIComponentProps } from '@/core/system';
|
||||
import { computed, defineComponent, ref, watch } from 'vue';
|
||||
import { SetupComponentOptions, TextContent } from '../components';
|
||||
import { DefaultProps, ElementLocator, Sprite, Font } from '@/core/render';
|
||||
import {
|
||||
DefaultProps,
|
||||
ElementLocator,
|
||||
Sprite,
|
||||
Font,
|
||||
MotaOffscreenCanvas2D
|
||||
} from '@motajs/render';
|
||||
import { transitionedColor } from '../use';
|
||||
import { linear } from 'mutate-animate';
|
||||
import { Scroll } from '../components/scroll';
|
||||
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
|
||||
import { getArea, MinimapDrawer } from '@/plugin/ui/fly';
|
||||
import {
|
||||
NumpadToolbar,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DefaultProps, ElementLocator, Font } from '@/core/render';
|
||||
import { DefaultProps, ElementLocator, Font } from '@motajs/render';
|
||||
import { computed, defineComponent, ref } from 'vue';
|
||||
import { SetupComponentOptions } from '../components';
|
||||
import {
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { Shader, ShaderProgram } from '@/core/render/shader';
|
||||
import {
|
||||
Shader,
|
||||
ShaderProgram,
|
||||
MotaRenderer,
|
||||
Container,
|
||||
IShaderUniform,
|
||||
UniformType
|
||||
} from '@motajs/render';
|
||||
import { IWeather } from './weather';
|
||||
import { MotaRenderer } from '@/core/render/render';
|
||||
import { Container } from '@/core/render/container';
|
||||
import { IShaderUniform, UniformType } from '@/core/render/gl2';
|
||||
|
||||
const rainVs = /* glsl */ `
|
||||
in vec2 a_rainVertex;
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { Shader, ShaderProgram } from '@/core/render/shader';
|
||||
import {
|
||||
Shader,
|
||||
ShaderProgram,
|
||||
MotaRenderer,
|
||||
Container,
|
||||
GL2Program,
|
||||
IShaderUniform,
|
||||
UniformType,
|
||||
Transform,
|
||||
MotaOffscreenCanvas2D
|
||||
} from '@motajs/render';
|
||||
import { IWeather } from './weather';
|
||||
import { MotaRenderer } from '@/core/render/render';
|
||||
import { Container } from '@/core/render/container';
|
||||
import { GL2Program, IShaderUniform, UniformType } from '@/core/render/gl2';
|
||||
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
|
||||
import { Transform } from '@/core/render/transform';
|
||||
|
||||
const snowVs = /* glsl */ `
|
||||
in vec2 a_snowVertex;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { logger } from '@/core/common/logger';
|
||||
import { RenderItem } from '@/core/render';
|
||||
import { logger } from '@motajs/common';
|
||||
import { RenderItem } from '@motajs/render';
|
||||
import { Ticker } from 'mutate-animate';
|
||||
|
||||
export interface IWeather {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
|
||||
import { RenderItem, RenderItemPosition } from '@/core/render/item';
|
||||
import { Transform } from '@/core/render/transform';
|
||||
import {
|
||||
MotaOffscreenCanvas2D,
|
||||
RenderItem,
|
||||
RenderItemPosition,
|
||||
Transform
|
||||
} from '@motajs/render';
|
||||
import { IStateDamageable } from '@/game/state/interface';
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import { Ticker } from 'mutate-animate';
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { IStateDamageable } from '@/game/state/interface';
|
||||
import { BarrageBoss, BossSprite, Hitbox } from './barrage';
|
||||
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
|
||||
import {
|
||||
Container,
|
||||
HeroRenderer,
|
||||
LayerGroup,
|
||||
MotaRenderer,
|
||||
RenderItem,
|
||||
Shader,
|
||||
Transform
|
||||
} from '@/core/render';
|
||||
Transform,
|
||||
MotaOffscreenCanvas2D
|
||||
} from '@motajs/render';
|
||||
import { Pop } from '../fx/pop';
|
||||
import { SplittableBall } from './palaceBossProjectile';
|
||||
import { PointEffect } from '../fx/pointShader';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
|
||||
import { Transform } from '@/core/render';
|
||||
import { Transform, MotaOffscreenCanvas2D } from '@motajs/render';
|
||||
import { IStateDamageable } from '@/game/state/interface';
|
||||
import { Hitbox, Projectile } from './barrage';
|
||||
import type { PalaceBoss } from './palaceBoss';
|
||||
@@ -223,7 +222,7 @@ export class SplittableBall extends Projectile<PalaceBoss> {
|
||||
}
|
||||
}
|
||||
|
||||
ai(boss: PalaceBoss, time: number, frame: number, dt: number): void {
|
||||
ai(boss: PalaceBoss, time: number, _frame: number, dt: number): void {
|
||||
if (this.splitData?.split) {
|
||||
if (time > this.splitData.time) {
|
||||
this.split(boss);
|
||||
@@ -256,7 +255,7 @@ export class SplittableBall extends Projectile<PalaceBoss> {
|
||||
this.setPosition(x + this.vx * p, y + this.vy * p);
|
||||
}
|
||||
|
||||
render(canvas: MotaOffscreenCanvas2D, transform: Transform): void {
|
||||
render(canvas: MotaOffscreenCanvas2D, _transform: Transform): void {
|
||||
if (!this.color) return;
|
||||
const texture = SplittableBall.ball.get(this.color);
|
||||
if (!texture) return;
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { Shader } from '@/core/render/shader';
|
||||
import {
|
||||
Shader,
|
||||
MotaRenderer,
|
||||
LayerGroup,
|
||||
RenderItem,
|
||||
MotaOffscreenCanvas2D,
|
||||
Container,
|
||||
HeroRenderer
|
||||
} from '@motajs/render';
|
||||
import { PointEffect } from '../fx/pointShader';
|
||||
import { BarrageBoss, BossSprite, Hitbox } from './barrage';
|
||||
import { MotaRenderer } from '@/core/render/render';
|
||||
import { LayerGroup } from '@/core/render/preset/layer';
|
||||
import { RenderItem } from '@/core/render/item';
|
||||
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
|
||||
import { Animation, hyper, power, sleep, Transition } from 'mutate-animate';
|
||||
import { Container } from '@/core/render/container';
|
||||
import {
|
||||
ArrowProjectile,
|
||||
AttackProjectile,
|
||||
@@ -19,7 +22,6 @@ import {
|
||||
ThunderProjectile
|
||||
} from './towerBossProjectile';
|
||||
import { IStateDamageable } from '@/game/state/interface';
|
||||
import { HeroRenderer } from '@/core/render/preset/hero';
|
||||
import { Pop } from '../fx/pop';
|
||||
import { WeatherController } from '@/module';
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { hyper, power, TimingFn } from 'mutate-animate';
|
||||
import { Hitbox, Projectile } from './barrage';
|
||||
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
|
||||
import { Transform } from '@/core/render/transform';
|
||||
import { MotaOffscreenCanvas2D, Transform } from '@motajs/render';
|
||||
import type { TowerBoss } from './towerBoss';
|
||||
import { IStateDamageable } from '@/game/state/interface';
|
||||
import { PointEffect, PointEffectType } from '../fx/pointShader';
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
|
||||
import { CameraAnimation } from '@/core/render/camera';
|
||||
import { Container } from '@/core/render/container';
|
||||
import { LayerGroup } from '@/core/render/preset/layer';
|
||||
import { MotaRenderer } from '@/core/render/render';
|
||||
import { Shader } from '@/core/render/shader';
|
||||
import { Sprite } from '@/core/render/sprite';
|
||||
import { disableViewport, enableViewport } from '@/core/render/utils';
|
||||
import { MotaOffscreenCanvas2D } from '@motajs/render';
|
||||
import {
|
||||
CameraAnimation,
|
||||
Container,
|
||||
LayerGroup,
|
||||
MotaRenderer,
|
||||
Shader,
|
||||
Sprite,
|
||||
disableViewport,
|
||||
enableViewport
|
||||
} from '@motajs/render';
|
||||
import type { HeroMover, MoveStep } from '@/game/state/move';
|
||||
import EventEmitter from 'eventemitter3';
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { Animation, hyper, linear, power, sleep } from 'mutate-animate';
|
||||
import { Chase, ChaseData, IChaseController } from './chase';
|
||||
import { completeAchievement } from '../ui/achievement';
|
||||
import { Camera, CameraAnimation, ICameraScale } from '@/core/render/camera';
|
||||
import { LayerGroup } from '@/core/render/preset/layer';
|
||||
import { MotaRenderer } from '@/core/render/render';
|
||||
import { Sprite } from '@/core/render/sprite';
|
||||
import {
|
||||
Camera,
|
||||
CameraAnimation,
|
||||
ICameraScale,
|
||||
LayerGroup,
|
||||
MotaRenderer,
|
||||
Sprite
|
||||
} from '@motajs/render';
|
||||
import { PointEffect, PointEffectType } from '../fx/pointShader';
|
||||
import { bgmController } from '@/module';
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
|
||||
import { wrapInstancedComponent } from '@/core/render';
|
||||
import { RenderItem, RenderItemPosition } from '@/core/render/item';
|
||||
import { Transform } from '@/core/render/transform';
|
||||
import {
|
||||
wrapInstancedComponent,
|
||||
MotaOffscreenCanvas2D,
|
||||
RenderItem,
|
||||
RenderItemPosition,
|
||||
Transform
|
||||
} from '@motajs/render';
|
||||
|
||||
// 渲染端的向后兼容用,会充当两个版本间过渡的作用
|
||||
class Change extends RenderItem {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { logger } from '@/core/common/logger';
|
||||
import { LayerGroupFloorBinder } from '@/core/render/preset/floor';
|
||||
import { logger } from '@motajs/common';
|
||||
import {
|
||||
LayerGroupFloorBinder,
|
||||
ILayerGroupRenderExtends,
|
||||
LayerGroup
|
||||
} from '@/core/render/preset/layer';
|
||||
} from '@motajs/render';
|
||||
|
||||
const filterMap: [FloorIds[], string][] = [];
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { logger } from '@/core/common/logger';
|
||||
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
|
||||
import { logger } from '@motajs/common';
|
||||
import { MotaOffscreenCanvas2D } from '@motajs/render';
|
||||
import { mainSetting } from '@/core/main/setting';
|
||||
import { LayerGroupFloorBinder } from '@/core/render/preset/floor';
|
||||
import {
|
||||
LayerGroupFloorBinder,
|
||||
ILayerGroupRenderExtends,
|
||||
LayerGroup
|
||||
} from '@/core/render/preset/layer';
|
||||
import { Sprite } from '@/core/render/sprite';
|
||||
import { Transform } from '@/core/render/transform';
|
||||
LayerGroup,
|
||||
Sprite,
|
||||
Transform
|
||||
} from '@motajs/render';
|
||||
|
||||
const gameListener = Mota.require('var', 'gameListener');
|
||||
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { logger } from '@/core/common/logger';
|
||||
import { logger } from '@motajs/common';
|
||||
import { mainSetting } from '@/core/main/setting';
|
||||
import {
|
||||
Damage,
|
||||
DamageRenderable,
|
||||
FloorDamageExtends
|
||||
} from '@/core/render/preset/damage';
|
||||
import { LayerGroupFloorBinder } from '@/core/render/preset/floor';
|
||||
import {
|
||||
FloorDamageExtends,
|
||||
LayerGroupFloorBinder,
|
||||
ILayerGroupRenderExtends,
|
||||
LayerGroup
|
||||
} from '@/core/render/preset/layer';
|
||||
} from '@motajs/render';
|
||||
|
||||
interface ItemDetailData {
|
||||
x: number;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { logger } from '@/core/common/logger';
|
||||
import { UniformType } from '@/core/render/gl2';
|
||||
import { Shader, ShaderProgram } from '@/core/render/shader';
|
||||
import { Transform } from '@/core/render/transform';
|
||||
import { logger } from '@motajs/common';
|
||||
import { UniformType, Shader, ShaderProgram, Transform } from '@motajs/render';
|
||||
|
||||
export const enum PointEffectType {
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
|
||||
import { wrapInstancedComponent } from '@/core/render';
|
||||
import { RenderItem, RenderItemPosition } from '@/core/render/item';
|
||||
import { Transform } from '@/core/render/transform';
|
||||
import {
|
||||
wrapInstancedComponent,
|
||||
RenderItem,
|
||||
RenderItemPosition,
|
||||
Transform,
|
||||
MotaOffscreenCanvas2D
|
||||
} from '@motajs/render';
|
||||
|
||||
import { TimingFn } from 'mutate-animate';
|
||||
|
||||
interface PopData {
|
||||
@@ -61,7 +65,7 @@ export class Pop extends RenderItem {
|
||||
|
||||
protected render(
|
||||
canvas: MotaOffscreenCanvas2D,
|
||||
transform: Transform
|
||||
_transform: Transform
|
||||
): void {
|
||||
const ctx = canvas.ctx;
|
||||
const toDelete = new Set<PopData>();
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { logger } from '@/core/common/logger';
|
||||
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
|
||||
import { logger } from '@motajs/common';
|
||||
import { MotaOffscreenCanvas2D } from '@motajs/render';
|
||||
import { mainSetting, MotaSettingItem } from '@/core/main/setting';
|
||||
import { LayerGroupFloorBinder } from '@/core/render/preset/floor';
|
||||
import {
|
||||
LayerGroupFloorBinder,
|
||||
ILayerGroupRenderExtends,
|
||||
LayerGroup
|
||||
} from '@/core/render/preset/layer';
|
||||
import { Sprite } from '@/core/render/sprite';
|
||||
LayerGroup,
|
||||
Sprite
|
||||
} from '@motajs/render';
|
||||
import type { BluePalace } from '@/game/mechanism/misc';
|
||||
|
||||
/** 最大粒子数 */
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
|
||||
import { GL2, GL2Program } from '@/core/render/gl2';
|
||||
import { Transform } from '@/core/render/transform';
|
||||
|
||||
const MAX_COUNT = 5;
|
||||
|
||||
export class ShadowEffect extends GL2 {
|
||||
protected preDraw(
|
||||
canvas: MotaOffscreenCanvas2D,
|
||||
transform: Transform,
|
||||
gl: WebGL2RenderingContext,
|
||||
program: GL2Program
|
||||
): boolean {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
protected postDraw(
|
||||
canvas: MotaOffscreenCanvas2D,
|
||||
transform: Transform,
|
||||
gl: WebGL2RenderingContext,
|
||||
program: GL2Program
|
||||
): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
||||
class ShadowProgam extends GL2Program {}
|
||||
@@ -1,19 +1,15 @@
|
||||
import type { RenderAdapter } from '@/core/render/adapter';
|
||||
import type { LayerGroupAnimate } from '@/core/render/preset/animate';
|
||||
import type {
|
||||
RenderAdapter,
|
||||
LayerDoorAnimate,
|
||||
LayerFloorBinder
|
||||
} from '@/core/render/preset/floor';
|
||||
import type { HeroRenderer } from '@/core/render/preset/hero';
|
||||
import type { Layer, LayerGroup } from '@/core/render/preset/layer';
|
||||
LayerGroupAnimate,
|
||||
LayerFloorBinder,
|
||||
HeroRenderer,
|
||||
Layer,
|
||||
LayerGroup,
|
||||
FloorViewport
|
||||
} from '@motajs/render';
|
||||
import type { TimingFn } from 'mutate-animate';
|
||||
import {
|
||||
BlockMover,
|
||||
heroMoveCollection,
|
||||
IMoveController,
|
||||
MoveStep
|
||||
} from '@/game/state/move';
|
||||
import type { FloorViewport } from '@/core/render/preset/viewport';
|
||||
import { BlockMover, heroMoveCollection, MoveStep } from '@/game/state/move';
|
||||
|
||||
// 向后兼容用,会充当两个版本间过渡的作用
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Container } from '@/core/render/container';
|
||||
import { FloorDamageExtends } from '@/core/render/preset/damage';
|
||||
import { LayerGroupFloorBinder } from '@/core/render/preset/floor';
|
||||
import { FloorLayer, LayerGroup } from '@/core/render/preset/layer';
|
||||
import { FloorViewport } from '@/core/render/preset/viewport';
|
||||
import { MotaRenderer } from '@/core/render/render';
|
||||
import { Transform } from '@/core/render/transform';
|
||||
import {
|
||||
Container,
|
||||
FloorDamageExtends,
|
||||
LayerGroupFloorBinder,
|
||||
FloorLayer,
|
||||
LayerGroup,
|
||||
FloorViewport,
|
||||
MotaRenderer
|
||||
} from '@motajs/render';
|
||||
import { FloorItemDetail } from '@/plugin/fx/itemDetail';
|
||||
|
||||
const loopMaps = Mota.require('module', 'Mechanism').MiscData.loopMaps;
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ import { decompressFromBase64 } from 'lz-string';
|
||||
import { Keyboard, KeyboardEmits } from '@/core/main/custom/keyboard';
|
||||
import { fixedUi, mainUi } from '@/core/main/init/ui';
|
||||
import { isAssist } from '@/core/main/custom/hotkey';
|
||||
import { logger } from '@/core/common/logger';
|
||||
import { logger } from '@motajs/common';
|
||||
|
||||
type CanParseCss = keyof {
|
||||
[P in keyof CSSStyleDeclaration as CSSStyleDeclaration[P] extends string
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
@click="addIcon(icon as AllIds)"
|
||||
>
|
||||
<BoxAnimate
|
||||
:id="(icon as AllIds)"
|
||||
:id="icon as AllIds"
|
||||
:noborder="true"
|
||||
:no-animate="true"
|
||||
:height="getIconHeight(icon as AllIds)"
|
||||
@@ -166,7 +166,7 @@ import { calStringSize, tip } from '@/plugin/utils';
|
||||
import { gameKey } from '@/core/main/custom/hotkey';
|
||||
import { isNil } from 'lodash-es';
|
||||
import { stringifyCSS, parseCss, getIconHeight } from '@/plugin/utils';
|
||||
import { logger, LogLevel } from '@/core/common/logger';
|
||||
import { logger, LogLevel } from '@motajs/common';
|
||||
import Scroll from '@/components/scroll.vue';
|
||||
import BoxAnimate from '@/components/boxAnimate.vue';
|
||||
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ import {
|
||||
} from '@/core/common/resource';
|
||||
import { GameUi } from '@/core/main/custom/ui';
|
||||
import { formatSize } from '@/plugin/utils';
|
||||
import { logger } from '@/core/common/logger';
|
||||
import { logger } from '@motajs/common';
|
||||
import { fixedUi } from '@/core/main/init/ui';
|
||||
import { sleep } from 'mutate-animate';
|
||||
|
||||
|
||||
Reference in new issue
Block a user