feat: 开关门动画

This commit is contained in:
unanmed 2025-11-26 17:48:56 +08:00
parent 3372337fdf
commit dce4cc5b7d
16 changed files with 305 additions and 104 deletions

View File

@ -17,5 +17,6 @@ export async function createMainExtension() {
const layer = state.layer.getLayerByAlias('event'); const layer = state.layer.getLayerByAlias('event');
if (layer) { if (layer) {
mainMapExtension.addHero(state.hero, layer); mainMapExtension.addHero(state.hero, layer);
mainMapExtension.addDoor(layer);
} }
} }

View File

@ -0,0 +1,73 @@
import {
IMapLayer,
IMapLayerHookController,
IMapLayerHooks
} from '@user/data-state';
import { IMapDoorRenderer } from './types';
import { IMapRenderer } from '../types';
import { sleep } from 'mutate-animate';
import { DOOR_ANIMATE_INTERVAL } from '../../shared';
export class MapDoorRenderer implements IMapDoorRenderer {
/** 钩子控制器 */
readonly controller: IMapLayerHookController;
/** 动画间隔 */
private interval: number = DOOR_ANIMATE_INTERVAL;
constructor(
readonly renderer: IMapRenderer,
readonly layer: IMapLayer
) {
this.controller = layer.addHook(new MapDoorHook(this));
this.controller.load();
}
setAnimateInterval(interval: number): void {
this.interval = interval;
}
async openDoor(x: number, y: number): Promise<void> {
const status = this.renderer.getBlockStatus(this.layer, x, y);
if (!status) return;
const array = this.layer.getMapRef().array;
const index = y * this.layer.width + x;
const num = array[index];
const data = this.renderer.manager.getIfBigImage(num);
if (!data) return;
const frames = data.frames;
for (let i = 0; i < frames; i++) {
status.useSpecifiedFrame(i);
await sleep(this.interval);
}
}
async closeDoor(num: number, x: number, y: number): Promise<void> {
const data = this.renderer.manager.getIfBigImage(num);
if (!data) return;
const moving = this.renderer.addMovingBlock(this.layer, num, x, y);
const frames = data.frames;
for (let i = frames - 1; i >= 0; i--) {
moving.useSpecifiedFrame(i);
await sleep(this.interval);
}
moving.destroy();
}
destroy(): void {
this.controller.unload();
}
}
class MapDoorHook implements Partial<IMapLayerHooks> {
constructor(readonly renderer: MapDoorRenderer) {}
onOpenDoor(x: number, y: number): Promise<void> {
return this.renderer.openDoor(x, y);
}
onCloseDoor(num: number, x: number, y: number): Promise<void> {
return this.renderer.closeDoor(num, x, y);
}
}

View File

@ -156,7 +156,8 @@ export class MapHeroRenderer implements IMapHeroRenderer {
offset: dirImage.width / 4, offset: dirImage.width / 4,
texture: dirImage, texture: dirImage,
cls: BlockCls.Unknown, cls: BlockCls.Unknown,
frames: 4 frames: 4,
defaultFrame: 0
}; };
this.textureMap.set(v, data); this.textureMap.set(v, data);
}); });

View File

