diff --git a/package.json b/package.json index c62d97d..2ae5c3f 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "scripts": { "dev": "ts-node-esm script/dev.ts", "build": "vue-tsc && vite build && ts-node-esm script/build.ts dist", + "build-local": "vue-tsc && vite build && ts-node-esm script/build.ts local", "preview": "vite preview", "update": "ts-node-esm script/update.ts", "declare": "ts-node-esm script/declare.ts", diff --git a/public/libs/control.js b/public/libs/control.js index eb28c96..ed37371 100644 --- a/public/libs/control.js +++ b/public/libs/control.js @@ -802,19 +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); - }); + // Mota.r(() => { + // const render = Mota.require('module', 'Render').heroRender; + // render.move(true); + // }); core.interval.heroMoveInterval = window.setInterval(function () { - render.offset += toAdd * 4; + // 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); + // render.offset = 0; + // render.move(false); if (callback) callback(); } }, ((core.values.moveSpeed / 8) * toAdd) / core.status.replay.speed); @@ -1014,29 +1014,19 @@ control.prototype.tryMoveDirectly = function (destX, destY) { 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; @@ -1048,6 +1038,9 @@ control.prototype.drawHero = function (status, offset = 0, 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); }; diff --git a/public/libs/maps.js b/public/libs/maps.js index 64acb55..b6f2d81 100644 --- a/public/libs/maps.js +++ b/public/libs/maps.js @@ -3291,6 +3291,14 @@ maps.prototype.setBlock = function (number, x, y, floorId, noredraw) { } } } + Mota.require('var', 'hook').emit( + 'setBlock', + x, + y, + floorId, + originBlock?.id ?? 0, + number + ); }; maps.prototype.animateSetBlock = function ( diff --git a/public/project/enemys.js b/public/project/enemys.js index 5d9a200..6a804b2 100644 --- a/public/project/enemys.js +++ b/public/project/enemys.js @@ -40,7 +40,7 @@ var enemys_fcae963b_31c9_42b4_b48c_bb48d09f3f80 = "angel": {"name":"天使","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[]}, "elemental": {"name":"元素生物","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[]}, "steelGuard": {"name":"铁守卫","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[18],"value":20}, - "evilBat": {"name":"邪恶蝙蝠","hp":1000,"atk":800,"def":350,"money":1,"exp":40,"point":0,"special":[2]}, + "evilBat": {"name":"邪恶蝙蝠","hp":1000,"atk":800,"def":350,"money":1,"exp":40,"point":0,"special":[2],"bigImage":"bear.png"}, "frozenSkeleton": {"name":"冻死骨","hp":7500,"atk":2500,"def":1000,"money":2,"exp":90,"point":0,"special":[1,20],"crit":500,"ice":10,"description":"弱小,无助,哀嚎,这就是残酷的现实。哪怕你身处极寒之中,也难有人对你伸出援手。人类总会帮助他人,但在这绝望的环境下,人类的本性便暴露无遗。这一个个精致却又无情的骷髅,便是那些在极寒之中死去的冤魂。"}, "silverSlimelord": {"name":"银怪王","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[]}, "goldSlimelord": {"name":"金怪王","hp":0,"atk":0,"def":0,"money":0,"exp":0,"point":0,"special":[]}, diff --git a/public/project/floors/MT76.js b/public/project/floors/MT76.js index 99883e2..bc901fc 100644 --- a/public/project/floors/MT76.js +++ b/public/project/floors/MT76.js @@ -1,31 +1,31 @@ main.floors.MT76= { -"floorId": "MT76", -"title": "苍蓝之殿-左上", -"name": "76", -"width": 15, -"height": 15, -"canFlyTo": true, -"canFlyFrom": true, -"canUseQuickShop": true, -"cannotViewMap": false, -"images": [], -"ratio": 8, -"defaultGround": "T650", -"bgm": "palaceNorth.mp3", -"firstArrive": [], -"eachArrive": [], -"parallelDo": "", -"events": {}, -"changeFloor": {}, -"beforeBattle": {}, -"afterBattle": {}, -"afterGetItem": {}, -"afterOpenDoor": {}, -"autoEvent": {}, -"cannotMove": {}, -"cannotMoveIn": {}, -"map": [ + "floorId": "MT76", + "title": "苍蓝之殿-左上", + "name": "76", + "width": 15, + "height": 15, + "canFlyTo": true, + "canFlyFrom": true, + "canUseQuickShop": true, + "cannotViewMap": false, + "images": [], + "ratio": 8, + "defaultGround": "T650", + "bgm": "palaceNorth.mp3", + "firstArrive": [], + "eachArrive": [], + "parallelDo": "", + "events": {}, + "changeFloor": {}, + "beforeBattle": {}, + "afterBattle": {}, + "afterGetItem": {}, + "afterOpenDoor": {}, + "autoEvent": {}, + "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], @@ -42,4 +42,16 @@ main.floors.MT76= [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] ], + "bgmap": [ + +], + "fgmap": [ + +], + "bg2map": [ + +], + "fg2map": [ + +] } \ No newline at end of file diff --git a/public/project/floors/MT81.js b/public/project/floors/MT81.js index 4b0a307..cf4d3dd 100644 --- a/public/project/floors/MT81.js +++ b/public/project/floors/MT81.js @@ -1,31 +1,31 @@ main.floors.MT81= { -"floorId": "MT81", -"title": "苍蓝之殿-左上", -"name": "81", -"width": 15, -"height": 15, -"canFlyTo": true, -"canFlyFrom": true, -"canUseQuickShop": true, -"cannotViewMap": false, -"images": [], -"ratio": 8, -"defaultGround": "T650", -"bgm": "palaceNorth.mp3", -"firstArrive": [], -"eachArrive": [], -"parallelDo": "", -"events": {}, -"changeFloor": {}, -"beforeBattle": {}, -"afterBattle": {}, -"afterGetItem": {}, -"afterOpenDoor": {}, -"autoEvent": {}, -"cannotMove": {}, -"cannotMoveIn": {}, -"map": [ + "floorId": "MT81", + "title": "苍蓝之殿-左上", + "name": "81", + "width": 15, + "height": 15, + "canFlyTo": true, + "canFlyFrom": true, + "canUseQuickShop": true, + "cannotViewMap": false, + "images": [], + "ratio": 8, + "defaultGround": "T650", + "bgm": "palaceNorth.mp3", + "firstArrive": [], + "eachArrive": [], + "parallelDo": "", + "events": {}, + "changeFloor": {}, + "beforeBattle": {}, + "afterBattle": {}, + "afterGetItem": {}, + "afterOpenDoor": {}, + "autoEvent": {}, + "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], @@ -42,4 +42,16 @@ main.floors.MT81= [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] ], + "bgmap": [ + +], + "fgmap": [ + +], + "bg2map": [ + +], + "fg2map": [ + +] } \ No newline at end of file diff --git a/public/project/maps.js b/public/project/maps.js index 72a269b..86ba03a 100644 --- a/public/project/maps.js +++ b/public/project/maps.js @@ -73,7 +73,7 @@ var maps_90f36752_8815_4be8_b32b_d7fad1d0542e = "83": {"cls":"animates","id":"redDoor","trigger":"openDoor","animate":1,"doorInfo":{"time":160,"openSound":"door.mp3","closeSound":"door.mp3","keys":{"redKey":1}},"name":"红门"}, "84": {"cls":"animates","id":"greenDoor","trigger":"openDoor","animate":1,"doorInfo":{"time":160,"openSound":"door.mp3","closeSound":"door.mp3","keys":{"greenKey":1}},"name":"绿门"}, "85": {"cls":"animates","id":"specialDoor","trigger":"openDoor","animate":1,"doorInfo":{"time":160,"openSound":"door.mp3","closeSound":"door.mp3","keys":{"specialKey":1}},"name":"机关门"}, - "86": {"cls":"animates","id":"steelDoor","trigger":"openDoor","animate":1,"doorInfo":{"time":160,"openSound":"door.mp3","closeSound":"door.mp3","keys":{"steelKey":1}},"name":"铁门"}, + "86": {"cls":"animates","id":"steelDoor","trigger":"openDoor","animate":1,"doorInfo":{"time":160,"openSound":"door.mp3","closeSound":"door.mp3","keys":{"steelKey":1}},"name":"铁门","bigImage":"bear.png"}, "87": {"cls":"terrains","id":"upFloor","canPass":true}, "88": {"cls":"terrains","id":"downFloor","canPass":true}, "89": {"cls":"animates","id":"portal","canPass":true}, diff --git a/public/styles.css b/public/styles.css index bf51619..fd8c8ca 100644 --- a/public/styles.css +++ b/public/styles.css @@ -392,7 +392,7 @@ p#name { } #hero { - display: none; + /* display: none; */ z-index: 40; } diff --git a/script/dev.ts b/script/dev.ts index 38db18d..85cf596 100644 --- a/script/dev.ts +++ b/script/dev.ts @@ -604,7 +604,7 @@ async function ensureConfig() { // 1. 启动vite服务 const vite = await createServer(); await vite.listen(5173); - console.log(`游戏地址:http://localhost:5173/games/${config.name}/`); + console.log(`游戏地址:http://localhost:5173/`); // 2. 启动样板http服务 await ensureConfig(); diff --git a/src/core/fx/canvas2d.ts b/src/core/fx/canvas2d.ts index cc11370..82dd56b 100644 --- a/src/core/fx/canvas2d.ts +++ b/src/core/fx/canvas2d.ts @@ -3,6 +3,7 @@ import { EmitableEvent, EventEmitter } from '../common/eventEmitter'; import { CSSObj } from '../interface'; interface OffscreenCanvasEvent extends EmitableEvent { + /** 当被动触发resize时(例如core.domStyle.scale变化、窗口大小变化)时触发,使用size函数并不会触发 */ resize: () => void; } @@ -19,6 +20,8 @@ export class MotaOffscreenCanvas2D extends EventEmitter { autoScale: boolean = false; /** 是否是高清画布 */ highResolution: boolean = true; + /** 是否启用抗锯齿 */ + antiAliasing: boolean = true; scale: number = 1; @@ -50,6 +53,7 @@ export class MotaOffscreenCanvas2D extends EventEmitter { this.height = height; this.ctx.setTransform(1, 0, 0, 1, 0, 0); this.ctx.scale(ratio, ratio); + this.ctx.imageSmoothingEnabled = this.antiAliasing; } /** @@ -68,6 +72,24 @@ export class MotaOffscreenCanvas2D extends EventEmitter { this.size(this.width, this.height); } + /** + * 设置当前画布的抗锯齿设置 + */ + setAntiAliasing(anti: boolean) { + this.antiAliasing = anti; + this.ctx.imageSmoothingEnabled = anti; + } + + /** + * 清空画布 + */ + clear() { + this.ctx.save(); + this.ctx.setTransform(1, 0, 0, 1, 0, 0); + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + this.ctx.restore(); + } + /** * 删除这个画布 */ diff --git a/src/core/index.ts b/src/core/index.ts index b68078b..b4e1df4 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -69,6 +69,7 @@ import { Sprite } from './render/sprite'; import { Camera } from './render/camera'; import { Image, Text } from './render/preset/misc'; import { RenderItem } from './render/item'; +import { texture } from './render/cache'; // ----- 类注册 Mota.register('class', 'AudioPlayer', AudioPlayer); @@ -148,6 +149,7 @@ Mota.register('module', 'Effect', { }); Mota.register('module', 'Render', { heroRender, + texture, MotaRenderer, Container, Sprite, diff --git a/src/core/render/cache.ts b/src/core/render/cache.ts new file mode 100644 index 0000000..c216d2f --- /dev/null +++ b/src/core/render/cache.ts @@ -0,0 +1,294 @@ +import { EmitableEvent, EventEmitter } from '../common/eventEmitter'; +import { MotaOffscreenCanvas2D } from '../fx/canvas2d'; + +// 经过测试(https://www.measurethat.net/Benchmarks/Show/30741/1/drawimage-img-vs-canvas-vs-bitmap-cropping-fix-loading) +// 得出结论,ImageBitmap和Canvas的绘制性能不如Image,于是直接画Image就行,所以缓存基本上就是存Image + +type ImageMapKeys = Exclude; +type ImageMap = Record; + +const i = (img: ImageMapKeys) => { + return core.material.images[img]; +}; + +const imageMap: Partial = {}; + +Mota.require('var', 'loading').once('loaded', () => { + [ + 'enemys', + 'enemy48', + 'npcs', + 'npc48', + 'terrains', + 'items', + 'animates' + ].forEach(v => (imageMap[v as ImageMapKeys] = i(v as ImageMapKeys))); +}); + +interface AutotileCache { + parent?: Set>; + frame: number; + cache: Record; +} +type AutotileCaches = Record, AutotileCache>; + +interface TextureRequire { + tileset: Record; + material: Record; + autotile: AutotileCaches; + images: Record; +} + +interface TextureCacheEvent extends EmitableEvent {} + +class TextureCache extends EventEmitter { + tileset!: Record; + material: Record; + autotile!: AutotileCaches; + images!: Record; + + idNumberMap!: IdToNumber; + + constructor() { + super(); + this.material = imageMap as Record; + + Mota.require('var', 'loading').once('loaded', () => { + const map = maps_90f36752_8815_4be8_b32b_d7fad1d0542e; + // @ts-ignore + this.idNumberMap = {}; + for (const [key, { id }] of Object.entries(map)) { + // @ts-ignore + this.idNumberMap[id] = parseInt(key) as AllNumbers; + } + this.tileset = core.material.images.tilesets; + this.autotile = splitAutotiles(this.idNumberMap); + this.images = core.material.images.images; + }); + } + + /** + * 获取纹理 + * @param type 纹理类型 + * @param key 纹理名称 + */ + require( + type: T, + key: K + ): TextureRequire[T][K] { + return this[type][key]; + } +} + +export const texture = new TextureCache(); + +// 3x4 与 2x3 的自动元件信息 +// 将自动元件按 16x16 切分后,数组分别表示 左上 右上 右下 左下 所在图块位置 +const bigAutotile: Record = {}; +const smallAutotile: Record = {}; + +function getAutotileIndices() { + // 应当从 0 - 255 进行枚举 + // 二进制从高位到低位依次是 左上 上 右上 右 右下 下 左下 左 + // 首先是3x4的 + // 有兴趣可以研究下这个算法 + const get = ( + target: Record, + mode: 1 | 2 + ) => { + const h = mode === 1 ? 4 : 2; + const v = mode === 1 ? 24 : 8; + const luo = mode === 1 ? 12 : 8; // leftup origin + const ruo = mode === 1 ? 17 : 11; // rightup origin + const ldo = mode === 1 ? 42 : 20; // leftdown origin + const rdo = mode === 1 ? 47 : 23; // rightdown origin + const luc = mode === 1 ? 4 : 2; // leftup corner + const ruc = mode === 1 ? 5 : 3; // rightup corner + const rdc = mode === 1 ? 11 : 7; // rightdown corner + const ldc = mode === 1 ? 10 : 6; // leftdown corner + + for (let i = 0; i <= 0b11111111; i++) { + let lu = luo; // leftup + let ru = ruo; // rightup + let ld = ldo; // leftdown + let rd = rdo; // rightdown + + // 先看四个方向,最后看斜角方向 + if (i & 0b00000001) { + lu += h; + ld += h; + if (i & 0b00010000) { + ru += h / 2; + rd += h / 2; + } + } + if (i & 0b00000100) { + ld -= v; + rd -= v; + if (i & 0b01000000) { + lu -= v / 2; + ru -= v / 2; + } + } + if (i & 0b00010000) { + ru -= h; + rd -= h; + if (i & 0b00000001) { + lu -= h / 2; + ld -= h / 2; + } + } + if (i & 0b01000000) { + lu += v; + ru += v; + if (i & 0b00000100) { + ld += v / 2; + rd += v / 2; + } + } + // 斜角 + if ((i & 0b11000001) === 0b01000001) { + lu = luc; + } + if ((i & 0b01110000) === 0b01010000) { + ru = ruc; + } + if ((i & 0b00011100) === 0b00010100) { + rd = rdc; + } + if ((i & 0b00000111) === 0b00000101) { + ld = ldc; + } + target[i] = [lu, ru, rd, ld]; + } + }; + + get(bigAutotile, 1); + get(smallAutotile, 2); +} +getAutotileIndices(); + +function getRepeatMap() { + // 实际上3x4与2x3的重复映射是一致的,因此只需要计算一个就行了 + // 这里使用2x3的进行计算 + const calculated: Record = {}; + const repeatMap: Record = {}; + for (const [num, [lu, ru, rd, ld]] of Object.entries(smallAutotile)) { + const n = lu + ru * 24 + rd * 24 ** 2 + ld * 24 ** 3; + calculated[num] = n; + for (const [num2, n2] of Object.entries(calculated)) { + if (n2 === n) { + repeatMap[Number(num)] = Number(num2); + break; + } + } + } + + return repeatMap; +} + +function splitAutotiles(map: IdToNumber): AutotileCaches { + const cache: Partial = {}; + /** 重复映射,由于自动元件只有48种,其余的208种是重复的,因此需要获取重复映射 */ + const repeatMap: Record = getRepeatMap(); + /** 每个自动元件左上角32*32的内容,用于判断父子关系 */ + const masterMap: Partial, string>> = {}; + + for (const [key, img] of Object.entries(core.material.images.autotile)) { + const auto = map[key as AllIdsOf<'autotile'>]; + + // 判断自动元件类型 + let mode: 1 | 2 = 1; + let frame = 1; + if (img.width === 384) { + frame = 4; + } else if (img.width === 192) { + mode = 2; + frame = 3; + } else if (img.width === 64) { + mode = 2; + } + + cache[auto] = { + frame, + cache: {} + }; + + // 父子关系,截取本图块的左上角存入map + const master = new MotaOffscreenCanvas2D(); + master.setHD(false); + master.setAntiAliasing(false); + master.withGameScale(false); + master.size(32, 32); + master.ctx.drawImage(img, 0, 0, 32, 32, 0, 0, 32, 32); + masterMap[auto] = master.canvas.toDataURL('image/png'); + + // 自动图块的绘制信息 + for (let i = 0; i <= 0b11111111; i++) { + const re = repeatMap[i]; + if (re) { + const cached = cache[auto]!.cache[re]; + if (cached) { + cache[auto]!.cache[i] = cached; + continue; + } + } + + const data = (mode === 1 ? bigAutotile : smallAutotile)[i]; + const row = mode === 1 ? 6 : 4; + const info: [number, number][] = data.map(v => [ + (v % row) * 16, + Math.floor(v / row) * 16 + ]); + + const canvas = new MotaOffscreenCanvas2D(); + canvas.setHD(false); + canvas.setAntiAliasing(false); + canvas.withGameScale(false); + canvas.size(32 * frame, 32); + const ctx = canvas.ctx; + for (let i = 0; i < frame; i++) { + const dx = 32 * i; + const sx1 = info[0][0]; + const sx2 = info[1][0]; + const sx3 = info[2][0]; + const sx4 = info[3][0]; + const sy1 = info[0][1]; + const sy2 = info[1][1]; + const sy3 = info[2][1]; + const sy4 = info[3][1]; + + ctx.drawImage(img, sx1, sy1, 16, 16, dx, 0, 16, 16); + ctx.drawImage(img, sx2, sy2, 16, 16, dx + 16, 0, 16, 16); + ctx.drawImage(img, sx3, sy3, 16, 16, dx + 16, 16, 16, 16); + ctx.drawImage(img, sx4, sy4, 16, 16, dx, 16, 16, 16); + } + cache[auto]!.cache[i] = canvas.canvas; + } + } + + // 进行父子关系判断 + for (const [key, img] of Object.entries(core.material.images.autotile)) { + const auto = map[key as AllIdsOf<'autotile'>]; + + // 只针对3*4的图块进行,截取第一行中间的,然后判断 + const judge = new MotaOffscreenCanvas2D(); + judge.setHD(false); + judge.setAntiAliasing(false); + judge.withGameScale(false); + judge.size(32, 32); + judge.ctx.drawImage(img, 32, 0, 32, 32, 0, 0, 32, 32); + const data = judge.canvas.toDataURL('image/png'); + + for (const [key, data2] of Object.entries(masterMap)) { + const auto2 = map[key as AllIdsOf<'autotile'>]; + + if (data === data2) { + cache[auto]!.parent ??= new Set(); + cache[auto]!.parent!.add(auto2); + } + } + } + + return cache as AutotileCaches; +} diff --git a/src/core/render/camera.ts b/src/core/render/camera.ts index 4035153..bf91adc 100644 --- a/src/core/render/camera.ts +++ b/src/core/render/camera.ts @@ -12,6 +12,8 @@ export class Camera extends EventEmitter { scaleY: number = 1; rad: number = 0; + private saveStack: number[][] = []; + /** * 重设摄像机的所有参数 */ @@ -140,6 +142,23 @@ export class Camera extends EventEmitter { this.rad = rad; } + /** + * 保存当前摄像机状态 + */ + save() { + this.saveStack.push(Array.from(this.mat)); + } + + /** + * 回退当前摄像机状态 + */ + restore() { + const data = this.saveStack.pop(); + if (!data) return; + const [a, b, c, d, e, f, g, h, i] = data; + this.mat = mat3.fromValues(a, b, c, d, e, f, g, h, i); + } + /** * 根据摄像机的信息,将一个点转换为计算后的位置 * @param camera 摄像机 diff --git a/src/core/render/container.ts b/src/core/render/container.ts index 9d9a242..c850bb2 100644 --- a/src/core/render/container.ts +++ b/src/core/render/container.ts @@ -25,14 +25,19 @@ export class Container extends RenderItem implements ICanvasCachedRenderItem { ctx: CanvasRenderingContext2D, camera: Camera ): void { - this.emit('beforeUpdate', this); + this.emit('beforeRender'); withCacheRender(this, canvas, ctx, camera, c => { this.sortedChildren.forEach(v => { + if (!v.antiAliasing) { + ctx.imageSmoothingEnabled = false; + } else { + ctx.imageSmoothingEnabled = true; + } v.render(c.canvas, c.ctx, camera); }); }); this.writing = void 0; - this.emit('afterUpdate', this); + this.emit('afterRender'); } size(width: number, height: number) { @@ -55,8 +60,24 @@ export class Container extends RenderItem implements ICanvasCachedRenderItem { appendChild(children: RenderItem[]) { children.forEach(v => (v.parent = this)); this.children.push(...children); + this.sortChildren(); + } + + sortChildren() { this.sortedChildren = this.children .slice() .sort((a, b) => a.zIndex - b.zIndex); } + + setHD(hd: boolean): void { + this.highResolution = hd; + this.canvas.setHD(hd); + this.update(this); + } + + setAntiAliasing(anti: boolean): void { + this.antiAliasing = anti; + this.canvas.setAntiAliasing(anti); + this.update(this); + } } diff --git a/src/core/render/hero.ts b/src/core/render/hero.ts index 292136b..f4145e8 100644 --- a/src/core/render/hero.ts +++ b/src/core/render/hero.ts @@ -101,6 +101,7 @@ export class HeroRenderer extends EventEmitter { */ draw() { if (!core.isPlaying()) return; + return; const { ctx, canvas: can } = canvas; const { x, y, direction: dir } = core.status.hero.loc; ctx.clearRect(0, 0, can.width, can.height); diff --git a/src/core/render/item.ts b/src/core/render/item.ts index a2a2ebe..b4d914f 100644 --- a/src/core/render/item.ts +++ b/src/core/render/item.ts @@ -2,6 +2,8 @@ import { isNil } from 'lodash-es'; import { EmitableEvent, EventEmitter } from '../common/eventEmitter'; import { MotaOffscreenCanvas2D } from '../fx/canvas2d'; import { Camera } from './camera'; +import { Ticker } from 'mutate-animate'; +import type { Container } from './container'; export type RenderFunction = ( canvas: MotaOffscreenCanvas2D, @@ -62,15 +64,48 @@ interface IRenderAnchor { setAnchor(x: number, y: number): void; } +interface IRenderConfig { + /** 是否是高清画布 */ + highResolution: boolean; + /** 是否启用抗锯齿 */ + antiAliasing: boolean; + + /** + * 设置当前渲染元素是否使用高清画布 + * @param hd 是否高清 + */ + setHD(hd: boolean): void; + + /** + * 设置当前渲染原始是否启用抗锯齿 + * @param anti 是否抗锯齿 + */ + setAntiAliasing(anti: boolean): void; +} + +export interface IRenderDestroyable { + /** + * 摧毁这个渲染对象,被摧毁后理应不被继续使用 + */ + destroy(): void; +} + interface RenderItemEvent extends EmitableEvent { beforeUpdate: (item?: RenderItem) => void; afterUpdate: (item?: RenderItem) => void; + beforeRender: () => void; + afterRender: () => void; } export abstract class RenderItem extends EventEmitter - implements IRenderCache, IRenderUpdater, IRenderAnchor + implements IRenderCache, IRenderUpdater, IRenderAnchor, IRenderConfig { + /** 渲染的全局ticker */ + static ticker: Ticker = new Ticker(); + /** 包括但不限于怪物、npc、自动元件的动画帧数 */ + static animatedFrame: number = 0; + zIndex: number = 0; x: number = 0; @@ -88,9 +123,15 @@ export abstract class RenderItem /** 渲染模式,absolute表示绝对位置,static表示跟随摄像机移动,只对顶层元素有效 */ type: 'absolute' | 'static' = 'static'; + /** 是否是高清画布 */ + highResolution: boolean = true; + /** 是否抗锯齿 */ + antiAliasing: boolean = true; parent?: RenderItem; + protected needUpdate: boolean = false; + constructor() { super(); @@ -151,11 +192,52 @@ export abstract class RenderItem } update(item?: RenderItem): void { - this.cache(this.writing); - this.parent?.update(item); + if (this.needUpdate) return; + this.needUpdate = true; + requestAnimationFrame(() => { + this.needUpdate = false; + if (!this.parent) return; + this.cache(this.writing); + this.refresh(item); + }); + } + + /** + * 立刻更新这个组件,不延迟到下一个tick + */ + protected refresh(item?: RenderItem) { + this.emit('beforeUpdate', item); + this.parent?.refresh(item); + this.emit('afterUpdate', item); + } + + setHD(hd: boolean): void { + this.highResolution = hd; + this.update(this); + } + + setAntiAliasing(anti: boolean): void { + this.antiAliasing = anti; + this.update(this); + } + + setZIndex(zIndex: number) { + this.zIndex = zIndex; + (this.parent as Container).sortChildren?.(); } } +Mota.require('var', 'hook').once('reset', () => { + let lastTime = 0; + RenderItem.ticker.add(time => { + if (!core.isPlaying()) return; + if (time - lastTime > core.values.animateSpeed) { + RenderItem.animatedFrame++; + lastTime = time; + } + }); +}); + export function withCacheRender( item: RenderItem & ICanvasCachedRenderItem, canvas: HTMLCanvasElement, diff --git a/src/core/render/preset/layer.ts b/src/core/render/preset/layer.ts new file mode 100644 index 0000000..e19ed22 --- /dev/null +++ b/src/core/render/preset/layer.ts @@ -0,0 +1,1229 @@ +import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d'; +import { Container } from '../container'; +import { Sprite } from '../sprite'; +import { Camera } from '../camera'; +import { TimingFn } from 'mutate-animate'; +import { IRenderDestroyable, RenderItem } from '../item'; +import { logger } from '@/core/common/logger'; +import { texture } from '../cache'; +import { SizedCanvasImageSource } from './misc'; +import { glMatrix } from 'gl-matrix'; + +interface LayerCacheItem { + floorId?: FloorIds; + canvas: HTMLCanvasElement; +} + +interface LayerRenderableData { + image: SizedCanvasImageSource; + frame: number; + render: [x: number, y: number, width: number, height: number][]; +} + +interface LayerMovingRenderable extends LayerRenderableData { + zIndex: number; + x: number; + y: number; +} + +interface BigImageData { + image: HTMLImageElement; + line: number; + totalLines: number; +} + +interface NeedRenderData { + /** 需要渲染的地图内容 */ + res: Set; + /** 需要渲染的背景内容 */ + back: [x: number, y: number][]; +} + +interface LayerBlockData { + /** 横向宽度,包括rest的那一个块 */ + width: number; + /** 纵向宽度,包括rest的那一个块 */ + height: number; + /** 横向最后一个块的宽度 */ + restWidth: number; + /** 纵向最后一个块的高度 */ + restHeight: number; +} + +interface MovingStepLinearSwap { + /** 线性差值移动(也就是平移)或者是瞬移 */ + type: 'linear' | 'swap'; + x: number; + y: number; + /** 这次移动的总时长,不是每格时长 */ + time?: number; +} + +interface MovingStepFunction { + /** 自定义移动方式 */ + type: 'fn'; + /** + * 移动函数,返回一个三元素数组,表示当前所在格子数,以及在纵向上的深度(一般图块的深度就是它的纵坐标), + * 注意不是像素数,可以是小数 + */ + fn: TimingFn<3>; + time?: number; + relative?: boolean; +} + +type MovingStep = MovingStepFunction | MovingStepLinearSwap; + +interface MovingBlock { + steps: MovingStep[]; + /** 当前正在执行哪一步 */ + index: number; + /** 目标横坐标 */ + x: number; + /** 目标纵坐标 */ + y: number; + /** 渲染信息 */ + render: LayerRenderableData; + /** 当前的纵深 */ + nowZ: number; +} + +type FloorLayer = 'bg' | 'bg2' | 'event' | 'fg' | 'fg2'; + +export class Layer extends Container implements IRenderDestroyable { + /** 地图渲染对象映射,用于当地图更新时定向更新渲染信息 */ + static LayerMap: Map> = new Map(); + + // 一些会用到的常量 + static readonly FRAME_0 = 1; + static readonly FRAME_1 = 2; + static readonly FRAME_2 = 4; + static readonly FRAME_3 = 8; + static readonly FRAME_ALL = 15; + + /** + * 每个渲染块的缓存,索引的理应计算公式为 (x + y * width) * 4 + frame\ + * 其中x和y表示块的位置,width表示横向有多少个块,frame表示第几帧,移动层没办法缓存,所以没有缓存 + */ + blockCache: Map = new Map(); + + /** 静态层,包含除大怪物及正在移动的内容外的内容 */ + protected staticMap: MotaOffscreenCanvas2D = new MotaOffscreenCanvas2D(); + /** 移动层,包含大怪物及正在移动的内容 */ + protected movingMap: MotaOffscreenCanvas2D = new MotaOffscreenCanvas2D(); + /** 背景图层 */ + protected backMap: MotaOffscreenCanvas2D = new MotaOffscreenCanvas2D(); + + /** 最终渲染至的Sprite */ + main: Sprite = new Sprite(); + + /** 与当前层绑定,当前层改变时渲染的楼层会一同改变 */ + bindThisFloor: boolean = false; + /** 渲染的楼层 */ + floorId?: FloorIds; + /** 渲染的层 */ + layer?: FloorLayer; + /** 渲染数据 */ + renderData: number[] = []; + /** 可以直接被渲染的内容 */ + renderable: Map = new Map(); + /** 移动层中可以直接被渲染的内容 */ + movingRenderable: LayerMovingRenderable[] = []; + /** 自动元件的连接信息,键表示图块在渲染数据中的索引,值表示连接信息,是个8位二进制 */ + autotiles: Record = {}; + /** 楼层宽度 */ + mapWidth: number = 0; + /** 楼层高度 */ + mapHeight: number = 0; + /** 每个图块的大小 */ + cellSize: number = 32; + /** moving层的缓存信息,从低位到高位依次是第1帧至第4帧 */ + movingCached: number = 0b0000; + + /** 背景图块 */ + background: AllNumbers = 0; + /** 背景图块画布 */ + backImage: HTMLCanvasElement[] = []; + + /** 分块信息。每个块的默认大小为画面宽度,这样的话有可能会包含三个大小不足的剩余块,这三个块单独处理即可 */ + blockData: LayerBlockData = { + width: 0, + height: 0, + restHeight: 0, + restWidth: 0 + }; + blockSize: number = core._WIDTH_; + /** 正在移动的图块 */ + moving: MovingBlock[] = []; + /** 大怪物(大图块)信息,键是图块在渲染数据中的索引,值是大怪物所用图片 */ + bigImage: Map = new Map(); + + constructor() { + super('absolute'); + + this.setHD(false); + this.setAntiAliasing(false); + this.size(core._PX_, core._PY_); + + this.staticMap.setHD(false); + this.staticMap.setAntiAliasing(false); + this.staticMap.withGameScale(false); + this.staticMap.size(core._PX_, core._PY_); + this.movingMap.setHD(false); + this.movingMap.setAntiAliasing(false); + this.movingMap.withGameScale(false); + this.movingMap.size(core._PX_, core._PY_); + this.backMap.setHD(false); + this.backMap.setAntiAliasing(false); + this.backMap.withGameScale(false); + this.backMap.size(core._PX_, core._PY_); + this.main.setAntiAliasing(false); + this.main.setHD(false); + this.main.size(core._PX_, core._PY_); + + this.appendChild([this.main]); + this.main.setRenderFn((canvas, camera) => { + const { ctx } = canvas; + const { width, height } = canvas.canvas; + ctx.save(); + ctx.imageSmoothingEnabled = false; + const need = this.calNeedRender(camera); + this.renderMap(camera, need); + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.drawImage(this.backMap.canvas, 0, 0, width, height); + ctx.drawImage(this.staticMap.canvas, 0, 0, width, height); + ctx.drawImage(this.movingMap.canvas, 0, 0, width, height); + ctx.restore(); + }); + } + + /** + * 设置背景图块 + * @param background 背景图块 + */ + setBackground(background: AllNumbers) { + this.background = background; + this.generateBackground(); + } + + /** + * 生成背景图块 + */ + generateBackground() { + const num = this.background; + + const data = this.getRenderableByNum(num); + this.backImage = []; + if (!data) return; + + const frame = data.frame; + for (let i = 0; i < frame; i++) { + const canvas = new MotaOffscreenCanvas2D(); + const temp = new MotaOffscreenCanvas2D(); + const ctx = canvas.ctx; + const tempCtx = temp.ctx; + const [sx, sy, w, h] = data.render[i]; + canvas.setHD(false); + canvas.setAntiAliasing(false); + canvas.withGameScale(false); + canvas.size(core._PX_, core._PY_); + temp.setHD(false); + temp.setAntiAliasing(false); + temp.withGameScale(false); + temp.size(w, h); + + const img = data.image; + tempCtx.drawImage(img, sx, sy, w, h, 0, 0, w, h); + const pattern = ctx.createPattern(temp.canvas, 'repeat'); + if (!pattern) continue; + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + this.backImage.push(canvas.canvas); + } + } + + /** + * 修改地图渲染数据,对于溢出的内容会进行裁剪 + * @param data 要渲染的地图数据 + * @param width 数据的宽度 + * @param x 第一个数据的横坐标,默认是0 + * @param y 第一个数据的纵坐标,默认是0 + */ + putRenderData( + data: number[], + width: number, + x: number = 0, + y: number = 0, + calAutotile: boolean = true + ) { + console.trace(); + + if (data.length % width !== 0) { + logger.warn( + 8, + `Incomplete render data is put. None will be filled to the lacked data.` + ); + data.push(...Array(data.length % width).fill(0)); + } + const height = Math.round(data.length / width); + if (width + x > this.mapWidth || height + y > this.mapHeight) { + logger.warn( + 9, + `Data transfered is partially (or totally) out of range. Overflowed data will be ignored.` + ); + if (x >= this.mapWidth || y >= this.mapHeight) return; + } + for (let nx = 0; nx < width; nx++) { + for (let ny = 0; ny < height; ny++) { + const dx = nx + x; + const dy = ny + y; + if (dx >= this.mapWidth || dy >= this.mapHeight) { + continue; + } + const index = nx + ny * width; + const indexData = dx + dy * this.mapWidth; + this.renderData[indexData] = data[index]; + } + } + if (calAutotile) this.calAutotiles(x, y, width, height); + this.updateBigImages(x, y, width, height); + this.updateRenderableData(x, y, width, height); + this.updateBlocks(x, y, width, height); + this.update(this); + } + + /** + * 更新给定区域内的大怪物信息 + */ + updateBigImages(x: number, y: number, width: number, height: number) { + const ex = Math.min(x + width, this.mapWidth); + const ey = Math.min(y + height, this.mapHeight); + const size = this.blockSize; + const images = this.bigImage; + const data = this.renderData; + const enemys = enemys_fcae963b_31c9_42b4_b48c_bb48d09f3f80; + const icons = icons_4665ee12_3a1f_44a4_bea3_0fccba634dc1; + const map = maps_90f36752_8815_4be8_b32b_d7fad1d0542e; + + for (let nx = Math.max(x, 0); nx < ex; nx++) { + for (let ny = Math.max(y, 0); ny < ey; ny++) { + const index = ny * size + nx; + images.delete(index); + const num = data[index]; + + // 如果不存在图块或图块是空气墙,跳过 + if (num === 0 || num === 17 || num >= 10000) continue; + + let { cls, id, bigImage, faceIds } = + map[num as Exclude]; + if (cls === 'enemys' || cls === 'enemy48') { + // 怪物需要特殊处理,因为它的大怪物信息不在 maps 里面 + ({ bigImage, faceIds } = enemys[id as EnemyIds]); + } + if (bigImage) { + const image = core.material.images.images[bigImage]; + if (!image) { + logger.warn( + 10, + `Cannot resolve big image of enemy '${id}'.` + ); + continue; + } + let line = 0; + if (faceIds) { + const arr = ['down', 'left', 'right', 'up']; + for (let i = 0; i < arr.length; i++) { + if (faceIds[arr[i] as Dir] === id) { + line = i; + break; + } + } + } + const totalLines = image.width / image.height >= 2 ? 1 : 4; + images.set(index, { + image, + line, + totalLines + }); + } + if (cls === 'enemy48' || cls === 'npc48') { + // 32 * 48 视为大怪物 + const img = core.material.images[cls]; + const totalLines = Math.round(img.height / 48); + // @ts-ignore + const line = icons[cls][id]; + images.set(index, { + image: img, + line, + totalLines + }); + } + } + } + } + + /** + * 根据图块数字获取渲染信息 + * @param num 图块数字 + */ + getRenderableByNum(num: number): LayerRenderableData | null { + const cell = this.cellSize; + const map = maps_90f36752_8815_4be8_b32b_d7fad1d0542e; + const icons = icons_4665ee12_3a1f_44a4_bea3_0fccba634dc1; + const auto = texture.autotile; + + if (num >= 10000) { + // 额外素材 + const offset = core.getTilesetOffset(num); + if (!offset) return null; + const { image, x, y } = offset; + return { + image: core.material.images.tilesets[image], + frame: 1, + render: [[x * cell, y * cell, cell, cell]] + }; + } else { + if (num === 0 || num === 17) return null; + const { cls, id } = map[num as Exclude]; + // 普通素材 + if (cls !== 'autotile') { + const image = + core.material.images[ + cls as Exclude + ]; + const frame = core.getAnimateFrames(cls); + // @ts-ignore + const offset = (icons[cls][id] as number) * cell; + const render: [number, number, number, number][] = [ + [0, offset, cell, cell] + ]; + if (frame === 2) { + render.push([cell, offset, cell, cell]); + } + if (frame === 4) { + render.push( + [cell, offset, cell, cell], + [cell * 2, offset, cell, cell], + [cell * 3, offset, cell, cell] + ); + } + return { + image, + frame, + render + }; + } else { + // 自动元件 + const tile = auto[num as AllNumbersOf<'autotile'>]; + const image = tile.cache[0b11111111]; + const frame = tile.frame; + const render: [number, number, number, number][] = [ + [0, 0, cell, cell] + ]; + if (frame === 4) { + render.push( + [cell, 0, cell, cell], + [cell * 2, 0, cell, cell], + [cell * 3, 0, cell, cell] + ); + } + return { + image, + frame, + render + }; + } + } + } + + /** + * 根据索引及横坐标获取对应的位置图块的渲染信息 + * @param x 横坐标 + * @param index 索引 + */ + getRenderableData( + x: number, + _y: number, + index: number + ): LayerRenderableData | LayerMovingRenderable | null { + const data = this.renderData; + const cell = this.cellSize; + const map = maps_90f36752_8815_4be8_b32b_d7fad1d0542e; + const icons = icons_4665ee12_3a1f_44a4_bea3_0fccba634dc1; + const auto = texture.autotile; + + const bigImage = this.bigImage.get(index); + if (bigImage) { + // 对于大怪物 + const img = bigImage.image; + const w = Math.round(img.width / 4); + const h = Math.round(img.height / bigImage.totalLines); + const y = h * bigImage.line; + return { + image: bigImage.image, + frame: 4, + render: [ + [0, y, w, h], + [w, y, w, h], + [w * 2, y, w, h], + [w * 3, y, w, h] + ], + x: x, + y: y, + zIndex: y + }; + } else { + // 对于普通图块 + const num = data[index]; + if (num >= 10000) { + // 额外素材 + return this.getRenderableByNum(num); + } else { + if (num === 0 || num === 17) return null; + const { cls } = map[num as Exclude]; + // 普通素材 + if (cls !== 'autotile') { + return this.getRenderableByNum(num); + } else { + // 自动元件 + const tile = auto[num as AllNumbersOf<'autotile'>]; + const link = this.autotiles[index]; + const image = tile.cache[link]; + const frame = tile.frame; + const render: [number, number, number, number][] = [ + [0, 0, cell, cell] + ]; + if (frame === 4) { + render.push( + [cell, 0, cell, cell], + [cell * 2, 0, cell, cell], + [cell * 3, 0, cell, cell] + ); + } + return { + image, + frame, + render + }; + } + } + } + } + + /** + * 更新给定区域内的绘制信息 + */ + updateRenderableData(x: number, y: number, width: number, height: number) { + const ex = Math.min(x + width, this.mapWidth); + const ey = Math.min(y + height, this.mapHeight); + const size = this.blockSize; + + for (let nx = Math.max(x, 0); nx < ex; nx++) { + for (let ny = Math.max(y, 0); ny < ey; ny++) { + const index = ny * size + nx; + const bigImage = this.bigImage.get(index); + const data = this.getRenderableData(nx, ny, index); + if (!data) continue; + if (bigImage) { + this.movingRenderable.push(data as LayerMovingRenderable); + } else { + this.renderable.set(index, data); + } + } + } + } + + /** + * 计算自动元件的连接信息 + */ + calAutotiles(x: number, y: number, width: number, height: number) { + const ex = x + width; + const ey = y + height; + const data = this.renderData; + const tile = texture.autotile; + const map = maps_90f36752_8815_4be8_b32b_d7fad1d0542e; + + /** + * 检查连接信息 + * @param id 比较对象的id(就是正在检查周围的那个自动元件,九宫格中心的) + * @param index1 比较对象 + * @param index2 被比较对象 + * @param replace1 被比较对象相对比较对象应该处理的位数 + * @param replace2 比较对象相对被比较对象应该处理的位数 + */ + const check = ( + index1: number, + index2: number, + replace1: number, + replace2: number + ) => { + const num1 = data[index1] as AllNumbersOf<'autotile'>; // 这个一定是自动元件 + const num2 = data[index2] as AllNumbersOf<'autotile'>; + const info = map[num2 as Exclude]; + if (info.cls !== 'autotile') { + // 被比较对象不是自动元件 + this.autotiles[num1] ??= 0; + this.autotiles[num1] &= ~replace1; + } else { + const parent1 = tile[num1].parent; + const parent2 = tile[num2].parent; + if (num2 === num1) { + // 二者一样,视为连接 + this.autotiles[index1] |= replace1; + this.autotiles[index2] |= replace2; + } else if (parent2?.has(num1)) { + // 被比较对象是比较对象的父元件,那么比较对象视为连接 + this.autotiles[index1] |= replace1; + } else if (parent1?.has(num2)) { + // 比较对象是被比较对象的父元件,那么被比较对象视为连接 + this.autotiles[index2] |= replace2; + } else { + // 上述条件都不满足,那么不连接 + this.autotiles[index1] &= ~replace1; + this.autotiles[index2] &= ~replace2; + } + } + }; + + const w = this.mapWidth; + const h = this.mapHeight; + + for (let nx = x; nx < ex; nx++) { + for (let ny = y; ny < ey; ny++) { + if (nx > w || ny > w) continue; + const index = nx + ny * h; + const num = data[index]; + // 特判空气墙与空图块 + if (num === 17 || num >= 10000 || num <= 0) continue; + console.log(this); + + const info = map[num as Exclude]; + const { cls } = info; + if (cls !== 'autotile') continue; + + // 只有最左一列和最上一列需要计算一周,其他的只计算右 右下 下即可 + // 太地狱了这个,看看就好 + if (nx === x) { + // 左上 左 左下 + check(index, index - w - 1, 0b10000000, 0b00001000); + check(index, index - 1, 0b00000001, 0b00010000); + check(index, index + w - 1, 0b00000010, 0b00100000); + } + if (ny === y) { + if (nx !== x) { + check(index, index - w - 1, 0b10000000, 0b00001000); + } + // 上 右上 + check(index, index - w, 0b01000000, 0b00000100); + check(index, index - w + 1, 0b00100000, 0b00000010); + } + // 右 右下 下 + check(index, index + 1, 0b00010000, 0b00000001); + check(index, index + w + 1, 0b00001000, 0b10000000); + check(index, index + w, 0b00000100, 0b01000000); + } + } + } + + /** + * 绑定渲染楼层 + * @param floor 楼层id + * @param layer 渲染的层数,例如是背景层还是事件层等 + */ + bindData(floor: FloorIds, layer: FloorLayer) { + const before = this.floorId; + this.floorId = floor; + this.layer = layer; + if (before) { + const floor = Layer.LayerMap.get(before); + floor?.delete(this); + } + const now = Layer.LayerMap.get(floor); + if (!now) { + Layer.LayerMap.set(floor, new Set([this])); + } else { + now.add(this); + } + const f = core.status.maps[floor]; + this.mapWidth = f.width; + this.mapHeight = f.height; + this.splitBlock(); + this.updateDataFromFloor(); + } + + /** + * 将这个渲染内容绑定为当前所在楼层,当前楼层改变时,渲染内容会一并改变 + */ + bindThis(layer: FloorLayer, noUpdate: boolean = false) { + this.bindThisFloor = true; + this.layer = layer; + + if (!noUpdate) this.bindData(core.status.floorId, layer); + } + + /** + * 从地图数据更新渲染数据,要求已经绑定渲染楼层,否则无事发生 + */ + updateDataFromFloor() { + if (!this.floorId || !this.layer) return; + const floor = core.status.maps[this.floorId]; + if (this.layer === 'event') { + const map = floor.map; + this.putRenderData(map.flat(), floor.width, 0, 0); + } else { + const map = core.maps._getBgFgMapArray(this.layer, this.floorId); + this.putRenderData(map.flat(), floor.width, 0, 0); + } + } + + /** + * 切分当前地图为多个块 + */ + splitBlock() { + const size = this.blockSize; + + this.blockData = { + width: Math.floor(this.mapWidth / size), + height: Math.floor(this.mapHeight / size), + restWidth: this.mapWidth % size, + restHeight: this.mapHeight % size + }; + } + + /** + * 设置地图大小,会清空渲染数据,因此后面应当紧跟 putRenderData,以保证渲染正常进行 + * @param width 地图宽度 + * @param height 地图高度 + */ + setMapSize(width: number, height: number) { + this.mapWidth = width; + this.mapHeight = height; + this.renderData = Array(width * height).fill(0); + this.autotiles = {}; + this.splitBlock(); + } + + /** + * 设置渲染块的大小 + */ + setBlockSize(size: number) { + this.blockSize = size; + this.splitBlock(); + } + + /** + * 给定一个矩形,更新其包含的块信息,注意由于自动元件的存在,实际判定范围会大一圈 + * @param x 左上角横坐标 + * @param y 左上角纵坐标 + * @param width 宽度 + * @param height 高度 + */ + updateBlocks(x: number, y: number, width: number, height: number) { + const start = this.getBlockIndex(x - 1, y - 1); + const end = this.getBlockIndex(x + width + 1, y - 1); + const blockHeight = + Math.ceil((y + 1 + height) / this.blockSize) - + Math.floor((y - 1) / this.blockSize); + const blockWidth = end - start; + for (let nx = 0; nx < blockWidth; nx++) { + for (let ny = 0; ny < blockHeight; ny++) { + this.clearBlockCache( + this.getBlockIndex(nx + start, ny + y), + Layer.FRAME_ALL + ); + } + } + + this.update(this); + } + + /** + * 根据图块位置获取渲染块的索引 + * @param x 图块的横坐标 + * @param y 图块的纵坐标 + */ + getBlockIndex(x: number, y: number) { + const bx = Math.floor(x / this.blockSize); + const by = Math.floor(y / this.blockSize); + return by * this.blockData.width + bx; + } + + /** + * 清空某个块的缓存 + * @param index 要清空的块的位置索引 + * @param frame 要清空块的第几帧,可以通过 `Layer.FRAME_0 | Layer.FRAME_1` 的方式一次清空多个帧, + * 不要填1234,否则结果不一定符合预期 + */ + clearBlockCache(index: number, frame: number) { + for (let i = 0; i < 4; i++) { + if (frame & (1 << i)) { + this.blockCache.delete(index * frame); + } + } + } + + /** + * 清空指定缓存索引的块缓存。与 {@link clearBlockCache} 不同之处在于这个是精确控制要清空的缓存, + * 对于需要大量精确清空的场景,此函数效率更高 + * @param index 指定的缓存索引 + */ + clearBlockByPreciseIndex(index: number) { + this.blockCache.delete(index); + } + + /** + * 清空所有块缓存 + */ + clearAllBlockCache() { + this.blockCache.clear(); + } + + /** + * 根据像素位置获取其所在的块 + * @param x 像素横坐标 + * @param y 像素纵坐标 + * @returns 像素位置所在块的索引,不在任何块中时会返回-1 + */ + getBlockByLoc(x: number, y: number) { + const size = this.blockSize; + const width = this.mapWidth * this.cellSize; + const height = this.mapHeight * this.cellSize; + if (x >= width || y >= height) return -1; + const bx = Math.floor(x / size); + const by = Math.floor(y / size); + return by * this.blockData.width + bx; + } + + /** + * 根据像素位置获取应当所在的块的横坐标与纵坐标,即使这个块不存在,例如可以返回 [-1, -2] + * @param x 像素横坐标 + * @param y 像素纵坐标 + */ + getBlockXYByLoc(x: number, y: number): LocArr { + return [Math.floor(x / this.blockSize), Math.floor(y / this.blockSize)]; + } + + /** + * 计算在传入的摄像机的视角下,应该渲染哪些内容 + * @param camera 摄像机 + */ + calNeedRender(camera: Camera): NeedRenderData { + const w = core._WIDTH_; + const h = core._HEIGHT_; + const size = this.blockSize; + const { width } = this.blockData; + const cell = this.cellSize; + + const [x1, y1] = Camera.transformed(camera, 0, 0); + const [x2, y2] = Camera.transformed(camera, w * cell, 0); + const [x3, y3] = Camera.transformed(camera, w * cell, h * cell); + const [x4, y4] = Camera.transformed(camera, 0, h * cell); + + const res: Set = new Set(); + /** 一个纵坐标对应的所有横坐标,用于填充 */ + const xyMap: Map = new Map(); + + [ + [x1, y1, x2, y2], + [x2, y2, x3, y3], + [x3, y3, x4, y4], + [x4, y4, x1, y1] + ].forEach(([fx0, fy0, tx0, ty0]) => { + const fx = Math.floor(fx0 / cell); + const fy = Math.floor(fy0 / cell); + const tx = Math.floor(tx0 / cell); + const ty = Math.floor(ty0 / cell); + const dx = tx - fx; + const dy = ty - fy; + const k = dy / dx; + + // console.log(fx, fy, tx, ty); + + // 斜率无限的时候,竖直 + if (!isFinite(k)) { + const min = k < 0 ? ty : fy; + const max = k < 0 ? fy : ty; + const [x, y] = this.getBlockXYByLoc(fx, min); + + // 在地图左侧或右侧时,将每个纵坐标对应的横坐标填充为0 + + const p = x < 0 ? 0 : x >= width ? width - 1 : x; + const [, ey] = this.getBlockXYByLoc(fx, max); + for (let i = y; i <= ey; i++) { + let arr = xyMap.get(i); + if (!arr) { + arr = []; + xyMap.set(i, arr); + } + arr.push(p); + } + // console.log(y, ey, p); + + return; + } + + const [fbx, fby] = this.getBlockXYByLoc(fx, fy); + // 当斜率为0时 + if (glMatrix.equals(k, 0)) { + const [ex] = this.getBlockXYByLoc(tx, fy); + let arr = xyMap.get(fby); + if (!arr) { + arr = []; + xyMap.set(fby, arr); + } + arr.push(fbx, ex); + // console.log(fbx, ex); + + return; + } + + // 否则使用 Bresenham 直线算法 + if (Math.abs(k) >= 1) { + // 斜率大于一,y方向递增 + const d = Math.sign(dy) * size; + const f = dx > 0 ? fby * size : (fby + 1) * size; + const dir = dy > 0; + + let now = f; + let last = fbx; + let ny = fby; + do { + const bx = Math.floor(fx + (now - fy) / k); + let arr = xyMap.get(ny); + if (!arr) { + arr = []; + xyMap.set(ny, arr); + } + if (bx !== last) { + arr.push(last); + } + arr.push(bx); + last = bx; + ny++; + } while (dir ? (now += d) < ty : (now += d) > ty); + } else { + // 斜率小于一,x方向递增 + const d = Math.sign(dx) * size; + const f = dx > 0 ? fbx * size : (fbx + 1) * size; + const dir = dx > 0; + + let now = f; + let last = fby; + let nx = fbx; + do { + const by = Math.floor(fy + k * (now - fx)); + + if (by !== last) { + let arr = xyMap.get(last); + if (!arr) { + arr = []; + xyMap.set(last, arr); + } + arr.push(nx); + } + let arr = xyMap.get(nx); + if (!arr) { + arr = []; + xyMap.set(by, arr); + } + arr.push(nx); + nx++; + } while (dir ? (now += d) < tx : (now += d) > tx); + } + }); + + // 然后进行填充 + const { width: bw, height: bh } = this.blockData; + const back: [number, number][] = []; + xyMap.forEach((x, y) => { + if (x.length === 1) { + const index = y * bw + x[0]; + + back.push([x[0], y]); + if (index < 0 || index >= bw * bh) return; + res.add(index); + } + const max = Math.max(...x); + const min = Math.min(...x); + + for (let i = min; i <= max; i++) { + const index = y * bw + i; + + back.push([i, y]); + if (index < 0 || index >= bw * bh) continue; + res.add(index); + } + }); + + // console.log([...res], xyMap); + + return { res, back }; + } + + /** + * 渲染当前地图 + */ + renderMap(camera: Camera, need: NeedRenderData) { + this.staticMap.clear(); + this.movingMap.clear(); + this.backMap.clear(); + + this.renderBack(camera, need); + this.renderStatic(camera, need); + this.renderMoving(camera); + } + + /** + * 渲染背景图 + * @param camera 摄像机 + * @param need 需要渲染的块 + */ + protected renderBack(camera: Camera, need: NeedRenderData) { + const cell = this.cellSize; + const frame = (RenderItem.animatedFrame % 4) + 1; + const { width } = this.blockData; + const blockSize = this.blockSize; + const { back } = need; + const { ctx, canvas } = this.backMap; + + const mat = camera.mat; + const a = mat[0]; + const b = mat[1]; + const c = mat[3]; + const d = mat[4]; + const e = mat[6]; + const f = mat[7]; + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.translate(core._PX_ / 2, core._PY_ / 2); + ctx.transform(a, b, c, d, e, f); + + if (this.background !== 0) { + // 画背景图 + const length = this.backImage.length; + const img = this.backImage[frame % length]; + back.forEach(([x, y]) => { + const sx = x * blockSize; + const sy = y * blockSize; + ctx.drawImage( + img, + sx * cell, + sy * cell, + blockSize * cell, + blockSize * cell + ); + }); + } + } + + /** + * 渲染静态层 + */ + protected renderStatic(camera: Camera, need: NeedRenderData) { + const cell = this.cellSize; + const renderable = this.renderable; + const frame = (RenderItem.animatedFrame % 4) + 1; + const { width } = this.blockData; + const blockSize = this.blockSize; + const { ctx, canvas } = this.staticMap; + + ctx.save(); + + const { res: render } = need; + // console.log(render); + const mat = camera.mat; + const a = mat[0]; + const b = mat[1]; + const c = mat[3]; + const d = mat[4]; + const e = mat[6]; + const f = mat[7]; + + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.translate(core._PX_ / 2, core._PY_ / 2); + ctx.transform(a, b, c, d, e, f); + + render.forEach(v => { + const x = v % width; + const y = Math.floor(v / width); + const sx = x * blockSize; + const sy = y * blockSize; + const index = v * 4 + frame - 1; + + const cache = this.blockCache.get(index); + if (cache && cache.floorId === this.floorId) { + ctx.drawImage( + cache.canvas, + sx * cell, + sy * cell, + blockSize * cell, + blockSize * cell + ); + return; + } + + const ex = sx + blockSize; + const ey = sy + blockSize; + + const temp = new MotaOffscreenCanvas2D(); + temp.setAntiAliasing(false); + temp.setHD(false); + temp.withGameScale(false); + temp.size(core._PX_, core._PY_); + + // 先画到临时画布,用于缓存 + for (let nx = sx; nx < ex; nx++) { + for (let ny = sy; ny < ey; ny++) { + const blockIndex = nx + ny * this.mapWidth; + const data = renderable.get(blockIndex); + if (!data) continue; + + const f = frame % data.frame; + const i = frame === 4 && data.frame === 3 ? 1 : f; + const [sx, sy, w, h] = data.render[i]; + const px = nx * cell; + const py = ny * cell; + const image = data.image; + + temp.ctx.drawImage(image, sx, sy, w, h, px, py, w, h); + } + } + ctx.drawImage( + temp.canvas, + sx * cell, + sy * cell, + blockSize * cell, + blockSize * cell + ); + this.blockCache.set(index, { + canvas: temp.canvas, + floorId: this.floorId + }); + }); + + ctx.restore(); + } + + /** + * 渲染移动/大怪物层 + */ + protected renderMoving(camera: Camera) { + const frame = (RenderItem.animatedFrame % 4) + 1; + const cell = this.cellSize; + const halfCell = cell / 2; + const { ctx } = this.movingMap; + + ctx.save(); + const mat = camera.mat; + const a = mat[0]; + const b = mat[1]; + const c = mat[3]; + const d = mat[4]; + const e = mat[6]; + const f = mat[7]; + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.translate(core._PX_ / 2, core._PY_ / 2); + ctx.transform(a, b, c, d, e, f); + const r = + Math.max(a, b, c, d) ** 2 * Math.max(core._PX_, core._PY_) * 2; + + this.movingRenderable.sort((a, b) => { + return a.zIndex - b.zIndex; + }); + + this.movingRenderable.forEach(v => { + const { x, y, image, frame: blockFrame, render } = v; + const f = frame % 4; + const i = frame === 4 && blockFrame === 3 ? 1 : f; + const [sx, sy, w, h] = render[i]; + const px = x * cell - w / 2 + halfCell; + const py = y * cell - h + cell; + const ex = px + w; + const ey = py + h; + if (px ** 2 > r || py ** 2 > r || ex ** 2 > r || ey ** 2 > r) { + return; + } + ctx.drawImage(image, sx, sy, w, h, px, py, w, h); + }); + + ctx.restore(); + } + + /** + * 对图块进行线性插值移动或瞬移\ + * 线性插值移动:就是匀速平移,可以斜向移动\ + * 瞬移:立刻移动到目标点 + * @param index 要移动的图块在渲染数据中的索引位置 + * @param type 线性插值移动或瞬移 + * @param x 目标点横坐标 + * @param y 目标点纵坐标 + * @param time 移动总时长,注意不是每格时长 + */ + move( + index: number, + type: 'linear' | 'swap', + x: number, + y: number, + time?: number + ): Promise; + /** + * 让图块按照一个函数进行移动 + * @param index 要移动的图块在渲染数据中的索引位置 + * @param type 函数式移动 + * @param fn 移动函数,传入一个完成度(范围0-1),返回一个三元素数组,表示横纵格子坐标,可以是小数。 + * 第三个元素表示图块纵深,一般图块的纵深就是其纵坐标,当地图上有大怪物时,此举可以辅助渲染, + * 否则可能会导致移动过程中与大怪物的层级关系不正确,比如全在大怪物身后。注意不建议频繁改动这个值, + * 因为此举会导致层级的重新排序,降低渲染性能 + * @param time 移动总时长 + * @param relative 是否是相对模式 + */ + move( + index: number, + type: 'fn', + fn: TimingFn<3>, + time?: number, + relative?: boolean + ): Promise; + move( + index: number, + type: 'linear' | 'swap' | 'fn', + x: number | TimingFn<3>, + y?: number, + time?: number | boolean + ): Promise { + // todo + return Promise.resolve(); + } + + destroy(): void { + if (!this.floorId) return; + const floor = Layer.LayerMap.get(this.floorId); + if (!floor) return; + floor.delete(this); + } +} + +// 当地图发生变化时更新地图 +const hook = Mota.require('var', 'hook'); +hook.on('changingFloor', floorId => { + Layer.LayerMap.forEach(v => { + const needBind: Layer[] = []; + v.forEach(v => { + if (v.bindThisFloor) { + needBind.push(v); + } + }); + needBind.forEach(v => { + v.bindData(floorId, v.layer!); + }); + }); +}); +hook.on('setBlock', (x, y, floor, old, n) => { + const layers = Layer.LayerMap.get(floor); + if (layers) { + layers.forEach(v => { + if (v.layer === 'event') { + v.putRenderData([n], 1, x, y, true); + } + }); + } +}); diff --git a/src/core/render/preset/misc.ts b/src/core/render/preset/misc.ts index a0be8cc..20b02d7 100644 --- a/src/core/render/preset/misc.ts +++ b/src/core/render/preset/misc.ts @@ -90,7 +90,7 @@ export class Text extends Sprite { } } -type SizedCanvasImageSource = Exclude< +export type SizedCanvasImageSource = Exclude< CanvasImageSource, VideoFrame | SVGElement >; diff --git a/src/core/render/render.ts b/src/core/render/render.ts index 95004f5..8f5f31b 100644 --- a/src/core/render/render.ts +++ b/src/core/render/render.ts @@ -1,34 +1,41 @@ -import { isNil } from 'lodash-es'; +import { Animation, hyper, sleep } from 'mutate-animate'; 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'; -import { Animation, hyper } from 'mutate-animate'; +import { IRenderDestroyable, RenderItem, withCacheRender } from './item'; +import { Layer } from './preset/layer'; + +export class MotaRenderer extends Container implements IRenderDestroyable { + static list: Set = new Set(); -export class MotaRenderer extends Container { canvas: MotaOffscreenCanvas2D; camera: Camera; /** 摄像机缓存,如果是需要快速切换摄像机的场景,使用缓存可以大幅提升性能表现 */ cameraCache: Map = new Map(); target: MotaCanvas2D; + /** 这个渲染对象的id */ + id: string; - private needUpdate: boolean = false; + protected needUpdate: boolean = false; - constructor() { + constructor(id: string = 'render-main') { super(); + this.id = id; + this.canvas = new MotaOffscreenCanvas2D(); this.camera = new Camera(); - this.target = new MotaCanvas2D(`render-main`); - this.width = 480; - this.height = 480; + this.target = new MotaCanvas2D(id); + this.width = core._PX_; + this.height = core._PY_; this.target.withGameScale(true); - this.target.size(480, 480); + this.target.size(core._PX_, core._PY_); this.canvas.withGameScale(true); - this.canvas.size(480, 480); + this.canvas.size(core._PX_, core._PY_); this.target.css(`z-index: 100`); + + MotaRenderer.list.add(this); } /** @@ -60,6 +67,7 @@ export class MotaRenderer extends Container { render() { const { canvas, ctx } = this.target; const camera = this.camera; + this.emit('beforeRender'); ctx.clearRect(0, 0, canvas.width, canvas.height); withCacheRender(this, canvas, ctx, camera, canvas => { const { canvas: ca, ctx: ct, scale } = canvas; @@ -72,15 +80,21 @@ export class MotaRenderer extends Container { const f = mat[7] * scale; this.sortedChildren.forEach(v => { if (v.type === 'absolute') { - ct.transform(scale, 0, 0, scale, 0, 0); + ct.setTransform(scale, 0, 0, scale, 0, 0); } else { ct.setTransform(1, 0, 0, 1, 0, 0); ct.translate(ca.width / 2, ca.height / 2); ct.transform(a, b, c, d, e, f); } + if (!v.antiAliasing) { + ctx.imageSmoothingEnabled = false; + } else { + ctx.imageSmoothingEnabled = true; + } v.render(ca, ct, camera); }); }); + this.emit('afterRender'); } /** @@ -92,12 +106,16 @@ export class MotaRenderer extends Container { requestAnimationFrame(() => { this.cache(this.writing); this.needUpdate = false; - this.emit('beforeUpdate', item); - this.render(); - this.emit('afterUpdate', item); + this.refresh(item); }); } + protected refresh(item?: RenderItem): void { + this.emit('beforeUpdate', item); + this.render(); + this.emit('afterUpdate', item); + } + /** * 将缓存内容渲染至画面 * @param cache 渲染缓存,是一个离屏Canvas2D对象 @@ -114,44 +132,51 @@ export class MotaRenderer extends Container { mount() { this.target.mount(); } + + destroy() { + MotaRenderer.list.delete(this); + } } +window.addEventListener('resize', () => { + MotaRenderer.list.forEach(v => v.update(v)); +}); + Mota.require('var', 'hook').once('reset', () => { const render = new MotaRenderer(); - const con = new Container('static'); + const layer = new Layer(); + const bgLayer = new Layer(); const camera = render.camera; render.mount(); - const testText = new Text(); - testText.setText('测试测试'); - testText.pos(240, 240); - testText.setFont('32px normal'); - testText.setStyle('#fff'); - con.size(480, 480); - con.pos(-240, -240); - const testImage = new Image(core.material.images.images['arrow.png']); - testImage.pos(240, 240); - - con.appendChild([testText, testImage]); - - render.appendChild([con]); - - render.update(render); + layer.zIndex = 2; + bgLayer.zIndex = 1; + render.appendChild([layer, bgLayer]); + layer.bindThis('event', true); + bgLayer.bindThis('bg', true); + bgLayer.setBackground(650); const ani = new Animation(); - ani.mode(hyper('sin', 'in-out')) - .time(10000) - .absolute() - .rotate(360) - .scale(0.7) - .move(100, 100); + ani.ticker.add(() => { - render.cache('@default'); camera.reset(); - camera.move(ani.x, ani.y); - camera.scale(ani.size); camera.rotate((ani.angle / 180) * Math.PI); + camera.move(240, 240); render.update(render); }); - setTimeout(() => ani.ticker.destroy(), 10000); + + sleep(1000).then(() => { + ani.mode(hyper('sin', 'out')).time(100).absolute().rotate(30); + sleep(100).then(() => { + ani.time(3000).rotate(0); + }); + sleep(3100).then(() => { + ani.time(5000).mode(hyper('tan', 'in-out')).rotate(3600); + }); + // ani.mode(shake2(5, hyper('sin', 'in-out')), true) + // .time(5000) + // .shake(1, 0); + }); + + console.log(layer); }); diff --git a/src/core/render/sprite.ts b/src/core/render/sprite.ts index 5241d0e..fcc3f89 100644 --- a/src/core/render/sprite.ts +++ b/src/core/render/sprite.ts @@ -1,8 +1,13 @@ import { Camera } from './camera'; -import { RenderFunction, RenderItem, withCacheRender } from './item'; +import { + ICanvasCachedRenderItem, + RenderFunction, + RenderItem, + withCacheRender +} from './item'; import { MotaOffscreenCanvas2D } from '../fx/canvas2d'; -export class Sprite extends RenderItem { +export class Sprite extends RenderItem implements ICanvasCachedRenderItem { renderFn: RenderFunction; canvas: MotaOffscreenCanvas2D; @@ -19,12 +24,12 @@ export class Sprite extends RenderItem { ctx: CanvasRenderingContext2D, camera: Camera ): void { - this.emit('beforeUpdate', this); + this.emit('beforeRender'); withCacheRender(this, canvas, ctx, camera, canvas => { this.renderFn(canvas, camera); }); this.writing = void 0; - this.emit('afterUpdate', this); + this.emit('afterRender'); } size(width: number, height: number) { @@ -39,4 +44,20 @@ export class Sprite extends RenderItem { this.x = x; this.y = y; } + + setRenderFn(fn: RenderFunction) { + this.renderFn = fn; + } + + setHD(hd: boolean): void { + this.highResolution = hd; + this.canvas.setHD(hd); + this.update(this); + } + + setAntiAliasing(anti: boolean): void { + this.antiAliasing = anti; + this.canvas.setAntiAliasing(anti); + this.update(this); + } } diff --git a/src/game/game.ts b/src/game/game.ts index ac03c74..693be47 100644 --- a/src/game/game.ts +++ b/src/game/game.ts @@ -6,7 +6,7 @@ interface GameLoadEvent extends EmitableEvent { coreLoaded: () => void; autotileLoaded: () => void; coreInit: () => void; - materialLoaded: () => void; + loaded: () => void; } class GameLoading extends EventEmitter { @@ -14,9 +14,6 @@ class GameLoading extends EventEmitter { private autotileNum?: number; private autotileListened: boolean = false; - private materialsNum: number = main.materials.length; - private materialsLoaded: number = 0; - constructor() { super(); this.on( @@ -33,13 +30,6 @@ class GameLoading extends EventEmitter { }); } - addMaterialLoaded() { - this.materialsLoaded++; - if (this.materialsLoaded === this.materialsNum) { - this.emit('materialLoaded'); - } - } - addAutotileLoaded() { this.autotileLoaded++; if (this.autotileLoaded === this.autotileNum) { @@ -100,11 +90,20 @@ export interface GameEvent extends EmitableEvent { 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; + /** Emitted in libs/maps.js setBlock */ + setBlock: ( + x: number, + y: number, + floorId: FloorIds, + oldBlock: AllNumbers, + newBlock: AllNumbers + ) => void; } export const hook = new EventEmitter(); diff --git a/src/game/system.ts b/src/game/system.ts index 28de887..0d07f80 100644 --- a/src/game/system.ts +++ b/src/game/system.ts @@ -28,6 +28,13 @@ 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'; +import type { texture } from '@/core/render/cache'; +import type { MotaRenderer } from '@/core/render/render'; +import type { Container } from '@/core/render/container'; +import type { Sprite } from '@/core/render/sprite'; +import type { Camera } from '@/core/render/camera'; +import type { Image, Text } from '@/core/render/preset/misc'; +import type { RenderItem } from '@/core/render/item'; interface ClassInterface { // 渲染进程与游戏进程通用 @@ -99,6 +106,14 @@ interface ModuleInterface { }; Render: { heroRender: HeroRenderer; + texture: typeof texture; + MotaRenderer: MotaRenderer; + Container: Container; + Sprite: Sprite; + Camera: Camera; + Text: Text; + Image: Image; + RenderItem: RenderItem; }; } diff --git a/src/plugin/fx/smoothView.ts b/src/plugin/fx/smoothView.ts index c40dfe6..75d95cc 100644 --- a/src/plugin/fx/smoothView.ts +++ b/src/plugin/fx/smoothView.ts @@ -57,11 +57,13 @@ export function init() { const hso = hyper('sin', 'out'); let time2 = Date.now(); Mota.rewrite(core.control, '_moveAction_moving', 'front', () => { - const t = setting.getValue('screen.smoothView', false) ? 200 : 0; + const f = core.status.floorId === 'tower6'; + const t = setting.getValue('screen.smoothView', false) && !f ? 200 : 0; if (Date.now() - time2 > 20) tran.mode(hso).time(t).absolute(); }); Mota.rewrite(core.control, 'moveDirectly', 'front', () => { - const t = setting.getValue('screen.smoothView', false) ? 600 : 0; + const f = core.status.floorId === 'tower6'; + const t = setting.getValue('screen.smoothView', false) && !f ? 600 : 0; time2 = Date.now(); tran.mode(hso).time(t).absolute(); }); diff --git a/src/plugin/game/fx/halo.ts b/src/plugin/game/fx/halo.ts index 7551cfc..4e7b71a 100644 --- a/src/plugin/game/fx/halo.ts +++ b/src/plugin/game/fx/halo.ts @@ -14,6 +14,7 @@ export function drawHalo( if (main.replayChecking) return; const setting = Mota.require('var', 'mainSetting'); if (!setting.getValue('screen.halo', true)) return; + Mota.require('fn', 'ensureFloorDamage')(floorId); const col = core.status.maps[floorId].enemy; const [dx, dy] = col.translation; const list = col.haloList.concat( diff --git a/src/plugin/game/fx/itemDetail.ts b/src/plugin/game/fx/itemDetail.ts index 5beffdc..0bae38f 100644 --- a/src/plugin/game/fx/itemDetail.ts +++ b/src/plugin/game/fx/itemDetail.ts @@ -103,7 +103,7 @@ function drawItemDetail(diff: any, x: number, y: number) { let color = '#fff'; if (typeof diff[name] === 'number') - content = core.formatBigNumber(diff[name], true); + content = core.formatBigNumber(Math.round(diff[name]), true); else content = diff[name]; switch (name) { diff --git a/src/plugin/game/loopMap.ts b/src/plugin/game/loopMap.ts index ed18317..dea4bd9 100644 --- a/src/plugin/game/loopMap.ts +++ b/src/plugin/game/loopMap.ts @@ -192,7 +192,7 @@ export function init() { maps.prototype._getBgFgMapArray = function ( name: string, floorId: FloorIds, - noCache: boolean + noCache: boolean = false ) { floorId = floorId || core.status.floorId; if (!floorId) return []; diff --git a/src/types/core.d.ts b/src/types/core.d.ts index 3f5cf7d..85a24c5 100644 --- a/src/types/core.d.ts +++ b/src/types/core.d.ts @@ -76,7 +76,7 @@ type MaterialImages = { /** * 各个类型的图块的图片 */ - [C in Exclude]: HTMLImageElement; + [C in Exclude]: HTMLImageElement; } & { /** * 空气墙 @@ -1415,6 +1415,10 @@ interface MapDataOf { * 图块的类型 */ cls: ClsOf; + + bigImage?: ImageIds; + + faceIds?: Record; } /** diff --git a/src/types/enemy.d.ts b/src/types/enemy.d.ts index 35f8733..799b0a2 100644 --- a/src/types/enemy.d.ts +++ b/src/types/enemy.d.ts @@ -89,6 +89,9 @@ type Enemy = { specialHalo?: number[]; translation?: [number, number]; + + /** 大怪物绑定贴图 */ + bigImage?: ImageIds; } & { [P in PartialNumbericEnemyProperty]?: number; } & { diff --git a/src/types/map.d.ts b/src/types/map.d.ts index 3976321..126605f 100644 --- a/src/types/map.d.ts +++ b/src/types/map.d.ts @@ -1397,6 +1397,12 @@ interface Maps { }; _getBigImageInfo(bigImage: HTMLImageElement, face: Dir, posX: number): any; + + _getBgFgMapArray( + name: string, + floorId: FloorIds, + noCache?: boolean + ): number[][]; } declare const maps: new () => Maps; diff --git a/src/ui/load.vue b/src/ui/load.vue index 93c65a4..b6cc517 100644 --- a/src/ui/load.vue +++ b/src/ui/load.vue @@ -74,6 +74,7 @@ onMounted(async () => { core._afterLoadResources(props.callback); logger.log(`Resource load end.`); loadDiv.style.opacity = '0'; + Mota.require('var', 'loading').emit('loaded'); await sleep(1000); fixedUi.close(props.num); fixedUi.open('start');