mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-06-29 13:47:59 +08:00
357 lines
10 KiB
TypeScript
357 lines
10 KiB
TypeScript
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<Record<FloorIds, LocArr[]>>;
|
|
camera: Partial<Record<FloorIds, CameraAnimation>>;
|
|
}
|
|
|
|
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<ChaseEvent> {
|
|
static shader: Shader;
|
|
|
|
/** 本次追逐战的数据 */
|
|
private readonly data: ChaseData;
|
|
|
|
/** 是否显示路线 */
|
|
private showPath: boolean = false;
|
|
/** 每层的路线显示 */
|
|
private pathMap: Map<FloorIds, MotaOffscreenCanvas2D> = 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<Record<FloorIds, TimeListener[]>> = {};
|
|
/** 勇士位置监听器 */
|
|
private onHeroLocListener: Map<number, Set<LocListener>> = 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<LocListener>();
|
|
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<LocListener>();
|
|
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);
|
|
});
|