feat: 伤害渲染 & EventEmitter自动补全

This commit is contained in:
unanmed 2024-05-23 23:55:08 +08:00
parent fe57adf6af
commit c916b957c2
29 changed files with 1180 additions and 497 deletions

View File

@ -5,8 +5,8 @@
[] 同化 [] 同化
[] 同化+阻击 [] 同化+阻击
[x] 电摇嘲讽:到同行或同列直接怼过去,门和墙撞碎,不消耗钥匙,攻击怪物,捡道具,改变 bgm可吃补给用 [x] 电摇嘲讽:到同行或同列直接怼过去,门和墙撞碎,不消耗钥匙,攻击怪物,捡道具,改变 bgm可吃补给用
[] 乾坤挪移:平移光环位置 [x] 乾坤挪移:平移光环位置
[] 加光环的光环 [x] 加光环的光环
### Boss ### Boss
@ -21,8 +21,8 @@
### 机制 ### 机制
[] 苍蓝之殿 1: 红蓝黄门转换 [x] 苍蓝之殿 1: 红蓝黄门转换
[] 苍蓝之殿 2: 乾坤挪移、杀戮光环、同化、光环范围扩大等 [x] 苍蓝之殿 2: 乾坤挪移、杀戮光环、同化、光环范围扩大等
[] 苍蓝之殿 3: 传送门 [] 苍蓝之殿 3: 传送门
[] 苍蓝之殿 4: 地形变换,如平移、翻转、旋转等 [] 苍蓝之殿 4: 地形变换,如平移、翻转、旋转等
[] 苍蓝之殿中: 让我们把这些东西结合起来... [] 苍蓝之殿中: 让我们把这些东西结合起来...

View File

@ -4347,7 +4347,7 @@ events.prototype._jumpHero_doJump = function (jumpInfo, callback) {
var cb = function () { var cb = function () {
core.setHeroLoc('x', jumpInfo.ex); core.setHeroLoc('x', jumpInfo.ex);
core.setHeroLoc('y', jumpInfo.ey); core.setHeroLoc('y', jumpInfo.ey);
render.move(false); // render.move(false);
core.status.heroMoving = 0; core.status.heroMoving = 0;
core.drawHero(); core.drawHero();
if (callback) callback(); if (callback) callback();

View File

@ -3296,8 +3296,8 @@ maps.prototype.setBlock = function (number, x, y, floorId, noredraw) {
x, x,
y, y,
floorId, floorId,
originBlock?.id ?? 0, number,
number originBlock?.id ?? 0
); );
}; };

View File

