master
humingxing 3 days ago
parent e02909c9df
commit 5e79b343ea
  1. 2
      assets/mx/module/merge/rules/MapPlaceRules.ts
  2. 2
      assets/mx/module/merge/types/MergeTypes.ts
  3. 41
      assets/script/battle/bag/BattleBagSession.ts
  4. 26
      assets/script/battle/map/legacy/MapAreaUnlock.ts
  5. 4
      assets/script/battle/view/CocosBagBoardView.ts
  6. 8
      assets/script/battle/view/MapDragGuideHelper.ts
  7. 44
      preview-template/index.ejs
  8. 131
      preview-template/workbench/css/workbench.css
  9. 8
      preview-template/workbench/js/host.js

@ -99,6 +99,8 @@ export function rejectReasonToMessage(reason: PlaceRejectReason): string {
return '只能放在己方区域';
case PlaceRejectReason.UnlockDrag:
return '';
case PlaceRejectReason.UnlockNeedElement:
return '';
default:
return '';
}

@ -57,6 +57,8 @@ export enum PlaceRejectReason {
WrongCampArea = 'WrongCampArea',
/** 拖文字推进当前解锁区域:字不落下,也不弹失败提示 */
UnlockDrag = 'UnlockDrag',
/** 解锁区要求特定五行,拖入属性不符(文案由业务层按需求属性拼接) */
UnlockNeedElement = 'UnlockNeedElement',
}
/** 组合英雄激活后写到每个字上的单层 Buff */

@ -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 = [];

@ -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,等级须达到配置等级加段位加值。

@ -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(),

@ -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);
}

@ -37,19 +37,27 @@
<div id="pw-toolbar" class="pw-toolbar pw-shell-only">
<div class="pw-toolbar-left" title="菜单">
<span class="pw-brand">调试工作台</span>
<button type="button" class="pw-btn pw-panel-btn" data-panel="hierarchy">层级</button>
<button type="button" class="pw-btn pw-panel-btn" data-panel="inspector">属性</button>
<button type="button" class="pw-btn pw-panel-btn" data-panel="assets">资源</button>
<button type="button" class="pw-btn pw-panel-btn" data-panel="perf">性能</button>
<button type="button" class="pw-btn pw-panel-btn" data-panel="console">控制台</button>
<div class="pw-btn-group" role="group" aria-label="面板">
<button type="button" class="pw-btn pw-panel-btn" data-panel="hierarchy">层级</button>
<button type="button" class="pw-btn pw-panel-btn" data-panel="inspector">属性</button>
<button type="button" class="pw-btn pw-panel-btn" data-panel="assets">资源</button>
<button type="button" class="pw-btn pw-panel-btn" data-panel="perf">性能</button>
<button type="button" class="pw-btn pw-panel-btn" data-panel="console">控制台</button>
</div>
</div>
<div class="pw-toolbar-center" title="设备与快捷">
<select id="pw-device-select" class="pw-select" title="设备"></select>
<button type="button" id="pw-device-rotate" class="pw-btn" title="旋转">旋转</button>
<button type="button" id="pw-game-refresh" class="pw-btn" title="刷新并重启游戏">刷新</button>
<button type="button" id="pw-device-rotate" class="pw-btn pw-btn-icon" title="旋转" aria-label="旋转">
<svg class="pw-icon" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 2a6 6 0 1 0 5.7 4.2" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M13 2v4h-4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<button type="button" id="pw-game-refresh" class="pw-btn pw-btn-icon" title="刷新并重启游戏" aria-label="刷新">
<svg class="pw-icon" viewBox="0 0 16 16" aria-hidden="true"><path d="M13.5 8a5.5 5.5 0 1 1-1.6-3.9" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M13.5 2.5v3.2h-3.2" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<button type="button" id="pw-show-fps" class="pw-btn" title="显示 FPS 浮窗" aria-pressed="false">FPS</button>
<button type="button" id="pw-game-pause" class="pw-btn">暂停</button>
<button type="button" id="pw-game-step" class="pw-btn" title="暂停时推进一帧">按帧执行</button>
<button type="button" id="pw-game-step" class="pw-btn pw-btn-icon" title="暂停时推进一帧" aria-label="按帧执行">
<svg class="pw-icon" viewBox="0 0 16 16" aria-hidden="true"><path d="M3 3.5 10 8l-7 4.5V3.5z" fill="currentColor"/><path d="M11.5 3.5h2v9h-2z" fill="currentColor"/></svg>
</button>
<button type="button" id="pw-pure-measure" class="pw-btn" title="关闭全部探针,测真实帧率" aria-pressed="false">纯净测帧</button>
<div class="pw-mode-toggle" role="group" aria-label="输入模式">
<button type="button" id="pw-mode-game" class="pw-btn active" data-mode="game">游戏</button>
@ -63,14 +71,18 @@
<span id="pw-stat-mem" class="pw-stat">内存 --</span>
</div>
<span id="pw-engine-status" class="pw-status">引擎等待中…</span>
<select id="pw-layout-preset" class="pw-select" title="布局预设">
<option value="analyze">分析(按横竖屏)</option>
<option value="portrait">竖屏分析</option>
<option value="landscape">横屏分析</option>
<option value="scene">场景</option>
<option value="minimal">精简</option>
</select>
<button type="button" id="pw-layout-reset" class="pw-btn" title="重置布局">重置布局</button>
<div class="pw-btn-group" role="group" aria-label="布局">
<select id="pw-layout-preset" class="pw-select" title="布局预设">
<option value="analyze">分析(按横竖屏)</option>
<option value="portrait">竖屏分析</option>
<option value="landscape">横屏分析</option>
<option value="scene">场景</option>
<option value="minimal">精简</option>
</select>
<button type="button" id="pw-layout-reset" class="pw-btn pw-btn-icon" title="重置布局" aria-label="重置布局">
<svg class="pw-icon" viewBox="0 0 16 16" aria-hidden="true"><path d="M3 4h10M3 8h10M3 12h6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M12.5 10.5 14 12l-1.5 1.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
</div>
<button type="button" id="pw-workbench-toggle" class="pw-btn active" title="开启/关闭调试工作台(关闭后仍保留设备选项)" aria-pressed="true">工作台</button>
</div>
</div>

@ -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;

@ -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');

Loading…
Cancel
Save