From d92b16cd6d57921d9e9fb3e4119459dc773b555c Mon Sep 17 00:00:00 2001 From: humingxing <930621236@qq.com> Date: Mon, 19 Oct 2026 15:44:12 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/mx/module/gm/GmEasy.ts | 14 + assets/mx/module/gm/GmRegistry.ts | 36 ++ assets/mx/module/gm/GmTypes.ts | 4 + assets/mx/module/gm/GmView.ts | 90 ++++- assets/script/gm/ProjectGmBootstrap.ts | 30 +- assets/script/gm/ProjectGmPanels.ts | 516 ++++++++++++++++++------- docs/方案-GM可视化模块.md | 45 ++- 7 files changed, 547 insertions(+), 188 deletions(-) diff --git a/assets/mx/module/gm/GmEasy.ts b/assets/mx/module/gm/GmEasy.ts index 4dc6ef9..696c692 100644 --- a/assets/mx/module/gm/GmEasy.ts +++ b/assets/mx/module/gm/GmEasy.ts @@ -20,6 +20,7 @@ export class GmEasy { private _group = '常用'; private _order = 0; private _seq = 0; + private _pinNext = false; /** * 切换 Tab 分组(后续命令都进这个分组)。 @@ -33,6 +34,14 @@ export class GmEasy { return this; } + /** + * 下一条命令可钉到「常用」页。 + */ + pinable(): this { + this._pinNext = true; + return this; + } + /** * 快捷按钮:点一下执行。 */ @@ -179,8 +188,13 @@ export class GmEasy { private _push(cmd: IGmCommand): void { if (!cmd.title) { console.warn('[GmEasy] 跳过无标题命令'); + this._pinNext = false; return; } + if (this._pinNext) { + cmd.pinable = true; + this._pinNext = false; + } this._cmds.push(cmd); } } diff --git a/assets/mx/module/gm/GmRegistry.ts b/assets/mx/module/gm/GmRegistry.ts index 7542113..dbcd749 100644 --- a/assets/mx/module/gm/GmRegistry.ts +++ b/assets/mx/module/gm/GmRegistry.ts @@ -1,5 +1,9 @@ +import { sys } from 'cc'; import type { GmPanelFactory, IGmCommand } from './GmTypes'; +/** 钉选本地存储前缀 */ +const PIN_KEY = 'gm.pin.'; + /** * GM 命令与内容面板注册表(普通对象,避免 Map)。 */ @@ -94,6 +98,38 @@ export class GmRegistry { return this._order.length > 0; } + /** + * 命令是否已钉到常用。 + */ + static isPinned(id: string): boolean { + if (!id) return false; + return sys.localStorage.getItem(PIN_KEY + id) === '1'; + } + + /** + * 设置钉到常用。 + */ + static setPinned(id: string, on: boolean): void { + if (!id) { + console.warn('[GmRegistry] setPinned 空 id'); + return; + } + if (on) sys.localStorage.setItem(PIN_KEY + id, '1'); + else sys.localStorage.removeItem(PIN_KEY + id); + } + + /** + * 已钉到常用的命令(注册顺序)。 + */ + static listPinned(): IGmCommand[] { + const out: IGmCommand[] = []; + for (let i = 0; i < this._order.length; i++) { + const c = this._cmds[this._order[i]]; + if (c?.pinable && this.isPinned(c.id)) out.push(c); + } + return out; + } + /** * 清空(测试 / 热重载)。 */ diff --git a/assets/mx/module/gm/GmTypes.ts b/assets/mx/module/gm/GmTypes.ts index bb4185c..aa09f96 100644 --- a/assets/mx/module/gm/GmTypes.ts +++ b/assets/mx/module/gm/GmTypes.ts @@ -56,6 +56,8 @@ export interface IGmCommand { summary?: () => string; /** 为 true 时该 Tab 直接嵌入面板(不显示列表入口) */ embed?: boolean; + /** 为 true 时可钉到「常用」页展示 */ + pinable?: boolean; } /** @@ -70,6 +72,8 @@ export interface IGmPanelContext { goBack: () => void; /** 刷新列表摘要 */ refreshList: () => void; + /** 是否 Tab 内嵌整页(无返回) */ + embedded?: boolean; } /** diff --git a/assets/mx/module/gm/GmView.ts b/assets/mx/module/gm/GmView.ts index 225767d..55b85ea 100644 --- a/assets/mx/module/gm/GmView.ts +++ b/assets/mx/module/gm/GmView.ts @@ -33,6 +33,8 @@ const TAB_H = 60; const ROW_H = 76; const QUICK_H = 72; const SECTION_H = 36; +/** 常用 Tab 名 */ +const COMMON_TAB = '常用'; /** * GM 主视图:设备全屏、高对比;数值支持设为覆盖 + 增减。 @@ -291,7 +293,7 @@ export class GmView { this._tabBar.removeAllChildren(); const n = this._groups.length || 1; const barW = this._tabBar.getComponent(UITransform)?.width ?? this._listW; - const btnW = Math.max(110, (barW - (n - 1) * 10) / n); + const btnW = Math.max(88, Math.min(130, (barW - (n - 1) * 8) / n)); for (let i = 0; i < this._groups.length; i++) { const gname = this._groups[i]; const idx = i; @@ -305,7 +307,7 @@ export class GmView { const lb = btn.getChildByName('lb')?.getComponent(Label); if (lb) { lb.isBold = true; - lb.fontSize = 26; + lb.fontSize = n > 5 ? 22 : 26; } } this._tabBar.getComponent(Layout)?.updateLayout(); @@ -350,26 +352,48 @@ export class GmView { this._listHost.active = true; this._contentHost.active = false; + const onCommon = group === COMMON_TAB; if (quicks.length) { this._addSection('快捷 · 点一下即执行'); this._addQuickGrid(quicks); } if (toggles.length) { this._addSection('开关 · 点右侧切换'); - for (let i = 0; i < toggles.length; i++) this._addToggle(toggles[i]); + for (let i = 0; i < toggles.length; i++) this._addToggle(toggles[i], !onCommon); } if (contents.length) { - this._addSection('数值 / 选择'); + this._addSection(onCommon ? '数值 / 选择' : '数值 / 选择 · 右侧可钉常用'); for (let i = 0; i < contents.length; i++) { const c = contents[i]; - if (c.panelId === GmBuiltinPanelId.numberStepper) this._addInlineStepper(c); - else this._addContentEntry(c); + if (c.panelId === GmBuiltinPanelId.numberStepper) this._addInlineStepper(c, !onCommon); + else this._addContentEntry(c, !onCommon); } } + if (onCommon) this._appendPinnedExtras(); this._scrollContent.getComponent(Layout)?.updateLayout(); } + /** 常用页尾追加已钉选命令 */ + private _appendPinnedExtras(): void { + const pinned = GmRegistry.listPinned(); + const extras: IGmCommand[] = []; + for (let i = 0; i < pinned.length; i++) { + const c = pinned[i]; + if (c.group === COMMON_TAB) continue; + extras.push(c); + } + if (extras.length <= 0) return; + this._addSection('钉到常用'); + for (let i = 0; i < extras.length; i++) { + const c = extras[i]; + if (c.kind === GmKind.Toggle) this._addToggle(c, false); + else if (c.panelId === GmBuiltinPanelId.numberStepper) this._addInlineStepper(c, false); + else if (c.kind === GmKind.Content) this._addContentEntry(c, false); + else console.warn('[GM] 钉选项 kind 不支持', c.id, c.kind); + } + } + /** Tab 内嵌整页面板 */ private _openEmbed(cmd: IGmCommand): void { if (!cmd.panelId) { @@ -393,6 +417,7 @@ export class GmView { host: this._host, goBack: () => { /* 嵌入页无返回 */ }, refreshList: () => this.refresh(), + embedded: true, }; this._embedPanel.open(this._contentHost, ctx); } @@ -488,15 +513,17 @@ export class GmView { } } - private _addToggle(cmd: IGmCommand): void { + private _addToggle(cmd: IGmCommand, showPin: boolean): void { const n = new Node(cmd.id); n.layer = Layers.Enum.UI_2D; n.addComponent(UITransform).setContentSize(this._listW, ROW_H); this._fillRound(n, C.card, this._listW, ROW_H, 14, C.cardLine); + const pinW = showPin && cmd.pinable ? 72 : 0; + const titleW = this._listW - 180 - pinW; const title = this._label(n, cmd.title, 28, C.text, HorizontalTextAlignment.LEFT); - title.getComponent(UITransform)!.setContentSize(this._listW - 180, ROW_H - 8); - title.setPosition(-(this._listW / 2) + (this._listW - 180) / 2 + 18, 0); + title.getComponent(UITransform)!.setContentSize(titleW, ROW_H - 8); + title.setPosition(-(this._listW / 2) + titleW / 2 + 18, 0); const on = !!(cmd.get?.()); const sw = this._makeSwitch(n, on, () => { @@ -508,9 +535,22 @@ export class GmView { this.refresh(); }); sw.setPosition(this._listW / 2 - 72, 0); + if (showPin && cmd.pinable) this._addPinChip(n, cmd, this._listW / 2 - 172, 0); this._scrollContent.addChild(n); } + /** 钉到常用芯片 */ + private _addPinChip(parent: Node, cmd: IGmCommand, x: number, y: number): void { + const pinned = GmRegistry.isPinned(cmd.id); + const chip = this._pillBtn(parent, pinned ? '常用✓' : '常用', 68, 36, () => { + GmRegistry.setPinned(cmd.id, !GmRegistry.isPinned(cmd.id)); + this.refresh(); + }, pinned ? C.on : C.chip, pinned ? new Color(8, 40, 20, 255) : C.text); + chip.setPosition(x, y); + const lb = chip.getChildByName('lb')?.getComponent(Label); + if (lb) lb.fontSize = 18; + } + private _makeSwitch(parent: Node, on: boolean, onClick: () => void): Node { const trackW = 100; const trackH = 48; @@ -539,11 +579,11 @@ export class GmView { /** * 数值卡:EditBox +「设为」覆盖;下方 ± 立刻增减。 */ - private _addInlineStepper(cmd: IGmCommand): void { + private _addInlineStepper(cmd: IGmCommand, showPin: boolean): void { const data = cmd.panelData as IGmNumberStepperData; if (!data?.getValue || !data?.apply) { console.warn('[GM] numberStepper 缺少 panelData', cmd.id); - this._addContentEntry(cmd); + this._addContentEntry(cmd, showPin); return; } @@ -556,6 +596,7 @@ export class GmView { const title = this._label(n, cmd.title, 26, C.textDim, HorizontalTextAlignment.LEFT); title.getComponent(UITransform)!.setContentSize(this._listW * 0.4, 30); title.setPosition(-(this._listW / 2) + this._listW * 0.2 + 14, 48); + if (showPin && cmd.pinable) this._addPinChip(n, cmd, this._listW / 2 - 40, 48); const cur = Math.floor(Number(data.getValue()) || 0); const editNode = new Node('edit'); @@ -587,7 +628,8 @@ export class GmView { eb.inputMode = EditBox.InputMode.NUMERIC; eb.inputFlag = EditBox.InputFlag.DEFAULT; eb.returnType = EditBox.KeyboardReturnType.DONE; - editNode.setPosition(this._listW / 2 - editW / 2 - 130, 48); + const setShift = showPin && cmd.pinable ? 78 : 0; + editNode.setPosition(this._listW / 2 - editW / 2 - 130 - setShift, 48); n.addChild(editNode); const syncEdit = (): void => { @@ -606,7 +648,7 @@ export class GmView { syncEdit(); this._host.toast(`${cmd.title} 已设为 ${Math.floor(Number(data.getValue()) || 0)}`); }, C.setBtn, C.text); - setBtn.setPosition(this._listW / 2 - 52, 48); + setBtn.setPosition(this._listW / 2 - 52 - setShift, 48); const steps = [-100, -10, -1, 1, 10, 100]; const gap = 8; @@ -633,23 +675,32 @@ export class GmView { this._scrollContent.addChild(n); } - private _addContentEntry(cmd: IGmCommand): void { + private _addContentEntry(cmd: IGmCommand, showPin: boolean): void { const n = new Node(cmd.id); n.layer = Layers.Enum.UI_2D; n.addComponent(UITransform).setContentSize(this._listW, ROW_H); this._fillRound(n, C.card, this._listW, ROW_H, 14, C.cardLine); + const pinW = showPin && cmd.pinable ? 80 : 0; const title = this._label(n, cmd.title, 28, C.text, HorizontalTextAlignment.LEFT); title.getComponent(UITransform)!.setContentSize(280, ROW_H - 8); title.setPosition(-(this._listW / 2) + 150, 0); const sumText = cmd.summary ? cmd.summary() : '选择'; + const sumW = this._listW - 320 - pinW; const sum = this._label(n, `${sumText} ›`, 24, C.accent, HorizontalTextAlignment.RIGHT); - sum.getComponent(UITransform)!.setContentSize(this._listW - 320, ROW_H - 8); - sum.setPosition(this._listW / 2 - (this._listW - 320) / 2 - 16, 0); - - n.addComponent(Button); - n.on(Button.EventType.CLICK, () => this._openContent(cmd), this); + sum.getComponent(UITransform)!.setContentSize(sumW, ROW_H - 8); + sum.setPosition(this._listW / 2 - sumW / 2 - 16 - pinW, 0); + + if (showPin && cmd.pinable) this._addPinChip(n, cmd, this._listW / 2 - 40, 0); + + const hit = new Node('hit'); + hit.layer = Layers.Enum.UI_2D; + hit.addComponent(UITransform).setContentSize(this._listW - pinW - 8, ROW_H); + hit.setPosition(-pinW / 2, 0); + hit.addComponent(Button); + hit.on(Button.EventType.CLICK, () => this._openContent(cmd), this); + n.addChild(hit); this._scrollContent.addChild(n); } @@ -676,6 +727,7 @@ export class GmView { host: this._host, goBack: () => this._showList(), refreshList: () => this.refresh(), + embedded: false, }; this._activePanel.open(this._contentHost, ctx); } diff --git a/assets/script/gm/ProjectGmBootstrap.ts b/assets/script/gm/ProjectGmBootstrap.ts index db182d8..b119cf3 100644 --- a/assets/script/gm/ProjectGmBootstrap.ts +++ b/assets/script/gm/ProjectGmBootstrap.ts @@ -27,7 +27,7 @@ export class ProjectGmBootstrap { /** * 注册全部命令(开发者主要改这里)。 - * btn=快捷 / sw=开关 / num=数值 / page=子面板 / danger=确认 / text=粘贴 + * btn=快捷 / sw=开关 / pinable=可钉常用 / num=数值 / page=子面板 / embed=整页 / danger=确认 */ private static _register(): void { const A = ProjectGmActions; @@ -46,19 +46,23 @@ export class ProjectGmBootstrap { .sw('显示广告', () => A.getShowAd(), (v) => A.setShowAd(v)) .sw('内购版本', () => A.getIap(), (v) => A.setIap(v)) .sw('快速胜利', () => A.getFastWin(), (v) => A.setFastWin(v)) - .page('设置道具', P.itemPicker, () => '图标点选') - .page('设置段位和星级', P.rankPicker, () => A.rankSummary()) - .num('设置金钱', () => A.getCurrency(ItemNumType.money), (v) => A.setCurrency(ItemNumType.money, v, 'set')) - .num('设置体力', () => A.getCurrency(ItemNumType.vigour), (v) => A.setCurrency(ItemNumType.vigour, v, 'set')) - .num('设置钻石', () => A.getCurrency(ItemNumType.diamond), (v) => A.setCurrency(ItemNumType.diamond, v, 'set')) - .num('通关怪物数量', () => A.getFastWinNum(), (v) => A.setFastWinNum(v)) - .danger('GM重置进度', '将关闭所有面板并重置游戏进度,确定?', () => A.hardReset()) - .tab('战斗') - .sw('己方营地不掉血', () => A.getLocalCampImmune(), (v) => A.setCampImmune(true, v)) - .sw('敌方营地不掉血', () => A.getEnemyCampImmune(), (v) => A.setCampImmune(false, v)) - .page('固定地图', P.levelPicker, () => A.fixedMapSummary()) - .num('玩家局内血量', () => A.getWallHp() || 1000, (v) => A.setWallHp(v), 1) + .tab('道具') + .embed(P.itemPicker) + + .tab('段位') + .embed(P.rankPicker) + + .tab('通用') + .pinable().sw('己方营地不掉血', () => A.getLocalCampImmune(), (v) => A.setCampImmune(true, v)) + .pinable().sw('敌方营地不掉血', () => A.getEnemyCampImmune(), (v) => A.setCampImmune(false, v)) + .pinable().page('固定地图', P.levelPicker, () => A.fixedMapSummary()) + .pinable().num('设置金钱', () => A.getCurrency(ItemNumType.money), (v) => A.setCurrency(ItemNumType.money, v, 'set')) + .pinable().num('设置体力', () => A.getCurrency(ItemNumType.vigour), (v) => A.setCurrency(ItemNumType.vigour, v, 'set')) + .pinable().num('设置钻石', () => A.getCurrency(ItemNumType.diamond), (v) => A.setCurrency(ItemNumType.diamond, v, 'set')) + .pinable().num('通关怪物数量', () => A.getFastWinNum(), (v) => A.setFastWinNum(v)) + .pinable().num('玩家局内血量', () => A.getWallHp() || 1000, (v) => A.setWallHp(v), 1) + .danger('GM重置进度', '将关闭所有面板并重置游戏进度,确定?', () => A.hardReset()) .tab('词条') .embed(P.buffPicker, { expectType: 0, mode: 'carryPassive' }) diff --git a/assets/script/gm/ProjectGmPanels.ts b/assets/script/gm/ProjectGmPanels.ts index 49f9665..79b60b0 100644 --- a/assets/script/gm/ProjectGmPanels.ts +++ b/assets/script/gm/ProjectGmPanels.ts @@ -116,17 +116,35 @@ function makeVScroll(parent: Node, w: number, h: number): { host: Node; content: return { host, content }; } +const ITEM_C = { + text: new Color(255, 255, 255, 255), + dim: new Color(170, 185, 210, 255), + accent: new Color(255, 220, 70, 255), + card: new Color(28, 36, 56, 255), + cellBg: new Color(18, 24, 40, 255), + cellLine: new Color(90, 120, 170, 160), + selLine: new Color(255, 220, 70, 255), + selBg: new Color(48, 42, 18, 255), + btn: new Color(45, 105, 190, 255), + btnHot: new Color(18, 92, 78, 255), +}; + /** * 道具图标点选 + 数量步进。 */ class GmItemPickerPanel implements IGmContentPanel { private _root: Node | null = null; + private _ctx: IGmPanelContext | null = null; private _selectedId = 0; - private _qty = 1; - private _lbSel: Label | null = null; + private _selectedName = ''; + private _lbHead: Label | null = null; + private _lbQty: Label | null = null; + private _cells: { [id: number]: Node } = {}; + private _cellSz = 76; open(parent: Node, ctx: IGmPanelContext): void { this.close(); + this._ctx = ctx; const pUt = parent.getComponent(UITransform); const W = pUt?.width || 660; const H = pUt?.height || 900; @@ -139,117 +157,202 @@ class GmItemPickerPanel implements IGmContentPanel { rw.left = rw.right = rw.top = rw.bottom = 0; parent.addChild(this._root); - const tip = new Node('tip'); - tip.layer = Layers.Enum.UI_2D; - tip.addComponent(UITransform).setContentSize(W - 24, 40); - const tl = tip.addComponent(Label); - tl.string = '点图标立刻 +1 · 底部可一次 +10/+100'; - tl.fontSize = 22; - tl.color = new Color(255, 196, 72, 255); - tl.horizontalAlign = HorizontalTextAlignment.CENTER; - tip.setPosition(0, H / 2 - 28); - this._root.addChild(tip); - - const barH = 120; - const scrollH = H - 80 - barH; + const headH = 88; + const barH = 108; + const head = new Node('head'); + head.layer = Layers.Enum.UI_2D; + head.addComponent(UITransform).setContentSize(W - 16, headH); + this._drawRound(head, ITEM_C.card, W - 16, headH, 14, ITEM_C.selLine); + head.setPosition(0, H / 2 - headH / 2 - 4); + this._root.addChild(head); + + this._lbHead = this._makeLb(head, '点击道具图标:选中并 +1', W - 40, 32, 24, ITEM_C.accent); + this._lbHead.node.setPosition(0, 18); + this._lbQty = this._makeLb(head, '底部可一次 +10 / +100', W - 40, 28, 20, ITEM_C.dim); + this._lbQty.node.setPosition(0, -18); + + const scrollH = H - headH - barH - 16; const { host, content } = makeVScroll(this._root, W - 16, scrollH); - host.setPosition(0, barH / 2 - 10); + host.setPosition(0, (barH - headH) / 2); const items = gg.data.table.getTableList(TableNames.Item) ?? []; items.sort((a, b) => Number(a.id) - Number(b.id)); - const cell = 80; - const cols = Math.max(4, Math.floor((W - 24) / (cell + 6))); + const gap = 8; + this._cellSz = 76; + const cols = Math.max(4, Math.floor((W - 24) / (this._cellSz + gap))); let rowNode: Node | null = null; for (let i = 0; i < items.length; i++) { if (i % cols === 0) { rowNode = new Node(`row${i}`); rowNode.layer = Layers.Enum.UI_2D; - rowNode.addComponent(UITransform).setContentSize(W - 24, cell); + rowNode.addComponent(UITransform).setContentSize(W - 24, this._cellSz); const lay = rowNode.addComponent(Layout); lay.type = Layout.Type.HORIZONTAL; - lay.spacingX = 6; + lay.spacingX = gap; content.addChild(rowNode); } const row = items[i]; - const cellN = new Node(`i${row.id}`); - cellN.layer = Layers.Enum.UI_2D; - cellN.addComponent(UITransform).setContentSize(cell - 4, cell - 4); - const sp = cellN.addComponent(Sprite); - sp.sizeMode = Sprite.SizeMode.CUSTOM; - const loader = cellN.addComponent(SpriteLoad); - if (row.icon) loader.setSprite(GPath.Item(row.icon), GBundle.Items); - cellN.addComponent(Button); const id = Number(row.id); const name = row.name ?? ''; - cellN.on(Button.EventType.CLICK, () => { - this._selectedId = id; - ProjectGmActions.addItemNum(id, 1); - this._qty = gg.data.getItemNum(id); - this._paint(); - ctx.host.toast(`${name} +1 → ${this._qty}`); - }, this); + const cellN = this._makeCell(id, name, row.icon); rowNode!.addChild(cellN); + this._cells[id] = cellN; } content.getComponent(Layout)?.updateLayout(); const bar = new Node('bar'); bar.layer = Layers.Enum.UI_2D; - bar.addComponent(UITransform).setContentSize(W - 16, barH - 16); + bar.addComponent(UITransform).setContentSize(W - 16, barH - 12); + this._drawRound(bar, ITEM_C.card, W - 16, barH - 12, 14, ITEM_C.cellLine); bar.setPosition(0, -H / 2 + barH / 2); this._root.addChild(bar); - this._lbSel = (() => { - const sel = new Node('sel'); - sel.layer = Layers.Enum.UI_2D; - sel.addComponent(UITransform).setContentSize(W - 40, 28); - const lb = sel.addComponent(Label); - lb.fontSize = 20; - lb.color = new Color(180, 220, 255, 255); - lb.horizontalAlign = HorizontalTextAlignment.CENTER; - lb.overflow = Overflow.SHRINK; - sel.setPosition(0, 28); - bar.addChild(sel); - return lb; - })(); - makeLabelBtn(bar, '+10', 100, 44, () => { - if (!this._selectedId) { - ctx.host.toast('先点一个图标'); - return; - } - ProjectGmActions.addItemNum(this._selectedId, 10); - this._qty = gg.data.getItemNum(this._selectedId); - this._paint(); - }).setPosition(-160, -16); - - makeLabelBtn(bar, '+100', 100, 44, () => { - if (!this._selectedId) { - ctx.host.toast('先点一个图标'); - return; - } - ProjectGmActions.addItemNum(this._selectedId, 100); - this._qty = gg.data.getItemNum(this._selectedId); - this._paint(); - }).setPosition(-40, -16); - - makeLabelBtn(bar, '返回', 120, 44, () => { - ctx.refreshList(); - ctx.goBack(); - }).setPosition(120, -16); + const embedded = !!ctx.embedded; + const btnY = 0; + this._pill(bar, '+10', 120, 48, ITEM_C.btnHot, () => this._addQty(10)).setPosition(embedded ? -140 : -180, btnY); + this._pill(bar, '+100', 120, 48, ITEM_C.btn, () => this._addQty(100)).setPosition(embedded ? 140 : -40, btnY); + if (!embedded) { + this._pill(bar, '返回', 110, 48, new Color(70, 78, 96, 255), () => { + ctx.refreshList(); + ctx.goBack(); + }).setPosition(160, btnY); + } - this._paint(); + this._paintSel(); } close(): void { if (this._root?.isValid) this._root.destroy(); this._root = null; - this._lbSel = null; + this._ctx = null; + this._lbHead = null; + this._lbQty = null; + this._cells = {}; } - private _paint(): void { - if (this._lbSel) { - this._lbSel.string = this._selectedId - ? `最近:${this._selectedId} 持有 ${gg.data.getItemNum(this._selectedId)}` - : '点图标立刻 +1'; + private _makeCell(id: number, name: string, icon: string | undefined): Node { + const sz = this._cellSz; + const cellN = new Node(`i${id}`); + cellN.layer = Layers.Enum.UI_2D; + cellN.addComponent(UITransform).setContentSize(sz, sz); + this._drawRound(cellN, ITEM_C.cellBg, sz, sz, 12, ITEM_C.cellLine); + + const iconN = new Node('icon'); + iconN.layer = Layers.Enum.UI_2D; + iconN.addComponent(UITransform).setContentSize(sz - 16, sz - 16); + const sp = iconN.addComponent(Sprite); + sp.sizeMode = Sprite.SizeMode.CUSTOM; + const loader = iconN.addComponent(SpriteLoad); + if (icon) loader.setSprite(GPath.Item(icon), GBundle.Items); + cellN.addChild(iconN); + + const badge = new Node('qty'); + badge.layer = Layers.Enum.UI_2D; + badge.addComponent(UITransform).setContentSize(sz - 8, 20); + const bl = badge.addComponent(Label); + bl.string = `${gg.data.getItemNum(id)}`; + bl.fontSize = 16; + bl.isBold = true; + bl.color = ITEM_C.accent; + bl.horizontalAlign = HorizontalTextAlignment.RIGHT; + bl.verticalAlign = VerticalTextAlignment.CENTER; + bl.overflow = Overflow.SHRINK; + badge.setPosition(0, -sz / 2 + 12); + cellN.addChild(badge); + + cellN.addComponent(Button); + cellN.on(Button.EventType.CLICK, () => { + this._selectedId = id; + this._selectedName = name; + ProjectGmActions.addItemNum(id, 1); + this._paintSel(); + this._ctx?.host.toast(`${name} +1 → ${gg.data.getItemNum(id)}`); + }, this); + return cellN; + } + + private _addQty(delta: number): void { + if (!this._selectedId) { + this._ctx?.host.toast('先点选一个道具'); + return; + } + ProjectGmActions.addItemNum(this._selectedId, delta); + this._paintSel(); + this._ctx?.host.toast(`${this._selectedName || this._selectedId} +${delta}`); + } + + private _paintSel(): void { + const ids = Object.keys(this._cells); + for (let i = 0; i < ids.length; i++) { + const id = Number(ids[i]); + const cell = this._cells[id]; + if (!cell?.isValid) continue; + const on = id === this._selectedId; + this._drawRound(cell, on ? ITEM_C.selBg : ITEM_C.cellBg, this._cellSz, this._cellSz, 12, on ? ITEM_C.selLine : ITEM_C.cellLine); + const qtyLb = cell.getChildByName('qty')?.getComponent(Label); + if (qtyLb) qtyLb.string = `${gg.data.getItemNum(id)}`; + } + if (this._lbHead) { + this._lbHead.string = this._selectedId + ? `已选 · ${this._selectedId} ${this._selectedName}` + : '点击道具图标:选中并 +1'; + } + if (this._lbQty) { + this._lbQty.string = this._selectedId + ? `持有 ${gg.data.getItemNum(this._selectedId)} · 点图标再 +1` + : '底部可一次 +10 / +100'; + } + } + + private _makeLb(parent: Node, text: string, w: number, h: number, size: number, color: Color): Label { + const n = new Node('lb'); + n.layer = Layers.Enum.UI_2D; + n.addComponent(UITransform).setContentSize(w, h); + const lb = n.addComponent(Label); + lb.string = text; + lb.fontSize = size; + lb.isBold = true; + lb.color = color; + lb.horizontalAlign = HorizontalTextAlignment.CENTER; + lb.verticalAlign = VerticalTextAlignment.CENTER; + lb.overflow = Overflow.SHRINK; + parent.addChild(n); + return lb; + } + + private _pill(parent: Node, text: string, w: number, h: number, bg: Color, onClick: () => void): Node { + const n = new Node(text); + n.layer = Layers.Enum.UI_2D; + n.addComponent(UITransform).setContentSize(w, h); + this._drawRound(n, bg, w, h, 12); + const lbN = new Node('lb'); + lbN.layer = Layers.Enum.UI_2D; + lbN.addComponent(UITransform).setContentSize(w - 6, h - 4); + const lb = lbN.addComponent(Label); + lb.string = text; + lb.fontSize = 24; + lb.isBold = true; + lb.color = ITEM_C.text; + lb.horizontalAlign = HorizontalTextAlignment.CENTER; + lb.verticalAlign = VerticalTextAlignment.CENTER; + n.addChild(lbN); + n.addComponent(Button); + n.on(Button.EventType.CLICK, onClick, this); + parent.addChild(n); + return n; + } + + private _drawRound(node: Node, fill: Color, w: number, h: number, r: number, stroke?: Color): void { + const g = node.getComponent(Graphics) || node.addComponent(Graphics); + g.clear(); + g.fillColor = fill; + g.roundRect(-w / 2, -h / 2, w, h, r); + g.fill(); + if (stroke) { + g.strokeColor = stroke; + g.lineWidth = stroke === ITEM_C.selLine ? 3 : 2; + g.roundRect(-w / 2, -h / 2, w, h, r); + g.stroke(); } } } @@ -575,26 +678,39 @@ class GmLevelPickerPanel implements IGmContentPanel { open(parent: Node, ctx: IGmPanelContext): void { this.close(); + const pUt = parent.getComponent(UITransform); + const W = pUt?.width || 660; + const H = pUt?.height || 900; + const cur = ProjectGmActions.fixedMapSummary(); + this._root = new Node('levelPicker'); this._root.layer = Layers.Enum.UI_2D; - this._root.addComponent(UITransform).setContentSize(660, 900); + this._root.addComponent(UITransform).setContentSize(W, H); + const rw = this._root.addComponent(Widget); + rw.isAlignLeft = rw.isAlignRight = rw.isAlignTop = rw.isAlignBottom = true; + rw.left = rw.right = rw.top = rw.bottom = 0; parent.addChild(this._root); - const title = new Node('t'); - title.layer = Layers.Enum.UI_2D; - title.addComponent(UITransform).setContentSize(600, 36); - const tl = title.addComponent(Label); - tl.string = ctx.command.title; - tl.fontSize = 26; - tl.color = Color.WHITE; - tl.horizontalAlign = HorizontalTextAlignment.CENTER; - title.setPosition(0, 420); - this._root.addChild(title); - - const { host, content } = makeVScroll(this._root, 640, 700); - host.setPosition(0, 20); - - makeLabelBtn(content, '0 关闭固定(随机)', 620, 44, () => { + const head = new Node('head'); + head.layer = Layers.Enum.UI_2D; + head.addComponent(UITransform).setContentSize(W - 16, 72); + const hl = head.addComponent(Label); + hl.string = `固定地图 · 当前 ${cur}`; + hl.fontSize = 26; + hl.isBold = true; + hl.color = new Color(255, 220, 70, 255); + hl.horizontalAlign = HorizontalTextAlignment.CENTER; + hl.verticalAlign = VerticalTextAlignment.CENTER; + head.setPosition(0, H / 2 - 40); + this._root.addChild(head); + + const footH = 72; + const { host, content } = makeVScroll(this._root, W - 16, H - 120 - footH); + const lay = content.getComponent(Layout); + if (lay) lay.spacingY = 8; + host.setPosition(0, footH / 2 - 20); + + makeLabelBtn(content, '0 关闭固定(随机)', W - 40, 52, () => { ProjectGmActions.setFixedMap(0); ctx.refreshList(); ctx.goBack(); @@ -606,14 +722,14 @@ class GmLevelPickerPanel implements IGmContentPanel { const row = list[i]; const id = Number(row.id); const name = row.name ?? ''; - makeLabelBtn(content, `${id} ${name}`, 620, 44, () => { + makeLabelBtn(content, `${id} ${name}`, W - 40, 52, () => { ProjectGmActions.setFixedMap(id); ctx.refreshList(); ctx.goBack(); }); } content.getComponent(Layout)?.updateLayout(); - makeLabelBtn(this._root, '返回', 140, 44, () => ctx.goBack()).setPosition(0, -400); + makeLabelBtn(this._root, '返回', 160, 48, () => ctx.goBack()).setPosition(0, -H / 2 + footH / 2); } close(): void { @@ -622,6 +738,21 @@ class GmLevelPickerPanel implements IGmContentPanel { } } +const RANK_C = { + text: new Color(255, 255, 255, 255), + dim: new Color(170, 185, 210, 255), + accent: new Color(255, 220, 70, 255), + card: new Color(28, 36, 56, 255), + row: new Color(22, 30, 48, 255), + rowLine: new Color(90, 120, 170, 140), + selBg: new Color(48, 42, 18, 255), + selLine: new Color(255, 220, 70, 255), + btn: new Color(45, 105, 190, 255), + btnHot: new Color(18, 92, 78, 255), + starOn: new Color(255, 200, 60, 255), + starOff: new Color(80, 88, 110, 255), +}; + /** * 段位点选 + 星级步进。 */ @@ -629,41 +760,52 @@ class GmRankPickerPanel implements IGmContentPanel { private _root: Node | null = null; private _rankId = 1; private _star = 1; - private _lb: Label | null = null; + private _rankName = ''; + private _lbHead: Label | null = null; + private _lbStar: Label | null = null; + private _listW = 640; + private _rows: { [id: number]: Node } = {}; open(parent: Node, ctx: IGmPanelContext): void { this.close(); this._rankId = RankDataManager.getPlayerRankId() || 1; this._star = RankDataManager.getIntraTierStars() || 1; + const pUt = parent.getComponent(UITransform); + const W = pUt?.width || 660; + const H = pUt?.height || 900; + this._listW = W - 16; + this._root = new Node('rankPicker'); this._root.layer = Layers.Enum.UI_2D; - this._root.addComponent(UITransform).setContentSize(660, 900); + this._root.addComponent(UITransform).setContentSize(W, H); + const rw = this._root.addComponent(Widget); + rw.isAlignLeft = rw.isAlignRight = rw.isAlignTop = rw.isAlignBottom = true; + rw.left = rw.right = rw.top = rw.bottom = 0; parent.addChild(this._root); - const title = new Node('t'); - title.layer = Layers.Enum.UI_2D; - title.addComponent(UITransform).setContentSize(600, 36); - const tl = title.addComponent(Label); - tl.string = ctx.command.title; - tl.fontSize = 26; - tl.color = Color.WHITE; - tl.horizontalAlign = HorizontalTextAlignment.CENTER; - title.setPosition(0, 420); - this._root.addChild(title); - - const cur = new Node('cur'); - cur.layer = Layers.Enum.UI_2D; - cur.addComponent(UITransform).setContentSize(600, 36); - this._lb = cur.addComponent(Label); - this._lb.fontSize = 24; - this._lb.color = Color.YELLOW; - this._lb.horizontalAlign = HorizontalTextAlignment.CENTER; - cur.setPosition(0, 360); - this._root.addChild(cur); - - const { host, content } = makeVScroll(this._root, 640, 480); - host.setPosition(0, 40); + const headH = 100; + const footH = 120; + const head = new Node('head'); + head.layer = Layers.Enum.UI_2D; + head.addComponent(UITransform).setContentSize(this._listW, headH); + this._drawRound(head, RANK_C.card, this._listW, headH, 14, RANK_C.selLine); + head.setPosition(0, H / 2 - headH / 2 - 4); + this._root.addChild(head); + + this._lbHead = this._makeLb(head, '', this._listW - 24, 36, 26, RANK_C.accent); + this._lbHead.node.setPosition(0, 22); + this._lbStar = this._makeLb(head, '', this._listW - 24, 32, 28, RANK_C.starOn); + this._lbStar.node.setPosition(0, -22); + + const scrollH = H - headH - footH - 16; + const { host, content } = makeVScroll(this._root, this._listW, scrollH); + const lay = content.getComponent(Layout); + if (lay) { + lay.spacingY = 8; + lay.paddingBottom = 12; + } + host.setPosition(0, (footH - headH) / 2); const list = gg.data.table.getTableList(TableNames.Rank) ?? []; list.sort((a, b) => Number(a.id) - Number(b.id)); @@ -671,40 +813,138 @@ class GmRankPickerPanel implements IGmContentPanel { const row = list[i]; const id = Number(row.id); const name = row.name ?? ''; - makeLabelBtn(content, `${id} ${name}`, 620, 44, () => { - this._rankId = id; - this._paint(); - }); + if (id === this._rankId) this._rankName = name; + const card = this._makeRankRow(id, name); + content.addChild(card); + this._rows[id] = card; } content.getComponent(Layout)?.updateLayout(); - makeLabelBtn(this._root, '星-1', 100, 40, () => { + const foot = new Node('foot'); + foot.layer = Layers.Enum.UI_2D; + foot.addComponent(UITransform).setContentSize(this._listW, footH - 12); + this._drawRound(foot, RANK_C.card, this._listW, footH - 12, 14, RANK_C.rowLine); + foot.setPosition(0, -H / 2 + footH / 2); + this._root.addChild(foot); + + this._pill(foot, '星 -1', 110, 48, new Color(70, 78, 96, 255), () => { this._star = Math.max(1, this._star - 1); this._paint(); - }).setPosition(-160, -260); - makeLabelBtn(this._root, '星+1', 100, 40, () => { + }).setPosition(-200, 0); + this._pill(foot, '星 +1', 110, 48, new Color(70, 78, 96, 255), () => { this._star = Math.min(5, this._star + 1); this._paint(); - }).setPosition(-40, -260); - - makeLabelBtn(this._root, '应用', 120, 44, () => { + }).setPosition(-70, 0); + this._pill(foot, '应用', 140, 52, RANK_C.btnHot, () => { ProjectGmActions.setRankAndStar(this._rankId, this._star); + ctx.host.toast(`已设段位 ${this._rankId} 星 ${this._star}`); ctx.refreshList(); - ctx.goBack(); - }).setPosition(100, -260); + if (!ctx.embedded) ctx.goBack(); + }).setPosition(120, 0); - makeLabelBtn(this._root, '返回', 120, 44, () => ctx.goBack()).setPosition(0, -320); this._paint(); } close(): void { if (this._root?.isValid) this._root.destroy(); this._root = null; - this._lb = null; + this._lbHead = null; + this._lbStar = null; + this._rows = {}; + } + + private _makeRankRow(id: number, name: string): Node { + const h = 64; + const w = this._listW; + const card = new Node(`rank${id}`); + card.layer = Layers.Enum.UI_2D; + card.addComponent(UITransform).setContentSize(w, h); + this._drawRound(card, RANK_C.row, w, h, 12, RANK_C.rowLine); + + const idLb = this._makeLb(card, `${id}`, 56, 36, 24, RANK_C.accent); + idLb.node.setPosition(-w / 2 + 40, 0); + const nameLb = this._makeLb(card, name, w - 120, 36, 24, RANK_C.text); + nameLb.horizontalAlign = HorizontalTextAlignment.LEFT; + nameLb.node.setPosition(20, 0); + + card.addComponent(Button); + card.on(Button.EventType.CLICK, () => { + this._rankId = id; + this._rankName = name; + this._paint(); + }, this); + return card; } private _paint(): void { - if (this._lb) this._lb.string = `段位 ${this._rankId} 星 ${this._star}`; + const ids = Object.keys(this._rows); + for (let i = 0; i < ids.length; i++) { + const id = Number(ids[i]); + const row = this._rows[id]; + if (!row?.isValid) continue; + const on = id === this._rankId; + this._drawRound(row, on ? RANK_C.selBg : RANK_C.row, this._listW, 64, 12, on ? RANK_C.selLine : RANK_C.rowLine); + } + if (this._lbHead) { + this._lbHead.string = `当前选中 · ${this._rankId} ${this._rankName || ''}`.trim(); + } + if (this._lbStar) { + let stars = ''; + for (let i = 1; i <= 5; i++) stars += i <= this._star ? '★' : '☆'; + this._lbStar.string = `${stars} 星级 ${this._star}`; + } + } + + private _makeLb(parent: Node, text: string, w: number, h: number, size: number, color: Color): Label { + const n = new Node('lb'); + n.layer = Layers.Enum.UI_2D; + n.addComponent(UITransform).setContentSize(w, h); + const lb = n.addComponent(Label); + lb.string = text; + lb.fontSize = size; + lb.isBold = true; + lb.color = color; + lb.horizontalAlign = HorizontalTextAlignment.CENTER; + lb.verticalAlign = VerticalTextAlignment.CENTER; + lb.overflow = Overflow.SHRINK; + parent.addChild(n); + return lb; + } + + private _pill(parent: Node, text: string, w: number, h: number, bg: Color, onClick: () => void): Node { + const n = new Node(text); + n.layer = Layers.Enum.UI_2D; + n.addComponent(UITransform).setContentSize(w, h); + this._drawRound(n, bg, w, h, 12); + const lbN = new Node('lb'); + lbN.layer = Layers.Enum.UI_2D; + lbN.addComponent(UITransform).setContentSize(w - 6, h - 4); + const lb = lbN.addComponent(Label); + lb.string = text; + lb.fontSize = 22; + lb.isBold = true; + lb.color = RANK_C.text; + lb.horizontalAlign = HorizontalTextAlignment.CENTER; + lb.verticalAlign = VerticalTextAlignment.CENTER; + n.addChild(lbN); + n.addComponent(Button); + n.on(Button.EventType.CLICK, onClick, this); + parent.addChild(n); + return n; + } + + private _drawRound(node: Node, fill: Color, w: number, h: number, r: number, stroke?: Color): void { + const g = node.getComponent(Graphics) || node.addComponent(Graphics); + g.clear(); + g.fillColor = fill; + g.roundRect(-w / 2, -h / 2, w, h, r); + g.fill(); + if (stroke) { + g.strokeColor = stroke; + g.lineWidth = stroke === RANK_C.selLine ? 3 : 2; + g.roundRect(-w / 2, -h / 2, w, h, r); + g.stroke(); + } } } diff --git a/docs/方案-GM可视化模块.md b/docs/方案-GM可视化模块.md index a13b38f..1ba2b4a 100644 --- a/docs/方案-GM可视化模块.md +++ b/docs/方案-GM可视化模块.md @@ -1,6 +1,6 @@ # 方案:可视化可扩展 GM 模块 -> 更新日期:2026-10-16 +> 更新日期:2026-10-19 ## 1. 目标 @@ -24,42 +24,53 @@ |------|------|------| | Quick | 一键动作 | 复制存档、每日重置、发宝石 | | Toggle | 开/关 | 广告、营地免伤、商店无限 | -| Content | 可视化子页 | 道具图标点选、关卡列表、段位步进;JSON 用 textArea | +| Content | 可视化子页 / 内嵌整页 | 道具图标点选、关卡列表、段位步进;JSON 用 textArea | -## 4. 开发者扩展(优先用 GmEasy) +## 4. Tab 结构(本项目) + +| Tab | 内容 | +|-----|------| +| 常用 | 仅快捷按钮 + 快捷开关;尾部追加「钉到常用」的项 | +| 道具 | `embed` 道具点选(选中高亮) | +| 段位 | `embed` 段位/星级(选中高亮) | +| 通用 | 数值、营地开关、固定地图、局内血量、危险重置;每项可钉常用 | +| 词条 | `embed` buff 选择 | +| 存档 | `embed` 存档编辑 | + +已去掉独立「战斗」Tab,原战斗项并入「通用」。 + +## 5. 开发者扩展(优先用 GmEasy) 日常加功能只改 `ProjectGmBootstrap._register`,链式几行即可: ```ts new GmEasy() .tab('常用') - .btn('复制存档', () => copy()) // 快捷 - .sw('显示广告', () => on, v => set(v)) // 开关 - .num('金钱', () => get(), v => set(v)) // 数值设为+增减 - .page('道具', 'itemPicker', () => '点选') // 自定义面板 + .btn('复制存档', () => copy()) + .sw('显示广告', () => on, v => set(v)) + .tab('道具') + .embed('itemPicker') + .tab('通用') + .pinable().num('金钱', () => get(), v => set(v)) // 可钉到常用 + .pinable().sw('营地免伤', () => on, v => set(v)) .danger('重置', '确定?', () => reset()) - .text('存档', raw => apply(raw)) .commit(); ``` +- `.pinable()`:标记下一条命令可钉到「常用」;钉选状态存 `sys.localStorage`(`gm.pin.{id}`) +- `.embed(panelId)`:当前 Tab 整页嵌入面板(无二次点击入口) + 底层仍是 `IGmCommand` / `GmRegistry`;自定义面板:`GmRegistry.registerPanel(id, () => new XxxPanel())`。 内置面板 id:`numberStepper`、`confirmQuick`、`textArea`。 -## 5. 换项目接入 +## 6. 换项目接入 1. 拷贝 `assets/mx/module/gm/` 2. 实现 `IGmHost`(约 30 行) 3. `new GmEasy()...commit()` 写命令;需要时 `registerPanel` 4. `GmShell.bindHost` + `GmShell.show()` -## 6. 本项目迁移 - -- Toggle:广告、内购、商店无限、营地免伤、快速胜利 -- Quick:每日重置、复制存档、发宝石/武器、清地图、商店重置、排行测试等 -- Content:道具/BUFF/关卡/段位/货币/血量/存档;武器相关可打开原 PopGM -- 删除帮助有、代码无的死条目;空实现等级命令不注册 - ## 7. 入口 - 设置页 `GM.show()` → 委托 `ProjectGmBootstrap.show()` → `GmShell` @@ -74,5 +85,3 @@ new GmEasy() | 类型/注册/壳/视图/内置面板 | `assets/mx/module/gm/` | | Host / Actions / Panels / Bootstrap | `assets/script/gm/` | | 门面 | `assets/script/game/GM.ts`(薄委托,无 EditBox/switch) | - -未迁入本版(原孤儿/死代码):玩家等级空实现、七十二变帮助项、指定背包文字(可后续加 Content)。