@ -1,22 +1,30 @@
import { IHeroState, IMapLayer } from '@user/data-state'; import { IHeroState, IMapLayer } from '@user/data-state';
import { IMapExtensionManager, IMapHeroRenderer } from './types'; import {
IMapDoorRenderer,
IMapExtensionManager,
IMapHeroRenderer
} from './types';
import { IMapRenderer } from '../types'; import { IMapRenderer } from '../types';
import { MapHeroRenderer } from './hero'; import { MapHeroRenderer } from './hero';
import { logger } from '@motajs/common'; import { logger } from '@motajs/common';
import { MapDoorRenderer } from './door';
export class MapExtensionManager implements IMapExtensionManager { export class MapExtensionManager implements IMapExtensionManager {
/** 勇士状态至勇士渲染器的映射 */ /** 勇士状态至勇士渲染器的映射 */
readonly heroMap: WeakMap<IHeroState, IMapHeroRenderer> = new WeakMap(); readonly heroMap: Map<IHeroState, IMapHeroRenderer> = new Map();
/** 地图图层到门渲染器的映射 */
readonly doorMap: Map<IMapLayer, IMapDoorRenderer> = new Map();
constructor(readonly renderer: IMapRenderer) {} constructor(readonly renderer: IMapRenderer) {}
addHero(state: IHeroState, layer: IMapLayer): void { addHero(state: IHeroState, layer: IMapLayer): IMapHeroRenderer | null {
if (this.heroMap.has(state)) { if (this.heroMap.has(state)) {
logger.error(45); logger.error(45, 'hero renderer');
return; return null;
} }
const heroRenderer = new MapHeroRenderer(this.renderer, layer, state); const heroRenderer = new MapHeroRenderer(this.renderer, layer, state);
this.heroMap.set(state, heroRenderer); this.heroMap.set(state, heroRenderer);
return heroRenderer;
} }
removeHero(state: IHeroState): void { removeHero(state: IHeroState): void {
@ -25,4 +33,28 @@ export class MapExtensionManager implements IMapExtensionManager {
renderer.destroy(); renderer.destroy();
this.heroMap.delete(state); this.heroMap.delete(state);
} }
addDoor(layer: IMapLayer): IMapDoorRenderer | null {
if (this.doorMap.has(layer)) {
logger.error(45, 'door renderer');
return null;
}
const doorRenderer = new MapDoorRenderer(this.renderer, layer);
this.doorMap.set(layer, doorRenderer);
return doorRenderer;
}
removeDoor(layer: IMapLayer): void {
const renderer = this.doorMap.get(layer);
if (!renderer) return;
renderer.destroy();
this.doorMap.delete(layer);
}
destroy(): void {
this.heroMap.forEach(v => void v.destroy());
this.doorMap.forEach(v => void v.destroy());
this.heroMap.clear();
this.doorMap.clear();
}
} }

View File

@ -12,13 +12,28 @@ export interface IMapExtensionManager {
* @param state * @param state
* @param layer * @param layer
*/ */
addHero(state: IHeroState, layer: IMapLayer): void; addHero(state: IHeroState, layer: IMapLayer): IMapHeroRenderer | null;
/** /**
* *
* @param state * @param state
*/ */
removeHero(state: IHeroState): void; removeHero(state: IHeroState): void;
/**
*
*/
addDoor(layer: IMapLayer): IMapDoorRenderer | null;
/**
*
*/
removeDoor(layer: IMapLayer): void;
/**
*
*/
destroy(): void;
} }
export interface IMapHeroRenderer { export interface IMapHeroRenderer {
@ -119,3 +134,31 @@ export interface IMapHeroRenderer {
*/ */
destroy(): void; destroy(): void;
} }
export interface IMapDoorRenderer {
/**
*
* @param x
* @param y
*/
openDoor(x: number, y: number): Promise<void>;
/**
*
* @param num
* @param x
* @param y
*/
closeDoor(num: number, x: number, y: number): Promise<void>;
/**
*
* @param interval
*/
setAnimateInterval(interval: number): void;
/**
*
*/
destroy(): void;
}

View File

@ -1348,7 +1348,6 @@ export class MapRenderer
} }
render(): HTMLCanvasElement { render(): HTMLCanvasElement {
// todo: 改为 FBO最后把 FBO 画到画布上
const gl = this.gl; const gl = this.gl;
const data = this.contextData; const data = this.contextData;
if (!this.assetData) { if (!this.assetData) {
@ -1711,6 +1710,10 @@ export class MapRenderer
this.needUpdateTransform = true; this.needUpdateTransform = true;
} }
requestUpdate(): void {
this.updateRequired = true;
}
needUpdate(): boolean { needUpdate(): boolean {
return ( return (
this.updateRequired || this.updateRequired ||

View File

@ -41,6 +41,11 @@ export interface IMapDataGetter {
* @param moving * @param moving
*/ */
hasMoving(moving: IMovingBlock): boolean; hasMoving(moving: IMovingBlock): boolean;
/**
*
*/
requestUpdate(): void;
} }
interface BlockMapPos { interface BlockMapPos {
@ -106,7 +111,13 @@ export class MapVertexGenerator
/** 空顶点数组,因为空顶点很常用,所以直接定义一个全局常量 */ /** 空顶点数组,因为空顶点很常用,所以直接定义一个全局常量 */
private static readonly EMPTY_VETREX: Float32Array = new Float32Array( private static readonly EMPTY_VETREX: Float32Array = new Float32Array(
INSTANCED_COUNT // prettier-ignore
[
0, 0, 0, 0, // 顶点坐标
0, 0, 0, 0, // 纹理坐标
0, 1, 0, 0, // a_tileData不透明度需要设为 1
-1, 0, 0, 0, // a_texData当前帧数需要设为 -1
]
); );
readonly block: IBlockSplitter<MapVertexBlock>; readonly block: IBlockSplitter<MapVertexBlock>;
@ -1044,7 +1055,7 @@ class MapVertexBlock implements IMapVertexBlock {
* @param blockHeight * @param blockHeight
*/ */
constructor( constructor(
readonly renderer: IMapRenderer, readonly renderer: IMapRenderer & IMapDataGetter,
originArray: IMapVertexData, originArray: IMapVertexData,
startIndex: number, startIndex: number,
count: number, count: number,
@ -1074,6 +1085,7 @@ class MapVertexBlock implements IMapVertexBlock {
*/ */
markRenderDirty() { markRenderDirty() {
this.renderDirty = true; this.renderDirty = true;
this.renderer.requestUpdate();
} }
markDirty( markDirty(
@ -1101,6 +1113,7 @@ class MapVertexBlock implements IMapVertexBlock {
data.dirtyBottom = Math.max(db, data.dirtyBottom); data.dirtyBottom = Math.max(db, data.dirtyBottom);
} }
this.dirty = true; this.dirty = true;
this.renderer.requestUpdate();
} }
getDirtyArea(layer: IMapLayer): Readonly<ILayerDirtyData> | null { getDirtyArea(layer: IMapLayer): Readonly<ILayerDirtyData> | null {

View File

@ -33,6 +33,8 @@ export const DYNAMIC_RESERVE = 16;
* *
*/ */
export const MOVING_TOLERANCE = 60; export const MOVING_TOLERANCE = 60;
/** 开关门动画的动画时长 */
export const DOOR_ANIMATE_INTERVAL = 50;
//#region 状态栏 //#region 状态栏

View File

@ -24,13 +24,13 @@ function createCoreState() {
//#region 图块部分 //#region 图块部分
loading.once('coreInit', () => {
const data = Object.entries(core.maps.blocksInfo); const data = Object.entries(core.maps.blocksInfo);
for (const [key, block] of data) { for (const [key, block] of data) {
const num = Number(key); const num = Number(key);
state.idNumberMap.set(block.id, num); state.idNumberMap.set(block.id, num);
state.numberIdMap.set(num, block.id); state.numberIdMap.set(num, block.id);
} }
for (const [key, block] of data) { for (const [key, block] of data) {
if (!block.faceIds) continue; if (!block.faceIds) continue;
const { down, up, left, right } = block.faceIds; const { down, up, left, right } = block.faceIds;
@ -50,7 +50,6 @@ function createCoreState() {
state.roleFace.bind(rightNum, downNum, FaceDirection.Right); state.roleFace.bind(rightNum, downNum, FaceDirection.Right);
} }
} }
});
//#endregion //#endregion
} }

View File

@ -36,7 +36,7 @@ export class LayerState
this.forEachHook(hook => { this.forEachHook(hook => {
hook.onUpdateLayer?.(this.layerList); hook.onUpdateLayer?.(this.layerList);
}); });
const controller = layer.addHook(new StateMapLayerHook(this)); const controller = layer.addHook(new StateMapLayerHook(this, layer));
this.layerHookMap.set(layer, controller); this.layerHookMap.set(layer, controller);
controller.load(); controller.load();
return layer; return layer;
@ -114,38 +114,26 @@ export class LayerState
} }
class StateMapLayerHook implements Partial<IMapLayerHooks> { class StateMapLayerHook implements Partial<IMapLayerHooks> {
constructor(readonly state: LayerState) {} constructor(
readonly state: LayerState,
readonly layer: IMapLayer
) {}
onUpdateArea( onUpdateArea(x: number, y: number, width: number, height: number): void {
controller: IMapLayerHookController,
x: number,
y: number,
width: number,
height: number
): void {
this.state.forEachHook(hook => { this.state.forEachHook(hook => {
hook.onUpdateLayerArea?.(controller.layer, x, y, width, height); hook.onUpdateLayerArea?.(this.layer, x, y, width, height);
}); });
} }
onUpdateBlock( onUpdateBlock(block: number, x: number, y: number): void {
controller: IMapLayerHookController,
block: number,
x: number,
y: number
): void {
this.state.forEachHook(hook => { this.state.forEachHook(hook => {
hook.onUpdateLayerBlock?.(controller.layer, block, x, y); hook.onUpdateLayerBlock?.(this.layer, block, x, y);
}); });
} }
onResize( onResize(width: number, height: number): void {
controller: IMapLayerHookController,
width: number,
height: number
): void {
this.state.forEachHook(hook => { this.state.forEachHook(hook => {
hook.onResizeLayer?.(controller.layer, width, height); hook.onResizeLayer?.(this.layer, width, height);
}); });
} }
} }

View File

@ -72,8 +72,8 @@ export class MapLayer
expired: false, expired: false,
array: this.mapArray array: this.mapArray
}; };
this.forEachHook((hook, controller) => { this.forEachHook(hook => {
hook.onResize?.(controller, width, height); hook.onResize?.(width, height);
}); });
} }
@ -91,8 +91,8 @@ export class MapLayer
array: this.mapArray array: this.mapArray
}; };
this.empty = true; this.empty = true;
this.forEachHook((hook, controller) => { this.forEachHook(hook => {
hook.onResize?.(controller, width, height); hook.onResize?.(width, height);
}); });
} }
@ -100,8 +100,8 @@ export class MapLayer
const index = y * this.width + x; const index = y * this.width + x;
if (block === this.mapArray[index]) return; if (block === this.mapArray[index]) return;
this.mapArray[index] = block; this.mapArray[index] = block;
this.forEachHook((hook, controller) => { this.forEachHook(hook => {
hook.onUpdateBlock?.(controller, block, x, y); hook.onUpdateBlock?.(block, x, y);
}); });
if (block !== 0) { if (block !== 0) {
this.empty = false; this.empty = false;
@ -123,8 +123,8 @@ export class MapLayer
const height = Math.ceil(array.length / width); const height = Math.ceil(array.length / width);
if (width === this.width && height === this.height) { if (width === this.width && height === this.height) {
this.mapArray.set(array); this.mapArray.set(array);
this.forEachHook((hook, controller) => { this.forEachHook(hook => {
hook.onUpdateArea?.(controller, x, y, width, height); hook.onUpdateArea?.(x, y, width, height);
}); });
return; return;
} }
@ -151,8 +151,8 @@ export class MapLayer
} }
this.mapArray.set(array.subarray(start, start + nw), offset); this.mapArray.set(array.subarray(start, start + nw), offset);
} }
this.forEachHook((hook, controller) => { this.forEachHook(hook => {
hook.onUpdateArea?.(controller, x, y, width, height); hook.onUpdateArea?.(x, y, width, height);
}); });
this.empty &&= empty; this.empty &&= empty;
} }
@ -214,6 +214,33 @@ export class MapLayer
setZIndex(zIndex: number): void { setZIndex(zIndex: number): void {
this.zIndex = zIndex; this.zIndex = zIndex;
} }
async openDoor(x: number, y: number): Promise<void> {
const index = y * this.width + x;
const num = this.mapArray[index];
if (num === 0) return;
await Promise.all(
this.forEachHook(hook => {
return hook.onOpenDoor?.(x, y);
})
);
this.setBlock(0, x, y);
}
async closeDoor(num: number, x: number, y: number): Promise<void> {
const index = y * this.width + x;
const nowNum = this.mapArray[index];
if (nowNum !== 0) {
logger.error(46, x.toString(), y.toString());
return;
}
await Promise.all(
this.forEachHook(hook => {
return hook.onCloseDoor?.(num, x, y);
})
);
this.setBlock(num, x, y);
}
} }
class MapLayerHookController class MapLayerHookController

