import { MotaOffscreenCanvas2D } from '@motajs/render'; import { Container, MotaRenderer, Shader, Sprite } from '@motajs/render'; import { CameraAnimation, LayerGroup, disableViewport, enableViewport } from '@user/client-modules'; import { loading } from '@user/data-base'; import { heroMoveCollection, type HeroMover, type MoveStep } from '@user/data-state'; import EventEmitter from 'eventemitter3'; import { mainRenderer } from '@user/client-modules'; export interface IChaseController { /** 本次追逐战实例 */ readonly chase: Chase; /** * 开始这个追逐战 * @param fromSave 是否是从存档开始 */ start(fromSave: boolean): void; /** * 结束这个追逐战 * @param success 是否逃脱成功 */ end(success: boolean): void; /** * 初始化这次追逐战的音频 * @param fromSave 是否从存档开始 */ initAudio(fromSave: boolean): void; } export interface ChaseData { path: Partial>; camera: Partial>; } interface TimeListener { fn: (emitTime: number) => void; time: number; } interface LocListener { fn: (x: number, y: number) => void; floorId: FloorIds; once: boolean; } interface ChaseEvent { changeFloor: [floor: FloorIds]; end: [success: boolean]; start: []; step: [x: number, y: number]; frame: [totalTime: number, floorTime: number]; } export class Chase extends EventEmitter { static shader: Shader; /** 本次追逐战的数据 */ private readonly data: ChaseData; /** 是否显示路线 */ private showPath: boolean = false; /** 每层的路线显示 */ private pathMap: Map = new Map(); /** 当前的摄像机动画 */ private nowCamera?: CameraAnimation; /** 当前楼层 */ private nowFloor?: FloorIds; /** 开始时刻 */ startTime: number = 0; /** 进入当前楼层的时刻 */ nowFloorTime: number = 0; /** 是否正在进行追逐战 */ started: boolean = false; /** 路径显示的sprite */ private pathSprite?: Sprite; /** 当前 LayerGroup 渲染元素 */ private layer: LayerGroup; /** 委托ticker的id */ private delegation: number = -1; /** 时间监听器 */ private onTimeListener: TimeListener[] = []; /** 楼层时间监听器 */ private onFloorTimeListener: Partial> = {}; /** 勇士位置监听器 */ private onHeroLocListener: Map> = new Map(); /** 勇士移动实例 */ private heroMove: HeroMover; constructor(data: ChaseData, showPath: boolean = false) { super(); this.data = data; this.showPath = showPath; const render = MotaRenderer.get('render-main')!; const layer = render.getElementById('layer-main')! as LayerGroup; this.layer = layer; const mover = heroMoveCollection.mover; this.heroMove = mover; mover.on('stepEnd', this.onStepEnd); } private onStepEnd = (step: MoveStep) => { if (step.type === 'speed') return; const { x, y } = core.status.hero.loc; this.emitHeroLoc(x, y); this.emit('step', x, y); }; private emitHeroLoc(x: number, y: number) { if (!this.nowFloor) return; const floor = core.status.maps[this.nowFloor]; const width = floor.width; const index = x + y * width; const list = this.onHeroLocListener.get(index); if (!list) return; const toDelete = new Set(); list.forEach(v => { if (v.floorId === this.nowFloor) { v.fn(x, y); if (v.once) toDelete.add(v); } }); toDelete.forEach(v => list.delete(v)); } private emitTime() { const now = Date.now(); const nTime = now - this.startTime; const fTime = now - this.nowFloorTime; this.emit('frame', nTime, fTime); while (true) { const time = this.onTimeListener[0]; if (!time) break; if (time.time <= nTime) { time.fn(nTime); this.onTimeListener.shift(); } else { break; } } if (!this.nowFloor) return; const floor = this.onFloorTimeListener[this.nowFloor]; if (!floor) return; while (true) { const time = floor[0]; if (!time) break; if (time.time <= fTime) { time.fn(nTime); floor.shift(); } else { break; } } } private tick = () => { if (!this.started) return; const floor = core.status.floorId; if (floor !== this.nowFloor) { this.changeFloor(floor); } this.emitTime(); }; private readyPath() { for (const [key, nodes] of Object.entries(this.data.path)) { if (nodes.length === 0) return; const floor = key as FloorIds; const canvas = mainRenderer.requireCanvas(); const ctx = canvas.ctx; const cell = 32; const half = cell / 2; const { width, height } = core.status.maps[floor]; canvas.setHD(true); canvas.size(width * cell, height * cell); const [fx, fy] = nodes.shift()!; ctx.beginPath(); ctx.moveTo(fx * cell + half, fy * cell + half); nodes.forEach(([x, y]) => { ctx.lineTo(x * cell + half, y * cell + half); }); ctx.strokeStyle = '#0ff'; ctx.globalAlpha = 0.6; ctx.stroke(); this.pathMap.set(floor, canvas); } this.pathSprite = new Sprite('static', false, true); this.pathSprite.size(480, 480); this.pathSprite.pos(0, 0); this.pathSprite.setZIndex(120); this.pathSprite.setAntiAliasing(false); this.layer.appendChild(this.pathSprite); this.pathSprite.setRenderFn(canvas => { const ctx = canvas.ctx; const path = this.pathMap.get(core.status.floorId); if (!path) return; ctx.drawImage(path.canvas, 0, 0, path.width, path.height); }); } /** * 当到达某个时间时触发函数 * @param time 触发时刻 * @param fn 触发时执行的函数,函数的参数表示实际触发时间 */ onTime(time: number, fn: (emitTime: number) => void) { this.onTimeListener.push({ time, fn }); if (this.started) { this.onTimeListener.sort((a, b) => a.time - b.time); } } /** * 当在某个楼层中到达某个时间时触发函数 * @param floor 触发楼层 * @param time 从进入该楼层开始计算的触发时刻 * @param fn 触发时执行的函数 */ onFloorTime(floor: FloorIds, time: number, fn: (emitTime: number) => void) { this.onFloorTimeListener[floor] ??= []; const list = this.onFloorTimeListener[floor]; list.push({ time, fn }); if (this.started) { list.sort((a, b) => a.time - b.time); } } private ensureLocListener(index: number) { const listener = this.onHeroLocListener.get(index); if (listener) return listener; else { const set = new Set(); this.onHeroLocListener.set(index, set); return set; } } /** * 当勇士走到某一层的某一格时执行函数 * @param x 触发横坐标 * @param y 触发纵坐标 * @param floor 触发楼层 * @param fn 触发函数 * @param once 是否只执行一次 */ onLoc( x: number, y: number, floor: FloorIds, fn: (x: number, y: number) => void, once: boolean = false ) { const map = core.status.maps[floor]; const { width } = map; const index = x + y * width; const set = this.ensureLocListener(index); set.add({ floorId: floor, fn, once }); } /** * 当勇士走到某一层的某一格时执行函数,且只执行一次 * @param x 触发横坐标 * @param y 触发纵坐标 * @param floor 触发楼层 * @param fn 触发函数 */ onceLoc( x: number, y: number, floor: FloorIds, fn: (x: number, y: number) => void ) { this.onLoc(x, y, floor, fn, true); } /** * 切换楼层 * @param floor 目标楼层 */ changeFloor(floor: FloorIds) { if (floor === this.nowFloor) return; this.nowFloor = floor; if (this.nowCamera) { this.nowCamera.destroy(); } const camera = this.data.camera[floor]; if (camera) { camera.start(); this.nowCamera = camera; } this.nowFloorTime = Date.now(); this.emit('changeFloor', floor); } start() { disableViewport(); if (this.showPath) this.readyPath(); this.changeFloor(core.status.floorId); this.startTime = Date.now(); this.delegation = this.layer.delegateTicker(this.tick); this.started = true; for (const floorTime of Object.values(this.onFloorTimeListener)) { floorTime.sort((a, b) => a.time - b.time); } this.onTimeListener.sort((a, b) => a.time - b.time); const render = MotaRenderer.get('render-main')!; const mapDraw = render.getElementById('map-draw') as Container; Chase.shader.appendTo(mapDraw); this.emit('start'); } /** * 结束这次追逐战 * @param success 是否成功逃脱 */ end(success: boolean) { enableViewport(); this.layer.removeTicker(this.delegation); this.pathSprite?.destroy(); this.heroMove.off('stepEnd', this.onStepEnd); Chase.shader.remove(); this.emit('end', success); this.removeAllListeners(); this.pathMap.forEach(v => mainRenderer.deleteCanvas(v)); this.pathMap.clear(); } } loading.once('coreInit', () => { const shader = new Shader(); Chase.shader = shader; shader.size(480, 480); shader.setHD(true); });