import { Button, Color, Graphics, Label, Layout, Mask, Node, ScrollView, Sprite, UITransform, Widget, HorizontalTextAlignment, VerticalTextAlignment, Layers, Overflow, } from 'cc'; import { SpriteLoad } from '../../mx/components/SpriteLoad'; import type { IGmContentPanel, IGmPanelContext } from '../../mx/module/gm'; import { GmRegistry } from '../../mx/module/gm'; import { GBundle, GPath } from '../game/ConfigRes'; import { TableNames } from '../game/ConfigTableData'; import { battleBuffSlotType } from '../manager/BattleBuffParse'; import { BattleBuffSlotType } from '../manager/battleBuff/BattleBuffTypes'; import { RankDataManager } from '../manager/RankDataManager'; import { ProjectGmActions } from './ProjectGmActions'; import { GmSaveEditorPanel } from './ProjectGmSavePanel'; /** 本项目内容面板 id */ export const ProjectGmPanelId = { itemPicker: 'itemPicker', buffPicker: 'buffPicker', levelPicker: 'levelPicker', rankPicker: 'rankPicker', saveEditor: 'saveEditor', } as const; /** * buffPicker 面板数据。 * 新表已无 type 列时 expectType 请传 0,否则列表会被滤空。 */ export interface IGmBuffPickerData { /** BattleBuffSlotType:1主动 2被动 3自动;0=不限(推荐) */ expectType: number; /** 写入目标 */ mode: 'carryPassive' | 'aiActive' | 'aiPassive'; } const BUFF_C = { text: new Color(255, 255, 255, 255), dim: new Color(180, 190, 210, 255), accent: new Color(255, 220, 70, 255), rowBg: new Color(28, 36, 56, 255), rowLine: new Color(100, 160, 240, 160), rowOn: new Color(255, 220, 70, 255), btn: new Color(45, 105, 190, 255), btnClear: new Color(210, 120, 35, 255), btnBack: new Color(70, 78, 96, 255), }; const QUALITY_COLOR: { [q: number]: Color } = { 1: new Color(90, 210, 120, 255), 2: new Color(80, 160, 255, 255), 3: new Color(190, 110, 255, 255), 4: new Color(255, 170, 60, 255), }; const QUALITY_NAME: { [q: number]: string } = { 1: '绿', 2: '蓝', 3: '紫', 4: '橙', }; const SLOT_NAME: { [t: number]: string } = { [BattleBuffSlotType.Active]: '主动', [BattleBuffSlotType.Passive]: '被动', [BattleBuffSlotType.Auto]: '自动', }; /** * 创建文字按钮。 */ function makeLabelBtn(parent: Node, text: string, w: number, h: number, onClick: () => void): Node { const n = new Node('btn'); n.layer = Layers.Enum.UI_2D; n.addComponent(UITransform).setContentSize(w, h); const lb = n.addComponent(Label); lb.string = text; lb.fontSize = 22; lb.color = Color.WHITE; lb.horizontalAlign = HorizontalTextAlignment.CENTER; lb.verticalAlign = VerticalTextAlignment.CENTER; n.addComponent(Button); n.on(Button.EventType.CLICK, onClick, null); parent.addChild(n); return n; } /** * 垂直滚动列表宿主。 */ function makeVScroll(parent: Node, w: number, h: number): { host: Node; content: Node } { const host = new Node('scroll'); host.layer = Layers.Enum.UI_2D; host.addComponent(UITransform).setContentSize(w, h); const view = new Node('view'); view.layer = Layers.Enum.UI_2D; view.addComponent(UITransform).setContentSize(w, h); view.addComponent(Mask); host.addChild(view); const content = new Node('content'); content.layer = Layers.Enum.UI_2D; const cut = content.addComponent(UITransform); cut.setContentSize(w, h); cut.setAnchorPoint(0.5, 1); content.setPosition(0, h / 2); const layout = content.addComponent(Layout); layout.type = Layout.Type.VERTICAL; layout.resizeMode = Layout.ResizeMode.CONTAINER; layout.spacingY = 4; layout.paddingTop = 4; view.addChild(content); const sv = host.addComponent(ScrollView); sv.content = content; sv.horizontal = false; sv.vertical = true; parent.addChild(host); 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 _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; this._root = new Node('itemPicker'); this._root.layer = Layers.Enum.UI_2D; 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 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 - headH) / 2); const items = gg.data.table.getTableList(TableNames.Item) ?? []; items.sort((a, b) => Number(a.id) - Number(b.id)); 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, this._cellSz); const lay = rowNode.addComponent(Layout); lay.type = Layout.Type.HORIZONTAL; lay.spacingX = gap; content.addChild(rowNode); } const row = items[i]; const id = Number(row.id); const name = row.name ?? ''; 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 - 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); 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._paintSel(); } close(): void { if (this._root?.isValid) this._root.destroy(); this._root = null; this._ctx = null; this._lbHead = null; this._lbQty = null; this._cells = {}; } 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(); } } } /** * 词条页:上=未选、下=已选;点击即对调并立刻写入。 */ class GmBuffPickerPanel implements IGmContentPanel { private _root: Node | null = null; private _ctx: IGmPanelContext | null = null; private _data: IGmBuffPickerData | null = null; private _listW = 640; private _allRows: ITableBattlebuff[] = []; private _selected: number[] = []; private _availContent: Node | null = null; private _selContent: Node | null = null; private _lbAvail: Label | null = null; private _lbSel: Label | null = null; open(parent: Node, ctx: IGmPanelContext): void { this.close(); this._ctx = ctx; const raw = ctx.command.panelData as IGmBuffPickerData | undefined; const data: IGmBuffPickerData = { expectType: raw?.expectType ?? 0, mode: raw?.mode ?? 'carryPassive', }; this._data = data; this._selected = this._loadCurrentIds(); const pUt = parent.getComponent(UITransform); const W = pUt?.width || 660; const H = pUt?.height || 900; this._listW = W - 16; this._root = new Node('buffPicker'); this._root.layer = Layers.Enum.UI_2D; 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 headH = 36; const gap = 10; const clearH = 44; const usable = Math.max(200, H - headH * 2 - gap - clearH - 8); const availH = Math.floor(usable * 0.58); const selH = usable - availH; this._lbAvail = this._addHead(this._root, '未选择(点选加入)', this._listW, headH); this._lbAvail.node.setPosition(0, H / 2 - headH / 2 - 2); const { host: availHost, content: availContent } = makeVScroll(this._root, this._listW, availH); this._availContent = availContent; const availLay = availContent.getComponent(Layout); if (availLay) { availLay.spacingY = 8; availLay.paddingBottom = 12; } availHost.setPosition(0, H / 2 - headH - availH / 2 - 4); const selTopY = -H / 2 + clearH + selH + headH + 4; this._lbSel = this._addHead(this._root, '已选择', this._listW, headH); this._lbSel.node.setPosition(0, selTopY - headH / 2); const { host: selHost, content: selContent } = makeVScroll(this._root, this._listW, selH); this._selContent = selContent; const selLay = selContent.getComponent(Layout); if (selLay) { selLay.spacingY = 8; selLay.paddingBottom = 12; } selHost.setPosition(0, -H / 2 + clearH + selH / 2); this._pillBtn(this._root, '清空已选', 200, 40, BUFF_C.btnClear, () => this._clearSelected()) .setPosition(0, -H / 2 + clearH / 2); this._allRows = []; const list = gg.data.table.getTableList(TableNames.Battlebuff) ?? []; list.sort((a, b) => Number(a.id) - Number(b.id)); for (let i = 0; i < list.length; i++) { const row = list[i]; const id = Math.floor(Number(row.id) || 0); if (id <= 0) continue; const slot = battleBuffSlotType(row); if (data.expectType > 0 && slot !== data.expectType) continue; this._allRows.push(row); } if (this._allRows.length <= 0) { console.warn('[GM] buffPicker 列表为空', data); } this._rebuildLists(); } close(): void { if (this._root?.isValid) this._root.destroy(); this._root = null; this._ctx = null; this._data = null; this._allRows = []; this._selected = []; this._availContent = null; this._selContent = null; this._lbAvail = null; this._lbSel = null; } /** 读取当前已生效 id */ private _loadCurrentIds(): number[] { if (!this._data) return []; if (this._data.mode === 'carryPassive') return ProjectGmActions.getCarryBuffIds(); const cur = this._data.mode === 'aiActive' ? gg.game.gmAiActiveBuffIds : gg.game.gmAiPassiveBuffIds; return cur?.length ? cur.slice() : []; } private _hasId(id: number): boolean { for (let i = 0; i < this._selected.length; i++) { if (this._selected[i] === id) return true; } return false; } /** 点击未选 → 加入;点击已选 → 移除;立刻写入 */ private _move(id: number, toSelected: boolean): void { if (toSelected) { if (!this._hasId(id)) this._selected.push(id); } else { const next: number[] = []; for (let i = 0; i < this._selected.length; i++) { if (this._selected[i] !== id) next.push(this._selected[i]); } this._selected = next; } this._applyNow(); this._rebuildLists(); } private _clearSelected(): void { this._selected = []; this._applyNow(); this._rebuildLists(); } private _applyNow(): void { if (!this._data) return; const ids = this._selected.slice(); if (this._data.mode === 'carryPassive') { ProjectGmActions.setCarryBuffIds(ids, true); } else if (this._data.mode === 'aiActive') { ProjectGmActions.setAiBuffIds(ids.length ? ids : null, true); } else { ProjectGmActions.setAiBuffIds(ids.length ? ids : null, false); } } private _rebuildLists(): void { if (!this._availContent?.isValid || !this._selContent?.isValid) return; this._availContent.removeAllChildren(); this._selContent.removeAllChildren(); let availN = 0; let selN = 0; for (let i = 0; i < this._allRows.length; i++) { const row = this._allRows[i]; const id = Math.floor(Number(row.id) || 0); if (this._hasId(id)) { this._addBuffCard(this._selContent, this._listW, row, false); selN++; } else { this._addBuffCard(this._availContent, this._listW, row, true); availN++; } } if (availN <= 0) this._addEmptyHint(this._availContent, '已全部选中'); if (selN <= 0) this._addEmptyHint(this._selContent, '点击上方加入'); if (this._lbAvail) this._lbAvail.string = `未选择 ${availN}(点选加入)`; if (this._lbSel) this._lbSel.string = `已选择 ${selN}(点选移除)`; this._availContent.getComponent(Layout)?.updateLayout(); this._selContent.getComponent(Layout)?.updateLayout(); } private _addEmptyHint(parent: Node, text: string): void { const n = new Node('empty'); n.layer = Layers.Enum.UI_2D; n.addComponent(UITransform).setContentSize(this._listW, 48); const lb = n.addComponent(Label); lb.string = text; lb.fontSize = 22; lb.color = BUFF_C.dim; lb.horizontalAlign = HorizontalTextAlignment.CENTER; parent.addChild(n); } private _addHead(parent: Node, text: string, w: number, h: number): Label { const n = new Node('head'); n.layer = Layers.Enum.UI_2D; n.addComponent(UITransform).setContentSize(w, h); const lb = n.addComponent(Label); lb.string = text; lb.fontSize = 24; lb.isBold = true; lb.color = BUFF_C.accent; lb.horizontalAlign = HorizontalTextAlignment.LEFT; lb.verticalAlign = VerticalTextAlignment.CENTER; lb.overflow = Overflow.CLAMP; parent.addChild(n); return lb; } /** * @param toSelected true=点后加入已选;false=点后移回未选 */ private _addBuffCard(parent: Node, w: number, row: ITableBattlebuff, toSelected: boolean): void { const h = 84; const id = Math.floor(Number(row.id) || 0); const name = String(row.name ?? ''); const desc = String(row.desc ?? ''); const q = Math.max(0, Math.floor(Number(row.quality) || 0)); const slot = battleBuffSlotType(row); const qColor = QUALITY_COLOR[q] || BUFF_C.dim; const qName = QUALITY_NAME[q] || `Q${q}`; const slotName = slot != null ? (SLOT_NAME[slot] || `t${slot}`) : '?'; const card = new Node(`buff${id}`); card.layer = Layers.Enum.UI_2D; card.addComponent(UITransform).setContentSize(w, h); this._drawRound(card, BUFF_C.rowBg, w, h, 12, toSelected ? BUFF_C.rowLine : BUFF_C.rowOn); const iconSz = 56; const iconN = new Node('icon'); iconN.layer = Layers.Enum.UI_2D; iconN.addComponent(UITransform).setContentSize(iconSz, iconSz); const sp = iconN.addComponent(Sprite); sp.sizeMode = Sprite.SizeMode.CUSTOM; const loader = iconN.addComponent(SpriteLoad); const iconName = String(row.icon || row.name || '').trim(); if (iconName) loader.setSprite(GPath.ShopSkill(iconName), GBundle.Items); iconN.setPosition(-w / 2 + iconSz / 2 + 10, 0); card.addChild(iconN); const textW = w - iconSz - 28; const head = new Node('head'); head.layer = Layers.Enum.UI_2D; head.addComponent(UITransform).setContentSize(textW, 30); const hl = head.addComponent(Label); hl.string = `${id} ${name} · ${qName}/${slotName}`; hl.fontSize = 22; hl.isBold = true; hl.color = qColor; hl.horizontalAlign = HorizontalTextAlignment.LEFT; hl.verticalAlign = VerticalTextAlignment.CENTER; hl.overflow = Overflow.CLAMP; hl.enableWrapText = false; head.setPosition(iconSz / 2 + 4, 16); card.addChild(head); const body = new Node('desc'); body.layer = Layers.Enum.UI_2D; body.addComponent(UITransform).setContentSize(textW, 34); const bl = body.addComponent(Label); bl.string = desc || '(无描述)'; bl.fontSize = 17; bl.color = BUFF_C.dim; bl.horizontalAlign = HorizontalTextAlignment.LEFT; bl.verticalAlign = VerticalTextAlignment.TOP; bl.overflow = Overflow.CLAMP; bl.enableWrapText = true; body.setPosition(iconSz / 2 + 4, -16); card.addChild(body); card.addComponent(Button); card.on(Button.EventType.CLICK, () => this._move(id, toSelected), this); parent.addChild(card); } private _pillBtn(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, 10); 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 = BUFF_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 === BUFF_C.rowOn ? 3 : 2; g.roundRect(-w / 2, -h / 2, w, h, r); g.stroke(); } } } /** * 关卡列表点选。 */ class GmLevelPickerPanel implements IGmContentPanel { private _root: Node | null = null; 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(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 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(); }); const list = gg.data.table.getTableList(TableNames.Level) ?? []; list.sort((a, b) => Number(a.id) - Number(b.id)); for (let i = 0; i < list.length; i++) { const row = list[i]; const id = Number(row.id); const name = row.name ?? ''; makeLabelBtn(content, `${id} ${name}`, W - 40, 52, () => { ProjectGmActions.setFixedMap(id); ctx.refreshList(); ctx.goBack(); }); } content.getComponent(Layout)?.updateLayout(); makeLabelBtn(this._root, '返回', 160, 48, () => ctx.goBack()).setPosition(0, -H / 2 + footH / 2); } close(): void { if (this._root?.isValid) this._root.destroy(); this._root = null; } } 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), }; /** * 段位点选 + 星级步进。 */ class GmRankPickerPanel implements IGmContentPanel { private _root: Node | null = null; private _rankId = 1; private _star = 1; 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(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 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)); for (let i = 0; i < list.length; i++) { const row = list[i]; const id = Number(row.id); const name = row.name ?? ''; if (id === this._rankId) this._rankName = name; const card = this._makeRankRow(id, name); content.addChild(card); this._rows[id] = card; } content.getComponent(Layout)?.updateLayout(); 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(-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(-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(); if (!ctx.embedded) ctx.goBack(); }).setPosition(120, 0); this._paint(); } close(): void { if (this._root?.isValid) this._root.destroy(); this._root = 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 { 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(); } } } /** * 注册本项目内容面板。 */ export class ProjectGmPanels { private static _done = false; /** 幂等注册 */ static ensureRegistered(): void { if (this._done) return; this._done = true; GmRegistry.registerPanel(ProjectGmPanelId.itemPicker, () => new GmItemPickerPanel()); GmRegistry.registerPanel(ProjectGmPanelId.buffPicker, () => new GmBuffPickerPanel()); GmRegistry.registerPanel(ProjectGmPanelId.levelPicker, () => new GmLevelPickerPanel()); GmRegistry.registerPanel(ProjectGmPanelId.rankPicker, () => new GmRankPickerPanel()); GmRegistry.registerPanel(ProjectGmPanelId.saveEditor, () => new GmSaveEditorPanel()); } }