@ -1,4 +1,4 @@
import { EmitableEvent, EventEmitter } from '../common/eventEmitter'; import { EventEmitter } from '../common/eventEmitter';
const ac = new AudioContext(); const ac = new AudioContext();
@ -7,7 +7,7 @@ interface BaseNode {
channel?: number; channel?: number;
} }
interface AudioPlayerEvent extends EmitableEvent { interface AudioPlayerEvent {
play: (node: AudioBufferSourceNode) => void; play: (node: AudioBufferSourceNode) => void;
update: (audio: AudioBuffer) => void; update: (audio: AudioBuffer) => void;
end: (node: AudioBufferSourceNode) => void; end: (node: AudioBufferSourceNode) => void;

View File

@ -1,6 +1,6 @@
import { EmitableEvent, EventEmitter } from './eventEmitter'; import { EventEmitter } from './eventEmitter';
interface DisposableEvent<T> extends EmitableEvent { interface DisposableEvent<T> {
active: (value: T) => void; active: (value: T) => void;
dispose: (value: T) => void; dispose: (value: T) => void;
destroy: () => void; destroy: () => void;

View File

@ -2,9 +2,7 @@ function has<T>(value: T): value is NonNullable<T> {
return value !== null && value !== undefined; return value !== null && value !== undefined;
} }
export interface EmitableEvent { export type Callable = (...params: any) => any;
[event: string]: (...params: any) => any;
}
export interface Listener<T extends (...params: any) => any> { export interface Listener<T extends (...params: any) => any> {
fn: T; fn: T;
@ -22,14 +20,16 @@ type EmitFn<F extends (...params: any) => any> = (
...params: Parameters<F> ...params: Parameters<F>
) => any; ) => any;
export class EventEmitter<T extends EmitableEvent = {}> { type Key = number | string | symbol;
export class EventEmitter<T extends Record<keyof T, Callable>> {
protected events: { protected events: {
[P in keyof T]?: Listener<T[P]>[]; [x: Key]: Listener<Callable>[];
} = {}; } = {};
private emitted: (keyof T)[] = []; private emitted: Set<string> = new Set();
protected emitter: { protected emitter: {
[P in keyof T]?: EmitFn<T[P]>; [x: Key]: EmitFn<Callable> | undefined;
} = {}; } = {};
/** /**
@ -42,8 +42,10 @@ export class EventEmitter<T extends EmitableEvent = {}> {
event: K, event: K,
fn: T[K], fn: T[K],
options?: Partial<ListenerOptions> options?: Partial<ListenerOptions>
) { ): void;
if (options?.immediate && this.emitted.includes(event)) { on(event: string, fn: Callable, options?: Partial<ListenerOptions>): void;
on(event: string, fn: Callable, options?: Partial<ListenerOptions>): void {
if (options?.immediate && this.emitted.has(event)) {
fn(); fn();
if (!options.once) { if (!options.once) {
this.events[event] ??= []; this.events[event] ??= [];
@ -65,7 +67,9 @@ export class EventEmitter<T extends EmitableEvent = {}> {
* @param event * @param event
* @param fn * @param fn
*/ */
off<K extends keyof T>(event: K, fn: T[K]) { off<K extends keyof T>(event: K, fn: T[K]): void;
off(event: string, fn: Callable): void;
off(event: string, fn: Callable): void {
const index = this.events[event]?.findIndex(v => v.fn === fn); const index = this.events[event]?.findIndex(v => v.fn === fn);
if (index === -1 || index === void 0) return; if (index === -1 || index === void 0) return;
this.events[event]?.splice(index, 1); this.events[event]?.splice(index, 1);
@ -76,7 +80,9 @@ export class EventEmitter<T extends EmitableEvent = {}> {
* @param event * @param event
* @param fn * @param fn
*/ */
once<K extends keyof T>(event: K, fn: T[K]) { once<K extends keyof T>(event: K, fn: T[K]): void;
once(event: string, fn: Callable): void;
once(event: string, fn: Callable): void {
this.on(event, fn, { once: true }); this.on(event, fn, { once: true });
} }
@ -89,18 +95,18 @@ export class EventEmitter<T extends EmitableEvent = {}> {
event: K, event: K,
...params: Parameters<T[K]> ...params: Parameters<T[K]>
): ReturnType<T[K]>[]; ): ReturnType<T[K]>[];
emit(event: string, ...params: any[]): any[];
emit<K extends keyof T, R>(event: K, ...params: Parameters<T[K]>): R; emit<K extends keyof T, R>(event: K, ...params: Parameters<T[K]>): R;
emit<K extends keyof T>(event: K, ...params: Parameters<T[K]>): any { emit<R>(event: string, ...params: any[]): R;
if (!this.emitted.includes(event)) { emit(event: string, ...params: any[]): any[] {
this.emitted.push(event); this.emitted.add(event);
}
const events = (this.events[event] ??= []); const events = (this.events[event] ??= []);
if (!!this.emitter[event]) { if (!!this.emitter[event]) {
const returns = this.emitter[event]!(events, ...params); const returns = this.emitter[event]!(events, ...params);
this.events[event] = events.filter(v => !v.once); this.events[event] = events.filter(v => !v.once);
return returns; return returns;
} else { } else {
const returns: ReturnType<T[K]>[] = []; const returns: ReturnType<Callable>[] = [];
for (let i = 0; i < events.length; i++) { for (let i = 0; i < events.length; i++) {
const e = events[i]; const e = events[i];
returns.push(e.fn(...(params as any))); returns.push(e.fn(...(params as any)));
@ -115,7 +121,10 @@ export class EventEmitter<T extends EmitableEvent = {}> {
* @param event * @param event
* @param fn * @param fn
*/ */
setEmitter<K extends keyof T>(event: K, fn?: EmitFn<T[K]>) { // @ts-ignore
setEmitter<K extends keyof T>(event: K, fn?: EmitFn<T[K]>): void;
setEmitter(event: string, fn?: EmitFn<Callable>): void;
setEmitter(event: string, fn?: EmitFn<Callable>): void {
this.emitter[event] = fn; this.emitter[event] = fn;
} }
@ -128,7 +137,12 @@ export class EventEmitter<T extends EmitableEvent = {}> {
* @param event * @param event
*/ */
removeAllListeners(event: keyof T): void; removeAllListeners(event: keyof T): void;
removeAllListeners(event?: keyof T) { /**
*
* @param event
*/
removeAllListeners(event: string): void;
removeAllListeners(event?: string | keyof T) {
if (has(event)) this.events[event] = []; if (has(event)) this.events[event] = [];
else this.events = {}; else this.events = {};
} }
@ -137,7 +151,7 @@ export class EventEmitter<T extends EmitableEvent = {}> {
type IndexedSymbol = number | string | symbol; type IndexedSymbol = number | string | symbol;
export class IndexedEventEmitter< export class IndexedEventEmitter<
T extends EmitableEvent T extends Record<keyof T, Callable>
> extends EventEmitter<T> { > extends EventEmitter<T> {
private fnMap: { private fnMap: {
[P in keyof T]?: Map<IndexedSymbol, T[P]>; [P in keyof T]?: Map<IndexedSymbol, T[P]>;

View File

@ -1,5 +1,7 @@
import { debounce } from 'lodash-es'; import { debounce } from 'lodash-es';
// todo: 使用格式化输出?
export const enum LogLevel { export const enum LogLevel {
/** 输出所有,包括日志 */ /** 输出所有,包括日志 */
LOG, LOG,
@ -84,7 +86,7 @@ export class Logger {
logTip.textContent = `Error thrown, please check in console.`; logTip.textContent = `Error thrown, please check in console.`;
hideTipText(); hideTipText();
} }
throw `[ERROR Code ${code}] ${text}`; console.error(`[ERROR Code ${code}] ${text}`);
} }
} }

View File

@ -2,7 +2,7 @@ import axios, { AxiosRequestConfig, ResponseType } from 'axios';
import { Disposable } from './disposable'; import { Disposable } from './disposable';
import { logger } from './logger'; import { logger } from './logger';
import JSZip from 'jszip'; import JSZip from 'jszip';
import { EmitableEvent, EventEmitter } from './eventEmitter'; import { EventEmitter } from './eventEmitter';
type ProgressFn = (now: number, total: number) => void; type ProgressFn = (now: number, total: number) => void;
@ -305,7 +305,7 @@ export const resourceTypeMap = {
zip: ZipResource zip: ZipResource
}; };
interface LoadEvent<T extends keyof ResourceType> extends EmitableEvent { interface LoadEvent<T extends keyof ResourceType> {
progress: ( progress: (
type: keyof ResourceType, type: keyof ResourceType,
uri: string, uri: string,

View File

@ -1,8 +1,8 @@
import { parseCss } from '@/plugin/utils'; import { parseCss } from '@/plugin/utils';
import { EmitableEvent, EventEmitter } from '../common/eventEmitter'; import { EventEmitter } from '../common/eventEmitter';
import { CSSObj } from '../interface'; import { CSSObj } from '../interface';
interface OffscreenCanvasEvent extends EmitableEvent { interface OffscreenCanvasEvent {
/** 当被动触发resize时例如core.domStyle.scale变化、窗口大小变化时触发使用size函数并不会触发 */ /** 当被动触发resize时例如core.domStyle.scale变化、窗口大小变化时触发使用size函数并不会触发 */
resize: () => void; resize: () => void;
} }

View File

@ -1,8 +1,8 @@
import { Animation, Ticker, hyper } from 'mutate-animate'; import { Animation, Ticker, hyper } from 'mutate-animate';
import { EmitableEvent, EventEmitter } from '../common/eventEmitter'; import { EventEmitter } from '../common/eventEmitter';
import { ensureArray } from '@/plugin/utils'; import { ensureArray } from '@/plugin/utils';
interface ShaderEvent extends EmitableEvent {} interface ShaderEvent {}
const isWebGLSupported = (() => { const isWebGLSupported = (() => {
return !!document.createElement('canvas').getContext('webgl'); return !!document.createElement('canvas').getContext('webgl');

View File

@ -1,6 +1,6 @@
import { EmitableEvent, EventEmitter } from '../common/eventEmitter'; import { EventEmitter } from '../common/eventEmitter';
interface ResourceControllerEvent<D = any, T = D> extends EmitableEvent { interface ResourceControllerEvent<D = any, T = D> {
add: (uri: string, data: D) => void; add: (uri: string, data: D) => void;
delete: (uri: string, content: T) => void; delete: (uri: string, content: T) => void;
} }
@ -25,6 +25,6 @@ export abstract class ResourceController<
remove(uri: string) { remove(uri: string) {
const content = this.list[uri]; const content = this.list[uri];
delete this.list[uri]; delete this.list[uri];
this.emit(uri, content); // this.emit(uri, content);
} }
} }

View File

@ -1,5 +1,5 @@
import BoxAnimate from '@/components/boxAnimate.vue'; import BoxAnimate from '@/components/boxAnimate.vue';
import { EmitableEvent, EventEmitter } from '@/core/common/eventEmitter'; import { EventEmitter } from '@/core/common/eventEmitter';
import { logger } from '@/core/common/logger'; import { logger } from '@/core/common/logger';
import { ResponseBase } from '@/core/interface'; import { ResponseBase } from '@/core/interface';
import { import {
@ -52,7 +52,7 @@ interface PostLikeResponse extends ResponseBase {
liked: boolean; liked: boolean;
} }
interface DanmakuEvent extends EmitableEvent { interface DanmakuEvent {
showStart: (danmaku: Danmaku) => void; showStart: (danmaku: Danmaku) => void;
showEnd: (danmaku: Danmaku) => void; showEnd: (danmaku: Danmaku) => void;
like: (liked: boolean, danmaku: Danmaku) => void; like: (liked: boolean, danmaku: Danmaku) => void;

View File

@ -1,10 +1,10 @@
import { KeyCode } from '@/plugin/keyCodes'; import { KeyCode } from '@/plugin/keyCodes';
import { deleteWith, generateBinary, spliceBy } from '@/plugin/utils'; import { deleteWith, generateBinary, spliceBy } from '@/plugin/utils';
import { EmitableEvent, EventEmitter } from '../../common/eventEmitter'; import { EventEmitter } from '../../common/eventEmitter';
// todo: 按下时触发,长按(单次/连续)触发,按下连续触发,按下节流触发,按下加速节流触发 // todo: 按下时触发,长按(单次/连续)触发,按下连续触发,按下节流触发,按下加速节流触发
interface HotkeyEvent extends EmitableEvent { interface HotkeyEvent {
set: (id: string, key: KeyCode, assist: number) => void; set: (id: string, key: KeyCode, assist: number) => void;
emit: (key: KeyCode, assist: number, type: KeyEmitType) => void; emit: (key: KeyCode, assist: number, type: KeyEmitType) => void;
} }

View File

@ -1,8 +1,4 @@
import { import { EventEmitter, Listener } from '@/core/common/eventEmitter';
EmitableEvent,
EventEmitter,
Listener
} from '@/core/common/eventEmitter';
import { KeyCode } from '@/plugin/keyCodes'; import { KeyCode } from '@/plugin/keyCodes';
import { gameKey } from '../init/hotkey'; import { gameKey } from '../init/hotkey';
import { unwarpBinary } from './hotkey'; import { unwarpBinary } from './hotkey';
@ -36,7 +32,7 @@ type VirtualKeyEmitFn = (
ev: VirtualKeyEmit ev: VirtualKeyEmit
) => void; ) => void;
interface VirtualKeyboardEvent extends EmitableEvent { interface VirtualKeyboardEvent {
add: (item: KeyboardItem) => void; add: (item: KeyboardItem) => void;
remove: (item: KeyboardItem) => void; remove: (item: KeyboardItem) => void;
extend: (extended: Keyboard) => void; extend: (extended: Keyboard) => void;

View File

@ -1,4 +1,4 @@
import { EmitableEvent, EventEmitter } from '@/core/common/eventEmitter'; import { EventEmitter } from '@/core/common/eventEmitter';
import { deleteWith, has } from '@/plugin/utils'; import { deleteWith, has } from '@/plugin/utils';
import { Component, nextTick, reactive, shallowReactive } from 'vue'; import { Component, nextTick, reactive, shallowReactive } from 'vue';
import { fixedUi } from '../init/ui'; import { fixedUi } from '../init/ui';
@ -13,7 +13,7 @@ import type {
} from '../init/toolbar'; } from '../init/toolbar';
import { isMobile } from '@/plugin/use'; import { isMobile } from '@/plugin/use';
interface CustomToolbarEvent extends EmitableEvent { interface CustomToolbarEvent {
add: (item: ValueOf<ToolbarItemMap>) => void; add: (item: ValueOf<ToolbarItemMap>) => void;
delete: (item: ValueOf<ToolbarItemMap>) => void; delete: (item: ValueOf<ToolbarItemMap>) => void;
set: (id: string, data: Partial<SettableItemData>) => void; set: (id: string, data: Partial<SettableItemData>) => void;

View File

@ -1,10 +1,10 @@
import { Component, shallowReactive } from 'vue'; import { Component, shallowReactive } from 'vue';
import { EmitableEvent, EventEmitter } from '../../common/eventEmitter'; import { Callable, EventEmitter } from '../../common/eventEmitter';
import { KeyCode } from '@/plugin/keyCodes'; import { KeyCode } from '@/plugin/keyCodes';
import { Hotkey } from './hotkey'; import { Hotkey } from './hotkey';
import { generateBinary } from '@/plugin/utils'; import { generateBinary } from '@/plugin/utils';
interface FocusEvent<T> extends EmitableEvent { interface FocusEvent<T> {
focus: (before: T | null, after: T) => void; focus: (before: T | null, after: T) => void;
unfocus: (before: T | null) => void; unfocus: (before: T | null) => void;
add: (item: T) => void; add: (item: T) => void;
@ -109,7 +109,7 @@ export class Focus<T = any> extends EventEmitter<FocusEvent<T>> {
} }
} }
interface GameUiEvent extends EmitableEvent { interface GameUiEvent {
close: () => void; close: () => void;
open: () => void; open: () => void;
} }

View File

@ -1,5 +1,5 @@
import { FunctionalComponent, reactive } from 'vue'; import { FunctionalComponent, reactive } from 'vue';
import { EmitableEvent, EventEmitter } from '../common/eventEmitter'; import { EventEmitter } from '../common/eventEmitter';
import { GameStorage } from './storage'; import { GameStorage } from './storage';
import { has, triggerFullscreen } from '@/plugin/utils'; import { has, triggerFullscreen } from '@/plugin/utils';
import { createSettingComponents } from './init/settings'; import { createSettingComponents } from './init/settings';
@ -31,7 +31,7 @@ export interface MotaSettingItem<T extends MotaSettingType = MotaSettingType> {
display?: (value: T) => string; display?: (value: T) => string;
} }
interface SettingEvent extends EmitableEvent { interface SettingEvent {
valueChange: <T extends boolean | number>( valueChange: <T extends boolean | number>(
key: string, key: string,
newValue: T, newValue: T,
@ -230,7 +230,7 @@ export class MotaSetting extends EventEmitter<SettingEvent> {
} }
} }
interface SettingDisplayerEvent extends EmitableEvent { interface SettingDisplayerEvent {
update: (stack: string[], display: SettingDisplayInfo[]) => void; update: (stack: string[], display: SettingDisplayInfo[]) => void;
} }

View File

@ -1,4 +1,4 @@
import { EmitableEvent, EventEmitter } from '../common/eventEmitter'; import { EventEmitter } from '../common/eventEmitter';
import { logger } from '../common/logger'; import { logger } from '../common/logger';
import { MotaOffscreenCanvas2D } from '../fx/canvas2d'; import { MotaOffscreenCanvas2D } from '../fx/canvas2d';
import { SizedCanvasImageSource } from './preset/misc'; import { SizedCanvasImageSource } from './preset/misc';
@ -62,7 +62,7 @@ export interface AutotileRenderable extends RenderableDataBase {
bigImage: false; bigImage: false;
} }
interface TextureCacheEvent extends EmitableEvent {} interface TextureCacheEvent {}
class TextureCache extends EventEmitter<TextureCacheEvent> { class TextureCache extends EventEmitter<TextureCacheEvent> {
tileset!: Record<string, HTMLImageElement>; tileset!: Record<string, HTMLImageElement>;
@ -499,10 +499,10 @@ function splitAutotiles(map: IdToNumber): AutotileCaches {
const ctx = canvas.ctx; const ctx = canvas.ctx;
for (let i = 0; i < frame; i++) { for (let i = 0; i < frame; i++) {
const dx = 32 * i; const dx = 32 * i;
const sx1 = info[0][0]; const sx1 = info[0][0] + (i * row * 32) / 2;
const sx2 = info[1][0]; const sx2 = info[1][0] + (i * row * 32) / 2;
const sx3 = info[2][0]; const sx3 = info[2][0] + (i * row * 32) / 2;
const sx4 = info[3][0]; const sx4 = info[3][0] + (i * row * 32) / 2;
const sy1 = info[0][1]; const sy1 = info[0][1];
const sy2 = info[1][1]; const sy2 = info[1][1];
const sy3 = info[2][1]; const sy3 = info[2][1];

View File

@ -1,7 +1,7 @@
import { ReadonlyMat3, ReadonlyVec3, mat3, vec2, vec3 } from 'gl-matrix'; import { ReadonlyMat3, ReadonlyVec3, mat3, vec2, vec3 } from 'gl-matrix';
import { EmitableEvent, EventEmitter } from '../common/eventEmitter'; import { EventEmitter } from '../common/eventEmitter';
interface CameraEvent extends EmitableEvent {} interface CameraEvent {}
export class Camera extends EventEmitter<CameraEvent> { export class Camera extends EventEmitter<CameraEvent> {
mat: mat3 = mat3.create(); mat: mat3 = mat3.create();

View File

@ -2,12 +2,16 @@ import { MotaOffscreenCanvas2D } from '../fx/canvas2d';
import { Camera } from './camera'; import { Camera } from './camera';
import { import {
ICanvasCachedRenderItem, ICanvasCachedRenderItem,
IRenderChildable,
RenderItem, RenderItem,
RenderItemPosition, RenderItemPosition,
withCacheRender withCacheRender
} from './item'; } from './item';
export class Container extends RenderItem implements ICanvasCachedRenderItem { export class Container
extends RenderItem
implements ICanvasCachedRenderItem, IRenderChildable
{
children: RenderItem[] = []; children: RenderItem[] = [];
sortedChildren: RenderItem[] = []; sortedChildren: RenderItem[] = [];
@ -27,17 +31,20 @@ export class Container extends RenderItem implements ICanvasCachedRenderItem {
): void { ): void {
this.emit('beforeRender'); this.emit('beforeRender');
if (this.needUpdate) { if (this.needUpdate) {
this.cache(this.writing); this.cache(this.using);
this.needUpdate = false; this.needUpdate = false;
} }
withCacheRender(this, canvas, ctx, camera, c => { withCacheRender(this, canvas, ctx, camera, c => {
this.sortedChildren.forEach(v => { this.sortedChildren.forEach(v => {
if (v.hidden) return;
ctx.save();
if (!v.antiAliasing) { if (!v.antiAliasing) {
ctx.imageSmoothingEnabled = false; ctx.imageSmoothingEnabled = false;
} else { } else {
ctx.imageSmoothingEnabled = true; ctx.imageSmoothingEnabled = true;
} }
v.render(c.canvas, c.ctx, camera); v.render(c.canvas, c.ctx, camera);
ctx.restore();
}); });
}); });
this.writing = void 0; this.writing = void 0;
@ -61,10 +68,21 @@ export class Container extends RenderItem implements ICanvasCachedRenderItem {
* tick执行更新 * tick执行更新
* @param children * @param children
*/ */
appendChild(children: RenderItem[]) { appendChild(...children: RenderItem[]) {
children.forEach(v => (v.parent = this)); children.forEach(v => (v.parent = this));
this.children.push(...children); this.children.push(...children);
this.sortChildren(); this.sortChildren();
this.update(this);
}
removeChild(...child: RenderItem[]): void {
child.forEach(v => {
const index = this.children.indexOf(v);
if (index === -1) return;
this.children.splice(index, 1);
});
this.sortChildren();
this.update(this);
} }
sortChildren() { sortChildren() {
@ -84,4 +102,11 @@ export class Container extends RenderItem implements ICanvasCachedRenderItem {
this.canvas.setAntiAliasing(anti); this.canvas.setAntiAliasing(anti);
this.update(this); this.update(this);
} }
destroy(): void {
super.destroy();
this.children.forEach(v => {
v.destroy();
});
}
} }

View File

@ -1,6 +1,6 @@
import { Ticker } from 'mutate-animate'; import { Ticker } from 'mutate-animate';
import { MotaCanvas2D } from '../fx/canvas2d'; import { MotaCanvas2D } from '../fx/canvas2d';
import { EmitableEvent, EventEmitter } from '../common/eventEmitter'; import { EventEmitter } from '../common/eventEmitter';
import { debounce } from 'lodash-es'; import { debounce } from 'lodash-es';
// 重写样板的勇士绘制 // 重写样板的勇士绘制
@ -36,7 +36,7 @@ interface HeroDrawing extends HeroDrawItem {
dir: Dir; dir: Dir;
} }
interface HeroRendererEvent extends EmitableEvent { interface HeroRendererEvent {
beforeDraw: () => void; beforeDraw: () => void;
afterDraw: () => void; afterDraw: () => void;
} }

View File

@ -1,9 +1,8 @@
import { isNil } from 'lodash-es'; import { isNil } from 'lodash-es';
import { EmitableEvent, EventEmitter } from '../common/eventEmitter'; import { EventEmitter } from '../common/eventEmitter';
import { MotaOffscreenCanvas2D } from '../fx/canvas2d'; import { MotaOffscreenCanvas2D } from '../fx/canvas2d';
import { Camera } from './camera'; import { Camera } from './camera';
import { Ticker } from 'mutate-animate'; import { Ticker } from 'mutate-animate';
import type { Container } from './container';
export type RenderFunction = ( export type RenderFunction = (
canvas: MotaOffscreenCanvas2D, canvas: MotaOffscreenCanvas2D,
@ -83,14 +82,28 @@ interface IRenderConfig {
setAntiAliasing(anti: boolean): void; setAntiAliasing(anti: boolean): void;
} }
export interface IRenderDestroyable { export interface IRenderChildable {
children: RenderItem[];
/** /**
* 使 *
* @param child
*/ */
destroy(): void; appendChild(...child: RenderItem[]): void;
/**
*
* @param child
*/
removeChild(...child: RenderItem[]): void;
/**
*
*/
sortChildren(): void;
} }
interface RenderItemEvent extends EmitableEvent { interface RenderItemEvent {
beforeUpdate: (item?: RenderItem) => void; beforeUpdate: (item?: RenderItem) => void;
afterUpdate: (item?: RenderItem) => void; afterUpdate: (item?: RenderItem) => void;
beforeRender: () => void; beforeRender: () => void;
@ -127,8 +140,10 @@ export abstract class RenderItem
highResolution: boolean = true; highResolution: boolean = true;
/** 是否抗锯齿 */ /** 是否抗锯齿 */
antiAliasing: boolean = true; antiAliasing: boolean = true;
/** 是否被隐藏 */
hidden: boolean = false;
parent?: RenderItem; parent?: RenderItem & IRenderChildable;
protected needUpdate: boolean = false; protected needUpdate: boolean = false;
@ -209,10 +224,49 @@ export abstract class RenderItem
setZIndex(zIndex: number) { setZIndex(zIndex: number) {
this.zIndex = zIndex; this.zIndex = zIndex;
(this.parent as Container)?.sortChildren?.(); this.parent?.sortChildren?.();
}
/**
*
*/
hide() {
if (this.hidden) return;
this.hidden = true;
this.update(this);
}
/**
*
*/
show() {
if (!this.hidden) return;
this.hidden = false;
this.update(this);
}
/**
*
*/
remove() {
this.parent?.removeChild(this);
this.parent = void 0;
}
/**
* 使
*/
destroy(): void {
this.remove();
} }
} }
interface RenderEvent {
animateFrame: (frame: number, time: number) => void;
}
export const renderEmits = new EventEmitter<RenderEvent>();
Mota.require('var', 'hook').once('reset', () => { Mota.require('var', 'hook').once('reset', () => {
let lastTime = 0; let lastTime = 0;
RenderItem.ticker.add(time => { RenderItem.ticker.add(time => {
@ -220,13 +274,14 @@ Mota.require('var', 'hook').once('reset', () => {
if (time - lastTime > core.values.animateSpeed) { if (time - lastTime > core.values.animateSpeed) {
RenderItem.animatedFrame++; RenderItem.animatedFrame++;
lastTime = time; lastTime = time;
renderEmits.emit('animateFrame', RenderItem.animatedFrame, time);
} }
}); });
}); });
export function withCacheRender( export function withCacheRender(
item: RenderItem & ICanvasCachedRenderItem, item: RenderItem & ICanvasCachedRenderItem,
canvas: HTMLCanvasElement, _canvas: HTMLCanvasElement,
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
camera: Camera, camera: Camera,
fn: RenderFunction fn: RenderFunction
@ -264,3 +319,24 @@ export function withCacheRender(
ctx.drawImage(c, item.x - ax, item.y - ay, item.width, item.height); ctx.drawImage(c, item.x - ax, item.y - ay, item.width, item.height);
} }
export function transformCanvas(
canvas: MotaOffscreenCanvas2D,
camera: Camera,
clear: boolean = false
) {
const { canvas: ca, ctx, scale } = canvas;
const mat = camera.mat;
const a = mat[0] * scale;
const b = mat[1] * scale;
const c = mat[3] * scale;
const d = mat[4] * scale;
const e = mat[6] * scale;
const f = mat[7] * scale;
ctx.setTransform(1, 0, 0, 1, 0, 0);
if (clear) {
ctx.clearRect(0, 0, ca.width, ca.height);
}
ctx.translate(ca.width / 2, ca.height / 2);
ctx.transform(a, b, c, d, e, f);
}

View File

@ -0,0 +1,205 @@
import { EventEmitter } from '@/core/common/eventEmitter';
import { logger } from '@/core/common/logger';
interface BlockCacherEvent {
split: () => void;
}
interface BlockData {
/** 横向宽度包括rest的那一个块 */
width: number;
/** 纵向宽度包括rest的那一个块 */
height: number;
/** 横向最后一个块的宽度 */
restWidth: number;
/** 纵向最后一个块的高度 */
restHeight: number;
}
export class BlockCacher<T> 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;
}
/**
*
* @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, `Cache depth cannot larger than 31.`);
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');
}
/**
*
* @param index
* @param deep 310b111就是清除前三层的索引
*/
clearCache(index: number, deep: number) {
const depth = this.cacheDepth;
for (let i = 0; i < depth; i++) {
if (deep & (1 << i)) {
this.cache.delete(index * this.cacheDepth + i);
}
}
}
/**
* {@link clearCache}
*/
clearCacheByIndex(index: number) {
this.cache.delete(index);
}
/**
*
*/
clearAllCache() {
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
*/
updateArea(
x: number,
y: number,
width: number,
height: number,
deep: number = 2 ** 31 - 1
) {
const cleared = 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.getIndexByLoc(nx, ny);
if (cleared.has(index)) continue;
this.clearCache(index, deep);
cleared.add(index);
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -2,10 +2,10 @@ import { Animation, hyper, sleep } from 'mutate-animate';
import { MotaCanvas2D, MotaOffscreenCanvas2D } from '../fx/canvas2d'; import { MotaCanvas2D, MotaOffscreenCanvas2D } from '../fx/canvas2d';
import { Camera } from './camera'; import { Camera } from './camera';
import { Container } from './container'; import { Container } from './container';
import { IRenderDestroyable, RenderItem, withCacheRender } from './item'; import { RenderItem, transformCanvas, withCacheRender } from './item';
import { Layer } from './preset/layer'; import { Layer, LayerGroup } from './preset/layer';
export class MotaRenderer extends Container implements IRenderDestroyable { export class MotaRenderer extends Container {
static list: Set<MotaRenderer> = new Set(); static list: Set<MotaRenderer> = new Set();
canvas: MotaOffscreenCanvas2D; canvas: MotaOffscreenCanvas2D;
@ -71,40 +71,33 @@ export class MotaRenderer extends Container implements IRenderDestroyable {
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height);
withCacheRender(this, canvas, ctx, camera, canvas => { withCacheRender(this, canvas, ctx, camera, canvas => {
const { canvas: ca, ctx: ct, scale } = canvas; const { canvas: ca, ctx: ct, scale } = canvas;
const mat = camera.mat;
const a = mat[0] * scale;
const b = mat[1] * scale;
const c = mat[3] * scale;
const d = mat[4] * scale;
const e = mat[6] * scale;
const f = mat[7] * scale;
this.sortedChildren.forEach(v => { this.sortedChildren.forEach(v => {
if (v.hidden) return;
if (v.type === 'absolute') { if (v.type === 'absolute') {
ct.setTransform(scale, 0, 0, scale, 0, 0); ct.setTransform(scale, 0, 0, scale, 0, 0);
} else { } else {
ct.setTransform(1, 0, 0, 1, 0, 0); transformCanvas(canvas, camera, false);
ct.translate(ca.width / 2, ca.height / 2);
ct.transform(a, b, c, d, e, f);
} }
ct.save();
if (!v.antiAliasing) { if (!v.antiAliasing) {
ctx.imageSmoothingEnabled = false; ctx.imageSmoothingEnabled = false;
ct.imageSmoothingEnabled = false;
} else { } else {
ctx.imageSmoothingEnabled = true; ctx.imageSmoothingEnabled = true;
ct.imageSmoothingEnabled = true;
} }
v.render(ca, ct, camera); v.render(ca, ct, camera);
ct.restore();
}); });
}); });
this.emit('afterRender'); this.emit('afterRender');
} }
/**
* tick更新
*/
update(item?: RenderItem) { update(item?: RenderItem) {
if (this.needUpdate) return; if (this.needUpdate) return;
this.needUpdate = true; this.needUpdate = true;
requestAnimationFrame(() => { requestAnimationFrame(() => {
this.cache(this.writing); this.cache(this.using);
this.needUpdate = false; this.needUpdate = false;
this.refresh(item); this.refresh(item);
}); });
@ -144,57 +137,15 @@ window.addEventListener('resize', () => {
Mota.require('var', 'hook').once('reset', () => { Mota.require('var', 'hook').once('reset', () => {
const render = new MotaRenderer(); const render = new MotaRenderer();
const layer = new Layer();
const bgLayer = new Layer();
const camera = render.camera; const camera = render.camera;
render.mount(); render.mount();
layer.setZIndex(2); const layer = new LayerGroup();
bgLayer.setZIndex(1); layer.addDamage();
render.appendChild([layer, bgLayer]); render.appendChild(layer);
layer.bindThis('event');
bgLayer.bindThis('bg');
bgLayer.setBackground(305);
const ani = new Animation();
// ani.ticker.add(() => {
// camera.reset();
// camera.rotate((ani.angle / 180) * Math.PI);
// camera.move(ani.x, ani.y);
// camera.scale(ani.size);
// render.update(render);
// });
// camera.rotate(Math.PI * 1.23);
camera.move(240, 240); camera.move(240, 240);
// camera.scale(0.7);
render.update(); render.update();
// sleep(2000).then(() => {
// render.update();
// });
sleep(1000).then(() => {
// ani.mode(hyper('sin', 'out'))
// .time(100)
// .absolute()
// .rotate(30)
// .move(240, 240);
// sleep(100).then(() => {
// ani.time(3000).rotate(0);
// });
// sleep(3100).then(() => {
// ani.time(5000)
// .mode(hyper('sin', 'in-out'))
// .rotate(360)
// .move(200, 480)
// .scale(0.5);
// });
// ani.mode(shake2(5, hyper('sin', 'in-out')), true)
// .time(5000)
// .shake(1, 0);
});
console.log(layer); console.log(layer);
}); });

View File

@ -26,7 +26,7 @@ export class Sprite extends RenderItem implements ICanvasCachedRenderItem {
): void { ): void {
this.emit('beforeRender'); this.emit('beforeRender');
if (this.needUpdate) { if (this.needUpdate) {
this.cache(this.writing); this.cache(this.using);
this.needUpdate = false; this.needUpdate = false;
} }
withCacheRender(this, canvas, ctx, camera, canvas => { withCacheRender(this, canvas, ctx, camera, canvas => {
@ -40,7 +40,6 @@ export class Sprite extends RenderItem implements ICanvasCachedRenderItem {
this.width = width; this.width = width;
this.height = height; this.height = height;
this.canvas.size(width, height); this.canvas.size(width, height);
this.writing = this.using;
this.update(this); this.update(this);
} }

View File

@ -1,12 +1,13 @@
import { EmitableEvent, EventEmitter } from '../core/common/eventEmitter'; import { EventEmitter } from '../core/common/eventEmitter';
import type { DamageEnemy } from './enemy/damage'; import type { DamageEnemy } from './enemy/damage';
// ----- 加载事件 // ----- 加载事件
interface GameLoadEvent extends EmitableEvent { interface GameLoadEvent {
coreLoaded: () => void; coreLoaded: () => void;
autotileLoaded: () => void; autotileLoaded: () => void;
coreInit: () => void; coreInit: () => void;
loaded: () => void; loaded: () => void;
materialLoaded: () => void;
} }
class GameLoading extends EventEmitter<GameLoadEvent> { class GameLoading extends EventEmitter<GameLoadEvent> {
@ -64,7 +65,7 @@ class GameLoading extends EventEmitter<GameLoadEvent> {
export const loading = new GameLoading(); export const loading = new GameLoading();
export interface GameEvent extends EmitableEvent { export interface GameEvent {
/** Emitted in libs/events.js resetGame. */ /** Emitted in libs/events.js resetGame. */
reset: () => void; reset: () => void;
/** Emitted in src/App.vue setup. */ /** Emitted in src/App.vue setup. */
@ -101,14 +102,14 @@ export interface GameEvent extends EmitableEvent {
x: number, x: number,
y: number, y: number,
floorId: FloorIds, floorId: FloorIds,
oldBlock: AllNumbers, newBlock: AllNumbers,
newBlock: AllNumbers oldBlock: AllNumbers
) => void; ) => void;
} }
export const hook = new EventEmitter<GameEvent>(); export const hook = new EventEmitter<GameEvent>();
interface ListenerEvent extends EmitableEvent { interface ListenerEvent {
// block // block
hoverBlock: (block: Block, ev: MouseEvent) => void; hoverBlock: (block: Block, ev: MouseEvent) => void;
leaveBlock: (block: Block, ev: MouseEvent, leaveGame: boolean) => void; leaveBlock: (block: Block, ev: MouseEvent, leaveGame: boolean) => void;

View File

@ -96,3 +96,31 @@ Range.registerRangeType(
); );
} }
); );
Range.registerRangeType(
'rect',
(col, { x, y, w, h }) => {
const list = col.collection.list;
const ex = x + w;
const ey = y + h;
return list.filter(v => {
return (
has(v.x) &&
has(v.y) &&
v.x >= x &&
v.y >= y &&
v.x < ex &&
v.y < ey
);
});
},
(col, { x, y, w, h }, item) => {
const ex = x + w;
const ey = y + h;
const v = item;
return (
has(v.x) && has(v.y) && v.x >= x && v.y >= y && v.x < ex && v.y < ey
);
}
);

View File

@ -43,7 +43,7 @@ export function has<T>(value: T): value is NonNullable<T> {
* *
* @param damage * @param damage
*/ */
export function getDamageColor(damage: number): Color { export function getDamageColor(damage: number): string {
if (typeof damage !== 'number') return '#f00'; if (typeof damage !== 'number') return '#f00';
if (damage === 0) return '#2f2'; if (damage === 0) return '#2f2';
if (damage < 0) return '#7f7'; if (damage < 0) return '#7f7';