diff --git a/assets/mx/module/merge/rules/MapPlaceRules.ts b/assets/mx/module/merge/rules/MapPlaceRules.ts index 3c95d89..3344e2a 100644 --- a/assets/mx/module/merge/rules/MapPlaceRules.ts +++ b/assets/mx/module/merge/rules/MapPlaceRules.ts @@ -99,6 +99,8 @@ export function rejectReasonToMessage(reason: PlaceRejectReason): string { return '只能放在己方区域'; case PlaceRejectReason.UnlockDrag: return ''; + case PlaceRejectReason.UnlockNeedElement: + return ''; default: return ''; } diff --git a/assets/mx/module/merge/types/MergeTypes.ts b/assets/mx/module/merge/types/MergeTypes.ts index e0a440a..9798695 100644 --- a/assets/mx/module/merge/types/MergeTypes.ts +++ b/assets/mx/module/merge/types/MergeTypes.ts @@ -57,6 +57,8 @@ export enum PlaceRejectReason { WrongCampArea = 'WrongCampArea', /** 拖文字推进当前解锁区域:字不落下,也不弹失败提示 */ UnlockDrag = 'UnlockDrag', + /** 解锁区要求特定五行,拖入属性不符(文案由业务层按需求属性拼接) */ + UnlockNeedElement = 'UnlockNeedElement', } /** 组合英雄激活后写到每个字上的单层 Buff */ diff --git a/assets/script/battle/bag/BattleBagSession.ts b/assets/script/battle/bag/BattleBagSession.ts index 968473f..e6b0bed 100644 --- a/assets/script/battle/bag/BattleBagSession.ts +++ b/assets/script/battle/bag/BattleBagSession.ts @@ -41,7 +41,7 @@ import { buildGmWeaponPresetQueue } from '../debug/GmBattlePreset'; import type { ProjectBattleHandle } from '../runtime/BattleRuntime'; import { TableNames } from '../../game/ConfigTableData'; import { RankDataManager } from '../../manager/RankDataManager'; -import type { AreaUnlockHero } from '../map/legacy/MapAreaUnlock'; +import { unlockHeroTypeName, type AreaUnlockHero } from '../map/legacy/MapAreaUnlock'; import type { DailyShopParsed } from '../../manager/DailyShopTypes'; import { resolveCarriedAttrCardSpawnWan, @@ -89,6 +89,8 @@ export class BattleBagSession { private _skipOpenEffect = false; /** 最近一次区域解锁打开的格(供对端 merge 对齐) */ private _lastUnlockOpened: number[] = []; + /** UnlockNeedElement 动态提示(take 后清空) */ + private _unlockFailToast = ''; /** * 寻金尺飞币回调(入账后通知表现层)。 * @param gridIndex 解锁锚点格 @@ -972,6 +974,29 @@ export class BattleBagSession { return this._map.areaUnlock.canDrag(grid, this._heroOfGlyph({ weapon2Id, level: lv })); } + /** + * 落点所属阵营当前解锁区需求 herotype;0=任意或非待解锁。 + * @param gridIndex 落点 + */ + currentUnlockNeedHeroType(gridIndex: number): number { + const grid = this.validator.getGridByIndex(gridIndex); + if (!grid?.waitingUnlock) return 0; + return this._map.areaUnlock.currentNeedHeroType(grid.CampAreaID | 0); + } + + /** + * 拒绝原因文案;UnlockNeedElement 用动态属性提示。 + * @param reason 拒绝原因 + */ + formatRejectMessage(reason: PlaceRejectReason): string { + if (reason === PlaceRejectReason.UnlockNeedElement) { + const tip = this._unlockFailToast; + this._unlockFailToast = ''; + return tip; + } + return rejectReasonToMessage(reason); + } + /** * 五行字拖到当前解锁区域:类型/显示等级符合则整区可放置并播 openSpine,随后字落在当前格。 * @param source 拖拽源 @@ -986,9 +1011,19 @@ export class BattleBagSession { const w2Type = Math.floor(WeaponComposeConfigProvider.instance.getWeapon2Type(piece.weapon2Id)); if (w2Type < 1 || w2Type > 5) return 'skip'; - const hit = this._map.areaUnlock.tryDrag(grid, this._heroOfGlyph(piece)); + const hero = this._heroOfGlyph(piece); + const hit = this._map.areaUnlock.tryDrag(grid, hero); if (hit === 'skip') return 'skip'; - if (hit === 'blocked') return PlaceRejectReason.WrongCellKind; + if (hit === 'blocked') { + const camp = grid.CampAreaID | 0; + const needType = this._map.areaUnlock.currentNeedHeroType(camp); + const name = unlockHeroTypeName(needType); + if (grid.waitingUnlock && needType > 0 && name && (!hero || hero.heroType !== needType)) { + this._unlockFailToast = `需要用${name}属性数字解锁`; + return PlaceRejectReason.UnlockNeedElement; + } + return PlaceRejectReason.WrongCellKind; + } const camp = grid.CampAreaID | 0; const opened = this._map.areaUnlock.gridsOf(grid.unlockAreaId, camp); this._lastUnlockOpened = []; diff --git a/assets/script/battle/map/legacy/MapAreaUnlock.ts b/assets/script/battle/map/legacy/MapAreaUnlock.ts index 2097bab..a76d9f2 100644 --- a/assets/script/battle/map/legacy/MapAreaUnlock.ts +++ b/assets/script/battle/map/legacy/MapAreaUnlock.ts @@ -25,9 +25,19 @@ const LOCK_Y_WITH_BOX = -15; const _tipWorld = new Vec3(); /** 气泡本地坐标复用 */ const _tipLocal = new Vec3(); -/** herotype → 五行图标名(1金 2木 3水 4火 5土) */ +/** herotype → 五行名(1金 2木 3水 4火 5土) */ const HERO_TYPE_ICON: string[] = ['', '金', '木', '水', '火', '土']; +/** + * herotype 对应五行名;非法或 0 返回空串。 + * @param heroType Hero.herotype / unlockland 属性类型 + */ +export function unlockHeroTypeName(heroType: number): string { + const type = Math.floor(Number(heroType) || 0); + if (type <= 0 || type >= HERO_TYPE_ICON.length) return ''; + return HERO_TYPE_ICON[type]; +} + /** * 拖英雄推进区域解锁的结果。 * skip:不是解锁格,走原落子。 @@ -166,6 +176,20 @@ export class MapAreaUnlock { return n; } + /** + * 某阵营当前待解锁区需求 herotype;0=任意属性或无可解锁区。 + * @param campAreaId 阵营 CampAreaID + */ + currentNeedHeroType(campAreaId: number): number { + const track = this._trackOf(campAreaId); + if (!track) return 0; + const current = this.currentId(campAreaId); + if (current <= 0) return 0; + const index = this._indexOf(current); + if (index < 0 || !track.rolled[index]) return 0; + return Math.floor(Number(track.heroTypes[index]) || 0); + } + /** * 拖到当前解锁区域是否满足类型与等级(不改状态)。 * 类型 0 表示任意 herotype,等级须达到配置等级加段位加值。 diff --git a/assets/script/battle/view/CocosBagBoardView.ts b/assets/script/battle/view/CocosBagBoardView.ts index 6c03bed..83e7a18 100644 --- a/assets/script/battle/view/CocosBagBoardView.ts +++ b/assets/script/battle/view/CocosBagBoardView.ts @@ -3465,7 +3465,7 @@ export class CocosBagBoardView { this._bagSession.revertType3Pickup(drag.type3Backup.gridIndex, drag.type3Backup.tile); } grid.restoreOrigin(); - const msg = rejectReasonToMessage(reason); + const msg = this._bagSession?.formatRejectMessage(reason) ?? rejectReasonToMessage(reason); if (msg) gg.ui.showToast(msg); this._disposeEphemeralDragGrid(grid, drag.source); this._rebuildFromSession(); @@ -6192,7 +6192,7 @@ if (weapon2Id === GRID_WEAPON2_ID) return true; commitViews: (grid, target, swap) => this._commitViewsAfterSuccessfulDrop(grid, target, swap), validateBeforeDrop: opts.validateBeforeDrop, onDropRejected: (reason) => { - const msg = rejectReasonToMessage(reason); + const msg = this._bagSession?.formatRejectMessage(reason) ?? rejectReasonToMessage(reason); if (msg) gg.ui.showToast(msg); }, rebuildOnFailure: () => this._rebuildFromSession(), diff --git a/assets/script/battle/view/MapDragGuideHelper.ts b/assets/script/battle/view/MapDragGuideHelper.ts index 1c3af84..1d4138f 100644 --- a/assets/script/battle/view/MapDragGuideHelper.ts +++ b/assets/script/battle/view/MapDragGuideHelper.ts @@ -13,6 +13,7 @@ import type { BattleBagSession } from '../bag/BattleBagSession'; import { WeaponComposeConfigProvider } from '../bag/WeaponComposeConfigProvider'; import type { MapGrid } from '../map/legacy/MapGrid'; import type { MapManager } from '../map/legacy/MapManager'; +import { unlockHeroTypeName } from '../map/legacy/MapAreaUnlock'; /** 地图格在拖拽悬停中的落子语义 */ export type MapDragDropState = 'valid' | 'blocked' | 'none'; @@ -200,6 +201,13 @@ export class MapDragGuideHelper { if (!session.validator.isAllowedCampForPlace(mg.Index)) { return rejectReasonToMessage(PlaceRejectReason.WrongCampArea); } + if (mg.waitingUnlock && isElementGlyphType(w2Type)) { + const needType = session.currentUnlockNeedHeroType(mg.Index); + const name = unlockHeroTypeName(needType); + if (needType > 0 && name && Math.floor(Number(w2Type) || 0) !== needType) { + return `需要用${name}属性数字解锁`; + } + } if (MapDragGuideHelper.hasSpiderWeb(mg) && w2Type === Weapon2TileType.Grid) { return rejectReasonToMessage(PlaceRejectReason.SpiderWebBlocked); } diff --git a/preview-template/index.ejs b/preview-template/index.ejs index ce0c4b0..9e9db72 100644 --- a/preview-template/index.ejs +++ b/preview-template/index.ejs @@ -37,19 +37,27 @@
调试工作台 - - - - - +
+ + + + + +
- - + + - +
@@ -63,14 +71,18 @@ 内存 --
引擎等待中… - - +
+ + +
diff --git a/preview-template/workbench/css/workbench.css b/preview-template/workbench/css/workbench.css index 7af3310..fb4e225 100644 --- a/preview-template/workbench/css/workbench.css +++ b/preview-template/workbench/css/workbench.css @@ -119,6 +119,8 @@ html[data-pw='shell'] .pw-shell-dummy-canvas, .pw-root.pw-workbench-off #pw-layout-preset, .pw-root.pw-workbench-off #pw-layout-reset, +.pw-root.pw-workbench-off #pw-layout-preset ~ .pw-btn-group, +.pw-root.pw-workbench-off .pw-toolbar-right .pw-btn-group, .pw-root.pw-workbench-off #pw-engine-status { display: none; } @@ -488,6 +490,58 @@ body { .pw-mode-toggle .pw-btn + .pw-btn { margin-left: -1px; } .pw-mode-toggle .pw-btn.active { position: relative; z-index: 1; } +/* 图标按钮 / 按钮组 */ +.pw-btn-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 26px; + padding: 0; + flex-shrink: 0; +} + +.pw-icon { + width: 14px; + height: 14px; + display: block; + pointer-events: none; +} + +.pw-btn-group { + display: inline-flex; + align-items: center; + gap: 2px; + padding: 2px; + border: 1px solid var(--pw-border); + border-radius: 6px; + background: rgba(255, 255, 255, 0.02); +} + +.pw-btn-group .pw-btn { + border-color: transparent; + background: transparent; +} + +.pw-btn-group .pw-btn:hover { + background: rgba(255, 255, 255, 0.05); + border-color: transparent; +} + +.pw-btn-group .pw-btn.active { + background: var(--pw-accent); + border-color: var(--pw-accent); +} + +.pw-btn-group .pw-select { + border-color: transparent; + background: transparent; +} + +.pw-btn-group .pw-select:hover { + background: rgba(255, 255, 255, 0.05); +} + .pw-main { display: flex; flex: 1; @@ -772,7 +826,7 @@ body { } .pw-summary span { color: var(--pw-muted); } -.pw-summary b { color: #fff; font-weight: 600; } +.pw-summary b { color: var(--pw-text); font-weight: 600; } .pw-pick-overlay { position: fixed; @@ -967,8 +1021,19 @@ body { } .pw-console-empty { - color: #9aa0a6; - padding: 10px 12px; + color: var(--pw-muted); + padding: 28px 16px; + text-align: center; + font-size: var(--pw-fs-sm); + line-height: 1.6; +} + +.pw-console-empty::before { + content: "⌨"; + display: block; + font-size: 22px; + opacity: 0.35; + margin-bottom: 8px; } .pw-console-row { @@ -1239,9 +1304,19 @@ body { } .pw-inspector-empty { - padding: 24px 12px; + padding: 32px 16px; text-align: center; color: var(--pw-muted); + font-size: var(--pw-fs-sm); + line-height: 1.6; +} + +.pw-inspector-empty::before { + content: "◎"; + display: block; + font-size: 22px; + opacity: 0.35; + margin-bottom: 8px; } .pw-insp-section { @@ -1616,13 +1691,17 @@ body { width: 320px; height: 360px; background: var(--pw-panel); - border: 1px solid var(--pw-accent); - box-shadow: 0 8px 28px rgba(0, 0, 0, 0.45); + border: 1px solid var(--pw-border-strong); + box-shadow: 0 12px 36px rgba(0, 0, 0, 0.5); pointer-events: auto; display: flex; flex-direction: column; overflow: hidden; - border-radius: 4px; + border-radius: 6px; +} + +.pw-float-win:hover { + border-color: var(--pw-accent); } .pw-float-win .pw-panel-root, @@ -1639,8 +1718,7 @@ body { align-items: center; justify-content: center; overflow: hidden; - background: - radial-gradient(ellipse at center, #2c2c30 0%, #0e0e10 72%); + background: radial-gradient(ellipse at 50% 40%, #1a1e26 0%, #0b0d10 70%); } .pw-device-scale { @@ -2018,6 +2096,7 @@ body { z-index: 20; position: relative; touch-action: none; + transition: background 0.12s ease; } .pw-splitter-h { @@ -2052,8 +2131,40 @@ body { height: 12px; } +/* 中线把手 */ +.pw-splitter-h::before { + content: ''; + position: absolute; + left: 1px; + right: 1px; + top: 18%; + bottom: 18%; + border-radius: 1px; + background: transparent; + transition: background 0.12s ease; +} + +.pw-splitter-v::before { + content: ''; + position: absolute; + top: 1px; + bottom: 1px; + left: 18%; + right: 18%; + border-radius: 1px; + background: transparent; + transition: background 0.12s ease; +} + .pw-splitter:hover, -.pw-splitter:active { background: var(--pw-accent); } +.pw-splitter:active { + background: var(--pw-accent-soft); +} + +.pw-splitter:hover::before, +.pw-splitter:active::before { + background: var(--pw-accent); +} body.pw-split-dragging { cursor: inherit; diff --git a/preview-template/workbench/js/host.js b/preview-template/workbench/js/host.js index 5d08c99..c7c6d0b 100644 --- a/preview-template/workbench/js/host.js +++ b/preview-template/workbench/js/host.js @@ -295,7 +295,8 @@ var rotateBtn = document.getElementById('pw-device-rotate'); if (rotateBtn && st) { rotateBtn.classList.toggle('active', !!st.rotated); - rotateBtn.textContent = st.rotated ? '竖屏' : '旋转'; + rotateBtn.title = st.rotated ? '竖屏(当前已旋转)' : '旋转'; + rotateBtn.setAttribute('aria-label', st.rotated ? '竖屏' : '旋转'); } }).catch(function () { /* */ }); }; @@ -328,7 +329,8 @@ rotateBtn.addEventListener('click', function () { self.bridge.call('device.rotate').then(function (rotated) { rotateBtn.classList.toggle('active', !!rotated); - rotateBtn.textContent = rotated ? '竖屏' : '旋转'; + rotateBtn.title = rotated ? '竖屏(当前已旋转)' : '旋转'; + rotateBtn.setAttribute('aria-label', rotated ? '竖屏' : '旋转'); }); }); } @@ -509,7 +511,7 @@ var fps = Math.round(s.fps || 0); var mem = Math.round((s.totalMemory || 0) * 10) / 10; fpsEl.textContent = 'FPS ' + fps; - memEl.textContent = '内存 ' + mem + 'MB'; + memEl.textContent = '内存 ' + mem + ' MB'; clear(fpsEl); if (fps <= 0) fpsEl.classList.add('muted');