diff --git a/idea.md b/idea.md index 1b58ef5..9df705b 100644 --- a/idea.md +++ b/idea.md @@ -5,8 +5,8 @@ [] 同化 [] 同化+阻击 [x] 电摇嘲讽:到同行或同列直接怼过去,门和墙撞碎,不消耗钥匙,攻击怪物,捡道具,改变 bgm,可吃补给用 -[] 乾坤挪移:平移光环位置 -[] 加光环的光环 +[x] 乾坤挪移:平移光环位置 +[x] 加光环的光环 ### Boss @@ -21,8 +21,8 @@ ### 机制 -[] 苍蓝之殿 1: 红蓝黄门转换 -[] 苍蓝之殿 2: 乾坤挪移、杀戮光环、同化、光环范围扩大等 +[x] 苍蓝之殿 1: 红蓝黄门转换 +[x] 苍蓝之殿 2: 乾坤挪移、杀戮光环、同化、光环范围扩大等 [] 苍蓝之殿 3: 传送门 [] 苍蓝之殿 4: 地形变换,如平移、翻转、旋转等 [] 苍蓝之殿中: 让我们把这些东西结合起来... diff --git a/public/libs/events.js b/public/libs/events.js index 59fd0ad..afd6ab5 100644 --- a/public/libs/events.js +++ b/public/libs/events.js @@ -4347,7 +4347,7 @@ events.prototype._jumpHero_doJump = function (jumpInfo, callback) { var cb = function () { core.setHeroLoc('x', jumpInfo.ex); core.setHeroLoc('y', jumpInfo.ey); - render.move(false); + // render.move(false); core.status.heroMoving = 0; core.drawHero(); if (callback) callback(); diff --git a/public/libs/maps.js b/public/libs/maps.js index b6f2d81..0458eb7 100644 --- a/public/libs/maps.js +++ b/public/libs/maps.js @@ -3296,8 +3296,8 @@ maps.prototype.setBlock = function (number, x, y, floorId, noredraw) { x, y, floorId, - originBlock?.id ?? 0, - number + number, + originBlock?.id ?? 0 ); }; diff --git a/src/core/audio/audio.ts b/src/core/audio/audio.ts index 35eca82..603d43f 100644 --- a/src/core/audio/audio.ts +++ b/src/core/audio/audio.ts @@ -1,4 +1,4 @@ -import { EmitableEvent, EventEmitter } from '../common/eventEmitter'; +import { EventEmitter } from '../common/eventEmitter'; const ac = new AudioContext(); @@ -7,7 +7,7 @@ interface BaseNode { channel?: number; } -interface AudioPlayerEvent extends EmitableEvent { +interface AudioPlayerEvent { play: (node: AudioBufferSourceNode) => void; update: (audio: AudioBuffer) => void; end: (node: AudioBufferSourceNode) => void; diff --git a/src/core/common/disposable.ts b/src/core/common/disposable.ts index 45dffcd..eaf91c4 100644 --- a/src/core/common/disposable.ts +++ b/src/core/common/disposable.ts @@ -1,6 +1,6 @@ -import { EmitableEvent, EventEmitter } from './eventEmitter'; +import { EventEmitter } from './eventEmitter'; -interface DisposableEvent extends EmitableEvent { +interface DisposableEvent { active: (value: T) => void; dispose: (value: T) => void; destroy: () => void; diff --git a/src/core/common/eventEmitter.ts b/src/core/common/eventEmitter.ts index 0c46260..e6a8d80 100644 --- a/src/core/common/eventEmitter.ts +++ b/src/core/common/eventEmitter.ts @@ -2,9 +2,7 @@ function has(value: T): value is NonNullable { return value !== null && value !== undefined; } -export interface EmitableEvent { - [event: string]: (...params: any) => any; -} +export type Callable = (...params: any) => any; export interface Listener any> { fn: T; @@ -22,14 +20,16 @@ type EmitFn any> = ( ...params: Parameters ) => any; -export class EventEmitter { +type Key = number | string | symbol; + +export class EventEmitter> { protected events: { - [P in keyof T]?: Listener[]; + [x: Key]: Listener[]; } = {}; - private emitted: (keyof T)[] = []; + private emitted: Set = new Set(); protected emitter: { - [P in keyof T]?: EmitFn; + [x: Key]: EmitFn | undefined; } = {}; /** @@ -42,8 +42,10 @@ export class EventEmitter { event: K, fn: T[K], options?: Partial - ) { - if (options?.immediate && this.emitted.includes(event)) { + ): void; + on(event: string, fn: Callable, options?: Partial): void; + on(event: string, fn: Callable, options?: Partial): void { + if (options?.immediate && this.emitted.has(event)) { fn(); if (!options.once) { this.events[event] ??= []; @@ -65,7 +67,9 @@ export class EventEmitter { * @param event 要取消监听的事件类型 * @param fn 要取消监听的函数 */ - off(event: K, fn: T[K]) { + off(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); if (index === -1 || index === void 0) return; this.events[event]?.splice(index, 1); @@ -76,7 +80,9 @@ export class EventEmitter { * @param event 要监听的事件 * @param fn 监听函数 */ - once(event: K, fn: T[K]) { + once(event: K, fn: T[K]): void; + once(event: string, fn: Callable): void; + once(event: string, fn: Callable): void { this.on(event, fn, { once: true }); } @@ -89,18 +95,18 @@ export class EventEmitter { event: K, ...params: Parameters ): ReturnType[]; + emit(event: string, ...params: any[]): any[]; emit(event: K, ...params: Parameters): R; - emit(event: K, ...params: Parameters): any { - if (!this.emitted.includes(event)) { - this.emitted.push(event); - } + emit(event: string, ...params: any[]): R; + emit(event: string, ...params: any[]): any[] { + this.emitted.add(event); const events = (this.events[event] ??= []); if (!!this.emitter[event]) { const returns = this.emitter[event]!(events, ...params); this.events[event] = events.filter(v => !v.once); return returns; } else { - const returns: ReturnType[] = []; + const returns: ReturnType[] = []; for (let i = 0; i < events.length; i++) { const e = events[i]; returns.push(e.fn(...(params as any))); @@ -115,7 +121,10 @@ export class EventEmitter { * @param event 要设置的事件 * @param fn 事件执行器,留空以清除触发器 */ - setEmitter(event: K, fn?: EmitFn) { + // @ts-ignore + setEmitter(event: K, fn?: EmitFn): void; + setEmitter(event: string, fn?: EmitFn): void; + setEmitter(event: string, fn?: EmitFn): void { this.emitter[event] = fn; } @@ -128,7 +137,12 @@ export class EventEmitter { * @param event 要取消监听的事件 */ 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] = []; else this.events = {}; } @@ -137,7 +151,7 @@ export class EventEmitter { type IndexedSymbol = number | string | symbol; export class IndexedEventEmitter< - T extends EmitableEvent + T extends Record > extends EventEmitter { private fnMap: { [P in keyof T]?: Map; diff --git a/src/core/common/logger.ts b/src/core/common/logger.ts index f639ba0..9a89ba9 100644 --- a/src/core/common/logger.ts +++ b/src/core/common/logger.ts @@ -1,5 +1,7 @@ import { debounce } from 'lodash-es'; +// todo: 使用格式化输出? + export const enum LogLevel { /** 输出所有,包括日志 */ LOG, @@ -84,7 +86,7 @@ export class Logger { logTip.textContent = `Error thrown, please check in console.`; hideTipText(); } - throw `[ERROR Code ${code}] ${text}`; + console.error(`[ERROR Code ${code}] ${text}`); } } diff --git a/src/core/common/resource.ts b/src/core/common/resource.ts index 9f22b37..3275a6e 100644 --- a/src/core/common/resource.ts +++ b/src/core/common/resource.ts @@ -2,7 +2,7 @@ import axios, { AxiosRequestConfig, ResponseType } from 'axios'; import { Disposable } from './disposable'; import { logger } from './logger'; import JSZip from 'jszip'; -import { EmitableEvent, EventEmitter } from './eventEmitter'; +import { EventEmitter } from './eventEmitter'; type ProgressFn = (now: number, total: number) => void; @@ -305,7 +305,7 @@ export const resourceTypeMap = { zip: ZipResource }; -interface LoadEvent extends EmitableEvent { +interface LoadEvent { progress: ( type: keyof ResourceType, uri: string, diff --git a/src/core/fx/canvas2d.ts b/src/core/fx/canvas2d.ts index 82dd56b..33629f5 100644 --- a/src/core/fx/canvas2d.ts +++ b/src/core/fx/canvas2d.ts @@ -1,8 +1,8 @@ import { parseCss } from '@/plugin/utils'; -import { EmitableEvent, EventEmitter } from '../common/eventEmitter'; +import { EventEmitter } from '../common/eventEmitter'; import { CSSObj } from '../interface'; -interface OffscreenCanvasEvent extends EmitableEvent { +interface OffscreenCanvasEvent { /** 当被动触发resize时(例如core.domStyle.scale变化、窗口大小变化)时触发,使用size函数并不会触发 */ resize: () => void; } diff --git a/src/core/fx/shader.ts b/src/core/fx/shader.ts index 2d68a7f..31dce52 100644 --- a/src/core/fx/shader.ts +++ b/src/core/fx/shader.ts @@ -1,8 +1,8 @@ import { Animation, Ticker, hyper } from 'mutate-animate'; -import { EmitableEvent, EventEmitter } from '../common/eventEmitter'; +import { EventEmitter } from '../common/eventEmitter'; import { ensureArray } from '@/plugin/utils'; -interface ShaderEvent extends EmitableEvent {} +interface ShaderEvent {} const isWebGLSupported = (() => { return !!document.createElement('canvas').getContext('webgl'); diff --git a/src/core/loader/controller.ts b/src/core/loader/controller.ts index d85baf1..8250a5c 100644 --- a/src/core/loader/controller.ts +++ b/src/core/loader/controller.ts @@ -1,6 +1,6 @@ -import { EmitableEvent, EventEmitter } from '../common/eventEmitter'; +import { EventEmitter } from '../common/eventEmitter'; -interface ResourceControllerEvent extends EmitableEvent { +interface ResourceControllerEvent { add: (uri: string, data: D) => void; delete: (uri: string, content: T) => void; } @@ -25,6 +25,6 @@ export abstract class ResourceController< remove(uri: string) { const content = this.list[uri]; delete this.list[uri]; - this.emit(uri, content); + // this.emit(uri, content); } } diff --git a/src/core/main/custom/danmaku.ts b/src/core/main/custom/danmaku.ts index bc1e703..a53dd12 100644 --- a/src/core/main/custom/danmaku.ts +++ b/src/core/main/custom/danmaku.ts @@ -1,5 +1,5 @@ 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 { ResponseBase } from '@/core/interface'; import { @@ -52,7 +52,7 @@ interface PostLikeResponse extends ResponseBase { liked: boolean; } -interface DanmakuEvent extends EmitableEvent { +interface DanmakuEvent { showStart: (danmaku: Danmaku) => void; showEnd: (danmaku: Danmaku) => void; like: (liked: boolean, danmaku: Danmaku) => void; diff --git a/src/core/main/custom/hotkey.ts b/src/core/main/custom/hotkey.ts index 2d1f26e..014351d 100644 --- a/src/core/main/custom/hotkey.ts +++ b/src/core/main/custom/hotkey.ts @@ -1,10 +1,10 @@ import { KeyCode } from '@/plugin/keyCodes'; import { deleteWith, generateBinary, spliceBy } from '@/plugin/utils'; -import { EmitableEvent, EventEmitter } from '../../common/eventEmitter'; +import { EventEmitter } from '../../common/eventEmitter'; // todo: 按下时触发,长按(单次/连续)触发,按下连续触发,按下节流触发,按下加速节流触发 -interface HotkeyEvent extends EmitableEvent { +interface HotkeyEvent { set: (id: string, key: KeyCode, assist: number) => void; emit: (key: KeyCode, assist: number, type: KeyEmitType) => void; } diff --git a/src/core/main/custom/keyboard.ts b/src/core/main/custom/keyboard.ts index fe90b6a..a7cdb42 100644 --- a/src/core/main/custom/keyboard.ts +++ b/src/core/main/custom/keyboard.ts @@ -1,8 +1,4 @@ -import { - EmitableEvent, - EventEmitter, - Listener -} from '@/core/common/eventEmitter'; +import { EventEmitter, Listener } from '@/core/common/eventEmitter'; import { KeyCode } from '@/plugin/keyCodes'; import { gameKey } from '../init/hotkey'; import { unwarpBinary } from './hotkey'; @@ -36,7 +32,7 @@ type VirtualKeyEmitFn = ( ev: VirtualKeyEmit ) => void; -interface VirtualKeyboardEvent extends EmitableEvent { +interface VirtualKeyboardEvent { add: (item: KeyboardItem) => void; remove: (item: KeyboardItem) => void; extend: (extended: Keyboard) => void; diff --git a/src/core/main/custom/toolbar.ts b/src/core/main/custom/toolbar.ts index 14ac0e9..e23bce5 100644 --- a/src/core/main/custom/toolbar.ts +++ b/src/core/main/custom/toolbar.ts @@ -1,4 +1,4 @@ -import { EmitableEvent, EventEmitter } from '@/core/common/eventEmitter'; +import { EventEmitter } from '@/core/common/eventEmitter'; import { deleteWith, has } from '@/plugin/utils'; import { Component, nextTick, reactive, shallowReactive } from 'vue'; import { fixedUi } from '../init/ui'; @@ -13,7 +13,7 @@ import type { } from '../init/toolbar'; import { isMobile } from '@/plugin/use'; -interface CustomToolbarEvent extends EmitableEvent { +interface CustomToolbarEvent { add: (item: ValueOf) => void; delete: (item: ValueOf) => void; set: (id: string, data: Partial) => void; diff --git a/src/core/main/custom/ui.ts b/src/core/main/custom/ui.ts index c7e7101..bacfbde 100644 --- a/src/core/main/custom/ui.ts +++ b/src/core/main/custom/ui.ts @@ -1,10 +1,10 @@ import { Component, shallowReactive } from 'vue'; -import { EmitableEvent, EventEmitter } from '../../common/eventEmitter'; +import { Callable, EventEmitter } from '../../common/eventEmitter'; import { KeyCode } from '@/plugin/keyCodes'; import { Hotkey } from './hotkey'; import { generateBinary } from '@/plugin/utils'; -interface FocusEvent extends EmitableEvent { +interface FocusEvent { focus: (before: T | null, after: T) => void; unfocus: (before: T | null) => void; add: (item: T) => void; @@ -109,7 +109,7 @@ export class Focus extends EventEmitter> { } } -interface GameUiEvent extends EmitableEvent { +interface GameUiEvent { close: () => void; open: () => void; } diff --git a/src/core/main/setting.ts b/src/core/main/setting.ts index 748b327..656b45c 100644 --- a/src/core/main/setting.ts +++ b/src/core/main/setting.ts @@ -1,5 +1,5 @@ import { FunctionalComponent, reactive } from 'vue'; -import { EmitableEvent, EventEmitter } from '../common/eventEmitter'; +import { EventEmitter } from '../common/eventEmitter'; import { GameStorage } from './storage'; import { has, triggerFullscreen } from '@/plugin/utils'; import { createSettingComponents } from './init/settings'; @@ -31,7 +31,7 @@ export interface MotaSettingItem { display?: (value: T) => string; } -interface SettingEvent extends EmitableEvent { +interface SettingEvent { valueChange: ( key: string, newValue: T, @@ -230,7 +230,7 @@ export class MotaSetting extends EventEmitter { } } -interface SettingDisplayerEvent extends EmitableEvent { +interface SettingDisplayerEvent { update: (stack: string[], display: SettingDisplayInfo[]) => void; } diff --git a/src/core/render/cache.ts b/src/core/render/cache.ts index 4b31f1d..b6807ad 100644 --- a/src/core/render/cache.ts +++ b/src/core/render/cache.ts @@ -1,4 +1,4 @@ -import { EmitableEvent, EventEmitter } from '../common/eventEmitter'; +import { EventEmitter } from '../common/eventEmitter'; import { logger } from '../common/logger'; import { MotaOffscreenCanvas2D } from '../fx/canvas2d'; import { SizedCanvasImageSource } from './preset/misc'; @@ -62,7 +62,7 @@ export interface AutotileRenderable extends RenderableDataBase { bigImage: false; } -interface TextureCacheEvent extends EmitableEvent {} +interface TextureCacheEvent {} class TextureCache extends EventEmitter { tileset!: Record; @@ -499,10 +499,10 @@ function splitAutotiles(map: IdToNumber): AutotileCaches { const ctx = canvas.ctx; for (let i = 0; i < frame; i++) { const dx = 32 * i; - const sx1 = info[0][0]; - const sx2 = info[1][0]; - const sx3 = info[2][0]; - const sx4 = info[3][0]; + 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]; diff --git a/src/core/render/camera.ts b/src/core/render/camera.ts index bf91adc..7eb8414 100644 --- a/src/core/render/camera.ts +++ b/src/core/render/camera.ts @@ -1,7 +1,7 @@ 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 { mat: mat3 = mat3.create(); diff --git a/src/core/render/container.ts b/src/core/render/container.ts index ee34400..bfb588d 100644 --- a/src/core/render/container.ts +++ b/src/core/render/container.ts @@ -2,12 +2,16 @@ import { MotaOffscreenCanvas2D } from '../fx/canvas2d'; import { Camera } from './camera'; import { ICanvasCachedRenderItem, + IRenderChildable, RenderItem, RenderItemPosition, withCacheRender } from './item'; -export class Container extends RenderItem implements ICanvasCachedRenderItem { +export class Container + extends RenderItem + implements ICanvasCachedRenderItem, IRenderChildable +{ children: RenderItem[] = []; sortedChildren: RenderItem[] = []; @@ -27,17 +31,20 @@ export class Container extends RenderItem implements ICanvasCachedRenderItem { ): void { this.emit('beforeRender'); if (this.needUpdate) { - this.cache(this.writing); + this.cache(this.using); this.needUpdate = false; } withCacheRender(this, canvas, ctx, camera, c => { this.sortedChildren.forEach(v => { + if (v.hidden) return; + ctx.save(); if (!v.antiAliasing) { ctx.imageSmoothingEnabled = false; } else { ctx.imageSmoothingEnabled = true; } v.render(c.canvas, c.ctx, camera); + ctx.restore(); }); }); this.writing = void 0; @@ -61,10 +68,21 @@ export class Container extends RenderItem implements ICanvasCachedRenderItem { * 添加子元素到这个容器上,然后在下一个tick执行更新 * @param children 要添加的子元素 */ - appendChild(children: RenderItem[]) { + appendChild(...children: RenderItem[]) { children.forEach(v => (v.parent = this)); this.children.push(...children); 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() { @@ -84,4 +102,11 @@ export class Container extends RenderItem implements ICanvasCachedRenderItem { this.canvas.setAntiAliasing(anti); this.update(this); } + + destroy(): void { + super.destroy(); + this.children.forEach(v => { + v.destroy(); + }); + } } diff --git a/src/core/render/hero.ts b/src/core/render/hero.ts index f4145e8..7b3ca75 100644 --- a/src/core/render/hero.ts +++ b/src/core/render/hero.ts @@ -1,6 +1,6 @@ import { Ticker } from 'mutate-animate'; import { MotaCanvas2D } from '../fx/canvas2d'; -import { EmitableEvent, EventEmitter } from '../common/eventEmitter'; +import { EventEmitter } from '../common/eventEmitter'; import { debounce } from 'lodash-es'; // 重写样板的勇士绘制 @@ -36,7 +36,7 @@ interface HeroDrawing extends HeroDrawItem { dir: Dir; } -interface HeroRendererEvent extends EmitableEvent { +interface HeroRendererEvent { beforeDraw: () => void; afterDraw: () => void; } diff --git a/src/core/render/item.ts b/src/core/render/item.ts index a882d68..5e354f6 100644 --- a/src/core/render/item.ts +++ b/src/core/render/item.ts @@ -1,9 +1,8 @@ import { isNil } from 'lodash-es'; -import { EmitableEvent, EventEmitter } from '../common/eventEmitter'; +import { EventEmitter } from '../common/eventEmitter'; import { MotaOffscreenCanvas2D } from '../fx/canvas2d'; import { Camera } from './camera'; import { Ticker } from 'mutate-animate'; -import type { Container } from './container'; export type RenderFunction = ( canvas: MotaOffscreenCanvas2D, @@ -83,14 +82,28 @@ interface IRenderConfig { 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; afterUpdate: (item?: RenderItem) => void; beforeRender: () => void; @@ -127,8 +140,10 @@ export abstract class RenderItem highResolution: boolean = true; /** 是否抗锯齿 */ antiAliasing: boolean = true; + /** 是否被隐藏 */ + hidden: boolean = false; - parent?: RenderItem; + parent?: RenderItem & IRenderChildable; protected needUpdate: boolean = false; @@ -209,10 +224,49 @@ export abstract class RenderItem setZIndex(zIndex: number) { 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(); + Mota.require('var', 'hook').once('reset', () => { let lastTime = 0; RenderItem.ticker.add(time => { @@ -220,13 +274,14 @@ Mota.require('var', 'hook').once('reset', () => { if (time - lastTime > core.values.animateSpeed) { RenderItem.animatedFrame++; lastTime = time; + renderEmits.emit('animateFrame', RenderItem.animatedFrame, time); } }); }); export function withCacheRender( item: RenderItem & ICanvasCachedRenderItem, - canvas: HTMLCanvasElement, + _canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D, camera: Camera, fn: RenderFunction @@ -264,3 +319,24 @@ export function withCacheRender( 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); +} diff --git a/src/core/render/preset/block.ts b/src/core/render/preset/block.ts new file mode 100644 index 0000000..f17f203 --- /dev/null +++ b/src/core/render/preset/block.ts @@ -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 extends EventEmitter { + /** 区域宽度 */ + 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 = 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 清除哪些深度的缓存,至多31位二进制数,例如填0b111就是清除前三层的索引 + */ + 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(); + 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); + } + } + } +} diff --git a/src/core/render/preset/layer.ts b/src/core/render/preset/layer.ts index f76ffaf..e6e14c6 100644 --- a/src/core/render/preset/layer.ts +++ b/src/core/render/preset/layer.ts @@ -3,10 +3,453 @@ import { Container } from '../container'; import { Sprite } from '../sprite'; import { Camera } from '../camera'; import { TimingFn } from 'mutate-animate'; -import { IRenderDestroyable, RenderItem } from '../item'; +import { RenderItem, renderEmits, transformCanvas } from '../item'; import { logger } from '@/core/common/logger'; import { AutotileRenderable, RenderableData, texture } from '../cache'; import { glMatrix } from 'gl-matrix'; +import { BlockCacher } from './block'; +import { isNil } from 'lodash-es'; +import { getDamageColor } from '@/plugin/utils'; + +type FloorLayer = 'bg' | 'bg2' | 'event' | 'fg' | 'fg2'; +type LayerGroupPreset = 'defaults' | 'noDamage'; + +const layers: FloorLayer[] = ['bg', 'bg2', 'event', 'fg', 'fg2']; +export class LayerGroup extends Container { + /** 地图组列表 */ + static list: Set = new Set(); + + cellSize: number = 32; + blockSize: number = core._WIDTH_; + + /** 当前楼层 */ + floorId?: FloorIds; + /** 是否绑定了当前层 */ + bindThisFloor: boolean = false; + /** 伤害显示层 */ + damage?: Damage; + /** 地图显示层 */ + layers: Set = new Set(); + + private needRender?: NeedRenderData; + + constructor(floor?: FloorIds) { + super(); + + this.setHD(true); + this.setAntiAliasing(false); + this.size(core._PX_, core._PY_); + + this.on('afterRender', () => { + this.releaseNeedRender(); + }); + + this.usePreset('defaults'); + LayerGroup.list.add(this); + this.bindFloor(floor); + } + + /** + * 设置渲染分块大小 + * @param size 分块大小 + */ + setBlockSize(size: number) { + this.blockSize = size; + this.layers.forEach(v => { + v.block.setBlockSize(size); + }); + this.damage?.block.setBlockSize(size); + } + + /** + * 设置每个图块的大小 + * @param size 每个图块的大小 + */ + setCellSize(size: number) { + this.cellSize = size; + } + + /** + * 使用预设显示模式,注意切换后所有的旧Layer实例会被摧毁 + * @param preset 预设名称 + */ + usePreset(preset: LayerGroupPreset) { + this.emptyLayer(); + + const child = layers.map((v, i) => { + const layer = new Layer(); + layer.bindLayer(v); + layer.setZIndex(i * 10); + this.layers.add(layer); + return layer; + }); + this.appendChild(...child); + if (preset === 'defaults') { + const damage = new Damage(this.floorId); + this.appendChild(damage); + this.damage = damage; + damage.setZIndex(60); + } + } + + /** + * 清空所有层 + */ + emptyLayer() { + this.removeChild(...this.layers); + if (this.damage) this.removeChild(this.damage); + this.damage?.destroy(); + this.layers.forEach(v => v.destroy()); + this.layers.clear(); + this.damage = void 0; + } + + /** + * 添加显示层 + * @param layer 显示层 + */ + addLayer(layer: FloorLayer) { + const l = new Layer(); + l.bindLayer(layer); + this.layers.add(l); + this.appendChild(l); + return l; + } + + /** + * 移除指定层 + * @param layer 要移除的层,可以是Layer实例,也可以是字符串。如果是字符串,那么会移除所有符合的层 + */ + removeLayer(layer: FloorLayer | Layer) { + if (typeof layer === 'string') { + const toRemove: Layer[] = []; + this.layers.forEach(v => { + if (v.layer === layer) { + toRemove.push(v); + } + }); + toRemove.forEach(v => { + v.destroy(); + this.layers.delete(v); + }); + this.removeChild(...toRemove); + } else { + if (this.layers.delete(layer)) { + layer.destroy(); + this.removeChild(layer); + } + } + } + + /** + * 获取一个地图层实例,例如获取背景层等 + * @param layer 地图层 + */ + getLayer(layer: FloorLayer) { + return [...this.layers].filter(v => v.layer === layer); + } + + /** + * 隐藏某个层 + * @param layer 要隐藏的层 + */ + hideLayer(layer: FloorLayer) { + this.layers.forEach(v => { + if (v.layer === layer) v.hide(); + }); + } + + /** + * 显示某个层 + * @param layer 要显示的层 + */ + showLayer(layer: FloorLayer) { + this.layers.forEach(v => { + if (v.layer === layer) v.show(); + }); + } + + /** + * 绑定当前楼层 + */ + bindThis() { + this.bindThisFloor = true; + this.updateFloor(); + } + + /** + * 绑定楼层信息 + * @param floor 绑定楼层,不填时表示绑定为当前楼层 + */ + bindFloor(floor?: FloorIds) { + if (!floor) { + this.bindThisFloor = true; + } else { + this.floorId = floor; + } + this.updateFloor(); + } + + /** + * 更新地图信息 + */ + updateFloor() { + if (this.bindThisFloor) { + this.floorId = core.status.floorId; + } + const floor = this.floorId; + if (!floor) return; + this.layers.forEach(v => { + v.bindData(floor); + if (v.layer === 'bg') { + v.bindBackground(floor); + } + }); + // this.damage?.bindFloor(floor); + } + + /** + * 缓存计算应该渲染的块 + * @param camera 摄像机 + * @param blockData 分块信息 + */ + cacheNeedRender(camera: Camera, block: BlockCacher) { + return ( + this.needRender ?? + (this.needRender = calNeedRenderOf(camera, this.cellSize, block)) + ); + } + + /** + * 释放应该渲染块缓存 + */ + releaseNeedRender() { + this.needRender = void 0; + } + + /** + * 添加伤害显示层,并将显示层返回,如果已经添加,则会返回已经添加的显示层 + */ + addDamage() { + if (!this.damage) { + const damage = new Damage(); + this.appendChild(damage); + this.damage = damage; + if (this.floorId) damage.bindFloor(this.floorId); + return damage; + } + return this.damage; + } + + /** + * 移除伤害显示层 + */ + removeDamage() { + if (this.damage) { + this.removeChild(this.damage); + this.damage = void 0; + } + } + + /** + * 更新指定区域内的伤害渲染信息 + */ + updateDamage(x: number, y: number, width: number, height: number) { + this.damage?.updateRenderable(x, y, width, height); + } + + /** + * 更新动画帧 + */ + updateFrameAnimate() { + this.layers.forEach(v => { + v.cache(v.using); + }); + this.damage?.cache(this.damage.using); + this.update(this); + } + + destroy(): void { + super.destroy(); + LayerGroup.list.delete(this); + } +} + +const hook = Mota.require('var', 'hook'); + +hook.on('changingFloor', floorId => { + LayerGroup.list.forEach(v => { + if (v.floorId === floorId || v.bindThisFloor) v.updateFloor(); + }); +}); +hook.on('setBlock', (x, y, floorId, block) => { + LayerGroup.list.forEach(v => { + if (v.floorId === floorId) { + v.updateDamage(x, y, 1, 1); + v.layers.forEach(v => { + if (v.layer === 'event') { + v.putRenderData([block], 1, x, y); + } + }); + } + }); +}); +hook.on('statusBarUpdate', () => { + LayerGroup.list.forEach(v => { + if (v.floorId) v.damage?.bindFloor(v.floorId); + }); +}); + +renderEmits.on('animateFrame', () => { + LayerGroup.list.forEach(v => { + v.updateFrameAnimate(); + }); +}); + +function calNeedRenderOf( + camera: Camera, + cell: number, + block: BlockCacher +): NeedRenderData { + const w = (core._WIDTH_ * cell) / 2; + const h = (core._HEIGHT_ * cell) / 2; + const size = block.blockSize; + + // -1是因为宽度是core._PX_,从0开始的话,末尾索引就是core._PX_ - 1 + const [x1, y1] = Camera.untransformed(camera, -w, -h); + const [x2, y2] = Camera.untransformed(camera, w - 1, -h); + const [x3, y3] = Camera.untransformed(camera, w - 1, h - 1); + const [x4, y4] = Camera.untransformed(camera, -w, h - 1); + + const res: Set = new Set(); + /** 一个纵坐标对应的所有横坐标,用于填充 */ + const xyMap: Map = new Map(); + + const pushXY = (x: number, y: number) => { + let arr = xyMap.get(y); + if (!arr) { + arr = []; + xyMap.set(y, arr); + } + arr.push(x); + return arr; + }; + + [ + [x1, y1, x2, y2], + [x2, y2, x3, y3], + [x3, y3, x4, y4], + [x4, y4, x1, y1] + ].forEach(([fx0, fy0, tx0, ty0]) => { + const fx = Math.floor(fx0 / cell); + const fy = Math.floor(fy0 / cell); + const tx = Math.floor(tx0 / cell); + const ty = Math.floor(ty0 / cell); + const dx = tx - fx; + const dy = ty - fy; + const k = dy / dx; + + // 斜率无限的时候,竖直 + if (!isFinite(k)) { + const min = k < 0 ? ty : fy; + const max = k < 0 ? fy : ty; + const [x, y] = block.getBlockXY(fx, min); + const [, ey] = block.getBlockXY(fx, max); + for (let i = y; i <= ey; i++) { + pushXY(x, i); + } + + return; + } + + const [fbx, fby] = block.getBlockXY(fx, fy); + + // 当斜率为0时 + if (glMatrix.equals(k, 0)) { + const [ex] = block.getBlockXY(tx, fy); + pushXY(fby, fbx).push(ex); + return; + } + + // 否则使用 Bresenham 直线算法 + if (Math.abs(k) >= 1) { + // 斜率大于一,y方向递增 + const d = Math.sign(dy) * size; + const f = fby * size; + const dir = dy > 0; + + const ex = Math.floor(tx / size); + const ey = Math.floor(ty / size); + pushXY(ex, ey); + + let now = f; + let last = fbx; + let ny = fby; + do { + const bx = Math.floor((fx + (now - fy) / k) / size); + pushXY(bx, ny); + if (bx !== last) { + if (dir) pushXY(bx, ny - Math.sign(dy)); + else pushXY(last, ny); + } + + last = bx; + ny += Math.sign(dy); + now += d; + } while (dir ? ny <= ey : ny >= ey); + } else { + // 斜率小于一,x方向递增 + const d = Math.sign(dx) * size; + const f = fbx * size; + const dir = dx > 0; + + const ex = Math.floor(tx / size); + const ey = Math.floor(ty / size); + pushXY(ex, ey); + + let now = f; + let last = fby; + let nx = fbx; + do { + const by = Math.floor((fy + k * (now - fx)) / size); + if (by !== last) { + if (dir) pushXY(nx - Math.sign(dx), by); + else pushXY(nx, last); + } + pushXY(nx, by); + last = by; + nx += Math.sign(dx); + now += d; + } while (dir ? nx <= ex : nx >= ex); + } + }); + + // 然后进行填充 + const { width: bw, height: bh } = block.blockData; + const back: [number, number][] = []; + xyMap.forEach((x, y) => { + if (x.length === 1) { + const index = y * bw + x[0]; + + if (!back.some(v => v[0] === x[0] && v[1] === y)) + back.push([x[0], y]); + if (index < 0 || index >= bw * bh) return; + res.add(index); + } + const max = Math.max(...x); + const min = Math.min(...x); + + for (let i = min; i <= max; i++) { + const index = y * bw + i; + + if (!back.some(v => v[0] === i && v[1] === y)) back.push([i, y]); + if (index < 0 || index >= bw * bh) continue; + res.add(index); + } + }); + + return { res, back }; +} interface LayerCacheItem { floorId?: FloorIds; @@ -26,17 +469,6 @@ interface NeedRenderData { back: [x: number, y: number][]; } -interface LayerBlockData { - /** 横向宽度,包括rest的那一个块 */ - width: number; - /** 纵向宽度,包括rest的那一个块 */ - height: number; - /** 横向最后一个块的宽度 */ - restWidth: number; - /** 纵向最后一个块的高度 */ - restHeight: number; -} - interface MovingStepLinearSwap { /** 线性差值移动(也就是平移)或者是瞬移 */ type: 'linear' | 'swap'; @@ -74,12 +506,7 @@ interface MovingBlock { nowZ: number; } -type FloorLayer = 'bg' | 'bg2' | 'event' | 'fg' | 'fg2'; - -export class Layer extends Container implements IRenderDestroyable { - /** 地图渲染对象映射,用于当地图更新时定向更新渲染信息 */ - static LayerMap: Map> = new Map(); - +export class Layer extends Container { // 一些会用到的常量 static readonly FRAME_0 = 1; static readonly FRAME_1 = 2; @@ -87,12 +514,6 @@ export class Layer extends Container implements IRenderDestroyable { static readonly FRAME_3 = 8; static readonly FRAME_ALL = 15; - /** - * 每个渲染块的缓存,索引的理应计算公式为 (x + y * width) * 4 + frame\ - * 其中x和y表示块的位置,width表示横向有多少个块,frame表示第几帧,移动层没办法缓存,所以没有缓存 - */ - blockCache: Map = new Map(); - /** 静态层,包含除大怪物及正在移动的内容外的内容 */ protected staticMap: MotaOffscreenCanvas2D = new MotaOffscreenCanvas2D(); /** 移动层,包含大怪物及正在移动的内容 */ @@ -103,8 +524,6 @@ export class Layer extends Container implements IRenderDestroyable { /** 最终渲染至的Sprite */ main: Sprite = new Sprite(); - /** 与当前层绑定,当前层改变时渲染的楼层会一同改变 */ - bindThisFloor: boolean = false; /** 渲染的楼层 */ floorId?: FloorIds; /** 渲染的层 */ @@ -125,14 +544,8 @@ export class Layer extends Container implements IRenderDestroyable { /** 背景图块画布 */ backImage: HTMLCanvasElement[] = []; - /** 分块信息。每个块的默认大小为画面宽度,这样的话有可能会包含三个大小不足的剩余块,这三个块单独处理即可 */ - blockData: LayerBlockData = { - width: 0, - height: 0, - restHeight: 0, - restWidth: 0 - }; - blockSize: number = core._WIDTH_; + /** 分块信息 */ + block: BlockCacher = new BlockCacher(0, 0, core._WIDTH_, 4); /** 正在移动的图块 */ moving: MovingBlock[] = []; @@ -166,7 +579,7 @@ export class Layer extends Container implements IRenderDestroyable { this.main.setHD(false); this.main.size(core._PX_, core._PY_); - this.appendChild([this.main]); + this.appendChild(this.main); this.main.setRenderFn((canvas, camera) => { const { ctx } = canvas; const { width, height } = canvas.canvas; @@ -191,6 +604,17 @@ export class Layer extends Container implements IRenderDestroyable { this.generateBackground(); } + /** + * 将当前地图的背景图块绑定为一个地图的背景图块 + * @param floorId 楼层id + */ + bindBackground(floorId: FloorIds) { + const { defaultGround } = core.status.maps[floorId]; + if (defaultGround) { + this.setBackground(texture.idNumberMap[defaultGround]); + } + } + /** * 生成背景图块 */ @@ -270,9 +694,8 @@ export class Layer extends Container implements IRenderDestroyable { } } if (calAutotile) this.calAutotiles(x, y, width, height); - // this.updateBigImages(x, y, width, height); - // this.updateRenderableData(x, y, width, height); this.updateBlocks(x, y, width, height); + this.updateBigImages(x, y, width, height); this.update(this); } @@ -404,35 +827,23 @@ export class Layer extends Container implements IRenderDestroyable { * @param floor 楼层id * @param layer 渲染的层数,例如是背景层还是事件层等 */ - bindData(floor: FloorIds, layer: FloorLayer) { - const before = this.floorId; + bindData(floor: FloorIds, layer?: FloorLayer) { this.floorId = floor; - this.layer = layer; - if (before) { - const floor = Layer.LayerMap.get(before); - floor?.delete(this); - } - const now = Layer.LayerMap.get(floor); - if (!now) { - Layer.LayerMap.set(floor, new Set([this])); - } else { - now.add(this); - } + if (layer) this.layer = layer; const f = core.status.maps[floor]; this.mapWidth = f.width; this.mapHeight = f.height; - this.splitBlock(); + this.block.size(f.width, f.height); this.updateDataFromFloor(); } /** - * 将这个渲染内容绑定为当前所在楼层,当前楼层改变时,渲染内容会一并改变 + * 绑定显示层 + * @param layer 绑定的层 */ - bindThis(layer: FloorLayer, noUpdate: boolean = false) { - this.bindThisFloor = true; + bindLayer(layer: FloorLayer) { this.layer = layer; - - if (!noUpdate) this.bindData(core.status.floorId, layer); + this.updateDataFromFloor(); } /** @@ -450,18 +861,8 @@ export class Layer extends Container implements IRenderDestroyable { } } - /** - * 切分当前地图为多个块 - */ - splitBlock() { - const size = this.blockSize; - - this.blockData = { - width: Math.ceil(this.mapWidth / size), - height: Math.ceil(this.mapHeight / size), - restWidth: this.mapWidth % size, - restHeight: this.mapHeight % size - }; + updateFloor(): void { + this.updateDataFromFloor(); } /** @@ -474,15 +875,7 @@ export class Layer extends Container implements IRenderDestroyable { this.mapHeight = height; this.renderData = Array(width * height).fill(0); this.autotiles = {}; - this.splitBlock(); - } - - /** - * 设置渲染块的大小 - */ - setBlockSize(size: number) { - this.blockSize = size; - this.splitBlock(); + this.block.size(width, height); } /** @@ -493,236 +886,21 @@ export class Layer extends Container implements IRenderDestroyable { * @param height 高度 */ updateBlocks(x: number, y: number, width: number, height: number) { - const start = this.getBlockIndex(x - 1, y - 1); - const end = this.getBlockIndex(x + width + 1, y - 1); - const blockHeight = - Math.ceil((y + 1 + height) / this.blockSize) - - Math.floor((y - 1) / this.blockSize); - const blockWidth = end - start; - for (let nx = 0; nx < blockWidth; nx++) { - for (let ny = 0; ny < blockHeight; ny++) { - this.clearBlockCache( - this.getBlockIndex(nx + start, ny + y), - Layer.FRAME_ALL - ); - } - } - + this.block.updateArea(x, y, width, height, Layer.FRAME_ALL); this.update(this); } - /** - * 根据图块位置获取渲染块的索引 - * @param x 图块的横坐标 - * @param y 图块的纵坐标 - */ - getBlockIndex(x: number, y: number) { - const bx = Math.floor(x / this.blockSize); - const by = Math.floor(y / this.blockSize); - return by * this.blockData.width + bx; - } - - /** - * 清空某个块的缓存 - * @param index 要清空的块的位置索引 - * @param frame 要清空块的第几帧,可以通过 `Layer.FRAME_0 | Layer.FRAME_1` 的方式一次清空多个帧, - * 不要填1234,否则结果不一定符合预期 - */ - clearBlockCache(index: number, frame: number) { - for (let i = 0; i < 4; i++) { - if (frame & (1 << i)) { - this.blockCache.delete(index * frame); - } - } - } - - /** - * 清空指定缓存索引的块缓存。与 {@link clearBlockCache} 不同之处在于这个是精确控制要清空的缓存, - * 对于需要大量精确清空的场景,此函数效率更高 - * @param index 指定的缓存索引 - */ - clearBlockByPreciseIndex(index: number) { - this.blockCache.delete(index); - } - - /** - * 清空所有块缓存 - */ - clearAllBlockCache() { - this.blockCache.clear(); - } - - /** - * 根据像素位置获取其所在的块 - * @param x 像素横坐标 - * @param y 像素纵坐标 - * @returns 像素位置所在块的索引,不在任何块中时会返回-1 - */ - getBlockByLoc(x: number, y: number) { - const size = this.blockSize; - const width = this.mapWidth * this.cellSize; - const height = this.mapHeight * this.cellSize; - if (x >= width || y >= height) return -1; - const bx = Math.floor(x / size); - const by = Math.floor(y / size); - return by * this.blockData.width + bx; - } - - /** - * 根据像素位置获取应当所在的块的横坐标与纵坐标,即使这个块不存在,例如可以返回 [-1, -2] - * @param x 像素横坐标 - * @param y 像素纵坐标 - */ - getBlockXYByLoc(x: number, y: number): LocArr { - return [Math.floor(x / this.blockSize), Math.floor(y / this.blockSize)]; - } - /** * 计算在传入的摄像机的视角下,应该渲染哪些内容 * @param camera 摄像机 */ calNeedRender(camera: Camera): NeedRenderData { - const cell = this.cellSize; - const w = (core._WIDTH_ * cell) / 2; - const h = (core._HEIGHT_ * cell) / 2; - const size = this.blockSize; - - // -1是因为宽度是core._PX_,从0开始的话,末尾索引就是core._PX_ - 1 - const [x1, y1] = Camera.untransformed(camera, -w, -h); - const [x2, y2] = Camera.untransformed(camera, w - 1, -h); - const [x3, y3] = Camera.untransformed(camera, w - 1, h - 1); - const [x4, y4] = Camera.untransformed(camera, -w, h - 1); - - const res: Set = new Set(); - /** 一个纵坐标对应的所有横坐标,用于填充 */ - const xyMap: Map = new Map(); - - const pushXY = (x: number, y: number) => { - let arr = xyMap.get(y); - if (!arr) { - arr = []; - xyMap.set(y, arr); - } - arr.push(x); - return arr; - }; - - [ - [x1, y1, x2, y2], - [x2, y2, x3, y3], - [x3, y3, x4, y4], - [x4, y4, x1, y1] - ].forEach(([fx0, fy0, tx0, ty0]) => { - const fx = Math.floor(fx0 / cell); - const fy = Math.floor(fy0 / cell); - const tx = Math.floor(tx0 / cell); - const ty = Math.floor(ty0 / cell); - const dx = tx - fx; - const dy = ty - fy; - const k = dy / dx; - - // 斜率无限的时候,竖直 - if (!isFinite(k)) { - const min = k < 0 ? ty : fy; - const max = k < 0 ? fy : ty; - const [x, y] = this.getBlockXYByLoc(fx, min); - - const [, ey] = this.getBlockXYByLoc(fx, max); - for (let i = y; i <= ey; i++) { - pushXY(x, i); - } - - return; - } - - const [fbx, fby] = this.getBlockXYByLoc(fx, fy); - - // 当斜率为0时 - if (glMatrix.equals(k, 0)) { - const [ex] = this.getBlockXYByLoc(tx, fy); - pushXY(fby, fbx).push(ex); - return; - } - - // 否则使用 Bresenham 直线算法 - if (Math.abs(k) >= 1) { - // 斜率大于一,y方向递增 - const d = Math.sign(dy) * size; - const f = fby * size; - const dir = dy > 0; - - const ex = Math.floor(tx / size); - const ey = Math.floor(ty / size); - pushXY(ex, ey); - - let now = f; - let last = fbx; - let ny = fby; - do { - const bx = Math.floor((fx + (now - fy) / k) / size); - pushXY(bx, ny); - if (bx !== last) { - if (dir) pushXY(bx, ny - Math.sign(dy)); - else pushXY(last, ny); - } - - last = bx; - ny += Math.sign(dy); - now += d; - } while (dir ? ny <= ey : ny >= ey); - } else { - // 斜率小于一,x方向递增 - const d = Math.sign(dx) * size; - const f = fbx * size; - const dir = dx > 0; - - const ex = Math.floor(tx / size); - const ey = Math.floor(ty / size); - pushXY(ex, ey); - - let now = f; - let last = fby; - let nx = fbx; - do { - const by = Math.floor((fy + k * (now - fx)) / size); - if (by !== last) { - if (dir) pushXY(nx - Math.sign(dx), by); - else pushXY(nx, last); - } - pushXY(nx, by); - last = by; - nx += Math.sign(dx); - now += d; - } while (dir ? nx <= ex : nx >= ex); - } - }); - - // 然后进行填充 - const { width: bw, height: bh } = this.blockData; - const back: [number, number][] = []; - xyMap.forEach((x, y) => { - if (x.length === 1) { - const index = y * bw + x[0]; - - if (!back.some(v => v[0] === x[0] && v[1] === y)) - back.push([x[0], y]); - if (index < 0 || index >= bw * bh) return; - res.add(index); - } - const max = Math.max(...x); - const min = Math.min(...x); - - for (let i = min; i <= max; i++) { - const index = y * bw + i; - - if (!back.some(v => v[0] === i && v[1] === y)) - back.push([i, y]); - if (index < 0 || index >= bw * bh) continue; - res.add(index); - } - }); - - return { res, back }; + if (this.parent instanceof LayerGroup) { + // 如果处于地图组中,每个地图的渲染区域应该是一样的,因此可以缓存优化 + return this.parent.cacheNeedRender(camera, this.block); + } else { + return calNeedRenderOf(camera, this.cellSize, this.block); + } } /** @@ -776,7 +954,7 @@ export class Layer extends Container implements IRenderDestroyable { protected renderBack(camera: Camera, need: NeedRenderData) { const cell = this.cellSize; const frame = (RenderItem.animatedFrame % 4) + 1; - const blockSize = this.blockSize; + const blockSize = this.block.blockSize; const { back } = need; const { ctx } = this.backMap; @@ -815,24 +993,14 @@ export class Layer extends Container implements IRenderDestroyable { protected renderStatic(camera: Camera, need: NeedRenderData) { const cell = this.cellSize; const frame = (RenderItem.animatedFrame % 4) + 1; - const { width } = this.blockData; - const blockSize = this.blockSize; + const { width } = this.block.blockData; + const blockSize = this.block.blockSize; const { ctx } = this.staticMap; ctx.save(); const { res: render } = need; - const mat = camera.mat; - const a = mat[0]; - const b = mat[1]; - const c = mat[3]; - const d = mat[4]; - const e = mat[6]; - const f = mat[7]; - - ctx.setTransform(1, 0, 0, 1, 0, 0); - ctx.translate(core._PX_ / 2, core._PY_ / 2); - ctx.transform(a, b, c, d, e, f); + transformCanvas(this.staticMap, camera, false); render.forEach(v => { const x = v % width; @@ -841,7 +1009,7 @@ export class Layer extends Container implements IRenderDestroyable { const sy = y * blockSize; const index = v * 4 + frame - 1; - const cache = this.blockCache.get(index); + const cache = this.block.cache.get(index); if (cache && cache.floorId === this.floorId) { ctx.drawImage( cache.canvas, @@ -897,7 +1065,7 @@ export class Layer extends Container implements IRenderDestroyable { blockSize * cell, blockSize * cell ); - this.blockCache.set(index, { + this.block.cache.set(index, { canvas: temp.canvas, floorId: this.floorId }); @@ -938,7 +1106,12 @@ export class Layer extends Container implements IRenderDestroyable { const py = y * cell - h + cell; const ex = px + w; const ey = py + h; - if (px ** 2 > r || py ** 2 > r || ex ** 2 > r || ey ** 2 > r) { + if ( + (px - e) ** 2 > r || + (py - f) ** 2 > r || + (ex - e) ** 2 > r || + (ey - f) ** 2 > r + ) { return; } ctx.drawImage(image, sx, sy, w, h, px, py, w, h); @@ -992,37 +1165,250 @@ export class Layer extends Container implements IRenderDestroyable { // todo return Promise.resolve(); } - - destroy(): void { - if (!this.floorId) return; - const floor = Layer.LayerMap.get(this.floorId); - if (!floor) return; - floor.delete(this); - } } -// 当地图发生变化时更新地图 -const hook = Mota.require('var', 'hook'); -hook.on('changingFloor', floorId => { - Layer.LayerMap.forEach(v => { - const needBind: Layer[] = []; - v.forEach(v => { - if (v.bindThisFloor) { - needBind.push(v); - } - }); - needBind.forEach(v => { - v.bindData(floorId, v.layer!); - }); - }); -}); -hook.on('setBlock', (x, y, floor, old, n) => { - const layers = Layer.LayerMap.get(floor); - if (layers) { - layers.forEach(v => { - if (v.layer === 'event') { - v.putRenderData([n], 1, x, y, true); - } +interface DamageRenderable { + x: number; + y: number; + align: CanvasTextAlign; + baseline: CanvasTextBaseline; + text: string; + color: CanvasStyle; +} + +export class Damage extends Sprite { + floorId?: FloorIds; + + mapWidth: number = 0; + mapHeight: number = 0; + + /** 键表示格子索引,值表示在这个格子上的渲染信息(当然实际渲染位置可以不在这个格子上) */ + renderable: Map = new Map(); + block: BlockCacher; + + cellSize: number = 32; + + /** 伤害渲染层,渲染至之后再渲染到目标层 */ + damageMap: MotaOffscreenCanvas2D = new MotaOffscreenCanvas2D(); + + constructor(floor?: FloorIds) { + super(); + + this.block = new BlockCacher(0, 0, core._WIDTH_, 1); + this.type = 'absolute'; + if (floor) this.bindFloor(floor); + this.size(core._PX_, core._PY_); + this.damageMap.withGameScale(true); + this.damageMap.setHD(true); + this.damageMap.setAntiAliasing(true); + this.damageMap.size(core._PX_, core._PY_); + + this.setRenderFn((canvas, camera) => { + const { ctx } = canvas; + const { width, height } = canvas.canvas; + ctx.save(); + ctx.imageSmoothingEnabled = false; + this.renderDamage(camera); + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.drawImage(this.damageMap.canvas, 0, 0, width, height); + ctx.restore(); }); } -}); + + updateFloor(floor: FloorIds): void { + core.extractBlocks(floor); + const f = core.status.maps[floor]; + this.updateRenderable(0, 0, f.width, f.height); + } + + /** + * 绑定显示楼层 + * @param floor 绑定的楼层 + */ + bindFloor(floor: FloorIds) { + this.floorId = floor; + core.extractBlocks(this.floorId); + const f = core.status.maps[this.floorId]; + this.mapWidth = f.width; + this.mapHeight = f.height; + this.block.size(f.width, f.height); + this.updateFloor(floor); + } + + /** + * 更新指定区域内的渲染信息,注意调用前需要保证怪物信息是最新的,也就是要在计算过怪物信息后才能调用这个 + */ + updateRenderable(x: number, y: number, width: number, height: number) { + if (!this.floorId) return; + this.block.updateArea(x, y, width, height, 1); + const sx = Math.max(0, x); + const sy = Math.max(0, y); + const ex = Math.min(this.mapWidth, x + width); + const ey = Math.min(this.mapHeight, y + height); + Mota.require('fn', 'ensureFloorDamage')(this.floorId); + const col = core.status.maps[this.floorId].enemy; + const obj = core.getMapBlocksObj(this.floorId); + + for (let nx = sx; nx < ex; nx++) { + for (let ny = sy; ny < ey; ny++) { + const index = this.block.getPreciseIndexByLoc(nx, ny, 0); + this.renderable.delete(index); + const arr: DamageRenderable[] = []; + const loc = `${nx},${ny}` as LocString; + const dam = col.mapDamage[loc]; + + if (!dam || obj[loc]?.event.noPass) continue; + let text = ''; + let color = '#fa3'; + 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'; + } else if (dam.hunt) { + text = '猎'; + color = '#fd4'; + } else { + continue; + } + + const mapDam: DamageRenderable = { + align: 'center', + baseline: 'middle', + text, + color, + x: nx * this.cellSize + this.cellSize / 2, + y: ny * this.cellSize + this.cellSize / 2 + }; + arr.push(mapDam); + this.renderable.set(index, arr); + } + } + + col.range.scan('rect', { x, y, w: width, h: height }).forEach(v => { + if (isNil(v.x) || isNil(v.y)) return; + + const dam = v.calDamage().damage; + const cri = v.calCritical(1)[0]?.atkDelta ?? Infinity; + const dam1: DamageRenderable = { + align: 'left', + baseline: 'alphabetic', + text: !isFinite(dam) ? '???' : core.formatBigNumber(dam, true), + color: getDamageColor(dam), + x: v.x * this.cellSize + 1, + y: v.y * this.cellSize + this.cellSize - 1 + }; + const dam2: DamageRenderable = { + align: 'left', + baseline: 'alphabetic', + text: !isFinite(cri) ? '?' : core.formatBigNumber(cri, true), + color: '#fff', + x: v.x * this.cellSize + 1, + y: v.y * this.cellSize + this.cellSize - 11 + }; + this.pushDamageRenderable(x, y, dam1, dam2); + }); + + this.emit('dataUpdate', x, y, width, height); + } + + /** + * 向渲染列表添加渲染内容,应当在 `dataUpdate` 的事件中进行调用,其他位置不应当直接调用 + */ + pushDamageRenderable(x: number, y: number, ...data: DamageRenderable[]) { + const index = x + y * this.mapWidth; + let arr = this.renderable.get(index); + if (!arr) { + arr = []; + this.renderable.set(index, arr); + } + arr.push(...data); + } + + /** + * 计算需要渲染哪些块 + */ + calNeedRender(camera: Camera) { + if (this.parent instanceof LayerGroup) { + // 如果处于地图组中,每个地图的渲染区域应该是一样的,因此可以缓存优化 + return this.parent.cacheNeedRender(camera, this.block); + } else if (this.parent instanceof Layer) { + // 如果是地图的子元素,直接调用Layer的计算函数 + return this.parent.calNeedRender(camera); + } else { + return calNeedRenderOf(camera, this.cellSize, this.block); + } + } + + /** + * 渲染伤害值 + */ + renderDamage(camera: Camera) { + if (!this.floorId) return; + + const { ctx } = this.damageMap; + ctx.save(); + transformCanvas(this.damageMap, camera, true); + + const { res: render } = this.calNeedRender(camera); + const block = this.block; + const cell = this.cellSize; + const size = cell * block.blockSize; + 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; + + // 检查有没有缓存 + const cache = block.cache.get(v * block.cacheDepth); + if (cache && cache.floorId === this.floorId) { + ctx.drawImage(cache.canvas, px, py, size, size); + return; + } + + // 否则依次渲染并写入缓存 + const temp = new MotaOffscreenCanvas2D(); + temp.setHD(true); + temp.setAntiAliasing(true); + temp.withGameScale(true); + temp.size(size, size); + const { ctx: ct } = temp; + ct.font = "14px 'normal'"; + ct.lineWidth = 1.5; + + const ex = bx + block.blockSize; + const ey = by + block.blockSize; + for (let nx = bx; nx < ex; nx++) { + for (let ny = by; ny < ey; ny++) { + const index = nx + ny * block.blockSize; + const render = this.renderable.get(index); + + render?.forEach(v => { + if (!v) return; + ct.fillStyle = v.color; + ct.strokeStyle = '#000'; + ct.textAlign = v.align; + ct.textBaseline = v.baseline; + ct.strokeText(v.text, v.x, v.y); + ct.fillText(v.text, v.x, v.y); + }); + } + } + + ct.drawImage(temp.canvas, px, py, size, size); + block.cache.set(v * block.cacheDepth, { + canvas: temp.canvas, + floorId: this.floorId + }); + }); + ctx.restore(); + } +} diff --git a/src/core/render/render.ts b/src/core/render/render.ts index 8559cc5..2eaaac5 100644 --- a/src/core/render/render.ts +++ b/src/core/render/render.ts @@ -2,10 +2,10 @@ import { Animation, hyper, sleep } from 'mutate-animate'; import { MotaCanvas2D, MotaOffscreenCanvas2D } from '../fx/canvas2d'; import { Camera } from './camera'; import { Container } from './container'; -import { IRenderDestroyable, RenderItem, withCacheRender } from './item'; -import { Layer } from './preset/layer'; +import { RenderItem, transformCanvas, withCacheRender } from './item'; +import { Layer, LayerGroup } from './preset/layer'; -export class MotaRenderer extends Container implements IRenderDestroyable { +export class MotaRenderer extends Container { static list: Set = new Set(); canvas: MotaOffscreenCanvas2D; @@ -71,40 +71,33 @@ export class MotaRenderer extends Container implements IRenderDestroyable { ctx.clearRect(0, 0, canvas.width, canvas.height); withCacheRender(this, canvas, ctx, camera, 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 => { + if (v.hidden) return; if (v.type === 'absolute') { ct.setTransform(scale, 0, 0, scale, 0, 0); } else { - ct.setTransform(1, 0, 0, 1, 0, 0); - ct.translate(ca.width / 2, ca.height / 2); - ct.transform(a, b, c, d, e, f); + transformCanvas(canvas, camera, false); } + ct.save(); if (!v.antiAliasing) { ctx.imageSmoothingEnabled = false; + ct.imageSmoothingEnabled = false; } else { ctx.imageSmoothingEnabled = true; + ct.imageSmoothingEnabled = true; } v.render(ca, ct, camera); + ct.restore(); }); }); this.emit('afterRender'); } - /** - * 更新渲染,在下一个tick更新 - */ update(item?: RenderItem) { if (this.needUpdate) return; this.needUpdate = true; requestAnimationFrame(() => { - this.cache(this.writing); + this.cache(this.using); this.needUpdate = false; this.refresh(item); }); @@ -144,57 +137,15 @@ window.addEventListener('resize', () => { Mota.require('var', 'hook').once('reset', () => { const render = new MotaRenderer(); - const layer = new Layer(); - const bgLayer = new Layer(); const camera = render.camera; render.mount(); - layer.setZIndex(2); - bgLayer.setZIndex(1); - render.appendChild([layer, bgLayer]); - layer.bindThis('event'); - bgLayer.bindThis('bg'); - bgLayer.setBackground(305); + const layer = new LayerGroup(); + layer.addDamage(); + render.appendChild(layer); - 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.scale(0.7); 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); }); diff --git a/src/core/render/sprite.ts b/src/core/render/sprite.ts index f4e916e..4b3007a 100644 --- a/src/core/render/sprite.ts +++ b/src/core/render/sprite.ts @@ -26,7 +26,7 @@ export class Sprite extends RenderItem implements ICanvasCachedRenderItem { ): void { this.emit('beforeRender'); if (this.needUpdate) { - this.cache(this.writing); + this.cache(this.using); this.needUpdate = false; } withCacheRender(this, canvas, ctx, camera, canvas => { @@ -40,7 +40,6 @@ export class Sprite extends RenderItem implements ICanvasCachedRenderItem { this.width = width; this.height = height; this.canvas.size(width, height); - this.writing = this.using; this.update(this); } diff --git a/src/game/game.ts b/src/game/game.ts index 693be47..2e7374d 100644 --- a/src/game/game.ts +++ b/src/game/game.ts @@ -1,12 +1,13 @@ -import { EmitableEvent, EventEmitter } from '../core/common/eventEmitter'; +import { EventEmitter } from '../core/common/eventEmitter'; import type { DamageEnemy } from './enemy/damage'; // ----- 加载事件 -interface GameLoadEvent extends EmitableEvent { +interface GameLoadEvent { coreLoaded: () => void; autotileLoaded: () => void; coreInit: () => void; loaded: () => void; + materialLoaded: () => void; } class GameLoading extends EventEmitter { @@ -64,7 +65,7 @@ class GameLoading extends EventEmitter { export const loading = new GameLoading(); -export interface GameEvent extends EmitableEvent { +export interface GameEvent { /** Emitted in libs/events.js resetGame. */ reset: () => void; /** Emitted in src/App.vue setup. */ @@ -101,14 +102,14 @@ export interface GameEvent extends EmitableEvent { x: number, y: number, floorId: FloorIds, - oldBlock: AllNumbers, - newBlock: AllNumbers + newBlock: AllNumbers, + oldBlock: AllNumbers ) => void; } export const hook = new EventEmitter(); -interface ListenerEvent extends EmitableEvent { +interface ListenerEvent { // block hoverBlock: (block: Block, ev: MouseEvent) => void; leaveBlock: (block: Block, ev: MouseEvent, leaveGame: boolean) => void; diff --git a/src/plugin/game/range.ts b/src/plugin/game/range.ts index 497d1f7..77e4991 100644 --- a/src/plugin/game/range.ts +++ b/src/plugin/game/range.ts @@ -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 + ); + } +); diff --git a/src/plugin/utils.ts b/src/plugin/utils.ts index 9948581..62c4160 100644 --- a/src/plugin/utils.ts +++ b/src/plugin/utils.ts @@ -43,7 +43,7 @@ export function has(value: T): value is NonNullable { * 根据伤害大小获取颜色 * @param damage 伤害大小 */ -export function getDamageColor(damage: number): Color { +export function getDamageColor(damage: number): string { if (typeof damage !== 'number') return '#f00'; if (damage === 0) return '#2f2'; if (damage < 0) return '#7f7';