diff --git a/public/libs/control.js b/public/libs/control.js index 6edad34..aed20fe 100644 --- a/public/libs/control.js +++ b/public/libs/control.js @@ -1533,7 +1533,7 @@ control.prototype.chooseReplayFile = function () { function (obj) { if (obj.name != core.firstData.name) return alert('存档和游戏不一致!'); - if (!obj.route) return alert('无效的录像!'); + if (!obj.route) return core.drawTip('无效的录像!'); var _replay = function () { core.startGame( core.flags.startUsingCanvas ? '' : obj.hard || '', @@ -2370,7 +2370,7 @@ control.prototype._doSL_load = function (id, callback) { }, function (err) { console.error(err); - alert('无效的存档'); + core.drawTip('无效的存档'); } ); } @@ -2393,7 +2393,7 @@ control.prototype._doSL_reload = function (id, callback) { }; control.prototype._doSL_load_afterGet = function (id, data) { - if (!data) return alert('无效的存档'); + if (!data) return core.drawTip('无效的存档'); var _replay = function () { core.startGame( data.hard, diff --git a/public/project/floors/MT16.js b/public/project/floors/MT16.js index e18824b..a52a9d5 100644 --- a/public/project/floors/MT16.js +++ b/public/project/floors/MT16.js @@ -161,15 +161,12 @@ main.floors.MT16= }, { "type": "animate", - "name": "amazed", - "async": true + "name": "amazed" }, { "type": "sleep", - "time": 1000 - }, - { - "type": "waitAsync" + "time": 1000, + "noSkip": true }, { "type": "moveHero", diff --git a/src/core/index.ts b/src/core/index.ts index f8137df..54cf632c 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -69,10 +69,12 @@ import { Image, Text } from './render/preset/misc'; import { RenderItem } from './render/item'; import { texture } from './render/cache'; import { RenderAdapter } from './render/adapter'; -import { getMainRenderer } from './render'; import { Layer } from './render/preset/layer'; import { LayerGroupFloorBinder } from './render/preset/floor'; import { HeroKeyMover } from './main/action/move'; +import { Camera } from './render/camera'; +import * as Animation from 'mutate-animate'; +import './render/index'; // ----- 类注册 Mota.register('class', 'AudioPlayer', AudioPlayer); @@ -152,7 +154,6 @@ Mota.register('module', 'Effect', { }); Mota.register('module', 'Render', { texture, - getMainRenderer: getMainRenderer, MotaRenderer, Container, Sprite, @@ -161,11 +162,13 @@ Mota.register('module', 'Render', { RenderItem, RenderAdapter, Layer, - LayerGroupFloorBinder + LayerGroupFloorBinder, + Camera }); Mota.register('module', 'Action', { HeroKeyMover }); +Mota.register('module', 'Animation', Animation); main.renderLoaded = true; Mota.require('var', 'hook').emit('renderLoaded'); diff --git a/src/core/render/camera.ts b/src/core/render/camera.ts new file mode 100644 index 0000000..2161888 --- /dev/null +++ b/src/core/render/camera.ts @@ -0,0 +1,335 @@ +import { Animation, Transition } from 'mutate-animate'; +import { RenderItem } from './item'; +import { logger } from '../common/logger'; +import { Transform } from './transform'; + +interface CameraTranslate { + readonly type: 'translate'; + readonly from: Camera; + x: number; + y: number; +} + +interface CameraRotate { + readonly type: 'rotate'; + readonly from: Camera; + /** 旋转角,单位弧度 */ + angle: number; +} + +interface CameraScale { + readonly type: 'scale'; + readonly from: Camera; + x: number; + y: number; +} + +type CameraOperation = CameraTranslate | CameraScale | CameraRotate; + +export class Camera { + /** 当前绑定的渲染元素 */ + readonly binded: RenderItem; + /** 目标变换矩阵,默认与 `this.binded.transform` 同引用 */ + transform: Transform; + + /** 委托ticker的id */ + private delegation: number; + /** 所有的动画id */ + private animationIds: Set = new Set(); + + /** 是否需要更新视角 */ + private needUpdate: boolean = false; + + /** 变换操作列表,因为矩阵乘法跟顺序有关,因此需要把各个操作拆分成列表进行 */ + protected operation: CameraOperation[] = []; + + /** 渲染元素到摄像机的映射 */ + private static cameraMap: Map = new Map(); + + /** + * 获取一个渲染元素的摄像机,如果不存在则为它创建一个并返回。注意使用`new Camera`创建的摄像机不在此列 + * @param item 渲染元素 + */ + static for(item: RenderItem) { + const camera = this.cameraMap.get(item); + if (!camera) { + const ca = new Camera(item); + this.cameraMap.set(item, ca); + return ca; + } else { + return camera; + } + } + + constructor(item: RenderItem) { + this.binded = item; + + this.delegation = item.delegateTicker(() => this.tick()); + this.transform = item.transform; + + item.on('destroy', () => { + this.destroy(); + }); + + if (Camera.cameraMap.has(item)) { + logger.warn(22); + } + } + + private tick = () => { + if (!this.needUpdate) return; + const trans = this.transform; + trans.reset(); + for (const o of this.operation) { + if (o.type === 'translate') { + trans.translate(o.x, o.y); + } else if (o.type === 'rotate') { + trans.rotate(o.angle); + } else { + trans.scale(o.x, o.y); + } + } + this.binded.update(this.binded); + this.needUpdate = false; + }; + + /** + * 在下一帧进行强制更新 + */ + requestUpdate() { + this.needUpdate = true; + } + + /** + * 移除一个变换操作 + * @param operation 要移除的操作 + */ + removeOperation(operation: CameraOperation) { + const index = this.operation.indexOf(operation); + if (index === -1) return; + this.operation.splice(index, 1); + } + + /** + * 清空变换操作列表 + */ + clearOperation() { + this.operation.splice(0); + } + + /** + * 添加一个平移操作 + * @returns 添加的平移变换操作 + */ + addTranslate(): CameraTranslate { + const item: CameraTranslate = { + type: 'translate', + x: 0, + y: 0, + from: this + }; + this.operation.push(item); + return item; + } + + /** + * 添加一个旋转操作 + * @returns 添加的旋转变换操作 + */ + addRotate(): CameraRotate { + const item: CameraRotate = { + type: 'rotate', + angle: 0, + from: this + }; + this.operation.push(item); + return item; + } + + /** + * 添加一个放缩操作 + * @returns 添加的放缩变换操作 + */ + addScale(): CameraScale { + const item: CameraScale = { + type: 'scale', + x: 0, + y: 0, + from: this + }; + this.operation.push(item); + return item; + } + + /** + * 施加动画 + * @param time 动画时长 + * @param update 每帧的更新函数 + */ + applyAnimation(time: number, update: () => void) { + const delegation = this.binded.delegateTicker( + () => { + update(); + this.needUpdate = true; + }, + time, + () => { + update(); + this.needUpdate = true; + this.animationIds.delete(delegation); + } + ); + this.animationIds.add(delegation); + } + + /** + * 为一个平移操作实施动画 + * @param operation 平移操作 + * @param animate 动画实例 + * @param time 动画时长 + */ + applyTranslateAnimation( + operation: CameraTranslate, + animate: Animation, + time: number + ) { + if (operation.from !== this) { + logger.warn(20); + return; + } + + const update = () => { + operation.x = animate.x; + operation.y = animate.y; + }; + + this.applyAnimation(time, update); + } + + /** + * 为一个旋转操作实施动画 + * @param operation 旋转操作 + * @param animate 动画实例 + * @param time 动画时长 + */ + applyRotateAnimation( + operation: CameraRotate, + animate: Animation, + time: number + ) { + if (operation.from !== this) { + logger.warn(20); + return; + } + + const update = () => { + operation.angle = animate.angle; + }; + + this.applyAnimation(time, update); + } + + /** + * 为一个缩放操作实施动画 + * @param operation 缩放操作 + * @param animate 动画实例 + * @param time 动画时长 + */ + applyScaleAnimation( + operation: CameraScale, + animate: Animation, + time: number + ) { + if (operation.from !== this) { + logger.warn(20); + return; + } + + const update = () => { + operation.x = animate.size; + operation.y = animate.size; + }; + + this.applyAnimation(time, update); + } + + /** + * 为一个平移操作实施渐变,使用渐变的 x,y 值,即`transition.value.x`与`transition.value.y` + * @param operation 平移操作 + * @param animate 渐变实例 + * @param time 渐变时长 + */ + applyTranslateTransition( + operation: CameraTranslate, + animate: Transition, + time: number + ) { + if (operation.from !== this) { + logger.warn(21); + return; + } + + const update = () => { + operation.x = animate.value.x; + operation.y = animate.value.y; + }; + + this.applyAnimation(time, update); + } + + /** + * 为一个旋转操作实施渐变,使用渐变的 angle 值,即`transition.value.angle` + * @param operation 旋转操作 + * @param animate 渐变实例 + * @param time 渐变时长 + */ + applyRotateTransition( + operation: CameraRotate, + animate: Transition, + time: number + ) { + if (operation.from !== this) { + logger.warn(21); + return; + } + + const update = () => { + operation.angle = animate.value.angle; + }; + + this.applyAnimation(time, update); + } + + /** + * 为一个缩放操作实施渐变,使用渐变的 size 值,即`transition.value.size` + * @param operation 缩放操作 + * @param animate 渐变实例 + * @param time 渐变时长 + */ + applyScaleTransition( + operation: CameraScale, + animate: Transition, + time: number + ) { + if (operation.from !== this) { + logger.warn(21); + return; + } + + const update = () => { + operation.x = animate.value.size; + operation.y = animate.value.size; + }; + + this.applyAnimation(time, update); + } + + /** + * 摧毁这个摄像机,当绑定元素被摧毁之后摄像机会一并摧毁,如果这个摄像机不使用了,一定要将它摧毁 + */ + destroy() { + this.binded.removeTicker(this.delegation); + this.animationIds.forEach(v => this.binded.removeTicker(v)); + Camera.cameraMap.delete(this.binded); + } +} diff --git a/src/core/render/index.ts b/src/core/render/index.ts index 68e72e3..b7f40cf 100644 --- a/src/core/render/index.ts +++ b/src/core/render/index.ts @@ -1,6 +1,6 @@ import { FloorItemDetail } from '@/plugin/fx/itemDetail'; import { FloorDamageExtends } from './preset/damage'; -import { LayerDoorAnimate, LayerGroupFloorBinder } from './preset/floor'; +import { LayerDoorAnimate } from './preset/floor'; import { HeroRenderer } from './preset/hero'; import { LayerGroup, FloorLayer } from './preset/layer'; import { MotaRenderer } from './render'; @@ -14,10 +14,6 @@ import { Container } from './container'; let main: MotaRenderer; -export function getMainRenderer() { - return main; -} - Mota.require('var', 'loading').once('loaded', () => { const render = new MotaRenderer(); main = render; @@ -29,6 +25,10 @@ Mota.require('var', 'loading').once('loaded', () => { mapDraw.id = 'map-draw'; layer.id = 'layer-main'; + mapDraw.setHD(true); + mapDraw.setAntiAliasing(false); + mapDraw.size(core._PX_, core._PY_); + ['bg', 'bg2', 'event', 'fg', 'fg2'].forEach(v => { layer.addLayer(v as FloorLayer); }); diff --git a/src/core/render/preset/viewport.ts b/src/core/render/preset/viewport.ts index b1badaf..4de854e 100644 --- a/src/core/render/preset/viewport.ts +++ b/src/core/render/preset/viewport.ts @@ -128,6 +128,7 @@ export class FloorViewport implements ILayerGroupRenderExtends { * @param y 目标图格纵坐标 */ setPosition(x: number, y: number) { + if (!this.enabled) return; const { x: nx, y: ny } = this.getBoundedPosition(x, y); this.group.removeTicker(this.transition, false); this.nx = nx; @@ -140,6 +141,7 @@ export class FloorViewport implements ILayerGroupRenderExtends { * @param y 目标图格纵坐标 */ moveTo(x: number, y: number) { + if (!this.enabled) return; const { x: nx, y: ny } = this.getBoundedPosition(x, y); if (this.inTransition) { const distance = Math.hypot(this.nx - nx, this.ny - ny); @@ -176,6 +178,7 @@ export class FloorViewport implements ILayerGroupRenderExtends { * @param y 目标图格纵坐标 */ mutateTo(x: number, y: number) { + if (!this.enabled) return; const { x: nx, y: ny } = this.getBoundedPosition(x, y); this.createTransition(nx, ny, this.transitionTime); } @@ -321,6 +324,7 @@ export class FloorViewport implements ILayerGroupRenderExtends { const halfWidth = core._PX_ / 2; const halfHeight = core._PY_ / 2; this.delegation = this.group.delegateTicker(() => { + if (!this.enabled) return; if (this.nx === nx && this.ny === ny) return; const cell = this.group.cellSize; const half = cell / 2; diff --git a/src/core/render/render.ts b/src/core/render/render.ts index 871a0e7..b6afca2 100644 --- a/src/core/render/render.ts +++ b/src/core/render/render.ts @@ -4,7 +4,7 @@ import { RenderItem } from './item'; import { Transform } from './transform'; export class MotaRenderer extends Container { - static list: Set = new Set(); + static list: Map = new Map(); target: MotaCanvas2D; @@ -13,18 +13,17 @@ export class MotaRenderer extends Container { constructor(id: string = 'render-main') { super('static', false); - this.id = id; - this.target = new MotaCanvas2D(id); this.size(core._PX_, core._PY_); this.target.withGameScale(true); this.target.size(core._PX_, core._PY_); this.target.css(`z-index: 100`); + this.target.setAntiAliasing(false); this.setAnchor(0.5, 0.5); this.transform.translate(240, 240); - MotaRenderer.list.add(this); + MotaRenderer.list.set(id, this); } update(item?: RenderItem) { @@ -80,7 +79,11 @@ export class MotaRenderer extends Container { } destroy() { - MotaRenderer.list.delete(this); + MotaRenderer.list.delete(this.id); + } + + static get(id: string) { + return this.list.get(id); } } diff --git a/src/core/render/transform.ts b/src/core/render/transform.ts index e5efab8..48bb7e3 100644 --- a/src/core/render/transform.ts +++ b/src/core/render/transform.ts @@ -38,7 +38,7 @@ export class Transform { /** * 移动,叠加关系 */ - move(x: number, y: number) { + translate(x: number, y: number) { mat3.translate(this.mat, this.mat, [x, y]); this.x += x; this.y += y; diff --git a/src/data/logger.json b/src/data/logger.json index 502359d..24d801a 100644 --- a/src/data/logger.json +++ b/src/data/logger.json @@ -40,6 +40,10 @@ "17": "Floor-damage extension needs 'floor-binder' extension as dependency.", "18": "Uncaught error in posting like info for danmaku. Danmaku id: $1.", "19": "Repeat light id: '$1'.", + "20": "Cannot apply animation to camera operation that is not belong to it.", + "21": "Cannot apply transition to camera operation that is not belong to it.", + "22": "There is already a camera for delivered render item. Consider using 'Camera.for' to avoid some exceptions.", + "23": "Render item with id of '$1' has already exists.", "1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency." } } \ No newline at end of file diff --git a/src/game/state/move.ts b/src/game/state/move.ts index bd44f08..889eda0 100644 --- a/src/game/state/move.ts +++ b/src/game/state/move.ts @@ -443,6 +443,9 @@ export class HeroMover extends ObjectMoverBase { /** 是否会在特殊时刻进行自动存档 */ private autoSave: boolean = false; + /** 本次移动开始时的移动速度 */ + private beforeMoveSpeed: number = 100; + /** 这一步的传送门信息 */ private portalData?: BluePalace.PortalTo; @@ -452,6 +455,7 @@ export class HeroMover extends ObjectMoverBase { inLockControl: boolean = false, autoSave: boolean = false ): IMoveController | null { + if (this.moving) return null; this.ignoreTerrain = ignoreTerrain; this.noRoute = noRoute; this.inLockControl = inLockControl; @@ -472,6 +476,7 @@ export class HeroMover extends ObjectMoverBase { } protected async onMoveStart(controller: IMoveController): Promise { + this.beforeMoveSpeed = this.moveSpeed; const adapter = HeroMover.adapter; if (!adapter) return; await adapter.all('readyMove'); @@ -479,6 +484,8 @@ export class HeroMover extends ObjectMoverBase { } protected async onMoveEnd(controller: IMoveController): Promise { + this.moveSpeed = this.beforeMoveSpeed; + this.onSetMoveSpeed(this.moveSpeed, controller); const adapter = HeroMover.adapter; if (!adapter) return; await adapter.all('endMove'); diff --git a/src/game/system.ts b/src/game/system.ts index 4bb8814..44b0c0c 100644 --- a/src/game/system.ts +++ b/src/game/system.ts @@ -38,6 +38,8 @@ import type { Layer } from '@/core/render/preset/layer'; import type { LayerGroupFloorBinder } from '@/core/render/preset/floor'; import type { HeroKeyMover } from '@/core/main/action/move'; import type { BlockMover, HeroMover, ObjectMoverBase } from './state/move'; +import type { Camera } from '@/core/render/camera'; +import type * as Animation from 'mutate-animate'; interface ClassInterface { // 渲染进程与游戏进程通用 @@ -119,6 +121,7 @@ interface ModuleInterface { RenderAdapter: typeof RenderAdapter; Layer: typeof Layer; LayerGroupFloorBinder: typeof LayerGroupFloorBinder; + Camera: typeof Camera; }; State: { ItemState: typeof ItemState; @@ -133,6 +136,7 @@ interface ModuleInterface { Action: { HeroKeyMover: typeof HeroKeyMover; }; + Animation: typeof Animation; } interface SystemInterfaceMap { @@ -148,20 +152,10 @@ interface PluginInterface { // 渲染进程定义的插件 pop_r: typeof import('../plugin/pop'); use_r: typeof import('../plugin/use'); - // animate: typeof import('../plugin/animateController'); - // utils: typeof import('../plugin/utils'); - // status: typeof import('../plugin/ui/statusBar'); fly_r: typeof import('../plugin/ui/fly'); chase_r: typeof import('../plugin/chase/chase'); - // webglUtils: typeof import('../plugin/webgl/utils'); - // shadow_r: typeof import('../plugin/shadow/shadow'); - // gameShadow_r: typeof import('../plugin/shadow/gameShadow'); - // achievement: typeof import('../plugin/ui/achievement'); completion_r: typeof import('../plugin/completion'); - // path: typeof import('../plugin/fx/path'); gameCanvas_r: typeof import('../plugin/fx/gameCanvas'); - // noise: typeof import('../plugin/fx/noise'); - smooth_r: typeof import('../plugin/fx/smoothView'); frag_r: typeof import('../plugin/fx/frag'); // 游戏进程定义的插件 utils_g: typeof import('../plugin/game/utils'); @@ -174,12 +168,9 @@ interface PluginInterface { chase_g: typeof import('../plugin/game/chase'); skill_g: typeof import('../plugin/game/skill'); towerBoss_g: typeof import('../plugin/game/towerBoss'); - // heroFourFrames_g: typeof import('../plugin/game/fx/heroFourFrames'); rewrite_g: typeof import('../plugin/game/fx/rewrite'); itemDetail_g: typeof import('../plugin/game/fx/itemDetail'); checkBlock_g: typeof import('../plugin/game/enemy/checkblock'); - // halo_g: typeof import('../plugin/game/fx/halo'); - // study_g: typeof import('../plugin/game/study'); } interface PackageInterface { diff --git a/src/plugin/game/fallback.ts b/src/plugin/game/fallback.ts index 887f91a..40a9e92 100644 --- a/src/plugin/game/fallback.ts +++ b/src/plugin/game/fallback.ts @@ -5,7 +5,7 @@ import type { LayerFloorBinder } from '@/core/render/preset/floor'; import type { HeroRenderer } from '@/core/render/preset/hero'; -import type { Layer } from '@/core/render/preset/layer'; +import type { Layer, LayerGroup } from '@/core/render/preset/layer'; import type { TimingFn } from 'mutate-animate'; import { BlockMover, heroMoveCollection, MoveStep } from '@/game/state/move'; import type { FloorViewport } from '@/core/render/preset/viewport'; @@ -85,6 +85,11 @@ export function init() { } Mota.r(() => { + // ----- 引入 + const Camera = Mota.require('module', 'Render').Camera; + const Renderer = Mota.require('module', 'Render').MotaRenderer; + const Animation = Mota.require('module', 'Animation'); + // ----- 勇士移动相关 control.prototype.moveAction = async function (callback?: () => void) { heroMover.clearMoveQueue(); @@ -578,6 +583,79 @@ export function init() { if (success) adapters.viewport?.all('mutateTo', destX, destY); return success; }; + + control.prototype.moveViewport = function ( + x: number, + y: number, + _moveMode: EaseMode, + time: number = 0, + callback?: () => void + ) { + const main = Renderer.get('render-main'); + const layer = main?.getElementById('layer-main') as LayerGroup; + if (!layer) return; + const camera = Camera.for(layer); + camera.clearOperation(); + const translate = camera.addTranslate(); + + const animateTime = time / Math.max(core.status.replay.speed, 1); + const animate = new Animation.Animation(); + animate + .absolute() + .time(1) + .mode(Animation.linear()) + .move(-core.bigmap.offsetX, -core.bigmap.offsetY); + animate.time(animateTime).move(-x * 32, -y * 32); + + camera.applyTranslateAnimation( + translate, + animate, + animateTime + 50 + ); + camera.transform = layer.camera; + + const timeout = window.setTimeout(() => { + core.bigmap.offsetX = x * 32; + core.bigmap.offsetY = y * 32; + callback?.(); + }, animateTime + 50); + + // time /= Math.max(core.status.replay.speed, 1); + // var per_time = 10, + // step = 0, + // steps = parseInt(time / per_time); + // if (steps <= 0) { + // this.setViewport(32 * x, 32 * y); + // if (callback) callback(); + // return; + // } + // var px = core.clamp(32 * x, 0, 32 * core.bigmap.width - core._PX_); + // var py = core.clamp(32 * y, 0, 32 * core.bigmap.height - core._PY_); + // var cx = core.bigmap.offsetX; + // var cy = core.bigmap.offsetY; + // var moveFunc = core.applyEasing(moveMode); + + // var animate = window.setInterval(function () { + // step++; + // core.setViewport( + // cx + moveFunc(step / steps) * (px - cx), + // cy + moveFunc(step / steps) * (py - cy) + // ); + // if (step == steps) { + // delete core.animateFrame.asyncId[animate]; + // clearInterval(animate); + // core.setViewport(px, py); + // if (callback) callback(); + // } + // }, per_time); + + const id = fallbackIds++; + core.animateFrame.lastAsyncId = id; + core.animateFrame.asyncId[id] = () => { + callback?.(); + clearTimeout(timeout); + }; + }; }); loading.once('loaded', () => {