From 2f8fbd375c4bb8811bfb77beed3999f5b98b792b Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Thu, 9 May 2024 23:49:53 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E7=9A=84=E6=B8=B2=E6=9F=93?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/libs/control.js | 118 +++++-------------- public/libs/events.js | 8 ++ public/project/data.js | 11 +- public/project/floors/MT59.js | 8 +- public/project/floors/MT69.js | 4 +- public/project/floors/MT73.js | 7 ++ public/project/floors/MT74.js | 47 +++++--- public/project/floors/MT75.js | 40 ++++--- public/project/functions.js | 21 ---- public/project/items.js | 2 +- public/styles.css | 1 + src/core/fx/canvas2d.ts | 12 +- src/core/fx/shadow.ts | 14 +-- src/core/index.ts | 23 ++++ src/core/interface.ts | 2 + src/core/main/init/toolbar.tsx | 6 +- src/core/main/setting.ts | 5 +- src/core/render/camera.ts | 184 ++++++++++++++++++++++++++++++ src/core/render/container.ts | 61 ++++++++++ src/core/render/hero.ts | 174 ++++++++++++++++++++++++++++ src/core/render/item.ts | 200 +++++++++++++++++++++++++++++++++ src/core/render/preset/misc.ts | 123 ++++++++++++++++++++ src/core/render/render.ts | 142 +++++++++++++++++++++++ src/core/render/sprite.ts | 41 +++++++ src/game/game.ts | 19 +++- src/game/mechanism/misc.ts | 29 +++++ src/game/system.ts | 14 ++- src/plugin/pop.ts | 2 +- src/plugin/use.ts | 7 ++ src/plugin/utils.ts | 3 - src/types/control.d.ts | 10 +- src/types/core.d.ts | 2 + src/types/status.d.ts | 6 + src/ui/shop.vue | 6 +- 34 files changed, 1173 insertions(+), 179 deletions(-) create mode 100644 src/core/render/camera.ts create mode 100644 src/core/render/container.ts create mode 100644 src/core/render/hero.ts create mode 100644 src/core/render/item.ts create mode 100644 src/core/render/preset/misc.ts create mode 100644 src/core/render/render.ts create mode 100644 src/core/render/sprite.ts diff --git a/public/libs/control.js b/public/libs/control.js index 06d878d..eb28c96 100644 --- a/public/libs/control.js +++ b/public/libs/control.js @@ -802,11 +802,19 @@ control.prototype.setHeroMoveInterval = function (callback) { if (core.status.replay.speed > 6) toAdd = 4; if (core.status.replay.speed > 12) toAdd = 8; + Mota.r(() => { + const render = Mota.require('module', 'Render').heroRender; + render.move(true); + }); + core.interval.heroMoveInterval = window.setInterval(function () { + render.offset += toAdd * 4; core.status.heroMoving += toAdd; if (core.status.heroMoving >= 8) { clearInterval(core.interval.heroMoveInterval); core.status.heroMoving = 0; + render.offset = 0; + render.move(false); if (callback) callback(); } }, ((core.values.moveSpeed / 8) * toAdd) / core.status.replay.speed); @@ -1003,22 +1011,32 @@ control.prototype.tryMoveDirectly = function (destX, destY) { }; ////// 绘制勇士 ////// -control.prototype.drawHero = function (status, offset, frame) { +control.prototype.drawHero = function (status, offset = 0, frame) { if (!core.isPlaying() || !core.status.floorId || core.status.gameOver) return; + if (main.mode === 'play') { + Mota.require('module', 'Render').heroRender.draw(); + if (!core.hasFlag('__lockViewport__')) { + const { x, y, direction } = core.status.hero.loc; + var way = core.utils.scan2[direction]; + var dx = way.x, + dy = way.y; + var offsetX = + typeof offset == 'number' ? dx * offset : offset.x || 0; + var offsetY = + typeof offset == 'number' ? dy * offset : offset.y || 0; + offset = { x: offsetX, y: offsetY, offset: offset }; + this._drawHero_updateViewport(x, y, offset); + } + return; + } + var x = core.getHeroLoc('x'), y = core.getHeroLoc('y'), direction = core.getHeroLoc('direction'); status = status || 'stop'; if (!offset) offset = 0; - var way = core.utils.scan2[direction]; - var dx = way.x, - dy = way.y; - var offsetX = typeof offset == 'number' ? dx * offset : offset.x || 0; - var offsetY = typeof offset == 'number' ? dy * offset : offset.y || 0; - offset = { x: offsetX, y: offsetY, offset: offset }; - core.clearAutomaticRouteNode(x + dx, y + dy); core.clearMap('hero'); core.status.heroCenter.px = 32 * x + offsetX + 16; @@ -1030,9 +1048,6 @@ control.prototype.drawHero = function (status, offset, frame) { delete core.canvas.hero._px; delete core.canvas.hero._py; core.status.preview.enabled = false; - if (!core.hasFlag('__lockViewport__')) { - this._drawHero_updateViewport(x, y, offset); - } this._drawHero_draw(direction, x, y, status, offset, frame); }; @@ -3499,75 +3514,6 @@ control.prototype.hideStatusBar = function (showToolbox) { ////// 改变工具栏为按钮1-8 ////// control.prototype.setToolbarButton = function (useButton) { // Deprecated. Use CustomToolbar instead. - return; - if (!core.domStyle.showStatusBar) { - // 隐藏状态栏时检查竖屏 - if (!core.domStyle.isVertical && !core.flags.extendToolbar) { - for (var i = 0; i < core.dom.tools.length; ++i) - core.dom.tools[i].style.display = 'none'; - return; - } - if (!core.hasFlag('showToolbox')) return; - else core.dom.tools.hard.style.display = 'block'; - } - - if (useButton == null) useButton = core.domStyle.toolbarBtn; - if (!core.domStyle.isVertical && !core.flags.extendToolbar) - useButton = false; - core.domStyle.toolbarBtn = useButton; - - if (useButton) { - [ - 'book', - 'fly', - 'toolbox', - 'keyboard', - 'shop', - 'save', - 'load', - 'settings' - ].forEach(function (t) { - core.statusBar.image[t].style.display = 'none'; - }); - [ - 'btn1', - 'btn2', - 'btn3', - 'btn4', - 'btn5', - 'btn6', - 'btn7', - 'btn8' - ].forEach(function (t) { - core.statusBar.image[t].style.display = 'block'; - }); - main.statusBar.image.btn8.style.filter = core.getLocalStorage('altKey') - ? 'sepia(1) contrast(1.5)' - : ''; - } else { - [ - 'btn1', - 'btn2', - 'btn3', - 'btn4', - 'btn5', - 'btn6', - 'btn7', - 'btn8' - ].forEach(function (t) { - core.statusBar.image[t].style.display = 'none'; - }); - ['book', 'fly', 'toolbox', 'save', 'load', 'settings'].forEach( - function (t) { - core.statusBar.image[t].style.display = 'block'; - } - ); - core.statusBar.image.keyboard.style.display = - core.statusBar.image.shop.style.display = - core.domStyle.isVertical || core.flags.extendToolbar - ? 'block' - : 'none'; - } }; ////// ------ resize处理 ------ // @@ -3789,16 +3735,4 @@ control.prototype._resize_toolBar = function (obj) { control.prototype._resize_tools = function (obj) { // Deprecated. Use CustomToolbar instead. - // var toolsHeight = 32 * core.domStyle.scale; - // var toolsMarginLeft; - // toolsMarginLeft = (core._HALF_WIDTH_ - 3) * 3 * core.domStyle.scale; - // for (var i = 0; i < core.dom.tools.length; ++i) { - // var style = core.dom.tools[i].style; - // style.height = toolsHeight + 'px'; - // style.marginLeft = toolsMarginLeft + 'px'; - // style.marginTop = 3 * core.domStyle.scale + 'px'; - // } - // core.dom.hard.style.lineHeight = toolsHeight + 'px'; - // core.dom.hard.style.width = - // obj.outerWidth - 9 * toolsMarginLeft - 8.5 * toolsHeight - 12 + 'px'; }; diff --git a/public/libs/events.js b/public/libs/events.js index 9c15700..59fd0ad 100644 --- a/public/libs/events.js +++ b/public/libs/events.js @@ -843,6 +843,7 @@ events.prototype._changeFloor_afterChange = function (info, callback) { events.prototype.changingFloor = function (floorId, heroLoc) { this.eventdata.changingFloor(floorId, heroLoc); + Mota.require('var', 'hook').emit('changingFloor', floorId, heroLoc); }; ////// 转换楼层结束的事件 ////// @@ -4252,6 +4253,7 @@ events.prototype._vibrate_update = function (shakeInfo) { /////// 使用事件让勇士移动。这个函数将不会触发任何事件 ////// events.prototype.eventMoveHero = function (steps, time, callback) { time = time || core.values.moveSpeed; + // const render = Mota.require('module', 'Render').heroRender; var step = 0, moveSteps = (steps || []) .map(function (t) { @@ -4275,9 +4277,11 @@ events.prototype.eventMoveHero = function (steps, time, callback) { ); }); core.status.heroMoving = -1; + // render.move(false); var _run = function () { var cb = function () { core.status.heroMoving = 0; + // render.move(false); core.drawHero(); if (callback) callback(); }; @@ -4339,15 +4343,18 @@ events.prototype.jumpHero = function (ex, ey, time, callback) { }; events.prototype._jumpHero_doJump = function (jumpInfo, callback) { + // const render = Mota.require('module', 'Render').heroRender; var cb = function () { core.setHeroLoc('x', jumpInfo.ex); core.setHeroLoc('y', jumpInfo.ey); + render.move(false); core.status.heroMoving = 0; core.drawHero(); if (callback) callback(); }; core.status.heroMoving = -1; + // render.move(false); var animate = window.setInterval(function () { if (jumpInfo.jump_count > 0) core.events._jumpHero_jumping(jumpInfo); else { @@ -4386,6 +4393,7 @@ events.prototype.setHeroIcon = function (name, noDraw) { core.material.images.hero = img; core.material.icons.hero.width = img.width / 4; core.material.icons.hero.height = img.height / 4; + // Mota.require('module', 'Render').heroRender.setHero(); if (!noDraw) core.drawHero(); }; diff --git a/public/project/data.js b/public/project/data.js index 48eaa9b..5bdac78 100644 --- a/public/project/data.js +++ b/public/project/data.js @@ -738,28 +738,29 @@ var data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d = }, { "need": "100000", - "title": "中级智人", + "title": "高级智人", "clear": true, "action": [ { "type": "setValue", "name": "status:mdef", "operator": "+=", - "value": "2000" + "value": "10000" }, { "type": "setValue", "name": "status:atk", "operator": "+=", - "value": "25" + "value": "250" }, { "type": "setValue", "name": "status:def", "operator": "+=", - "value": "25" + "value": "250" }, - "恭喜升级!攻防+25,智慧+2000!" + "恭喜升级!攻防+250,智慧+10000!", + "这是这个游戏的最后一级,第三章的玩法会改变,不再设置等级" ] } ] diff --git a/public/project/floors/MT59.js b/public/project/floors/MT59.js index b2b54c4..15d099d 100644 --- a/public/project/floors/MT59.js +++ b/public/project/floors/MT59.js @@ -16,7 +16,11 @@ main.floors.MT59= "firstArrive": [], "eachArrive": [], "parallelDo": "", - "events": {}, + "events": { + "12,5": [ + "注意这个boss现在不是必打的,只要能打过去苍蓝殿上方区域的那个黄卫兵就能去上面的区域了,之后回头再来清这个boss也可以" + ] + }, "changeFloor": { "14,7": { "floorId": "MT58", @@ -113,7 +117,7 @@ main.floors.MT59= [648,491, 28,491,648, 0,648, 0,648,648,648,648, 0, 29,648], [648, 27,468, 27,648,487,648, 21,648, 27, 0,648, 27, 0,648], [648, 0, 28, 0, 85, 0,648,487,648, 0, 29,657, 0, 28,648], - [648,648,648,648,648,390,648,390,648,484,648,648,648,648,648], + [648,648,648,648,648,390,648,390,648,484,648,648,129,648,648], [648,482, 0,482,648, 0,249, 0,648,539,648,482, 0,482,648], [648, 0,666, 0,492,403,648,648,648, 0,492, 0,381, 0, 94], [648,482, 0,482,648, 0,249, 0,648,539,648,482, 0,482,648], diff --git a/public/project/floors/MT69.js b/public/project/floors/MT69.js index 3a282dc..7815747 100644 --- a/public/project/floors/MT69.js +++ b/public/project/floors/MT69.js @@ -21,10 +21,10 @@ main.floors.MT69= "提示一个本地图的较优解法:从此处直接向右走,然后反过来把红骑士杀了,就基本上没光环了" ], "2,7": [ - "中间的那个木牌会提示一种较优解法" + "不要被这一层吓到了(,实际上光环只有四个黄光环,其他的都是黄光环产生的。如果实在想不到解法,中间的那个木牌会提示一种较优解法" ], "6,13": [ - "中间的那个木牌会提示一种较优解法" + "不要被这一层吓到了(,实际上光环只有四个黄光环,其他的都是黄光环产生的。如果实在想不到解法,中间的那个木牌会提示一种较优解法" ] }, "changeFloor": { diff --git a/public/project/floors/MT73.js b/public/project/floors/MT73.js index 678fbdf..2620309 100644 --- a/public/project/floors/MT73.js +++ b/public/project/floors/MT73.js @@ -29,6 +29,13 @@ main.floors.MT73= 7, 0 ] + }, + "7,0": { + "floorId": "MT74", + "loc": [ + 7, + 14 + ] } }, "beforeBattle": {}, diff --git a/public/project/floors/MT74.js b/public/project/floors/MT74.js index 7b896fe..8bc597f 100644 --- a/public/project/floors/MT74.js +++ b/public/project/floors/MT74.js @@ -17,7 +17,22 @@ main.floors.MT74= "eachArrive": [], "parallelDo": "", "events": {}, - "changeFloor": {}, + "changeFloor": { + "7,0": { + "floorId": "MT75", + "loc": [ + 7, + 14 + ] + }, + "7,14": { + "floorId": "MT73", + "loc": [ + 7, + 0 + ] + } + }, "beforeBattle": {}, "afterBattle": {}, "afterGetItem": {}, @@ -26,21 +41,21 @@ main.floors.MT74= "cannotMove": {}, "cannotMoveIn": {}, "map": [ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0,103, 0, 0, 0,103, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0,103, 0, 0, 0,103, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + [648,648,648,648,648,648,648, 91,648,648,648,648,648,648,648], + [648,430,472,420,648, 0, 0, 0, 0, 0,648,420,472,430,648], + [648,648, 85,648,648, 0, 0, 0, 0, 0,648,648, 85,648,648], + [648, 0, 0, 0,648, 0, 0, 0, 0, 0,648, 0, 0, 0,648], + [648, 0,487, 0,648, 0, 0, 0, 0, 0,648, 0,487, 0,648], + [648, 0, 0, 0,648,103, 0, 0, 0,103,648, 0, 0, 0,648], + [648,648,492,648,648, 0, 0, 0, 0, 0,648,648,492,648,648], + [ 92, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 94], + [648,648,492,648,648, 0, 0, 0, 0, 0,648,648,492,648,648], + [648, 0, 0, 0,648,103, 0, 0, 0,103,648, 0, 0, 0,648], + [648, 0,487, 0,648, 0, 0, 0, 0, 0,648, 0,487, 0,648], + [648, 0, 0, 0,648, 0, 0, 0, 0, 0,648, 0, 0, 0,648], + [648,648, 85,648,648, 0, 0, 0, 0, 0,648,648, 85,648,648], + [648,430,472,420,648, 0, 0, 0, 0, 0,648,420,472,430,648], + [648,648,648,648,648,648,648, 93,648,648,648,648,648,648,648] ], "bgmap": [ [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], diff --git a/public/project/floors/MT75.js b/public/project/floors/MT75.js index 423cf71..1d58b36 100644 --- a/public/project/floors/MT75.js +++ b/public/project/floors/MT75.js @@ -17,7 +17,15 @@ main.floors.MT75= "eachArrive": [], "parallelDo": "", "events": {}, - "changeFloor": {}, + "changeFloor": { + "7,14": { + "floorId": "MT74", + "loc": [ + 7, + 0 + ] + } + }, "beforeBattle": {}, "afterBattle": {}, "afterGetItem": {}, @@ -26,21 +34,21 @@ main.floors.MT75= "cannotMove": {}, "cannotMoveIn": {}, "map": [ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + [648,648,648,648,648,648,648,648,648,648,648,648,648,648,648], + [648,491,491,494,491,494, 0, 0, 0,494,491,494,491,491,648], + [648,648,648,648,648,648, 0, 0, 0,648,648,648,648,648,648], + [648,484,484,492,484,492, 0, 0, 0,492,484,492,484,484,648], + [648,648,648,648,648,648, 0, 0, 0,648,648,648,648,648,648], + [648, 0, 0, 0, 0,103, 0, 0, 0,103, 0, 0, 0, 0,648], + [648, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,648], + [ 92, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 94], + [648, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,648], + [648, 0, 0, 0, 0,103, 0, 0, 0,103, 0, 0, 0, 0,648], + [648,648,648,648,648,648, 0, 0, 0,648,648,648,648,648,648], + [648,484,484,492,484,492, 0, 0, 0,492,484,492,484,484,648], + [648,648,648,648,648,648, 0, 0, 0,648,648,648,648,648,648], + [648,491,491,494,491,494, 0, 0, 0,494,491,494,491,491,648], + [648,648,648,648,648,648,648, 93,648,648,648,648,648,648,648] ], "bgmap": [ [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], diff --git a/public/project/functions.js b/public/project/functions.js index 1681f9d..a2dfe1e 100644 --- a/public/project/functions.js +++ b/public/project/functions.js @@ -226,27 +226,6 @@ var functions_d6ad677b_427a_4623_b50f_a445a3b0ef8a = { if (core.flags.flyRecordPosition) { loc = core.getFlag('__leaveLoc__', {})[toId] || null; } - if ( - core.status.maps[toId].flyPoint != null && - core.status.maps[toId].flyPoint.length == 2 - ) { - loc = { - x: core.status.maps[toId].flyPoint[0], - y: core.status.maps[toId].flyPoint[1] - }; - } - if (loc == null) { - // 获得两个楼层的索引,以决定是上楼梯还是下楼梯 - var fromIndex = core.floorIds.indexOf(fromId), - toIndex = core.floorIds.indexOf(toId); - var stair = fromIndex <= toIndex ? 'downFloor' : 'upFloor'; - // 地下层:同层传送至上楼梯 - if ( - fromIndex == toIndex && - core.status.maps[fromId].underGround - ) - stair = 'upFloor'; - } // 记录录像 core.status.route.push('fly:' + toId); diff --git a/public/project/items.js b/public/project/items.js index da67db9..72d7201 100644 --- a/public/project/items.js +++ b/public/project/items.js @@ -1048,7 +1048,7 @@ var items_296f5d02_12fd_4166_a7c1_b5e830c9ee3a = }, "I472": { "cls": "items", - "name": "新物品", + "name": "传奇绿宝石", "text": ",防御+${core.values.blueGem}", "itemEffect": "core.status.hero.mdef += Math.round(640 * core.status.thisMap.ratio / core.getFlag(\"hard\") * (Mota.Plugin.require('skillTree_g').getSkillLevel(12) / 20 + 1))", "itemEffectTip": ",智慧+${Math.round(640 * core.status.thisMap.ratio / core.getFlag(\"hard\") * (Mota.Plugin.require('skillTree_g').getSkillLevel(12) / 20 + 1))}", diff --git a/public/styles.css b/public/styles.css index 03c3ed0..bf51619 100644 --- a/public/styles.css +++ b/public/styles.css @@ -392,6 +392,7 @@ p#name { } #hero { + display: none; z-index: 40; } diff --git a/src/core/fx/canvas2d.ts b/src/core/fx/canvas2d.ts index 18a6868..cc11370 100644 --- a/src/core/fx/canvas2d.ts +++ b/src/core/fx/canvas2d.ts @@ -20,6 +20,8 @@ export class MotaOffscreenCanvas2D extends EventEmitter { /** 是否是高清画布 */ highResolution: boolean = true; + scale: number = 1; + constructor() { super(); @@ -41,6 +43,7 @@ export class MotaOffscreenCanvas2D extends EventEmitter { if (this.autoScale && this.highResolution) { ratio *= core.domStyle.scale; } + this.scale = ratio; this.canvas.width = width * ratio; this.canvas.height = height * ratio; this.width = width; @@ -82,7 +85,13 @@ export class MotaOffscreenCanvas2D extends EventEmitter { newCanvas.setHD(canvas.highResolution); newCanvas.withGameScale(canvas.autoScale); newCanvas.size(canvas.width, canvas.height); - newCanvas.ctx.drawImage(canvas.canvas, 0, 0); + newCanvas.ctx.drawImage( + canvas.canvas, + 0, + 0, + canvas.width, + canvas.height + ); return newCanvas; } } @@ -144,6 +153,7 @@ export class MotaCanvas2D extends MotaOffscreenCanvas2D { this.canvas.style.width = `${width}px`; this.canvas.style.height = `${height}px`; } + this.scale = ratio; this.canvas.width = width * ratio; this.canvas.height = height * ratio; this.width = width; diff --git a/src/core/fx/shadow.ts b/src/core/fx/shadow.ts index 1856c69..800f25e 100644 --- a/src/core/fx/shadow.ts +++ b/src/core/fx/shadow.ts @@ -114,18 +114,18 @@ Mota.require('var', 'hook').once('reset', () => { Shadow.update(true); } }); - Mota.rewrite(core.events, 'changingFloor', 'add', (_, floorId) => { - if (!main.replayChecking) { - Shadow.clearBuffer(); - Shadow.update(); - setCanvasFilterByFloorId(floorId); - } - }); Mota.rewrite(core.control, 'loadData', 'add', () => { if (!main.replayChecking) { Shadow.update(true); } }); + Mota.require('var', 'hook').on('changingFloor', (floorId) => { + if (!main.replayChecking) { + Shadow.clearBuffer(); + Shadow.update(); + setCanvasFilterByFloorId(floorId); + } + }) }); // 深度测试着色器 diff --git a/src/core/index.ts b/src/core/index.ts index c3ce985..b68078b 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -60,6 +60,15 @@ import { ResourceController } from './loader/controller'; import { logger } from './common/logger'; import { Danmaku } from './main/custom/danmaku'; import * as Shadow from './fx/shadow'; +import { MotaCanvas2D } from './fx/canvas2d'; +import * as portal from './fx/portal'; +import { heroRender } from './render/hero'; +import { MotaRenderer } from './render/render'; +import { Container } from './render/container'; +import { Sprite } from './render/sprite'; +import { Camera } from './render/camera'; +import { Image, Text } from './render/preset/misc'; +import { RenderItem } from './render/item'; // ----- 类注册 Mota.register('class', 'AudioPlayer', AudioPlayer); @@ -78,6 +87,7 @@ Mota.register('class', 'UiController', UiController); Mota.register('class', 'MComponent', MComponent); Mota.register('class', 'ResourceController', ResourceController); Mota.register('class', 'Danmaku', Danmaku); +Mota.register('class', 'MotaCanvas2D', MotaCanvas2D); // ----- 函数注册 Mota.register('fn', 'm', m); Mota.register('fn', 'unwrapBinary', unwarpBinary); @@ -133,6 +143,19 @@ Mota.register('module', 'UIComponents', { }); Mota.register('module', 'MCGenerator', MCGenerator); Mota.register('module', 'Shadow', Shadow); +Mota.register('module', 'Effect', { + Portal: portal +}); +Mota.register('module', 'Render', { + heroRender, + MotaRenderer, + Container, + Sprite, + Camera, + Text, + Image, + RenderItem +}); main.renderLoaded = true; Mota.require('var', 'hook').emit('renderLoaded'); diff --git a/src/core/interface.ts b/src/core/interface.ts index bde8d78..364d8a5 100644 --- a/src/core/interface.ts +++ b/src/core/interface.ts @@ -17,3 +17,5 @@ export interface ResponseBase { code: number; message: string; } + +export type CSSObj = Partial>; diff --git a/src/core/main/init/toolbar.tsx b/src/core/main/init/toolbar.tsx index 31047ff..f59bd00 100644 --- a/src/core/main/init/toolbar.tsx +++ b/src/core/main/init/toolbar.tsx @@ -27,6 +27,8 @@ import { FolderOpenOutlined, LayoutOutlined, MessageOutlined, + RetweetOutlined, + RollbackOutlined, SwapOutlined } from '@ant-design/icons-vue'; import { generateKeyboardEvent } from '../custom/keyboard'; @@ -888,7 +890,7 @@ Mota.require('var', 'hook').once('reset', () => { () => { core.doSL('autoSave', 'load'); }, - h(BackwardOutlined) + h(RollbackOutlined) ); CustomToolbar.misc.register( 'redo', @@ -896,7 +898,7 @@ Mota.require('var', 'hook').once('reset', () => { () => { core.doSL('autoSave', 'reload'); }, - h(SwapOutlined) + h(RetweetOutlined) ); CustomToolbar.misc.register( 'setting', diff --git a/src/core/main/setting.ts b/src/core/main/setting.ts index 346b457..748b327 100644 --- a/src/core/main/setting.ts +++ b/src/core/main/setting.ts @@ -474,6 +474,7 @@ mainSetting new MotaSetting() .register('paraLight', '野外阴影', true, COM.Boolean) .register('frag', '打怪特效', true, COM.Boolean) + .register('portalParticle', '传送门特效', true, COM.Boolean) ) .register( 'ui', @@ -512,6 +513,7 @@ loading.once('coreInit', () => { 'utils.autoScale': !!storage.getValue('utils.autoScale', true), 'fx.paraLight': !!storage.getValue('fx.paraLight', true), 'fx.frag': !!storage.getValue('fx.frag', true), + 'fx.portalParticle': !!storage.getValue('fx.portalParticle', true), 'ui.mapScale': storage.getValue( 'ui.mapScale', isMobile ? 300 : Math.floor(window.innerWidth / 600) * 50 @@ -565,7 +567,8 @@ mainSetting .setDescription('ui.danmaku', '是否显示弹幕') .setDescription('ui.danmakuSpeed', '弹幕速度,刷新或开关弹幕显示后起效') .setDescription('screen.fontSizeStatus', `修改状态栏的字体大小`) - .setDescription('screen.blur', '打开任意ui界面时是否有背景虚化效果,移动端打开后可能会有掉帧或者发热现象。关闭ui后生效'); + .setDescription('screen.blur', '打开任意ui界面时是否有背景虚化效果,移动端打开后可能会有掉帧或者发热现象。关闭ui后生效') + .setDescription('fx.portalParticle', '是否启用苍蓝之殿的传送门粒子特效,启用后可能对性能及设备发热有所影响'); function setFontSize() { const absoluteSize = storage.getValue( diff --git a/src/core/render/camera.ts b/src/core/render/camera.ts new file mode 100644 index 0000000..bdf1b38 --- /dev/null +++ b/src/core/render/camera.ts @@ -0,0 +1,184 @@ +import { ReadonlyMat3, ReadonlyVec3, mat3, vec2, vec3 } from 'gl-matrix'; +import { EmitableEvent, EventEmitter } from '../common/eventEmitter'; + +interface CameraEvent extends EmitableEvent {} + +export class Camera extends EventEmitter { + mat: mat3 = mat3.create(); + + x: number = 0; + y: number = 0; + scaleX: number = 1; + scaleY: number = 1; + rad: number = 0; + + /** + * 重设摄像机的所有参数 + */ + reset() { + mat3.identity(this.mat); + this.x = 0; + this.y = 0; + this.scaleX = 1; + this.scaleY = 1; + this.rad = 0; + } + + /** + * 修改摄像机的缩放,叠加关系 + */ + scale(x: number, y: number = x) { + mat3.scale(this.mat, this.mat, [x, y]); + this.scaleX *= x; + this.scaleY *= y; + } + + /** + * 移动摄像机,叠加关系 + */ + move(x: number, y: number) { + mat3.translate(this.mat, this.mat, [x, y]); + this.x += x; + this.y += y; + } + + /** + * 旋转摄像机,叠加关系 + */ + rotate(rad: number) { + mat3.rotate(this.mat, this.mat, rad); + this.rad += rad; + if (this.rad >= Math.PI * 2) { + const n = Math.floor(this.rad / Math.PI / 2); + this.rad -= n * Math.PI * 2; + } + } + + /** + * 设置摄像机的缩放,非叠加关系 + */ + setScale(x: number, y: number = x) { + mat3.scale(this.mat, this.mat, [x / this.scaleX, y / this.scaleY]); + this.scaleX = x; + this.scaleY = y; + } + + /** + * 设置摄像机的位置,非叠加关系 + */ + setTranslate(x: number, y: number) { + mat3.translate(this.mat, this.mat, [x - this.x, y - this.y]); + this.x = x; + this.y = y; + } + + /** + * 设置摄像机的旋转,非叠加关系 + */ + setRotate(rad: number) { + mat3.rotate(this.mat, this.mat, rad - this.rad); + this.rad = rad; + } + + /** + * 设置摄像机的变换矩阵,叠加模式 + * @param a 水平缩放 + * @param b 垂直倾斜 + * @param c 水平倾斜 + * @param d 垂直缩放 + * @param e 水平移动 + * @param f 垂直移动 + */ + transform( + a: number, + b: number, + c: number, + d: number, + e: number, + f: number + ) { + mat3.multiply( + this.mat, + this.mat, + mat3.fromValues(a, b, 0, c, d, 0, e, f, 1) + ); + this.calAttributes(); + } + + /** + * 设置摄像机的变换矩阵,非叠加模式 + * @param a 水平缩放 + * @param b 垂直倾斜 + * @param c 水平倾斜 + * @param d 垂直缩放 + * @param e 水平移动 + * @param f 垂直移动 + */ + setTransform( + a: number, + b: number, + c: number, + d: number, + e: number, + f: number + ) { + mat3.set(this.mat, a, b, 0, c, d, 0, e, f, 1); + this.calAttributes(); + } + + /** + * 重新计算 translation scaling rotation + */ + calAttributes() { + const [x, y] = getTranslation(this.mat); + const [scaleX, scaleY] = getScaling(this.mat); + const rad = getRotation(this.mat); + this.x = x; + this.y = y; + this.scaleX = scaleX; + this.scaleY = scaleY; + this.rad = rad; + } + + /** + * 根据摄像机的信息,将一个点转换为计算后的位置 + * @param camera 摄像机 + * @param x 横坐标 + * @param y 纵坐标 + */ + static transformed(camera: Camera, x: number, y: number) { + return multiplyVec3(camera.mat, [x, y, 1]); + } + + /** + * 根据摄像机的信息,将一个计算后的位置逆转换为原位置 + * @param camera 摄像机 + * @param x 横坐标 + * @param y 纵坐标 + */ + static untransformed(camera: Camera, x: number, y: number) { + const invert = mat3.create(); + mat3.invert(invert, camera.mat); + return multiplyVec3(invert, [x, y, 1]); + } +} + +function multiplyVec3(mat: ReadonlyMat3, vec: ReadonlyVec3) { + return vec3.fromValues( + mat[0] * vec[0] + mat[3] * vec[1] + mat[6] * vec[2], + mat[1] * vec[0] + mat[4] * vec[1] + mat[7] * vec[2], + mat[2] * vec[0] + mat[5] * vec[1] + mat[8] * vec[2] + ); +} + +function getTranslation(mat: ReadonlyMat3): vec2 { + return [mat[6], mat[7]]; +} + +function getScaling(mat: ReadonlyMat3): vec2 { + return [Math.hypot(mat[0], mat[3]), Math.hypot(mat[1], mat[4])]; +} + +function getRotation(mat: ReadonlyMat3): number { + return Math.atan2(mat[3], mat[0]); +} diff --git a/src/core/render/container.ts b/src/core/render/container.ts new file mode 100644 index 0000000..65e275a --- /dev/null +++ b/src/core/render/container.ts @@ -0,0 +1,61 @@ +import { MotaOffscreenCanvas2D } from '../fx/canvas2d'; +import { Camera } from './camera'; +import { + ICanvasCachedRenderItem, + RenderItem, + RenderItemPosition, + withCacheRender +} from './item'; + +export class Container extends RenderItem implements ICanvasCachedRenderItem { + children: RenderItem[] = []; + sortedChildren: RenderItem[] = []; + + canvas: MotaOffscreenCanvas2D; + + constructor(type: RenderItemPosition = 'static') { + super(); + this.canvas = new MotaOffscreenCanvas2D(); + this.type = type; + this.canvas.withGameScale(true); + } + + render( + canvas: HTMLCanvasElement, + ctx: CanvasRenderingContext2D, + camera: Camera + ): void { + this.emit('beforeUpdate', this); + withCacheRender(this, canvas, ctx, camera, c => { + this.sortedChildren.forEach(v => { + v.render(c.canvas, c.ctx, camera); + }); + }); + this.emit('afterUpdate', this); + } + + size(width: number, height: number) { + this.width = width; + this.height = height; + this.canvas.size(width, height); + this.writing = this.using; + this.update(this); + } + + pos(x: number, y: number) { + this.x = x; + this.y = y; + } + + /** + * 添加子元素到这个容器上,然后在下一个tick执行更新 + * @param children 要添加的子元素 + */ + appendChild(children: RenderItem[]) { + children.forEach(v => (v.parent = this)); + this.children.push(...children); + this.sortedChildren = this.children + .slice() + .sort((a, b) => a.zIndex - b.zIndex); + } +} diff --git a/src/core/render/hero.ts b/src/core/render/hero.ts new file mode 100644 index 0000000..292136b --- /dev/null +++ b/src/core/render/hero.ts @@ -0,0 +1,174 @@ +import { Ticker } from 'mutate-animate'; +import { MotaCanvas2D } from '../fx/canvas2d'; +import { EmitableEvent, EventEmitter } from '../common/eventEmitter'; +import { debounce } from 'lodash-es'; + +// 重写样板的勇士绘制 + +const canvas = MotaCanvas2D.for('@hero', false); + +Mota.require('var', 'loading').once('coreInit', () => { + canvas.withGameScale(true); + canvas.setHD(false); + canvas.size(480, 480); + canvas.pos(0, 0); + canvas.css(`z-index: 40`); + canvas.canvas.classList.add('no-anti-aliasing'); + canvas.setTarget(core.dom.gameDraw); + canvas.mount(); +}); + +const DIR_INDEX: Record = { + down: 0, + left: 1, + right: 2, + up: 3 +}; + +interface HeroDrawItem { + id: AllIds | 'hero'; + image: HTMLCanvasElement; +} + +interface HeroDrawing extends HeroDrawItem { + x: number; + y: number; + dir: Dir; +} + +interface HeroRendererEvent extends EmitableEvent { + beforeDraw: () => void; + afterDraw: () => void; +} + +const resetMoving = debounce((render: HeroRenderer) => { + render.moving = 0; +}, 500); + +export class HeroRenderer extends EventEmitter { + followers: HeroDrawItem[] = []; + hero!: HeroDrawItem; + + /** 移动状态 */ + moving: number = 0; + /** 移动过程中的偏移 */ + offset: number = 0; + /** 勇士绘制时的alpha通道 */ + alpha: number = 1; + + ticker: Ticker = new Ticker(); + + /** 是否是后退状态 */ + private back: boolean = false; + /** 是否正在移动 */ + private isMoving: boolean = false; + /** 上一次换腿(?的时间 */ + private lastMoving: number = 0; + + constructor() { + super(); + + Mota.require('var', 'loading').once('coreInit', () => { + this.ticker.add(time => { + if (core.status.heroMoving < 0 || !this.isMoving) return; + if (time - this.lastMoving > core.values.moveSpeed) { + this.lastMoving = time; + this.moving++; + this.moving %= 4; + } + this.draw(); + }); + }); + } + + /** + * 设置勇士绘制信息 + */ + setHero() { + const image = core.material.images.hero; + const canvas = new MotaCanvas2D(); + canvas.setHD(false); + canvas.size(image.width, image.height); + canvas.ctx.drawImage(image, 0, 0); + this.hero = { + id: 'hero', + image: canvas.canvas + }; + } + + /** + * 执行绘制 + */ + draw() { + if (!core.isPlaying()) return; + const { ctx, canvas: can } = canvas; + const { x, y, direction: dir } = core.status.hero.loc; + ctx.clearRect(0, 0, can.width, can.height); + ctx.globalAlpha = this.alpha; + + this.emit('beforeDraw'); + + const data: HeroDrawing[] = []; + data.push({ ...this.hero, x, y, dir }); + core.status.hero.followers.forEach((v, i) => { + const { id, image } = this.followers[i]; + data.push({ x: v.x, y: v.y, dir: v.direction, id, image }); + }); + + const { offsetX: ox, offsetY: oy } = core.bigmap; + const sgn = this.back ? -1 : 1; + const offset = this.offset * sgn; + + const frame = this.isMoving ? this.moving % 4 : 0; + + data.forEach(v => { + // hero offset x + const hox = offset * core.utils.scan[v.dir].x; + const hoy = offset * core.utils.scan[v.dir].y; + const cx = v.x * 32 + 16 - ox + hox; + const cy = v.y * 32 + 16 - oy + hoy; + const { width, height } = v.image; + const pw = width / 4; + const ph = height / 4; + const line = DIR_INDEX[v.dir]; + + const px = cx - pw / 2; + const py = cy - ph + 16; + + const sx = frame * pw; + const sy = line * ph; + + ctx.drawImage(v.image, sx, sy, pw, ph, px, py, pw, ph); + }); + + this.emit('afterDraw'); + } + + /** + * 设置绘制状态为正在移动还是静止 + */ + move(moving: boolean) { + this.isMoving = moving; + if (!moving) { + resetMoving(this); + } + } + + /** + * 设置当前是否为后退状态 + */ + backward(back: boolean) { + this.back = back; + } + + /** + * 设置绘制时的alpha + */ + setAlpha(alpha: number) { + this.alpha = alpha; + } +} + +const render = new HeroRenderer(); + +export { render as heroRender }; diff --git a/src/core/render/item.ts b/src/core/render/item.ts new file mode 100644 index 0000000..8a83f18 --- /dev/null +++ b/src/core/render/item.ts @@ -0,0 +1,200 @@ +import { isNil } from 'lodash-es'; +import { EmitableEvent, EventEmitter } from '../common/eventEmitter'; +import { MotaOffscreenCanvas2D } from '../fx/canvas2d'; +import { Camera } from './camera'; + +export type RenderFunction = ( + canvas: MotaOffscreenCanvas2D, + camera: Camera +) => void; + +export type RenderItemPosition = 'absolute' | 'static'; + +interface IRenderCache { + /** 缓存列表 */ + cacheList: Map; + /** 当前正在使用的缓存 */ + using?: string; + /** 下次绘制需要写入的缓存 */ + writing?: string; + + /** + * 在之后的绘制中使用缓存,如果缓存不存在,那么就不使用缓存,并在下一次绘制结束后写入 + * @param index 缓存的索引,不填时表示不使用缓存 + */ + useCache(index?: string): void; + + /** + * 将下次绘制写入缓存并使用 + * @param index 缓存的索引,不填时表示不进行缓存 + */ + cache(index?: string): void; + + /** + * 清除指定或所有缓存 + * @param index 要清除的缓存,不填代表清除所有 + */ + clearCache(index?: string): void; +} + +export interface IRenderUpdater { + /** + * 更新这个渲染元素 + * @param item 触发更新事件的元素,可以是自身触发。如果不填表示手动触发,而非渲染内容发生变化而引起的触发 + */ + update(item?: RenderItem): void; +} + +export interface ICanvasCachedRenderItem { + /** 离屏画布,首先渲染到它上面,然后由Renderer渲染到最终画布上 */ + canvas: MotaOffscreenCanvas2D; +} + +interface IRenderAnchor { + anchorX: number; + anchorY: number; + + /** + * 设置渲染元素的位置锚点 + * @param x 锚点的横坐标,小数,0表示最左边,1表示最右边 + * @param y 锚点的纵坐标,小数,0表示最上边,1表示最下边 + */ + setAnchor(x: number, y: number): void; +} + +interface RenderItemEvent extends EmitableEvent { + beforeUpdate: (item?: RenderItem) => void; + afterUpdate: (item?: RenderItem) => void; +} + +export abstract class RenderItem + extends EventEmitter + implements IRenderCache, IRenderUpdater, IRenderAnchor +{ + zIndex: number = 0; + + x: number = 0; + y: number = 0; + width: number = 200; + height: number = 200; + + cacheList: Map = new Map(); + + using?: string; + writing?: string; + + anchorX: number = 0; + anchorY: number = 0; + + /** 渲染模式,absolute表示绝对位置,static表示跟随摄像机移动,只对顶层元素有效 */ + type: 'absolute' | 'static' = 'static'; + + parent?: RenderItem; + + constructor() { + super(); + + this.using = '@default'; + } + + /** + * 渲染这个对象 + * @param canvas 渲染至的画布 + * @param ctx 渲染至的画布的渲染上下文 + * @param camera 渲染时使用的摄像机 + */ + abstract render( + canvas: HTMLCanvasElement, + ctx: CanvasRenderingContext2D, + camera: Camera + ): void; + + /** + * 修改这个对象的大小 + */ + abstract size(width: number, height: number): void; + + /** + * 修改这个对象的位置 + */ + abstract pos(x: number, y: number): void; + + useCache(index?: string): void { + if (isNil(index)) { + this.using = void 0; + return; + } + if (!this.cacheList.has(index)) { + this.writing = index; + } + this.using = index; + } + + cache(index?: string): void { + this.writing = index; + this.using = index; + } + + clearCache(index?: string): void { + if (isNil(index)) { + this.writing = void 0; + this.using = void 0; + this.cacheList.clear(); + } else { + this.cacheList.delete(index); + } + } + + setAnchor(x: number, y: number): void { + this.anchorX = x; + this.anchorY = y; + } + + update(item?: RenderItem): void { + this.writing = this.using; + this.using = void 0; + this.parent?.update(item); + } +} + +export function withCacheRender( + item: RenderItem & ICanvasCachedRenderItem, + canvas: HTMLCanvasElement, + ctx: CanvasRenderingContext2D, + camera: Camera, + fn: RenderFunction +) { + const { width, height } = item; + const ax = width * item.anchorX; + const ay = height * item.anchorY; + let write = item.writing; + if (!isNil(item.using)) { + const cache = item.cacheList.get(item.using); + if (cache) { + ctx.drawImage( + cache.canvas, + item.x - ax, + item.y - ay, + item.width, + item.height + ); + return; + } + write = item.using; + } + const { canvas: c, ctx: ct } = item.canvas; + ct.clearRect(0, 0, c.width, c.height); + fn(item.canvas, camera); + if (!isNil(write)) { + const cache = item.cacheList.get(write); + if (cache) { + const { canvas, ctx } = cache; + ctx.drawImage(c, 0, 0); + } else { + item.cacheList.set(write, MotaOffscreenCanvas2D.clone(item.canvas)); + } + } + + ctx.drawImage(c, item.x - ax, item.y - ay, item.width, item.height); + item.using = write; +} diff --git a/src/core/render/preset/misc.ts b/src/core/render/preset/misc.ts new file mode 100644 index 0000000..59230f0 --- /dev/null +++ b/src/core/render/preset/misc.ts @@ -0,0 +1,123 @@ +import { Sprite } from '../sprite'; + +type CanvasStyle = string | CanvasGradient | CanvasPattern; + +export class Text extends Sprite { + text: string; + + fillStyle?: CanvasStyle = '#fff'; + strokeStyle?: CanvasStyle; + font?: string = ''; + + private length: number = 0; + private descent: number = 0; + + constructor(text: string = '') { + super(); + + this.text = text; + if (text.length > 0) this.calBox(); + + this.renderFn = ({ canvas, ctx }) => { + ctx.save(); + ctx.textBaseline = 'bottom'; + ctx.fillStyle = this.fillStyle ?? 'transparent'; + ctx.strokeStyle = this.strokeStyle ?? 'transparent'; + ctx.font = this.font ?? ''; + + if (this.fillStyle) { + ctx.fillText(this.text, 0, this.descent); + } + if (this.strokeStyle) { + ctx.strokeText(this.text, 0, this.descent); + } + ctx.restore(); + }; + } + + /** + * 获取文字的长度 + */ + measure() { + this.canvas.ctx.save(); + this.canvas.ctx.textBaseline = 'bottom'; + this.canvas.ctx.font = this.font ?? ''; + const res = this.canvas.ctx.measureText(this.text); + this.canvas.ctx.restore(); + return res; + } + + /** + * 设置显示文字 + * @param text 显示的文字 + */ + setText(text: string) { + this.text = text; + this.writing = this.using; + this.using = void 0; + this.calBox(); + if (this.parent) this.update(this); + } + + /** + * 设置使用的字体 + * @param font 字体 + */ + setFont(font: string) { + this.font = font; + this.calBox(); + if (this.parent) this.update(this); + } + + /** + * 设置字体样式 + * @param fill 填充样式 + * @param stroke 描边样式 + */ + setStyle(fill?: CanvasStyle, stroke?: CanvasStyle) { + this.fillStyle = fill; + this.strokeStyle = stroke; + } + + /** + * 计算字体所占空间,从而确定这个元素的大小 + */ + calBox() { + const { width, fontBoundingBoxAscent } = this.measure(); + this.length = width; + this.descent = fontBoundingBoxAscent; + this.size(width, fontBoundingBoxAscent); + } +} + +type SizedCanvasImageSource = Exclude< + CanvasImageSource, + VideoFrame | SVGElement +>; + +export class Image extends Sprite { + image: SizedCanvasImageSource; + + constructor(image: SizedCanvasImageSource) { + super(); + this.image = image; + this.canvas.withGameScale(false); + this.size(image.width, image.height); + + this.renderFn = ({ canvas, ctx }) => { + ctx.drawImage(this.image, 0, 0); + }; + } + + /** + * 设置图片资源 + * @param image 图片资源 + */ + setImage(image: SizedCanvasImageSource) { + this.image = image; + this.size(image.width, image.height); + this.writing = this.using; + this.using = void 0; + this.update(this); + } +} diff --git a/src/core/render/render.ts b/src/core/render/render.ts new file mode 100644 index 0000000..c74a9ff --- /dev/null +++ b/src/core/render/render.ts @@ -0,0 +1,142 @@ +import { isNil } from 'lodash-es'; +import { MotaCanvas2D, MotaOffscreenCanvas2D } from '../fx/canvas2d'; +import { Camera } from './camera'; +import { Container } from './container'; +import { RenderItem, withCacheRender } from './item'; +import { Image, Text } from './preset/misc'; + +export class MotaRenderer extends Container { + canvas: MotaOffscreenCanvas2D; + camera: Camera; + + /** 摄像机缓存,如果是需要快速切换摄像机的场景,使用缓存可以大幅提升性能表现 */ + cameraCache: Map = new Map(); + target: MotaCanvas2D; + + private needUpdate: boolean = false; + + constructor() { + super(); + + this.canvas = new MotaOffscreenCanvas2D(); + this.camera = new Camera(); + this.target = new MotaCanvas2D(`render-main`); + this.width = 480; + this.height = 480; + this.target.withGameScale(true); + this.target.size(480, 480); + this.canvas.withGameScale(true); + this.canvas.size(480, 480); + this.target.css(`z-index: 100`); + } + + /** + * 使用某个摄像机 + * @param camera 要使用的摄像机 + * @param noCache 是否不使用缓存,当切换至的目标摄像机相比切走时发生了例如位置的变化时,一般需要设置为true, + * 否则会使用上一次被切换走时的缓存 + */ + useCamera(camera: Camera, noCache: boolean = false) { + const cache = MotaOffscreenCanvas2D.clone(this.canvas); + this.cameraCache.set(this.camera, cache); + this.camera = camera; + const nowCache = this.cameraCache.get(camera); + if (!nowCache || !noCache) this.render(); + else this.renderCache(nowCache); + } + + /** + * 删除某个摄像机的画面缓存 + * @param camera 要删除的缓存对应的摄像机 + */ + freeCameraCache(camera: Camera) { + this.cameraCache.delete(camera); + } + + /** + * 渲染游戏画面 + */ + render() { + const { canvas, ctx } = this.target; + const camera = this.camera; + 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]; + const c = mat[3]; + const d = mat[4] * scale; + const e = mat[6]; + const f = mat[7]; + this.sortedChildren.forEach(v => { + if (v.type === 'absolute') { + ct.setTransform(scale, 0, 0, scale, 0, 0); + } else { + ct.setTransform(a, b, c, d, e, f); + } + v.render(ca, ct, camera); + }); + }); + } + + /** + * 更新渲染,在下一个tick更新 + */ + update(item?: RenderItem) { + if (this.needUpdate) return; + this.needUpdate = true; + requestAnimationFrame(() => { + this.writing = this.using; + this.using = void 0; + this.needUpdate = false; + this.emit('beforeUpdate', item); + this.render(); + this.emit('afterUpdate', item); + this.using = this.writing; + }); + } + + /** + * 将缓存内容渲染至画面 + * @param cache 渲染缓存,是一个离屏Canvas2D对象 + */ + renderCache(cache: MotaOffscreenCanvas2D) { + const { canvas, ctx } = this.canvas; + ctx.clearRect(0, 0, canvas.width, canvas.width); + ctx.drawImage(cache.canvas, 0, 0); + } + + /** + * 添加至游戏画面 + */ + mount() { + this.target.mount(); + } +} + +Mota.require('var', 'hook').once('reset', () => { + const render = new MotaRenderer(); + const con = new Container('static'); + const camera = render.camera; + render.mount(); + + const testText = new Text(); + testText.setText('测试测试'); + testText.pos(100, 100); + testText.setFont('32px normal'); + testText.setStyle('#fff'); + con.size(480, 480); + const testImage = new Image(core.material.images.images['arrow.png']); + testImage.pos(200, 200); + + con.appendChild([testText, testImage]); + + render.appendChild([con]); + + render.update(render); + + setTimeout(() => { + testText.setFont('18px normal'); + }, 2000); +}); diff --git a/src/core/render/sprite.ts b/src/core/render/sprite.ts new file mode 100644 index 0000000..f7c3c69 --- /dev/null +++ b/src/core/render/sprite.ts @@ -0,0 +1,41 @@ +import { Camera } from './camera'; +import { RenderFunction, RenderItem, withCacheRender } from './item'; +import { MotaOffscreenCanvas2D } from '../fx/canvas2d'; + +export class Sprite extends RenderItem { + renderFn: RenderFunction; + + canvas: MotaOffscreenCanvas2D; + + constructor() { + super(); + this.renderFn = () => {}; + this.canvas = new MotaOffscreenCanvas2D(); + this.canvas.withGameScale(true); + } + + render( + canvas: HTMLCanvasElement, + ctx: CanvasRenderingContext2D, + camera: Camera + ): void { + this.emit('beforeUpdate', this); + withCacheRender(this, canvas, ctx, camera, canvas => { + this.renderFn(canvas, camera); + }); + this.emit('afterUpdate', this); + } + + size(width: number, height: number) { + this.width = width; + this.height = height; + this.canvas.size(width, height); + this.writing = this.using; + this.update(this); + } + + pos(x: number, y: number) { + this.x = x; + this.y = y; + } +} diff --git a/src/game/game.ts b/src/game/game.ts index fdc1a89..ac03c74 100644 --- a/src/game/game.ts +++ b/src/game/game.ts @@ -1,5 +1,5 @@ import { EmitableEvent, EventEmitter } from '../core/common/eventEmitter'; -import { DamageEnemy } from './enemy/damage'; +import type { DamageEnemy } from './enemy/damage'; // ----- 加载事件 interface GameLoadEvent extends EmitableEvent { @@ -75,25 +75,36 @@ class GameLoading extends EventEmitter { export const loading = new GameLoading(); export interface GameEvent extends EmitableEvent { - /** Emitted in events.prototype.resetGame. */ + /** Emitted in libs/events.js resetGame. */ reset: () => void; /** Emitted in src/App.vue setup. */ mounted: () => void; - /** Emitted in plugin/ui.js */ + /** Emitted in plugin/game/ui.ts updateStatusBar_update */ statusBarUpdate: () => void; /** Emitted in core/index.ts */ renderLoaded: () => void; - // /** Emitted in libs/events.js */ + /** Emitted in libs/events.js getItem */ afterGetItem: ( itemId: AllIdsOf<'items'>, x: number, y: number, isGentleClick: boolean ) => void; + /** Emitted in libs/events.js _openDoor_animate */ afterOpenDoor: (doorId: AllIdsOf<'animates'>, x: number, y: number) => void; + /** Emitted in project/functions.js afterChangeFloor */ afterChangeFloor: (floorId: FloorIds) => void; + /** Emitted in project/functions.js moveOneStep */ moveOneStep: (x: number, y: number, floorId: FloorIds) => void; + /** Emitted in src/game/enemy/battle.ts afterBattle */ afterBattle: (enemy: DamageEnemy, x?: number, y?: number) => void; + /** Emitted in libs/events.js changingFloor */ + changingFloor: (floorId: FloorIds, heroLoc: Loc) => void; + drawHero: ( + status?: Exclude, + offset?: number, + frame?: number + ) => void; } export const hook = new EventEmitter(); diff --git a/src/game/mechanism/misc.ts b/src/game/mechanism/misc.ts index 9ee7696..d31a610 100644 --- a/src/game/mechanism/misc.ts +++ b/src/game/mechanism/misc.ts @@ -1,8 +1,11 @@ import { has } from '@/plugin/game/utils'; +import { loading } from '../game'; export namespace BluePalace { type DoorConvertInfo = [id: AllIds, x: number, y: number]; + // ---------- 黄蓝门转换 + function drawDoors( ctx: CanvasRenderingContext2D, convert: DoorConvertInfo[], @@ -75,4 +78,30 @@ export namespace BluePalace { }); }); } + + // ---------- 传送门部分 + + interface Portal { + fx: number; + fy: number; + dir: Dir; + tx: number; + ty: number; + toDir: Dir; + } + + export const portals: Partial> = { + MT75: [ + { fx: 7, fy: 7, dir: 'left', tx: 9, ty: 9, toDir: 'down' }, + { fx: 5, fy: 11, dir: 'right', tx: 7, ty: 9, toDir: 'up' }, + { fx: 4, fy: 6, dir: 'right', tx: 9, ty: 4, toDir: 'up' }, + { fx: 5, fy: 9, dir: 'right', tx: 3, ty: 7, toDir: 'up' }, + { fx: 7, fy: 5, dir: 'right', tx: 4, ty: 9, toDir: 'up' } + ] + }; + loading.once('coreInit', initPortals); + + function initPortals() { + // 主要是复写勇士绘制以及传送判定,还有自动寻路 + } } diff --git a/src/game/system.ts b/src/game/system.ts index 2e9d171..28de887 100644 --- a/src/game/system.ts +++ b/src/game/system.ts @@ -25,13 +25,16 @@ import type * as damage from './enemy/damage'; import type { Logger } from '@/core/common/logger'; import type { Danmaku } from '@/core/main/custom/danmaku'; import type * as misc from './mechanism/misc'; +import type { MotaCanvas2D } from '@/core/fx/canvas2d'; +import type * as portal from '@/core/fx/portal'; +import type { HeroRenderer } from '@/core/render/hero'; interface ClassInterface { // 渲染进程与游戏进程通用 EventEmitter: typeof EventEmitter; IndexedEventEmitter: typeof IndexedEventEmitter; Disposable: typeof Disposable; - // 定义于渲染进程,录像中会进行polyfill,但是不执行任何内容 + // 定义于渲染进程 GameStorage: typeof GameStorage; MotaSetting: typeof MotaSetting; SettingDisplayer: typeof SettingDisplayer; @@ -45,12 +48,13 @@ interface ClassInterface { SoundEffect: typeof SoundEffect; SoundController: typeof SoundController; BgmController: typeof BgmController; + MotaCanvas2D: typeof MotaCanvas2D; + Danmaku: typeof Danmaku; // todo: 放到插件 ShaderEffect: typeof ShaderEffect; // 定义于游戏进程,渲染进程依然可用 Range: typeof Range; EnemyCollection: typeof EnemyCollection; DamageEnemy: typeof DamageEnemy; - Danmaku: typeof Danmaku; } type _IBattle = typeof battle; @@ -90,6 +94,12 @@ interface ModuleInterface { Mechanism: { BluePalace: typeof misc.BluePalace; }; + Effect: { + Portal: typeof portal; + }; + Render: { + heroRender: HeroRenderer; + }; } interface SystemInterfaceMap { diff --git a/src/plugin/pop.ts b/src/plugin/pop.ts index cb5bf1c..3f9f465 100644 --- a/src/plugin/pop.ts +++ b/src/plugin/pop.ts @@ -6,7 +6,7 @@ let pop: any[] = []; let time = 0; export function init() { - core.registerAnimationFrame('pop', true, popValue); + // core.registerAnimationFrame('pop', true, popValue); } /** diff --git a/src/plugin/use.ts b/src/plugin/use.ts index 721cd05..943d3ed 100644 --- a/src/plugin/use.ts +++ b/src/plugin/use.ts @@ -1,3 +1,4 @@ +import { sleep } from 'mutate-animate'; import { tip } from './utils'; export default function init() { @@ -29,6 +30,12 @@ window.addEventListener('resize', () => { }); checkMobile(); +sleep(2000).then(() => { + if (!isMobile) { + tip('info', `注意,不推荐使用浏览器的缩放功能,使用游戏内的缩放即可`); + } +}); + function checkMobile() { if (isMobile && !alerted) { tip( diff --git a/src/plugin/utils.ts b/src/plugin/utils.ts index b3489d8..9948581 100644 --- a/src/plugin/utils.ts +++ b/src/plugin/utils.ts @@ -242,9 +242,6 @@ export function tip( class: 'antdv-message' }); } -sleep(2000).then(() => { - tip('info', `注意,不推荐使用浏览器的缩放功能,使用游戏内的缩放即可`); -}); /** * 设置文字分段换行等 diff --git a/src/types/control.d.ts b/src/types/control.d.ts index 2ae13d4..bce4592 100644 --- a/src/types/control.d.ts +++ b/src/types/control.d.ts @@ -368,7 +368,7 @@ interface Control { */ drawHero( status?: Exclude, - offset?: number, + offset?: number | (Loc & { offset: number }), frame?: number ): void; @@ -1071,6 +1071,14 @@ interface Control { _drawHero_updateViewport(x: number, y: number, offset: Loc): void; _moveAction_moving(callback: () => void): void; + _drawHero_draw( + direction: Dir, + x: number, + y: number, + status: Exclude, + offset: Loc & { offset: number }, + frame?: number + ): void; } declare const control: new () => Control; diff --git a/src/types/core.d.ts b/src/types/core.d.ts index a9a0d99..3f5cf7d 100644 --- a/src/types/core.d.ts +++ b/src/types/core.d.ts @@ -240,6 +240,7 @@ interface AnimateFrame { readonly animateTime: number; /** + * @deprecated * 勇士移动的时候上一次的换腿时间 */ moveTime: number; @@ -251,6 +252,7 @@ interface AnimateFrame { lastLegTime: number; /** + * @deprecated * 当前是否在左腿上,使用了四帧插件时无效 */ leftLeg: boolean; diff --git a/src/types/status.d.ts b/src/types/status.d.ts index 7f16d16..64951a6 100644 --- a/src/types/status.d.ts +++ b/src/types/status.d.ts @@ -628,6 +628,7 @@ interface InitGameStatus { lockControl: boolean; /** + * @deprecated 迟早给你删喽\ * 勇士移动状态,每个数字干啥的自己去libs翻,这东西太复杂了,不过应该不会有人用这个东西吧( */ heroMoving: number; @@ -792,6 +793,11 @@ interface Follower { * 跟随者的图片id */ name: ImageIds; + + direction: Dir; + x: number; + y: number; + stop: boolean; } interface HeroStatistics { diff --git a/src/ui/shop.vue b/src/ui/shop.vue index 37cbe44..6f46157 100644 --- a/src/ui/shop.vue +++ b/src/ui/shop.vue @@ -290,10 +290,10 @@ gameKey } }) .realize('@shop_add', () => { - count.value--; + count.value++; }) .realize('@shop_min', () => { - count.value++; + count.value--; }) .realize('exit', () => { exit(); @@ -311,10 +311,12 @@ function exit() { } onMounted(async () => { + core.lockControl(); core.status.route.push(`openShop:${id}`); }); onUnmounted(() => { + core.unlockControl(); gameKey.dispose(props.ui.symbol); });