View File

@ -10,45 +10,42 @@ export interface IMapLayerData {
export interface IMapLayerHooks extends IHookBase { export interface IMapLayerHooks extends IHookBase {
/** /**
* `resize` * `resize`
* @param controller
* @param width * @param width
* @param height * @param height
*/ */
onResize( onResize(width: number, height: number): void;
controller: IMapLayerHookController,
width: number,
height: number
): void;
/** /**
* *
* @param controller
* @param x * @param x
* @param y * @param y
* @param width * @param width
* @param height * @param height
*/ */
onUpdateArea( onUpdateArea(x: number, y: number, width: number, height: number): void;
controller: IMapLayerHookController,
x: number,
y: number,
width: number,
height: number
): void;
/** /**
* *
* @param controller
* @param block * @param block
* @param x * @param x
* @param y * @param y
*/ */
onUpdateBlock( onUpdateBlock(block: number, x: number, y: number): void;
controller: IMapLayerHookController,
block: number, /**
x: number, * `Promise`
y: number * @param x
): void; * @param y
*/
onOpenDoor(x: number, y: number): Promise<void>;
/**
* `Promise`
* @param num
* @param x
* @param y
*/
onCloseDoor(num: number, x: number, y: number): Promise<void>;
} }
export interface IMapLayerHookController export interface IMapLayerHookController
@ -143,6 +140,21 @@ export interface IMapLayer
* @param zIndex * @param zIndex
*/ */
setZIndex(zIndex: number): void; setZIndex(zIndex: number): void;
/**
*
* @param x
* @param y
*/
openDoor(x: number, y: number): Promise<void>;
/**
*
* @param num
* @param x
* @param y
*/
closeDoor(num: number, x: number, y: number): Promise<void>;
} }
export interface ILayerStateHooks extends IHookBase { export interface ILayerStateHooks extends IHookBase {

View File

@ -19,7 +19,14 @@ export interface ICoreState {
/** 图块数字到 id 的映射 */ /** 图块数字到 id 的映射 */
readonly numberIdMap: Map<number, string>; readonly numberIdMap: Map<number, string>;
/**
*
*/
saveState(): IStateSaveData; saveState(): IStateSaveData;
/**
*
* @param data
*/
loadState(data: IStateSaveData): void; loadState(data: IStateSaveData): void;
} }

View File

@ -341,9 +341,9 @@ export function initFallback() {
const locked = core.status.lockControl; const locked = core.status.lockControl;
core.lockControl(); core.lockControl();
core.status.replay.animate = true; core.status.replay.animate = true;
core.removeBlock(x, y);
const cb = () => { const cb = () => {
core.removeBlock(x, y);
core.maps._removeBlockFromMap( core.maps._removeBlockFromMap(
core.status.floorId, core.status.floorId,
block block
@ -359,7 +359,8 @@ export function initFallback() {
callback?.(); callback?.();
}; };
adapters['door-animate']?.all('openDoor', block).then(cb); const layer = state.layer.getLayerByAlias('event')!;
layer.openDoor(x, y).then(cb);
const animate = fallbackIds++; const animate = fallbackIds++;
core.animateFrame.lastAsyncId = animate; core.animateFrame.lastAsyncId = animate;
@ -406,11 +407,9 @@ export function initFallback() {
if (core.status.replay.speed === 24) { if (core.status.replay.speed === 24) {
cb(); cb();
} else { } else {
adapters['door-animate'] const num = state.idNumberMap.get(id)!;
?.all('closeDoor', block) const layer = state.layer.getLayerByAlias('event')!;
.then(() => { layer.closeDoor(num, x, y).then(cb);
cb();
});
const animate = fallbackIds++; const animate = fallbackIds++;
core.animateFrame.lastAsyncId = animate; core.animateFrame.lastAsyncId = animate;

View File

@ -44,7 +44,8 @@
"42": "The '$1' property of map-render element is required.", "42": "The '$1' property of map-render element is required.",
"43": "Cannot bind face direction to main block $1, please call malloc in advance.", "43": "Cannot bind face direction to main block $1, please call malloc in advance.",
"44": "Cannot bind face direction to main block $1, since main direction cannot be override.", "44": "Cannot bind face direction to main block $1, since main direction cannot be override.",
"45": "Cannot add hero renderer, sincehero renderer already exists for the given state.", "45": "Cannot add $1 map renderer extension, since $1 already exists for the given state.",
"46": "Cannot execute close door action on $1,$2, since the given position is not empty.",
"1101": "Shadow extension needs 'floor-hero' extension as dependency.", "1101": "Shadow extension needs 'floor-hero' extension as dependency.",
"1201": "Floor-damage extension needs 'floor-binder' extension as dependency.", "1201": "Floor-damage extension needs 'floor-binder' extension as dependency.",
"1301": "Portal extension need 'floor-binder' extension as dependency.", "1301": "Portal extension need 'floor-binder' extension as dependency.",

View File

@ -3197,8 +3197,8 @@ maps.prototype.hideBlockByIndex = function (index, floorId) {
this._updateMapArray(floorId, block.x, block.y); this._updateMapArray(floorId, block.x, block.y);
Mota.require('@user/data-base').hook.emit( Mota.require('@user/data-base').hook.emit(
'setBlock', 'setBlock',
x, block.x,
y, block.y,
floorId, floorId,
0, 0,
block?.id ?? 0 block?.id ?? 0
@ -3206,7 +3206,7 @@ maps.prototype.hideBlockByIndex = function (index, floorId) {
if (floorId === core.status.floorId) { if (floorId === core.status.floorId) {
const { layer } = Mota.require('@user/data-state').state; const { layer } = Mota.require('@user/data-state').state;
const event = layer.getLayerByAlias('event'); const event = layer.getLayerByAlias('event');
event.setBlock(0, x, y); event.setBlock(0, block.x, block.y);
} }
}; };
@ -3283,7 +3283,7 @@ maps.prototype.removeBlockByIndex = function (index, floorId) {
if (floorId === core.status.floorId) { if (floorId === core.status.floorId) {
const { layer } = Mota.require('@user/data-state').state; const { layer } = Mota.require('@user/data-state').state;
const event = layer.getLayerByAlias('event'); const event = layer.getLayerByAlias('event');
event.setBlock(0, x, y); event.setBlock(0, block.x, block.y);
} }
}; };
@ -3639,8 +3639,8 @@ maps.prototype.replaceBlock = function (fromNumber, toNumber, floorId) {
this._updateMapArray(floorId, block.x, block.y); this._updateMapArray(floorId, block.x, block.y);
Mota.require('@user/data-base').hook.emit( Mota.require('@user/data-base').hook.emit(
'setBlock', 'setBlock',
x, block.x,
y, block.y,
floorId, floorId,
fromNumber, fromNumber,
toNumber toNumber
@ -3648,7 +3648,7 @@ maps.prototype.replaceBlock = function (fromNumber, toNumber, floorId) {
if (floorId === core.status.floorId) { if (floorId === core.status.floorId) {
const { layer } = Mota.require('@user/data-state').state; const { layer } = Mota.require('@user/data-state').state;
const event = layer.getLayerByAlias('event'); const event = layer.getLayerByAlias('event');
event.setBlock(toNumber, x, y); event.setBlock(toNumber, block.x, block.y);
} }
} }
}, this); }, this);