From ac5eefdb0261adf53022fa66a1c66cafc506d9ab Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Fri, 21 Feb 2025 16:11:57 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20MotaOffscreenCanvas2D=20=E7=9A=84?= =?UTF-8?q?=E5=9E=83=E5=9C=BE=E5=9B=9E=E6=94=B6=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/libs/control.js | 2 +- src/core/fx/canvas2d.ts | 29 +++- src/core/render/item.ts | 17 ++- src/core/render/preset/damage.ts | 3 +- src/core/render/preset/layer.ts | 17 ++- src/core/render/preset/misc.ts | 6 +- src/module/render/components/misc.tsx | 42 ++++++ src/module/render/shared.ts | 2 +- src/module/render/ui/main.tsx | 56 +++++++- src/module/render/ui/statusBar.tsx | 195 +++++++++++++++++++++----- src/ui/statusBar.vue | 12 +- 11 files changed, 320 insertions(+), 61 deletions(-) create mode 100644 src/module/render/components/misc.tsx diff --git a/public/libs/control.js b/public/libs/control.js index 25a6f9f..ce62750 100644 --- a/public/libs/control.js +++ b/public/libs/control.js @@ -3149,7 +3149,7 @@ control.prototype.resize = function () { core.domStyle.scale = target - 0.25; } - const pw = (480 + 180) * core.domStyle.scale; + const pw = (480 + 180 * 2) * core.domStyle.scale; const ph = 480 * core.domStyle.scale; core.dom.gameDraw.style.width = `${pw}px`; core.dom.gameDraw.style.height = `${ph}px`; diff --git a/src/core/fx/canvas2d.ts b/src/core/fx/canvas2d.ts index d8d4226..ac4dd25 100644 --- a/src/core/fx/canvas2d.ts +++ b/src/core/fx/canvas2d.ts @@ -33,8 +33,14 @@ export class MotaOffscreenCanvas2D extends EventEmitter { return this._freezed; } + private _active: boolean = true; + get active() { + return this._active; + } + /** - * 创建一个新的离屏画布 + * 创建一个新的离屏画布\ + * **注意**:如果你在自定义渲染元素中使用,请避免使用此构造函数,而应该使用 `RenderItem.requireCanvas` * @param alpha 是否启用透明度通道 * @param canvas 指定画布,不指定时会自动创建一个新画布 */ @@ -146,6 +152,27 @@ export class MotaOffscreenCanvas2D extends EventEmitter { MotaOffscreenCanvas2D.list.delete(this); } + /** + * 使此画布生效,使用前请务必调用此函数,生效后会跟随游戏的放缩比例更改大小,但会导致不会被垃圾回收 + */ + activate() { + if (this._active || this._freezed) return; + MotaOffscreenCanvas2D.list.add(this); + if (this.autoScale) { + this.size(this.width, this.height); + this.symbol++; + this.emit('resize'); + } + } + + /** + * 使此画布失效,当这个画布暂时不会被使用时请务必调用此函数,失效后若没有对此画布的引用,那么会自动垃圾回收 + */ + deactivate() { + if (!this._active || this._freezed) return; + MotaOffscreenCanvas2D.list.delete(this); + } + /** * 复制一个离屏Canvas2D对象,一般用于缓存等操作 * @param canvas 被复制的MotaOffscreenCanvas2D对象 diff --git a/src/core/render/item.ts b/src/core/render/item.ts index 5cc11ae..8166d05 100644 --- a/src/core/render/item.ts +++ b/src/core/render/item.ts @@ -316,13 +316,15 @@ export abstract class RenderItem //#region 渲染配置与缓存 /** 渲染缓存信息 */ - protected cache: MotaOffscreenCanvas2D = new MotaOffscreenCanvas2D(); + protected cache: MotaOffscreenCanvas2D; /** 是否需要更新缓存 */ protected cacheDirty: boolean = false; /** 是否启用缓存机制 */ readonly enableCache: boolean = true; /** 是否启用transform下穿机制,即画布的变换是否会继续作用到下一层画布 */ readonly transformFallThrough: boolean = false; + /** 这个渲染元素使用到的所有画布 */ + protected readonly canvases: Set = new Set(); //#endregion //#region 交互事件 @@ -356,6 +358,7 @@ export abstract class RenderItem this.type = type; this._transform.bind(this); + this.cache = this.requireCanvas(); this.cache.withGameScale(true); } @@ -414,6 +417,16 @@ export abstract class RenderItem this.emit('afterRender', transform); } + /** + * 申请一个 `MotaOffscreenCanvas2D`,即申请一个画布 + * @param alpha 是否启用画布的 alpha 通道 + */ + protected requireCanvas(alpha: boolean = true) { + const canvas = new MotaOffscreenCanvas2D(alpha); + this.canvases.add(canvas); + return canvas; + } + //#region 修改元素属性 /** @@ -625,6 +638,7 @@ export abstract class RenderItem this.update(); this.checkRoot(); this._root?.connect(this); + this.canvases.forEach(v => v.activate()); } /** @@ -638,6 +652,7 @@ export abstract class RenderItem this._parent = void 0; parent.requestSort(); parent.update(); + this.canvases.forEach(v => v.deactivate()); if (!success) return false; this._root?.disconnect(this); this._root = void 0; diff --git a/src/core/render/preset/damage.ts b/src/core/render/preset/damage.ts index 2044a2d..7d076f3 100644 --- a/src/core/render/preset/damage.ts +++ b/src/core/render/preset/damage.ts @@ -498,8 +498,7 @@ export class Damage extends RenderItem { this.emit('dirtyUpdate', v); // 否则依次渲染并写入缓存 - const temp = - block.cache.get(v)?.canvas ?? new MotaOffscreenCanvas2D(); + const temp = block.cache.get(v)?.canvas ?? this.requireCanvas(); temp.clear(); temp.setHD(true); temp.setAntiAliasing(true); diff --git a/src/core/render/preset/layer.ts b/src/core/render/preset/layer.ts index ddbebf5..a7e0914 100644 --- a/src/core/render/preset/layer.ts +++ b/src/core/render/preset/layer.ts @@ -10,7 +10,6 @@ import { Transform } from '../transform'; import { LayerFloorBinder, LayerGroupFloorBinder } from './floor'; import { RenderAdapter } from '../adapter'; import { ElementNamespace, ComponentInternalInstance } from 'vue'; -import { Camera } from '../camera'; import { IAnimateFrame, renderEmits } from '../frame'; export interface ILayerGroupRenderExtends { @@ -358,7 +357,7 @@ export class LayerGroup return; } case 'camera': - if (!this.assertType(nextValue, Camera, key)) return; + if (!this.assertType(nextValue, Transform, key)) return; this.camera = nextValue; return; } @@ -571,11 +570,11 @@ export class Layer extends Container { static readonly FRAME_ALL = 15; /** 静态层,包含除大怪物及正在移动的内容外的内容 */ - protected staticMap: MotaOffscreenCanvas2D = new MotaOffscreenCanvas2D(); + protected staticMap: MotaOffscreenCanvas2D = this.requireCanvas(); /** 移动层,包含大怪物及正在移动的内容 */ - protected movingMap: MotaOffscreenCanvas2D = new MotaOffscreenCanvas2D(); + protected movingMap: MotaOffscreenCanvas2D = this.requireCanvas(); /** 背景图层 */ - protected backMap: MotaOffscreenCanvas2D = new MotaOffscreenCanvas2D(); + protected backMap: MotaOffscreenCanvas2D = this.requireCanvas(); /** 最终渲染至的Sprite */ main: Sprite = new Sprite('absolute', false, true); @@ -783,12 +782,12 @@ export class Layer extends Container { if (!data) return; const frame = data.frame; - const temp = new MotaOffscreenCanvas2D(); + const temp = this.requireCanvas(); temp.setHD(false); temp.setAntiAliasing(false); temp.withGameScale(false); for (let i = 0; i < frame; i++) { - const canvas = new MotaOffscreenCanvas2D(); + const canvas = this.requireCanvas(); const ctx = canvas.ctx; const tempCtx = temp.ctx; const [sx, sy, w, h] = data.render[i]; @@ -1206,7 +1205,7 @@ export class Layer extends Container { const ex = Math.min(sx + blockSize, this.mapWidth); const ey = Math.min(sy + blockSize, this.mapHeight); - const temp = new MotaOffscreenCanvas2D(); + const temp = this.requireCanvas(); temp.setAntiAliasing(false); temp.setHD(false); temp.withGameScale(false); @@ -1478,7 +1477,7 @@ export class Layer extends Container { return; case 'floorImage': if (!this.assertType(nextValue, Array, key)) return; - this.setFloorImage(nextValue); + this.setFloorImage(nextValue as FloorAnimate[]); return; } super.patchProp(key, prevValue, nextValue, namespace, parentComponent); diff --git a/src/core/render/preset/misc.ts b/src/core/render/preset/misc.ts index 647eb9f..5294721 100644 --- a/src/core/render/preset/misc.ts +++ b/src/core/render/preset/misc.ts @@ -19,7 +19,7 @@ export class Text extends RenderItem { fillStyle?: CanvasStyle = '#fff'; strokeStyle?: CanvasStyle; - font?: string = ''; + font: string = '16px Verdana'; strokeWidth: number = 1; private length: number = 0; @@ -42,7 +42,7 @@ export class Text extends RenderItem { ctx.textBaseline = 'bottom'; ctx.fillStyle = this.fillStyle ?? 'transparent'; ctx.strokeStyle = this.strokeStyle ?? 'transparent'; - ctx.font = this.font ?? ''; + ctx.font = this.font; ctx.lineWidth = this.strokeWidth; if (this.strokeStyle) { @@ -59,7 +59,7 @@ export class Text extends RenderItem { measure() { const ctx = Text.measureCanvas.ctx; ctx.textBaseline = 'bottom'; - ctx.font = this.font ?? ''; + ctx.font = this.font; const res = ctx.measureText(this.text); return res; } diff --git a/src/module/render/components/misc.tsx b/src/module/render/components/misc.tsx new file mode 100644 index 0000000..ad2053b --- /dev/null +++ b/src/module/render/components/misc.tsx @@ -0,0 +1,42 @@ +import { ElementLocator, Sprite } from '@/core/render'; +import { defineComponent, onMounted, ref, watch } from 'vue'; +import { SetupComponentOptions } from './types'; + +interface ProgressProps { + /** 进度条的位置 */ + loc: ElementLocator; + /** 进度条的进度,1表示完成,0表示未完成 */ + progress: number; + /** 已完成部分的样式,默认为绿色(green) */ + success?: CanvasStyle; + /** 未完成部分的样式,默认为灰色(gray) */ + background?: CanvasStyle; +} + +const progressProps = { + props: ['loc', 'progress', 'success', 'background'] +} satisfies SetupComponentOptions; + +export const Progress = defineComponent(props => { + const element = ref(); + + onMounted(() => { + element.value?.setRenderFn(canvas => { + const { ctx } = canvas; + const width = props.loc[2] ?? 200; + const height = props.loc[3] ?? 200; + ctx.fillStyle = props.background ?? 'gray'; + ctx.fillRect(0, 0, width, height); + ctx.fillStyle = props.success ?? 'green'; + ctx.fillRect(0, 0, width * props.progress, height); + }); + }); + + watch(props, () => { + element.value?.update(); + }); + + return () => { + return ; + }; +}, progressProps); diff --git a/src/module/render/shared.ts b/src/module/render/shared.ts index 2365800..ece65d4 100644 --- a/src/module/render/shared.ts +++ b/src/module/render/shared.ts @@ -6,5 +6,5 @@ export const ENABLE_RIGHT_STATUS_BAR = true; export const MAP_WIDTH = 480; export const MAP_HEIGHT = 480; -export const MAIN_WIDTH = 480 + 180; +export const MAIN_WIDTH = 480 + 180 * 2; export const MAIN_HEIGHT = 480; diff --git a/src/module/render/ui/main.tsx b/src/module/render/ui/main.tsx index 348a1b1..7257184 100644 --- a/src/module/render/ui/main.tsx +++ b/src/module/render/ui/main.tsx @@ -26,7 +26,12 @@ import { STATUS_BAR_HEIGHT, STATUS_BAR_WIDTH } from '../shared'; -import { IHeroStatus, StatusBar } from './statusBar'; +import { + ILeftHeroStatus, + IRightHeroStatus, + LeftStatusBar, + RightStatusBar +} from './statusBar'; import { onLoaded } from '../use'; const MainScene = defineComponent(() => { @@ -71,25 +76,58 @@ const MainScene = defineComponent(() => { weather.bind(map.value); }); - const status: IHeroStatus = reactive({ + const leftStatus: ILeftHeroStatus = reactive({ hp: 0, atk: 0, def: 0, - mdef: 0 + mdef: 0, + money: 0, + exp: 0, + yellowKey: 0, + blueKey: 0, + redKey: 0, + floor: 'MT0', + lv: '', + regen: 0, + exAtk: 0, + magicDef: 0 }); + const rightStatus: IRightHeroStatus = reactive({}); + + const { getHeroStatusOn } = Mota.requireAll('fn'); + + const updateStatus = () => { + const hero = core.status.hero; + leftStatus.atk = getHeroStatusOn('atk'); + leftStatus.hp = getHeroStatusOn('hp'); + leftStatus.def = getHeroStatusOn('def'); + leftStatus.mdef = getHeroStatusOn('mdef'); + leftStatus.money = getHeroStatusOn('money'); + leftStatus.exp = core.getNextLvUpNeed() ?? 0; + leftStatus.yellowKey = core.itemCount('yellowKey'); + leftStatus.blueKey = core.itemCount('blueKey'); + leftStatus.redKey = core.itemCount('redKey'); + leftStatus.floor = core.status.floorId; + leftStatus.lv = core.getLvName(hero.lv); + leftStatus.regen = getHeroStatusOn('hpmax'); + leftStatus.exAtk = getHeroStatusOn('mana'); + leftStatus.magicDef = getHeroStatusOn('magicDef'); + }; const loaded = ref(false); onLoaded(() => { loaded.value = true; }); + Mota.require('var', 'hook').on('statusBarUpdate', updateStatus); + return () => ( {loaded.value && ( - + status={leftStatus} + > )} @@ -103,6 +141,12 @@ const MainScene = defineComponent(() => { + {loaded.value && ( + + )} {mainUIController.render()} ); diff --git a/src/module/render/ui/statusBar.tsx b/src/module/render/ui/statusBar.tsx index ed59321..2277f23 100644 --- a/src/module/render/ui/statusBar.tsx +++ b/src/module/render/ui/statusBar.tsx @@ -3,54 +3,181 @@ import { defineComponent } from 'vue'; import { SetupComponentOptions } from '../components'; import { ElementLocator } from '@/core/render'; -export interface IHeroStatus { +export interface ILeftHeroStatus { hp: number; atk: number; def: number; mdef: number; + money: number; + exp: number; + yellowKey: number; + blueKey: number; + redKey: number; + floor: FloorIds; + lv: string; + /** 生命回复 */ + regen: number; + /** 额外攻击 */ + exAtk: number; + /** 魔法防御 */ + magicDef: number; } -interface StatusBarProps { +export interface IRightHeroStatus {} + +interface StatusBarProps { loc: ElementLocator; - status: IHeroStatus; + status: T; } const statusBarProps = { props: ['loc', 'status'] -} satisfies SetupComponentOptions; +} satisfies SetupComponentOptions>; -export const StatusBar = defineComponent(p => { - const hpIcon = core.material.images.images['hp.png']; - const atkIcon = core.material.images.images['atk.png']; - const defIcon = core.material.images.images['def.png']; - const mdefIcon = core.material.images.images['IQ.png']; +export const LeftStatusBar = defineComponent>( + p => { + const hpIcon = core.material.images.images['hp.png']; + const atkIcon = core.material.images.images['atk.png']; + const defIcon = core.material.images.images['def.png']; + const mdefIcon = core.material.images.images['IQ.png']; + const moneyIcon = core.material.images.images['money.png']; + const expIcon = core.material.images.images['exp.png']; - const s = p.status; - const f = core.formatBigNumber; + const s = p.status; + const f = core.formatBigNumber; - const iconLoc = (n: number): ElementLocator => { - return [16, 16 + 48 * n, 32, 32]; - }; + const floorName = core.floors[s.floor].title; - const textLoc = (n: number): ElementLocator => { - return [64, 32 + 48 * n, void 0, void 0, 0.5, 0.5]; - }; + const key = (num: number) => { + return num.toString().padStart(2, '0'); + }; - return () => { - return ( - - - - - - - - - - - - ); - }; -}, statusBarProps); + const font1 = '18px normal'; + const font2 = 'bold 18px normal'; + const font3 = 'bold 14px normal'; -export const statusBarUI = new GameUI('status-bar', StatusBar); + const iconLoc = (n: number): ElementLocator => { + return [16, 76 + 44 * n, 32, 32]; + }; + + const textLoc = (n: number): ElementLocator => { + return [60, 92 + 44 * n, void 0, void 0, 0, 0.5]; + }; + + const central = (y: number): ElementLocator => { + const width = p.loc[2] ?? 200; + return [width / 2, y, void 0, void 0, 0.5, 0.5]; + }; + + const right = (y: number): ElementLocator => { + const width = p.loc[2] ?? 200; + return [width - 16, y, void 0, void 0, 1, 0.5]; + }; + + const keyCount = 3; + const keyY = 92 + 44 * 6; + const keyLoc = (n: number): ElementLocator => { + const width = p.loc[2] ?? 200; + const per = width / (keyCount + 1); + return [per * (n + 1), keyY, void 0, void 0, 0.5, 0.5]; + }; + + return () => { + return ( + + + + + + + + + + + + + {s.magicDef > 0 && ( + + )} + + + + + + + + + + + + + ); + }; + }, + statusBarProps +); + +export const RightStatusBar = defineComponent>( + p => { + return () => { + return ( + + + + ); + }; + }, + statusBarProps +); + +export const leftStatusBarUI = new GameUI('left-status-bar', LeftStatusBar); +export const rightStatusBarUI = new GameUI('right-status-bar', RightStatusBar); diff --git a/src/ui/statusBar.vue b/src/ui/statusBar.vue index 7a5d4d5..39909ba 100644 --- a/src/ui/statusBar.vue +++ b/src/ui/statusBar.vue @@ -262,7 +262,9 @@ onUnmounted(() => { font-size: 200%; width: 100%; margin-bottom: 14px; - text-shadow: 3px 2px 3px #000, 0px 0px 3px #111; + text-shadow: + 3px 2px 3px #000, + 0px 0px 3px #111; display: flex; flex-direction: row; align-items: center; @@ -303,7 +305,9 @@ onUnmounted(() => { font-size: 200%; width: 100%; text-align: center; - text-shadow: 3px 2px 3px #000, 0px 0px 3px #111; + text-shadow: + 3px 2px 3px #000, + 0px 0px 3px #111; } #status-lv { @@ -311,7 +315,9 @@ onUnmounted(() => { font-size: 200%; width: 100%; text-align: center; - text-shadow: 3px 2px 3px #000, 0px 0px 3px #111; + text-shadow: + 3px 2px 3px #000, + 0px 0px 3px #111; } .status-extra {