parent
69d037971f
commit
489fba5896
File diff suppressed because it is too large
Load Diff
@ -1,9 +0,0 @@ |
|||||||
{ |
|
||||||
"ver": "4.0.24", |
|
||||||
"importer": "typescript", |
|
||||||
"imported": true, |
|
||||||
"uuid": "8ee138cf-ab76-449a-8b25-5ef56e1fe190", |
|
||||||
"files": [], |
|
||||||
"subMetas": {}, |
|
||||||
"userData": {} |
|
||||||
} |
|
||||||
@ -1 +0,0 @@ |
|||||||
bc52de6deb41df5b03ab84d33014e35c |
|
||||||
@ -1,5 +0,0 @@ |
|||||||
yyka-runtime-inspector 1.0.254 |
|
||||||
---------------------------------------- |
|
||||||
将整个目录作为 Cocos Creator 扩展安装(或复制到扩展目录)。 |
|
||||||
依赖已随目录复制(node_modules),无需额外 npm install。 |
|
||||||
游戏侧桥接脚本样例见 runtime-bridge-sample(未打包,需从源码取)。 |
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,3 +0,0 @@ |
|||||||
"use strict"; |
|
||||||
Object.defineProperty(exports, "__esModule", { value: true }); |
|
||||||
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHlwZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zb3VyY2UvcGFuZWxzL2RlZmF1bHQvdHlwZXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IiIsInNvdXJjZXNDb250ZW50IjpbImV4cG9ydCBpbnRlcmZhY2UgU2VydmVyU3RhdGUge1xuICAgIHJ1bm5pbmc6IGJvb2xlYW47XG4gICAgcG9ydDogbnVtYmVyO1xuICAgIGNsaWVudHM6IG51bWJlcjtcbiAgICBjbGllbnRUeXBlOiBzdHJpbmc7XG4gICAgY2xpZW50VXJsOiBzdHJpbmc7XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgVHJlZU5vZGUge1xuICAgIGlkOiBzdHJpbmc7XG4gICAgbmFtZTogc3RyaW5nO1xuICAgIGFjdGl2ZTogYm9vbGVhbjtcbiAgICBsYXllcjogbnVtYmVyO1xuICAgIGNvbXBzOiBudW1iZXI7XG4gICAgdHlwZT86IHN0cmluZztcbiAgICBjaGlsZHJlbjogVHJlZU5vZGVbXTtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBUcmVlUm93IGV4dGVuZHMgVHJlZU5vZGUge1xuICAgIGRlcHRoOiBudW1iZXI7XG4gICAgaGFzQ2hpbGRyZW46IGJvb2xlYW47XG59XG4iXX0=
|
|
||||||
@ -1,60 +0,0 @@ |
|||||||
"use strict"; |
|
||||||
var __importDefault = (this && this.__importDefault) || function (mod) { |
|
||||||
return (mod && mod.__esModule) ? mod : { "default": mod }; |
|
||||||
}; |
|
||||||
Object.defineProperty(exports, "__esModule", { value: true }); |
|
||||||
// @ts-ignore
|
|
||||||
const package_json_1 = __importDefault(require("../../../package.json")); |
|
||||||
exports.default = { |
|
||||||
// 日志新增/清空/过滤切换/展开时自动滚动到底部
|
|
||||||
logs: { |
|
||||||
deep: true, |
|
||||||
handler() { |
|
||||||
this.scrollLogToBottom(); |
|
||||||
}, |
|
||||||
}, |
|
||||||
logFilter() { |
|
||||||
this.scrollLogToBottom(); |
|
||||||
}, |
|
||||||
showLogs(nv) { |
|
||||||
if (nv) { |
|
||||||
this.scrollLogToBottom(); |
|
||||||
} |
|
||||||
}, |
|
||||||
showPreview(nv) { |
|
||||||
// 游戏 DOM 静态常驻(v-show 仅控制显隐,DOM 不销毁),切换回来无需重建;
|
|
||||||
// 仅当引擎尚未启动/上次加载失败时才重新渲染
|
|
||||||
if (nv && !this.previewLoaded) { |
|
||||||
this.$nextTick(() => this.renderPreview()); |
|
||||||
} |
|
||||||
}, |
|
||||||
filter() { |
|
||||||
window.clearTimeout(this.filterTimer); |
|
||||||
this.filterTimer = window.setTimeout(() => this.refreshTree(), 350); |
|
||||||
}, |
|
||||||
physicsDebug(nv) { |
|
||||||
Editor.Message.request(package_json_1.default.name, 'send-command', { cmd: 'set-physics-debug', payload: { enabled: !!nv } }).catch(() => { }); |
|
||||||
}, |
|
||||||
showPerf(nv) { |
|
||||||
if (nv) { |
|
||||||
this.$nextTick(() => this.drawPerf()); |
|
||||||
// 开启时每秒采样一次(1 点/秒),曲线窗口为最近 90 秒
|
|
||||||
this.stopPerfSampling(); |
|
||||||
this.perfTimer = window.setInterval(() => this.samplePerf(), 1000); |
|
||||||
} |
|
||||||
else { |
|
||||||
this.stopPerfSampling(); |
|
||||||
} |
|
||||||
}, |
|
||||||
showRes(nv) { |
|
||||||
if (nv) { |
|
||||||
this.loadResStats(); |
|
||||||
this.resTimer = window.setInterval(() => this.loadResStats(), 3000); |
|
||||||
} |
|
||||||
else if (this.resTimer) { |
|
||||||
window.clearInterval(this.resTimer); |
|
||||||
this.resTimer = 0; |
|
||||||
} |
|
||||||
}, |
|
||||||
}; |
|
||||||
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoid2F0Y2guanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zb3VyY2UvcGFuZWxzL2RlZmF1bHQvd2F0Y2gudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7QUFBQSxhQUFhO0FBQ2IseUVBQWdEO0FBRWhELGtCQUFlO0lBQ1gsMEJBQTBCO0lBQzFCLElBQUksRUFBRTtRQUNGLElBQUksRUFBRSxJQUFJO1FBQ1YsT0FBTztZQUNILElBQUksQ0FBQyxpQkFBaUIsRUFBRSxDQUFDO1FBQzdCLENBQUM7S0FDSjtJQUNELFNBQVM7UUFDTCxJQUFJLENBQUMsaUJBQWlCLEVBQUUsQ0FBQztJQUM3QixDQUFDO0lBQ0QsUUFBUSxDQUFZLEVBQVc7UUFDM0IsSUFBSSxFQUFFLEVBQUUsQ0FBQztZQUNMLElBQUksQ0FBQyxpQkFBaUIsRUFBRSxDQUFDO1FBQzdCLENBQUM7SUFDTCxDQUFDO0lBQ0QsV0FBVyxDQUFZLEVBQVc7UUFDOUIsOENBQThDO1FBQzlDLHdCQUF3QjtRQUN4QixJQUFJLEVBQUUsSUFBSSxDQUFDLElBQUksQ0FBQyxhQUFhLEVBQUUsQ0FBQztZQUM1QixJQUFJLENBQUMsU0FBUyxDQUFDLEdBQUcsRUFBRSxDQUFDLElBQUksQ0FBQyxhQUFhLEVBQUUsQ0FBQyxDQUFDO1FBQy9DLENBQUM7SUFDTCxDQUFDO0lBQ0QsTUFBTTtRQUNGLE1BQU0sQ0FBQyxZQUFZLENBQUMsSUFBSSxDQUFDLFdBQVcsQ0FBQyxDQUFDO1FBQ3RDLElBQUksQ0FBQyxXQUFXLEdBQUcsTUFBTSxDQUFDLFVBQVUsQ0FBQyxHQUFHLEVBQUUsQ0FBQyxJQUFJLENBQUMsV0FBVyxFQUFFLEVBQUUsR0FBRyxDQUFDLENBQUM7SUFDeEUsQ0FBQztJQUNELFlBQVksQ0FBWSxFQUFXO1FBQy9CLE1BQU0sQ0FBQyxPQUFPLENBQUMsT0FBTyxDQUNsQixzQkFBVyxDQUFDLElBQVcsRUFDdkIsY0FBcUIsRUFDckIsRUFBRSxHQUFHLEVBQUUsbUJBQW1CLEVBQUUsT0FBTyxFQUFFLEVBQUUsT0FBTyxFQUFFLENBQUMsQ0FBQyxFQUFFLEVBQUUsRUFBUyxDQUNsRSxDQUFDLEtBQUssQ0FBQyxHQUFHLEVBQUUsR0FBRSxDQUFDLENBQUMsQ0FBQztJQUN0QixDQUFDO0lBQ0QsUUFBUSxDQUFZLEVBQVc7UUFDM0IsSUFBSSxFQUFFLEVBQUUsQ0FBQztZQUNMLElBQUksQ0FBQyxTQUFTLENBQUMsR0FBRyxFQUFFLENBQUMsSUFBSSxDQUFDLFFBQVEsRUFBRSxDQUFDLENBQUM7WUFDdEMsZ0NBQWdDO1lBQ2hDLElBQUksQ0FBQyxnQkFBZ0IsRUFBRSxDQUFDO1lBQ3hCLElBQUksQ0FBQyxTQUFTLEdBQUcsTUFBTSxDQUFDLFdBQVcsQ0FBQyxHQUFHLEVBQUUsQ0FBQyxJQUFJLENBQUMsVUFBVSxFQUFFLEVBQUUsSUFBSSxDQUFDLENBQUM7UUFDdkUsQ0FBQzthQUFNLENBQUM7WUFDSixJQUFJLENBQUMsZ0JBQWdCLEVBQUUsQ0FBQztRQUM1QixDQUFDO0lBQ0wsQ0FBQztJQUNELE9BQU8sQ0FBWSxFQUFXO1FBQzFCLElBQUksRUFBRSxFQUFFLENBQUM7WUFDTCxJQUFJLENBQUMsWUFBWSxFQUFFLENBQUM7WUFDcEIsSUFBSSxDQUFDLFFBQVEsR0FBRyxNQUFNLENBQUMsV0FBVyxDQUFDLEdBQUcsRUFBRSxDQUFDLElBQUksQ0FBQyxZQUFZLEVBQUUsRUFBRSxJQUFJLENBQUMsQ0FBQztRQUN4RSxDQUFDO2FBQU0sSUFBSSxJQUFJLENBQUMsUUFBUSxFQUFFLENBQUM7WUFDdkIsTUFBTSxDQUFDLGFBQWEsQ0FBQyxJQUFJLENBQUMsUUFBUSxDQUFDLENBQUM7WUFDcEMsSUFBSSxDQUFDLFFBQVEsR0FBRyxDQUFDLENBQUM7UUFDdEIsQ0FBQztJQUNMLENBQUM7Q0FDSixDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLy8gQHRzLWlnbm9yZVxuaW1wb3J0IHBhY2thZ2VKU09OIGZyb20gJy4uLy4uLy4uL3BhY2thZ2UuanNvbic7XG5cbmV4cG9ydCBkZWZhdWx0IHtcbiAgICAvLyDml6Xlv5fmlrDlop4v5riF56m6L+i/h+a7pOWIh+aNoi/lsZXlvIDml7boh6rliqjmu5rliqjliLDlupXpg6hcbiAgICBsb2dzOiB7XG4gICAgICAgIGRlZXA6IHRydWUsXG4gICAgICAgIGhhbmRsZXIodGhpczogYW55KSB7XG4gICAgICAgICAgICB0aGlzLnNjcm9sbExvZ1RvQm90dG9tKCk7XG4gICAgICAgIH0sXG4gICAgfSxcbiAgICBsb2dGaWx0ZXIodGhpczogYW55KSB7XG4gICAgICAgIHRoaXMuc2Nyb2xsTG9nVG9Cb3R0b20oKTtcbiAgICB9LFxuICAgIHNob3dMb2dzKHRoaXM6IGFueSwgbnY6IGJvb2xlYW4pIHtcbiAgICAgICAgaWYgKG52KSB7XG4gICAgICAgICAgICB0aGlzLnNjcm9sbExvZ1RvQm90dG9tKCk7XG4gICAgICAgIH1cbiAgICB9LFxuICAgIHNob3dQcmV2aWV3KHRoaXM6IGFueSwgbnY6IGJvb2xlYW4pIHtcbiAgICAgICAgLy8g5ri45oiPIERPTSDpnZnmgIHluLjpqbvvvIh2LXNob3cg5LuF5o6n5Yi25pi+6ZqQ77yMRE9NIOS4jemUgOavge+8ie+8jOWIh+aNouWbnuadpeaXoOmcgOmHjeW7uu+8m1xuICAgICAgICAvLyDku4XlvZPlvJXmk47lsJrmnKrlkK/liqgv5LiK5qyh5Yqg6L295aSx6LSl5pe25omN6YeN5paw5riy5p+TXG4gICAgICAgIGlmIChudiAmJiAhdGhpcy5wcmV2aWV3TG9hZGVkKSB7XG4gICAgICAgICAgICB0aGlzLiRuZXh0VGljaygoKSA9PiB0aGlzLnJlbmRlclByZXZpZXcoKSk7XG4gICAgICAgIH1cbiAgICB9LFxuICAgIGZpbHRlcih0aGlzOiBhbnkpIHtcbiAgICAgICAgd2luZG93LmNsZWFyVGltZW91dCh0aGlzLmZpbHRlclRpbWVyKTtcbiAgICAgICAgdGhpcy5maWx0ZXJUaW1lciA9IHdpbmRvdy5zZXRUaW1lb3V0KCgpID0+IHRoaXMucmVmcmVzaFRyZWUoKSwgMzUwKTtcbiAgICB9LFxuICAgIHBoeXNpY3NEZWJ1Zyh0aGlzOiBhbnksIG52OiBib29sZWFuKSB7XG4gICAgICAgIEVkaXRvci5NZXNzYWdlLnJlcXVlc3QoXG4gICAgICAgICAgICBwYWNrYWdlSlNPTi5uYW1lIGFzIGFueSxcbiAgICAgICAgICAgICdzZW5kLWNvbW1hbmQnIGFzIGFueSxcbiAgICAgICAgICAgIHsgY21kOiAnc2V0LXBoeXNpY3MtZGVidWcnLCBwYXlsb2FkOiB7IGVuYWJsZWQ6ICEhbnYgfSB9IGFzIGFueSxcbiAgICAgICAgKS5jYXRjaCgoKSA9PiB7fSk7XG4gICAgfSxcbiAgICBzaG93UGVyZih0aGlzOiBhbnksIG52OiBib29sZWFuKSB7XG4gICAgICAgIGlmIChudikge1xuICAgICAgICAgICAgdGhpcy4kbmV4dFRpY2soKCkgPT4gdGhpcy5kcmF3UGVyZigpKTtcbiAgICAgICAgICAgIC8vIOW8gOWQr+aXtuavj+enkumHh+agt+S4gOasoe+8iDEg54K5L+enku+8ie+8jOabsue6v+eql+WPo+S4uuacgOi/kSA5MCDnp5JcbiAgICAgICAgICAgIHRoaXMuc3RvcFBlcmZTYW1wbGluZygpO1xuICAgICAgICAgICAgdGhpcy5wZXJmVGltZXIgPSB3aW5kb3cuc2V0SW50ZXJ2YWwoKCkgPT4gdGhpcy5zYW1wbGVQZXJmKCksIDEwMDApO1xuICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgICAgdGhpcy5zdG9wUGVyZlNhbXBsaW5nKCk7XG4gICAgICAgIH1cbiAgICB9LFxuICAgIHNob3dSZXModGhpczogYW55LCBudjogYm9vbGVhbikge1xuICAgICAgICBpZiAobnYpIHtcbiAgICAgICAgICAgIHRoaXMubG9hZFJlc1N0YXRzKCk7XG4gICAgICAgICAgICB0aGlzLnJlc1RpbWVyID0gd2luZG93LnNldEludGVydmFsKCgpID0+IHRoaXMubG9hZFJlc1N0YXRzKCksIDMwMDApO1xuICAgICAgICB9IGVsc2UgaWYgKHRoaXMucmVzVGltZXIpIHtcbiAgICAgICAgICAgIHdpbmRvdy5jbGVhckludGVydmFsKHRoaXMucmVzVGltZXIpO1xuICAgICAgICAgICAgdGhpcy5yZXNUaW1lciA9IDA7XG4gICAgICAgIH1cbiAgICB9LFxufTtcbiJdfQ==
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1 +0,0 @@ |
|||||||
"use strict";module.exports={open_panel:"Run Inspector",preview_panel:"Game Live",default_panel:"Runtime Inspector",menu_group:"Runtime Workbench",menu_root:"YYKA Runtime Inspector",send_to_panel:"Send message to Runtime Inspector",description:"Cocos Creator runtime debug tool: inspect & edit node/component properties, materials & asset references in real time; monitor performance & resources; device-shell preview (rotate/portrait-mute/fps control) and MCP service"}; |
|
||||||
@ -1 +0,0 @@ |
|||||||
"use strict";module.exports={open_panel:"运行检查",preview_panel:"游戏实况",default_panel:"运行时检查器",menu_group:"运行时工作台",menu_root:"YYKA 运行时检查器",send_to_panel:"发送消息给面板",description:"Cocos Creator 运行时调试工具:实时查看并编辑节点/组件属性、材质与资产引用,监控性能与资源,支持设备外壳预览(旋转/横竖屏/静音/帧率控制)及 MCP 服务"}; |
|
||||||
@ -1,142 +0,0 @@ |
|||||||
{ |
|
||||||
"$schema": "./@types/schema/package/index.json", |
|
||||||
"package_version": 2, |
|
||||||
"name": "yyka-runtime-inspector", |
|
||||||
"version": "1.0.254", |
|
||||||
"author": "YYKACN", |
|
||||||
"editor": ">=3.8.8", |
|
||||||
"description": "i18n:yyka-runtime-inspector.description", |
|
||||||
"main": "./dist/main.js", |
|
||||||
"dependencies": { |
|
||||||
"@modelcontextprotocol/sdk": "^1.30.0", |
|
||||||
"fs-extra": "^10.0.0", |
|
||||||
"vue": "^3.1.4", |
|
||||||
"ws": "^8.18.0" |
|
||||||
}, |
|
||||||
"panels": { |
|
||||||
"default": { |
|
||||||
"title": "i18n:yyka-runtime-inspector.default_panel", |
|
||||||
"type": "dockable", |
|
||||||
"main": "dist/panels/default", |
|
||||||
"size": { |
|
||||||
"min-width": 800, |
|
||||||
"min-height": 600, |
|
||||||
"width": 1280, |
|
||||||
"height": 720 |
|
||||||
} |
|
||||||
}, |
|
||||||
"preview": { |
|
||||||
"title": "i18n:yyka-runtime-inspector.preview_panel", |
|
||||||
"type": "dockable", |
|
||||||
"main": "dist/panels/preview", |
|
||||||
"size": { |
|
||||||
"min-width": 320, |
|
||||||
"min-height": 240, |
|
||||||
"width": 720, |
|
||||||
"height": 480 |
|
||||||
} |
|
||||||
} |
|
||||||
}, |
|
||||||
"contributions": { |
|
||||||
"menu": [ |
|
||||||
{ |
|
||||||
"path": "YYKA插件/i18n:yyka-runtime-inspector.menu_group", |
|
||||||
"label": "i18n:yyka-runtime-inspector.open_panel", |
|
||||||
"message": "open-panel" |
|
||||||
}, |
|
||||||
{ |
|
||||||
"path": "YYKA插件/i18n:yyka-runtime-inspector.menu_group", |
|
||||||
"label": "i18n:yyka-runtime-inspector.preview_panel", |
|
||||||
"message": "open-preview-panel" |
|
||||||
} |
|
||||||
], |
|
||||||
"messages": { |
|
||||||
"open-panel": { |
|
||||||
"methods": [ |
|
||||||
"openPanel" |
|
||||||
] |
|
||||||
}, |
|
||||||
"open-preview-panel": { |
|
||||||
"methods": [ |
|
||||||
"openPreviewPanel" |
|
||||||
] |
|
||||||
}, |
|
||||||
"start-server": { |
|
||||||
"methods": [ |
|
||||||
"startServer" |
|
||||||
] |
|
||||||
}, |
|
||||||
"stop-server": { |
|
||||||
"methods": [ |
|
||||||
"stopServer" |
|
||||||
] |
|
||||||
}, |
|
||||||
"get-server-state": { |
|
||||||
"methods": [ |
|
||||||
"getServerState" |
|
||||||
] |
|
||||||
}, |
|
||||||
"send-command": { |
|
||||||
"methods": [ |
|
||||||
"sendCommand" |
|
||||||
] |
|
||||||
}, |
|
||||||
"query-command": { |
|
||||||
"methods": [ |
|
||||||
"queryCommand" |
|
||||||
] |
|
||||||
}, |
|
||||||
"get-preview-info": { |
|
||||||
"methods": [ |
|
||||||
"getPreviewInfo" |
|
||||||
] |
|
||||||
}, |
|
||||||
"get-projects-settings": { |
|
||||||
"methods": [ |
|
||||||
"getProjectsSettings" |
|
||||||
] |
|
||||||
}, |
|
||||||
"query-log": { |
|
||||||
"methods": [ |
|
||||||
"queryLog" |
|
||||||
] |
|
||||||
}, |
|
||||||
"install-bridge": { |
|
||||||
"methods": [ |
|
||||||
"installBridge" |
|
||||||
] |
|
||||||
}, |
|
||||||
"get-token": { |
|
||||||
"methods": [ |
|
||||||
"getToken" |
|
||||||
] |
|
||||||
}, |
|
||||||
"get-logs": { |
|
||||||
"methods": [ |
|
||||||
"getLogs" |
|
||||||
] |
|
||||||
}, |
|
||||||
"start-mcp": { |
|
||||||
"methods": [ |
|
||||||
"startMcp" |
|
||||||
] |
|
||||||
}, |
|
||||||
"stop-mcp": { |
|
||||||
"methods": [ |
|
||||||
"stopMcp" |
|
||||||
] |
|
||||||
}, |
|
||||||
"get-mcp-state": { |
|
||||||
"methods": [ |
|
||||||
"getMcpState" |
|
||||||
] |
|
||||||
}, |
|
||||||
"highlight-node": { |
|
||||||
"methods": [ |
|
||||||
"highlightNode" |
|
||||||
] |
|
||||||
} |
|
||||||
} |
|
||||||
}, |
|
||||||
"_storeId": "866bfa25bd721c5d209462bfcfed2da0" |
|
||||||
} |
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,293 +0,0 @@ |
|||||||
html, |
|
||||||
body { |
|
||||||
height: 100%; |
|
||||||
margin: 0; |
|
||||||
padding: 0; |
|
||||||
overflow: hidden; |
|
||||||
} |
|
||||||
|
|
||||||
#app { |
|
||||||
height: 100%; |
|
||||||
margin: 0; |
|
||||||
padding: 0; |
|
||||||
} |
|
||||||
|
|
||||||
/* 面板根:占满整个面板,沿用 default 的 preview-col 布局 */ |
|
||||||
.preview-panel-root { |
|
||||||
position: relative; |
|
||||||
height: 100%; |
|
||||||
box-sizing: border-box; |
|
||||||
display: flex; |
|
||||||
flex-direction: column; |
|
||||||
border: 1px solid var(--color-normal-border); |
|
||||||
background: #171a1f; |
|
||||||
overflow: hidden; |
|
||||||
} |
|
||||||
|
|
||||||
.preview-toolbar { |
|
||||||
display: flex; |
|
||||||
align-items: center; |
|
||||||
gap: 6px; |
|
||||||
padding: 6px 8px; |
|
||||||
border-bottom: 1px solid var(--color-normal-border); |
|
||||||
background: linear-gradient(180deg, #1c1f24, #171a1f); |
|
||||||
flex: none; |
|
||||||
} |
|
||||||
.p-online { |
|
||||||
display: inline-flex; |
|
||||||
align-items: center; |
|
||||||
gap: 5px; |
|
||||||
font-size: 12px; |
|
||||||
color: #8b94a3; |
|
||||||
white-space: nowrap; |
|
||||||
} |
|
||||||
.p-online .dot { |
|
||||||
width: 7px; |
|
||||||
height: 7px; |
|
||||||
border-radius: 50%; |
|
||||||
background: #5a6270; |
|
||||||
flex: none; |
|
||||||
} |
|
||||||
.p-online.ok { |
|
||||||
color: #10b981; |
|
||||||
} |
|
||||||
.p-online.ok .dot { |
|
||||||
background: #34d399; |
|
||||||
box-shadow: 0 0 6px #34d399; |
|
||||||
} |
|
||||||
.p-online.warn { |
|
||||||
color: #f59e0b; |
|
||||||
} |
|
||||||
.p-online.warn .dot { |
|
||||||
background: #f59e0b; |
|
||||||
box-shadow: 0 0 6px #f59e0b; |
|
||||||
} |
|
||||||
.p-sep { |
|
||||||
width: 1px; |
|
||||||
height: 18px; |
|
||||||
background: var(--color-normal-border); |
|
||||||
margin: 0 4px; |
|
||||||
flex: none; |
|
||||||
} |
|
||||||
.p-group { |
|
||||||
display: inline-flex; |
|
||||||
align-items: center; |
|
||||||
gap: 5px; |
|
||||||
color: #8b94a3; |
|
||||||
} |
|
||||||
.p-group > ui-icon { |
|
||||||
font-size: 14px; |
|
||||||
} |
|
||||||
.p-device { |
|
||||||
min-width: 108px; |
|
||||||
} |
|
||||||
.p-fps-in { |
|
||||||
width: 56px; |
|
||||||
} |
|
||||||
.pbtn { |
|
||||||
display: inline-flex; |
|
||||||
align-items: center; |
|
||||||
justify-content: center; |
|
||||||
width: 26px; |
|
||||||
height: 26px; |
|
||||||
padding: 0; |
|
||||||
border: 1px solid var(--color-normal-border); |
|
||||||
border-radius: 5px; |
|
||||||
background: #21252b; |
|
||||||
color: #a3adbd; |
|
||||||
cursor: pointer; |
|
||||||
transition: all 0.12s ease; |
|
||||||
flex: none; |
|
||||||
} |
|
||||||
.pbtn:hover { |
|
||||||
background: #2a303a; |
|
||||||
color: #e6ebf2; |
|
||||||
border-color: #3a4250; |
|
||||||
} |
|
||||||
.pbtn.on { |
|
||||||
background: linear-gradient(180deg, #3b82f6, #2563eb); |
|
||||||
color: #fff; |
|
||||||
border-color: #3b82f6; |
|
||||||
box-shadow: 0 1px 6px rgba(59, 130, 246, 0.45); |
|
||||||
} |
|
||||||
.pbtn.major { |
|
||||||
width: 30px; |
|
||||||
background: linear-gradient(180deg, #3b82f6, #2563eb); |
|
||||||
color: #fff; |
|
||||||
border-color: #3b82f6; |
|
||||||
box-shadow: 0 1px 6px rgba(59, 130, 246, 0.45); |
|
||||||
} |
|
||||||
.pbtn.major:hover { |
|
||||||
filter: brightness(1.12); |
|
||||||
} |
|
||||||
.pbtn ui-icon { |
|
||||||
font-size: 15px; |
|
||||||
} |
|
||||||
.spacer { |
|
||||||
flex: 1; |
|
||||||
} |
|
||||||
.p-zoom { |
|
||||||
position: absolute; |
|
||||||
right: 8px; |
|
||||||
bottom: 8px; |
|
||||||
z-index: 5; |
|
||||||
padding: 2px 8px; |
|
||||||
border-radius: 10px; |
|
||||||
background: rgba(20, 22, 26, 0.78); |
|
||||||
color: #cfe0ff; |
|
||||||
font-size: 11px; |
|
||||||
font-variant-numeric: tabular-nums; |
|
||||||
pointer-events: none; |
|
||||||
backdrop-filter: blur(3px); |
|
||||||
} |
|
||||||
|
|
||||||
.preview-frame-wrap { |
|
||||||
flex: 1; |
|
||||||
display: flex; |
|
||||||
position: relative; |
|
||||||
min-height: 0; |
|
||||||
} |
|
||||||
|
|
||||||
/* 无 iframe 游戏渲染容器(引擎脚本直接跑在面板 DOM 里) */ |
|
||||||
.game-view { |
|
||||||
flex: 1; |
|
||||||
width: 100%; |
|
||||||
min-width: 0; |
|
||||||
background: #171a1f; |
|
||||||
overflow: hidden; |
|
||||||
position: relative; |
|
||||||
} |
|
||||||
|
|
||||||
/* 预览器 UI 控件条仅作为 Ui 构造依赖保留,不显示 */ |
|
||||||
#yyka-game-host .toolbar { |
|
||||||
display: none !important; |
|
||||||
} |
|
||||||
|
|
||||||
#yyka-game-host .content, |
|
||||||
#yyka-game-host .contentWrap, |
|
||||||
#yyka-game-host #GameDiv, |
|
||||||
#yyka-game-host #Cocos3dGameContainer { |
|
||||||
width: 100% !important; |
|
||||||
height: 100% !important; |
|
||||||
margin: 0 !important; |
|
||||||
padding: 0 !important; |
|
||||||
} |
|
||||||
|
|
||||||
#yyka-game-host #GameDiv { |
|
||||||
display: flex; |
|
||||||
align-items: center; |
|
||||||
justify-content: center; |
|
||||||
background: #171a1f; |
|
||||||
} |
|
||||||
|
|
||||||
#yyka-game-host #GameCanvas { |
|
||||||
width: 100%; |
|
||||||
height: 100%; |
|
||||||
display: block; |
|
||||||
} |
|
||||||
|
|
||||||
/* 加载画面 */ |
|
||||||
#yyka-game-host #splash { |
|
||||||
position: absolute; |
|
||||||
inset: 0; |
|
||||||
display: flex; |
|
||||||
align-items: center; |
|
||||||
justify-content: center; |
|
||||||
background: #141519; |
|
||||||
} |
|
||||||
|
|
||||||
#yyka-game-host #splash .progress-bar { |
|
||||||
width: 200px; |
|
||||||
height: 6px; |
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08); |
|
||||||
border-radius: 8px; |
|
||||||
background: rgba(255, 255, 255, 0.04); |
|
||||||
overflow: hidden; |
|
||||||
} |
|
||||||
|
|
||||||
#yyka-game-host #splash .progress-bar > span { |
|
||||||
display: block; |
|
||||||
height: 100%; |
|
||||||
width: 0; |
|
||||||
background: linear-gradient(90deg, #3b82f6, #60a5fa); |
|
||||||
border-radius: 8px; |
|
||||||
} |
|
||||||
|
|
||||||
#yyka-game-host #bulletin { |
|
||||||
position: absolute; |
|
||||||
inset: 0; |
|
||||||
display: none; |
|
||||||
align-items: center; |
|
||||||
justify-content: center; |
|
||||||
color: rgba(255, 255, 255, 0.45); |
|
||||||
font-size: 13px; |
|
||||||
} |
|
||||||
|
|
||||||
#yyka-game-host #error { |
|
||||||
position: absolute; |
|
||||||
inset: 0; |
|
||||||
display: none; |
|
||||||
z-index: 10; |
|
||||||
background: rgba(0, 0, 0, 0.85); |
|
||||||
color: #f87171; |
|
||||||
padding: 16px; |
|
||||||
overflow: auto; |
|
||||||
white-space: pre-wrap; |
|
||||||
font-size: 12px; |
|
||||||
} |
|
||||||
|
|
||||||
/* 面板层渲染错误提示(点击重新加载) */ |
|
||||||
.preview-err { |
|
||||||
position: absolute; |
|
||||||
inset: 0; |
|
||||||
display: flex; |
|
||||||
align-items: center; |
|
||||||
justify-content: center; |
|
||||||
background: rgba(16, 16, 16, 0.88); |
|
||||||
color: #f87171; |
|
||||||
font-size: 12px; |
|
||||||
line-height: 1.7; |
|
||||||
padding: 16px; |
|
||||||
text-align: center; |
|
||||||
cursor: pointer; |
|
||||||
z-index: 6; |
|
||||||
word-break: break-all; |
|
||||||
} |
|
||||||
|
|
||||||
/* 预览服务未启动时的遮罩提示 */ |
|
||||||
.preview-cover { |
|
||||||
position: absolute; |
|
||||||
inset: 0; |
|
||||||
display: flex; |
|
||||||
align-items: center; |
|
||||||
justify-content: center; |
|
||||||
background: rgba(16, 16, 16, 0.82); |
|
||||||
z-index: 5; |
|
||||||
} |
|
||||||
|
|
||||||
.cover-box { |
|
||||||
text-align: center; |
|
||||||
color: var(--color-normal-contrast-weak); |
|
||||||
font-size: 13px; |
|
||||||
line-height: 1.9; |
|
||||||
padding: 16px; |
|
||||||
} |
|
||||||
|
|
||||||
.cover-box b { |
|
||||||
display: block; |
|
||||||
font-size: 15px; |
|
||||||
color: var(--color-warn); |
|
||||||
margin-bottom: 10px; |
|
||||||
} |
|
||||||
|
|
||||||
.cover-box button { |
|
||||||
margin-top: 12px; |
|
||||||
} |
|
||||||
|
|
||||||
.ok { |
|
||||||
color: var(--color-success); |
|
||||||
} |
|
||||||
|
|
||||||
.warn { |
|
||||||
color: var(--color-warn); |
|
||||||
} |
|
||||||
@ -1,533 +0,0 @@ |
|||||||
<div class="inspector-root"> |
|
||||||
<base href="http://localhost:7456/" /> |
|
||||||
<div class="panel-toolbar"> |
|
||||||
<button class="server-btn" :class="{ running: state.running }" @click="onToggleServer">{{ state.running ? '停止服务' : '启动服务' }}</button> |
|
||||||
<button class="toggle" :class="{ active: autoRefresh }" @click="autoRefresh = !autoRefresh">自动刷新</button> |
|
||||||
<button class="toggle" :class="{ active: showPreview }" @click="showPreview = !showPreview">预览画面</button> |
|
||||||
<button class="toggle" :class="{ active: showPerf }" @click="showPerf = !showPerf">性能曲线</button> |
|
||||||
<button class="toggle" :class="{ active: showRes }" @click="showRes = !showRes" :disabled="!state.running || state.clients === 0">资源统计</button> |
|
||||||
<button class="toggle" :class="{ active: physicsDebug }" @click="physicsDebug = !physicsDebug" :disabled="!state.running || state.clients === 0">物理调试</button> |
|
||||||
<button class="toggle" :class="{ active: highlightOn }" @click="toggleHighlight" :disabled="!state.running || state.clients === 0">游戏高亮</button> |
|
||||||
<button class="toggle" :class="{ active: highlightMode === 'mask' }" @click="toggleHighlightMode" :disabled="!highlightOn || !state.running || state.clients === 0" title="切换高亮/遮罩模式">{{ highlightMode === 'mask' ? '遮罩' : '框选' }}</button> |
|
||||||
<button class="toggle" :class="{ active: mcp.running }" @click="openMcpDialog" title="打开 MCP 服务配置(供 Codex 等 AI Agent 连接)">MCP 服务</button> |
|
||||||
<button @click="refreshTree" :disabled="!state.running || state.clients === 0">刷新节点</button> |
|
||||||
<button class="install-btn" @click="onInstallBridge">安装脚本</button> |
|
||||||
<span v-if="state.clients > 0" class="client-badge" :class="clientTypeMeta.cls" :title="'客户端来源: ' + clientTypeMeta.label + (state.clientUrl ? '\n' + state.clientUrl : '')"> |
|
||||||
<i class="client-icon"></i>{{ clientTypeMeta.label }} |
|
||||||
</span> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="mcp-mask" v-if="showMcpDialog" @click.self="closeMcpDialog"> |
|
||||||
<div class="mcp-dialog"> |
|
||||||
<div class="mcp-dialog-head"> |
|
||||||
<span>MCP 服务配置</span> |
|
||||||
<button class="mini" @click="closeMcpDialog" title="关闭"><ui-icon value="close"></ui-icon></button> |
|
||||||
</div> |
|
||||||
<div class="mcp-dialog-body"> |
|
||||||
<div class="mcp-row"> |
|
||||||
<span class="mcp-label">状态</span> |
|
||||||
<span class="mcp-st" :class="mcp.running ? 'on' : 'off'">{{ mcp.running ? '运行中' : '未启动' }}</span> |
|
||||||
<span class="mcp-st-url" v-if="mcp.running">{{ mcp.url }}</span> |
|
||||||
</div> |
|
||||||
<div class="mcp-row"> |
|
||||||
<span class="mcp-label">端口</span> |
|
||||||
<input class="cmd-input mcp-port" type="number" v-model.number="mcpPort" min="1" max="65535" @change="applyMcpPort" /> |
|
||||||
<span class="mcp-help">修改后立即生效(运行中会重启)</span> |
|
||||||
</div> |
|
||||||
<div class="mcp-row"> |
|
||||||
<span class="mcp-label">令牌</span> |
|
||||||
<input class="token-input" :value="mcp.token" readonly spellcheck="false" @focus="selectToken" title="固定静态令牌" /> |
|
||||||
</div> |
|
||||||
<div class="mcp-row"> |
|
||||||
<span class="mcp-label">授权头</span> |
|
||||||
<span class="mcp-auth" title="Authorization 请求头,MCP 客户端鉴权用">Authorization: Bearer {{ mcp.token }}</span> |
|
||||||
</div> |
|
||||||
<div class="mcp-row mcp-actions"> |
|
||||||
<button class="toggle" :class="{ active: mcp.running }" @click="toggleMcp" :disabled="mcpBusy">{{ mcp.running ? '停止服务' : '启动服务' }}</button> |
|
||||||
<button class="mini" @click="copyMcpConfig" title="复制 Codex MCP 配置 JSON(mcpServers 片段)">{{ mcpCopied ? '已复制' : '复制配置' }}</button> |
|
||||||
</div> |
|
||||||
<div class="mcp-tip">在 Codex/支持 MCP 的客户端中粘贴复制的 mcpServers 配置,即可让 AI 连接当前游戏。</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="split"> |
|
||||||
<div class="tree-panel" :style="{ width: treeWidth + 'px' }"> |
|
||||||
<div class="tree-head"> |
|
||||||
<input v-model="filter" placeholder="筛选节点名…" class="cmd-input" /> |
|
||||||
<span class="scene">场景: {{ tree.scene || '-' }}</span> |
|
||||||
</div> |
|
||||||
<div class="tree-body" v-if="!tree.lastError"> |
|
||||||
<div v-for="row in flatTree" |
|
||||||
:key="row.id" |
|
||||||
class="tree-row" |
|
||||||
:class="{ selected: selectedId === row.id }" |
|
||||||
:data-id="row.id" |
|
||||||
:style="{ paddingLeft: (8 + row.depth * 14) + 'px', '--indent': (8 + row.depth * 14) + 'px' }" |
|
||||||
@click="select(row)"> |
|
||||||
<span class="expander" v-if="row.hasChildren" |
|
||||||
:class="{ open: expanded[row.id] }" |
|
||||||
@click.stop="toggle(row)"> |
|
||||||
<svg viewBox="0 0 10 10" aria-hidden="true"><path d="M2.5 1.5 7.5 5 2.5 8.5z"/></svg> |
|
||||||
</span> |
|
||||||
<span v-else class="expander placeholder"></span> |
|
||||||
<ui-icon class="node-ico" :class="{ inactive: !row.active }" :value="row.type || 'node'" default="node"></ui-icon> |
|
||||||
<span class="name" :class="{ inactive: !row.active }">{{ row.name }}</span> |
|
||||||
<span class="badge hide-badge" v-if="!row.active">隐藏</span> |
|
||||||
<span class="badge">{{ row.comps }}</span> |
|
||||||
</div> |
|
||||||
<div v-if="!flatTree.length" class="empty-hint">(空)无节点或未连接</div> |
|
||||||
</div> |
|
||||||
<div class="empty-hint" v-else>{{ tree.lastError }}</div> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="divider" @pointerdown="startDrag('left', $event)"></div> |
|
||||||
|
|
||||||
<div class="preview-col"> |
|
||||||
<div class="preview-toolbar"> |
|
||||||
<span class="p-online" :class="{ ok: !!previewLoaded }"> |
|
||||||
<i class="dot"></i>{{ previewLoaded ? '在线' : (previewOnline === false ? '未启动' : '') }} |
|
||||||
</span> |
|
||||||
<span class="p-sep"></span> |
|
||||||
<span class="p-group" title="设备 / 分辨率"> |
|
||||||
<ui-icon value="computer"></ui-icon> |
|
||||||
<ui-select class="p-device" :value="previewDevice" @confirm="onPreviewDevice($event.target.value)"> |
|
||||||
<option value="Default">设计分辨率</option> |
|
||||||
<option value="FullScreen">全屏</option> |
|
||||||
<option value="WebpageFullScreen">网页全屏</option> |
|
||||||
<option value="Apple iPhone 12; 13; 14; 12/13 Pro">iPhone 12/13/14 (390×844)</option> |
|
||||||
<option value="Apple iPhone 14 Pro">iPhone 14 Pro (393×852)</option> |
|
||||||
<option value="Apple iPhone 12/13 Pro Max; 14 Plus">iPhone Pro Max/14 Plus (428×926)</option> |
|
||||||
<option value="Apple iPhone XR; 11">iPhone XR/11 (414×896)</option> |
|
||||||
<option value="Apple iPhone X; XS; 11 Pro; 12/13 Mini">iPhone X/XS (375×812)</option> |
|
||||||
<option value="Apple iPad 10.2">iPad 10.2 (1620×2160)</option> |
|
||||||
<option value="HUAWEI Mate 40 Pro">HUAWEI Mate 40 Pro (448×924)</option> |
|
||||||
<option value="OPPO Reno 2">OPPO Reno 2 (360×800)</option> |
|
||||||
<option value="Xiaomi Redmi 8">Xiaomi Redmi 8 (360×760)</option> |
|
||||||
<option value="Lenovo Legion 2 Pro">Lenovo Legion 2 Pro (360×820)</option> |
|
||||||
</ui-select> |
|
||||||
</span> |
|
||||||
<button class="pbtn" :class="{ on: previewRotated }" @click="onPreviewRotate" title="横竖屏切换"><ui-icon value="rotate"></ui-icon></button> |
|
||||||
<span class="p-group p-fps" title="帧率上限"> |
|
||||||
<ui-icon value="setting"></ui-icon> |
|
||||||
<ui-num-input class="p-fps-in" :value="String(previewFps)" @confirm="onPreviewFps($event.target.value)"></ui-num-input> |
|
||||||
</span> |
|
||||||
<span class="p-sep"></span> |
|
||||||
<button class="pbtn" :class="{ on: previewShowFps }" @click="onPreviewShowFps" title="显示 / 隐藏帧率"><ui-icon :value="previewShowFps ? 'eye-open' : 'eye-close'"></ui-icon></button> |
|
||||||
<button class="pbtn" :class="{ on: previewPaused }" @click="onPreviewPause" :title="previewPaused ? '继续' : '暂停'"><ui-icon :value="previewPaused ? 'play' : 'pause'"></ui-icon></button> |
|
||||||
<button class="pbtn" :class="{ on: previewMuted }" @click="onPreviewMute" title="静音 / 取消静音"><ui-icon value="music"></ui-icon></button> |
|
||||||
<button class="pbtn" v-if="previewPaused" @click="onPreviewStep" title="步进一帧"><ui-icon value="next-step"></ui-icon></button> |
|
||||||
<span class="spacer"></span> |
|
||||||
<button class="pbtn major" @click="doRefreshPreview" title="重新加载预览"><ui-icon value="refresh"></ui-icon></button> |
|
||||||
</div> |
|
||||||
<div class="preview-frame-wrap"> |
|
||||||
<!-- 游戏 DOM 由 preview.ts 构建到顶层 document 的 #yyka-game-host 定位容器上 |
|
||||||
(面板内容可能位于 shadow DOM,Ui 构造的 document.querySelector 找不到面板内部元素); |
|
||||||
.game-view 仅作布局占位与定位参考(getBoundingClientRect 对 shadow 内元素同样返回视口坐标)。 |
|
||||||
滚轮在预览区缩放:#GameDiv 做 CSS transform,不触发引擎重排 --> |
|
||||||
<div v-show="showPreview" ref="gameView" class="game-view"></div> |
|
||||||
<div class="p-zoom" v-show="showPreview && previewZoom !== 1">{{ Math.round(previewZoom * 100) }}%</div> |
|
||||||
<div class="preview-err" v-if="showPreview && previewRenderErr" @click="onLoadPreview" title="点击重新加载">{{ previewRenderErr }}</div> |
|
||||||
<div class="preview-cover" v-if="showPreview && previewOnline === false"> |
|
||||||
<div class="cover-box"> |
|
||||||
<b>预览服务未启动</b> |
|
||||||
<div>请在 Cocos Creator 中点击「浏览器预览」启动预览服务,<br/>或在上方地址栏修改预览地址后点「刷新」。</div> |
|
||||||
<button @click="onLoadPreview">重试</button> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
<div class="statusbar"> |
|
||||||
<span class="spacer"></span> |
|
||||||
<span v-if="state.clients > 0" class="st-item-group"> |
|
||||||
<span class="st-item st-fps"><b>{{ stats.fps }}</b> 帧率</span> |
|
||||||
<span class="st-sep">|</span> |
|
||||||
<span class="st-item st-dc">绘制调用 <b>{{ stats.drawCall }}</b></span> |
|
||||||
<span class="st-sep">|</span> |
|
||||||
<span class="st-item st-tri">三角面 <b>{{ stats.triangles.toLocaleString() }}</b></span> |
|
||||||
<span class="st-sep" v-if="stats.memUsed > 0">|</span> |
|
||||||
<span class="st-item st-mem" v-if="stats.memUsed > 0">内存 <b>{{ stats.memUsed }}MB</b></span> |
|
||||||
<span class="st-sep" v-if="physicsInfo">|</span> |
|
||||||
<span class="st-item st-phy" v-if="physicsInfo">物理 <b>{{ physicsInfo.colliders + physicsInfo.colliders2d }}</b>碰撞 / <b>{{ physicsInfo.bodies + physicsInfo.bodies2d }}</b>刚体</span> |
|
||||||
</span> |
|
||||||
<span v-else class="st-item dim">未连接</span> |
|
||||||
<span class="spacer"></span> |
|
||||||
</div> |
|
||||||
<div class="perf-panel" v-if="showPerf"> |
|
||||||
<div class="perf-head"> |
|
||||||
<b>性能曲线</b> |
|
||||||
<span class="legend"><i class="dot fps"></i>FPS</span> |
|
||||||
<span class="legend"><i class="dot dc"></i>DrawCall</span> |
|
||||||
<span class="spacer"></span> |
|
||||||
<button class="mini" @click="showPerf = false">关闭</button> |
|
||||||
</div> |
|
||||||
<canvas ref="perfCanvas" class="perf-canvas" @mousemove="onPerfMove" @mouseleave="onPerfLeave"></canvas> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="divider" v-if="showPreview" @pointerdown="startDrag('right', $event)"></div> |
|
||||||
|
|
||||||
<div class="detail-panel" :style="{ width: detailWidth + 'px' }"> |
|
||||||
<div class="detail-head"> |
|
||||||
节点属性 |
|
||||||
</div> |
|
||||||
<div class="detail-body"> |
|
||||||
<div class="empty-hint" v-if="!selectedId">选中左侧节点查看属性</div> |
|
||||||
<div v-else-if="inspectorLoading" class="empty-hint">加载中…</div> |
|
||||||
<div v-else-if="inspector && inspector.error" class="empty-hint">{{ inspector.error }}</div> |
|
||||||
<template v-else-if="inspector"> |
|
||||||
<div class="detail-section"> |
|
||||||
<div class="section-title">节点</div> |
|
||||||
<div class="detail-row"><span class="k">名称</span><span class="v">{{ inspector.name }}</span></div> |
|
||||||
<div class="detail-row"> |
|
||||||
<span class="k">激活</span> |
|
||||||
<ui-checkbox :value="String(!!inspector.props.active.v)" @confirm="onNodeBool('active', chkVal($event.target.value))"></ui-checkbox> |
|
||||||
</div> |
|
||||||
<div class="detail-row"> |
|
||||||
<span class="k">层级</span> |
|
||||||
<ui-num-input class="v-editor" step="1" :value="fmtNum(inspector.props.layer.v)" @confirm="onNodeNumber('layer', $event.target.value)"></ui-num-input> |
|
||||||
</div> |
|
||||||
<div class="detail-row" v-if="inspector.props.position.editable"> |
|
||||||
<span class="k">位置</span> |
|
||||||
<span class="vec"> |
|
||||||
<ui-num-input :value="fmtNum(inspector.props.position.v.x)" @confirm="onNodeVec('position','x',$event.target.value)"></ui-num-input> |
|
||||||
<ui-num-input :value="fmtNum(inspector.props.position.v.y)" @confirm="onNodeVec('position','y',$event.target.value)"></ui-num-input> |
|
||||||
<ui-num-input :value="fmtNum(inspector.props.position.v.z)" @confirm="onNodeVec('position','z',$event.target.value)"></ui-num-input> |
|
||||||
</span> |
|
||||||
</div> |
|
||||||
<div class="detail-row" v-if="inspector.props.euler.editable"> |
|
||||||
<span class="k">旋转</span> |
|
||||||
<span class="vec"> |
|
||||||
<ui-num-input :value="fmtNum(inspector.props.euler.v.x)" @confirm="onNodeVec('euler','x',$event.target.value)"></ui-num-input> |
|
||||||
<ui-num-input :value="fmtNum(inspector.props.euler.v.y)" @confirm="onNodeVec('euler','y',$event.target.value)"></ui-num-input> |
|
||||||
<ui-num-input :value="fmtNum(inspector.props.euler.v.z)" @confirm="onNodeVec('euler','z',$event.target.value)"></ui-num-input> |
|
||||||
</span> |
|
||||||
</div> |
|
||||||
<div class="detail-row" v-if="inspector.props.scale.editable"> |
|
||||||
<span class="k">缩放</span> |
|
||||||
<span class="vec"> |
|
||||||
<ui-num-input :value="fmtNum(inspector.props.scale.v.x)" @confirm="onNodeVec('scale','x',$event.target.value)"></ui-num-input> |
|
||||||
<ui-num-input :value="fmtNum(inspector.props.scale.v.y)" @confirm="onNodeVec('scale','y',$event.target.value)"></ui-num-input> |
|
||||||
<ui-num-input :value="fmtNum(inspector.props.scale.v.z)" @confirm="onNodeVec('scale','z',$event.target.value)"></ui-num-input> |
|
||||||
</span> |
|
||||||
</div> |
|
||||||
<div class="detail-row"><span class="k">世界坐标</span><span class="v">{{ fmtNum(inspector.props.worldPosition.v.x) }}, {{ fmtNum(inspector.props.worldPosition.v.y) }}, {{ fmtNum(inspector.props.worldPosition.v.z) }}</span></div> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="detail-section" v-for="(comp, ci) in inspector.components" :key="ci"> |
|
||||||
<div class="comp-head"> |
|
||||||
<ui-checkbox :value="String(!!comp.enabled)" @confirm="onCompEnabled(ci, chkVal($event.target.value))"></ui-checkbox> |
|
||||||
<b class="comp-type">{{ comp.type }}</b> |
|
||||||
</div> |
|
||||||
<template v-if="comp.widget"> |
|
||||||
<div class="detail-row"> |
|
||||||
<span class="k">对齐模式</span> |
|
||||||
<ui-select class="v-editor" :value="String(comp.widget.alignMode)" @confirm="onWidgetSet(ci, 'alignMode', $event.target.value)"> |
|
||||||
<option value="0">对齐一次</option> |
|
||||||
<option value="1">窗口缩放时</option> |
|
||||||
</ui-select> |
|
||||||
</div> |
|
||||||
<div class="widget-grid"> |
|
||||||
<div class="wg-cell" :class="{ on: comp.widget.isAlignTop && comp.widget.isAlignLeft }" @click="onWidgetToggle(ci, '__top_left')" title="左上角对齐"></div> |
|
||||||
<div class="wg-cell" :class="{ on: comp.widget.isAlignTop }" @click="onWidgetToggle(ci, 'isAlignTop')" title="顶部对齐"></div> |
|
||||||
<div class="wg-cell" :class="{ on: comp.widget.isAlignTop && comp.widget.isAlignRight }" @click="onWidgetToggle(ci, '__top_right')" title="右上角对齐"></div> |
|
||||||
<div class="wg-cell" :class="{ on: comp.widget.isAlignLeft }" @click="onWidgetToggle(ci, 'isAlignLeft')" title="左对齐"></div> |
|
||||||
<div class="wg-cell wg-center" :class="{ on: comp.widget.isAlignHorizontalCenter && comp.widget.isAlignVerticalCenter }" @click="onWidgetToggle(ci, 'center')" title="水平+垂直居中"></div> |
|
||||||
<div class="wg-cell" :class="{ on: comp.widget.isAlignRight }" @click="onWidgetToggle(ci, 'isAlignRight')" title="右对齐"></div> |
|
||||||
<div class="wg-cell" :class="{ on: comp.widget.isAlignBottom && comp.widget.isAlignLeft }" @click="onWidgetToggle(ci, '__bottom_left')" title="左下角对齐"></div> |
|
||||||
<div class="wg-cell" :class="{ on: comp.widget.isAlignBottom }" @click="onWidgetToggle(ci, 'isAlignBottom')" title="底部对齐"></div> |
|
||||||
<div class="wg-cell" :class="{ on: comp.widget.isAlignBottom && comp.widget.isAlignRight }" @click="onWidgetToggle(ci, '__bottom_right')" title="右下角对齐"></div> |
|
||||||
</div> |
|
||||||
<div class="widget-row"> |
|
||||||
<span class="k">左</span> |
|
||||||
<button class="wg-abs" :class="{ on: comp.widget.isAbsLeft }" @click="onWidgetToggle(ci, 'isAbsLeft')" title="绝对/百分比">Abs</button> |
|
||||||
<ui-num-input class="v-editor" :value="fmtNum(comp.widget.left)" @confirm="onWidgetSet(ci, 'left', $event.target.value)"></ui-num-input> |
|
||||||
<span class="k">右</span> |
|
||||||
<button class="wg-abs" :class="{ on: comp.widget.isAbsRight }" @click="onWidgetToggle(ci, 'isAbsRight')">Abs</button> |
|
||||||
<ui-num-input class="v-editor" :value="fmtNum(comp.widget.right)" @confirm="onWidgetSet(ci, 'right', $event.target.value)"></ui-num-input> |
|
||||||
</div> |
|
||||||
<div class="widget-row"> |
|
||||||
<span class="k">上</span> |
|
||||||
<button class="wg-abs" :class="{ on: comp.widget.isAbsTop }" @click="onWidgetToggle(ci, 'isAbsTop')">Abs</button> |
|
||||||
<ui-num-input class="v-editor" :value="fmtNum(comp.widget.top)" @confirm="onWidgetSet(ci, 'top', $event.target.value)"></ui-num-input> |
|
||||||
<span class="k">下</span> |
|
||||||
<button class="wg-abs" :class="{ on: comp.widget.isAbsBottom }" @click="onWidgetToggle(ci, 'isAbsBottom')">Abs</button> |
|
||||||
<ui-num-input class="v-editor" :value="fmtNum(comp.widget.bottom)" @confirm="onWidgetSet(ci, 'bottom', $event.target.value)"></ui-num-input> |
|
||||||
</div> |
|
||||||
<div class="detail-row"> |
|
||||||
<span class="k">目标</span> |
|
||||||
<span class="ref-wrap"> |
|
||||||
<span class="ref-pick" :class="{ empty: !comp.widget.targetName }" @click="openNodePicker('widget', ci, '__widget_target', 'cc.Node')"> |
|
||||||
<ui-icon value="node"></ui-icon> |
|
||||||
<span class="ref-name">{{ comp.widget.targetName || '选择节点' }}</span> |
|
||||||
</span> |
|
||||||
<button class="mini" v-if="comp.widget.targetName" @click="onWidgetSet(ci, '__widget_target', '')">置空</button> |
|
||||||
</span> |
|
||||||
</div> |
|
||||||
</template> |
|
||||||
<template v-else> |
|
||||||
<div class="detail-row" v-for="(p, pi) in comp.props" :key="pi"> |
|
||||||
<span class="k">{{ p.key }}</span> |
|
||||||
<ui-num-input v-if="p.t==='number'" class="v-editor" :value="fmtNum(p.v)" @confirm="onCompScalar(ci, p.key, $event.target.value, 'number')"></ui-num-input> |
|
||||||
<ui-input v-else-if="p.t==='string'" class="v-editor" :value="p.v" @confirm="onCompScalar(ci, p.key, $event.target.value, 'string')"></ui-input> |
|
||||||
<ui-checkbox v-else-if="p.t==='bool'" :value="String(!!p.v)" @confirm="onCompScalar(ci, p.key, chkVal($event.target.value), 'bool')"></ui-checkbox> |
|
||||||
<ui-select v-else-if="p.t==='enum'" class="v-editor" :value="String(p.v)" @confirm="onCompEnum(ci, p.key, $event.target.value)"> |
|
||||||
<option v-for="op in p.options" :key="op.value" :value="String(op.value)">{{ op.name }} ({{ op.value }})</option> |
|
||||||
</ui-select> |
|
||||||
<span v-else-if="p.t==='vec3'" class="vec"> |
|
||||||
<ui-num-input :value="fmtNum(p.v.x)" @confirm="onCompVec(ci, p.key, 'x', $event.target.value)"></ui-num-input> |
|
||||||
<ui-num-input :value="fmtNum(p.v.y)" @confirm="onCompVec(ci, p.key, 'y', $event.target.value)"></ui-num-input> |
|
||||||
<ui-num-input :value="fmtNum(p.v.z)" @confirm="onCompVec(ci, p.key, 'z', $event.target.value)"></ui-num-input> |
|
||||||
</span> |
|
||||||
<span v-else-if="p.t==='vec2'" class="vec"> |
|
||||||
<ui-num-input :value="fmtNum(p.v.x)" @confirm="onCompVec(ci, p.key, 'x', $event.target.value)"></ui-num-input> |
|
||||||
<ui-num-input :value="fmtNum(p.v.y)" @confirm="onCompVec(ci, p.key, 'y', $event.target.value)"></ui-num-input> |
|
||||||
</span> |
|
||||||
<span v-else-if="p.t==='quat'" class="vec"> |
|
||||||
<ui-num-input :value="fmtNum(p.v.x)" @confirm="onCompVec(ci, p.key, 'x', $event.target.value)"></ui-num-input> |
|
||||||
<ui-num-input :value="fmtNum(p.v.y)" @confirm="onCompVec(ci, p.key, 'y', $event.target.value)"></ui-num-input> |
|
||||||
<ui-num-input :value="fmtNum(p.v.z)" @confirm="onCompVec(ci, p.key, 'z', $event.target.value)"></ui-num-input> |
|
||||||
<ui-num-input :value="fmtNum(p.v.w)" @confirm="onCompVec(ci, p.key, 'w', $event.target.value)"></ui-num-input> |
|
||||||
</span> |
|
||||||
<span v-else-if="p.t==='vec4'" class="vec"> |
|
||||||
<ui-num-input :value="fmtNum(p.v.x)" @confirm="onCompVec(ci, p.key, 'x', $event.target.value)"></ui-num-input> |
|
||||||
<ui-num-input :value="fmtNum(p.v.y)" @confirm="onCompVec(ci, p.key, 'y', $event.target.value)"></ui-num-input> |
|
||||||
<ui-num-input :value="fmtNum(p.v.z)" @confirm="onCompVec(ci, p.key, 'z', $event.target.value)"></ui-num-input> |
|
||||||
<ui-num-input :value="fmtNum(p.v.w)" @confirm="onCompVec(ci, p.key, 'w', $event.target.value)"></ui-num-input> |
|
||||||
</span> |
|
||||||
<span v-else-if="p.t==='size'" class="vec"> |
|
||||||
<ui-num-input :value="fmtNum(p.v.w)" @confirm="onCompVec(ci, p.key, 'w', $event.target.value)"></ui-num-input> |
|
||||||
<ui-num-input :value="fmtNum(p.v.h)" @confirm="onCompVec(ci, p.key, 'h', $event.target.value)"></ui-num-input> |
|
||||||
</span> |
|
||||||
<span v-else-if="p.t==='rect'" class="vec"> |
|
||||||
<ui-num-input :value="fmtNum(p.v.x)" @confirm="onCompVec(ci, p.key, 'x', $event.target.value)"></ui-num-input> |
|
||||||
<ui-num-input :value="fmtNum(p.v.y)" @confirm="onCompVec(ci, p.key, 'y', $event.target.value)"></ui-num-input> |
|
||||||
<ui-num-input :value="fmtNum(p.v.w)" @confirm="onCompVec(ci, p.key, 'w', $event.target.value)"></ui-num-input> |
|
||||||
<ui-num-input :value="fmtNum(p.v.h)" @confirm="onCompVec(ci, p.key, 'h', $event.target.value)"></ui-num-input> |
|
||||||
</span> |
|
||||||
<ui-color v-else-if="p.t==='color'" class="color-editor" :value="colorArr(p.v)" @confirm="onCompColor(ci, p.key, $event.target.value)"></ui-color> |
|
||||||
<span v-else-if="p.t==='array'" class="array-wrap"> |
|
||||||
<div v-for="(it, ii) in p.v" :key="ii" class="array-item"> |
|
||||||
<span class="array-idx">{{ ii }}</span> |
|
||||||
<ui-num-input v-if="it.t==='number'" class="v-editor" :value="fmtNum(it.v)" @confirm="onCompArray(ci, p.key, ii, $event.target.value, 'number')"></ui-num-input> |
|
||||||
<ui-input v-else-if="it.t==='string'" class="v-editor" :value="it.v" @confirm="onCompArray(ci, p.key, ii, $event.target.value, 'string')"></ui-input> |
|
||||||
<ui-checkbox v-else-if="it.t==='bool'" :value="String(!!it.v)" @confirm="onCompArray(ci, p.key, ii, chkVal($event.target.value), 'bool')"></ui-checkbox> |
|
||||||
<span v-else-if="it.t==='vec3'" class="vec"> |
|
||||||
<ui-num-input :value="fmtNum(it.v.x)" @confirm="onCompArrayVec(ci, p.key, ii, 'x', $event.target.value)"></ui-num-input> |
|
||||||
<ui-num-input :value="fmtNum(it.v.y)" @confirm="onCompArrayVec(ci, p.key, ii, 'y', $event.target.value)"></ui-num-input> |
|
||||||
<ui-num-input :value="fmtNum(it.v.z)" @confirm="onCompArrayVec(ci, p.key, ii, 'z', $event.target.value)"></ui-num-input> |
|
||||||
</span> |
|
||||||
<span v-else-if="it.t==='vec2'" class="vec"> |
|
||||||
<ui-num-input :value="fmtNum(it.v.x)" @confirm="onCompArrayVec(ci, p.key, ii, 'x', $event.target.value)"></ui-num-input> |
|
||||||
<ui-num-input :value="fmtNum(it.v.y)" @confirm="onCompArrayVec(ci, p.key, ii, 'y', $event.target.value)"></ui-num-input> |
|
||||||
</span> |
|
||||||
<span v-else-if="it.t==='size'" class="vec"> |
|
||||||
<ui-num-input :value="fmtNum(it.v.w)" @confirm="onCompArrayVec(ci, p.key, ii, 'w', $event.target.value)"></ui-num-input> |
|
||||||
<ui-num-input :value="fmtNum(it.v.h)" @confirm="onCompArrayVec(ci, p.key, ii, 'h', $event.target.value)"></ui-num-input> |
|
||||||
</span> |
|
||||||
<span v-else-if="it.t==='rect'" class="vec"> |
|
||||||
<ui-num-input :value="fmtNum(it.v.x)" @confirm="onCompArrayVec(ci, p.key, ii, 'x', $event.target.value)"></ui-num-input> |
|
||||||
<ui-num-input :value="fmtNum(it.v.y)" @confirm="onCompArrayVec(ci, p.key, ii, 'y', $event.target.value)"></ui-num-input> |
|
||||||
<ui-num-input :value="fmtNum(it.v.w)" @confirm="onCompArrayVec(ci, p.key, ii, 'w', $event.target.value)"></ui-num-input> |
|
||||||
<ui-num-input :value="fmtNum(it.v.h)" @confirm="onCompArrayVec(ci, p.key, ii, 'h', $event.target.value)"></ui-num-input> |
|
||||||
</span> |
|
||||||
<ui-color v-else-if="it.t==='color'" class="color-editor" :value="colorArr(it.v)" @confirm="onCompArrayColor(ci, p.key, ii, $event.target.value)"></ui-color> |
|
||||||
<span v-else-if="it.t==='material'" class="ref-wrap mat"> |
|
||||||
<span class="asset-pick pick" :class="{ empty: !it.v.name }" @click="openAssetPicker(ci, p.key, ii, 'Material')" title="选择材质资产"> |
|
||||||
<ui-icon value="material"></ui-icon> |
|
||||||
<span class="ref-name">{{ it.v.name || it.v.type }}</span> |
|
||||||
<span class="asset-type">{{ it.v.type }}</span> |
|
||||||
</span> |
|
||||||
<button class="mini" @click="onCompArrayNull(ci, p.key, ii)">置空</button> |
|
||||||
<span v-if="it.v.props && it.v.props.length" class="mat-props"> |
|
||||||
<div v-for="mp in it.v.props" :key="mp.key" class="mat-prop"> |
|
||||||
<span class="k" :title="mp.key">{{ mp.key }}</span> |
|
||||||
<ui-num-input v-if="mp.t==='float'" :value="fmtNum(mp.v)" @confirm="onCompMaterialProp(ci, p.key, ii, mp.key, toNum($event.target.value))"></ui-num-input> |
|
||||||
<ui-color v-else-if="mp.t==='color'" class="color-editor" :value="colorArr(mp.v)" @confirm="onCompMaterialProp(ci, p.key, ii, mp.key, { r: $event.target.value[0], g: $event.target.value[1], b: $event.target.value[2], a: $event.target.value[3] })"></ui-color> |
|
||||||
<span v-else-if="mp.t==='vec4' || mp.t==='vec3' || mp.t==='vec2'" class="vec"> |
|
||||||
<ui-num-input v-for="ax in vecAxes(mp.t)" :key="ax" :value="fmtNum(mp.v[ax])" @confirm="onCompMaterialProp(ci, p.key, ii, mp.key, { ...mp.v, [ax]: toNum($event.target.value) })"></ui-num-input> |
|
||||||
</span> |
|
||||||
<span v-else-if="mp.t==='texture'" class="asset-pick tex"> |
|
||||||
<ui-icon :value="assetIcon(mp.v.type)"></ui-icon> |
|
||||||
<span class="ref-name">{{ mp.v.name || mp.v.type }}</span> |
|
||||||
<span class="asset-type">{{ mp.v.type }}</span> |
|
||||||
</span> |
|
||||||
</div> |
|
||||||
</span> |
|
||||||
</span> |
|
||||||
<span v-else-if="it.t==='asset'" class="ref-wrap"> |
|
||||||
<span class="asset-pick pick" :class="{ empty: !it.v.name }" @click="it.v.settable && openAssetPicker(ci, p.key, ii, it.v.type)" title="选择资产"> |
|
||||||
<ui-icon :value="assetIcon(it.v.type)"></ui-icon> |
|
||||||
<span class="ref-name">{{ it.v.name || it.v.type }}</span> |
|
||||||
<span class="asset-type">{{ it.v.type }}</span> |
|
||||||
</span> |
|
||||||
<button class="mini" @click="onCompArrayNull(ci, p.key, ii)">置空</button> |
|
||||||
</span> |
|
||||||
<span v-else-if="it.t==='ref'" class="ref-wrap"> |
|
||||||
<span class="ref-pick" :class="{ component: !isNodeRef(it.v) }"> |
|
||||||
<ui-icon :value="isNodeRef(it.v) ? 'node' : 'component'"></ui-icon> |
|
||||||
<span class="ref-name">{{ it.v.name || '?' }}</span> |
|
||||||
<span class="ref-type" v-if="!isNodeRef(it.v)">{{ it.v.componentType }}</span> |
|
||||||
</span> |
|
||||||
<button class="mini" @click="onCompArrayNull(ci, p.key, ii)">置空</button> |
|
||||||
</span> |
|
||||||
<span v-else class="v">{{ it.v }}</span> |
|
||||||
</div> |
|
||||||
<span v-if="p.len > p.v.length" class="array-more">… 共 {{ p.len }} 项</span> |
|
||||||
</span> |
|
||||||
<span v-else-if="p.t==='material'" class="ref-wrap mat"> |
|
||||||
<span class="asset-pick pick" :class="{ empty: !p.v.name }" @click="p.v.settable && openAssetPicker(ci, p.key, -1, 'Material')" title="选择材质资产"> |
|
||||||
<ui-icon value="material"></ui-icon> |
|
||||||
<span class="ref-name">{{ p.v.name || p.v.type }}</span> |
|
||||||
<span class="asset-type">{{ p.v.type }}</span> |
|
||||||
</span> |
|
||||||
<button class="mini" v-if="p.v.name && p.v.settable" @click="onCompNull(ci, p.key)">置空</button> |
|
||||||
<span v-if="p.v.props && p.v.props.length" class="mat-props"> |
|
||||||
<div v-for="mp in p.v.props" :key="mp.key" class="mat-prop"> |
|
||||||
<span class="k" :title="mp.key">{{ mp.key }}</span> |
|
||||||
<ui-num-input v-if="mp.t==='float'" :value="fmtNum(mp.v)" @confirm="onCompMaterialProp(ci, p.key, undefined, mp.key, toNum($event.target.value))"></ui-num-input> |
|
||||||
<ui-color v-else-if="mp.t==='color'" class="color-editor" :value="colorArr(mp.v)" @confirm="onCompMaterialProp(ci, p.key, undefined, mp.key, { r: $event.target.value[0], g: $event.target.value[1], b: $event.target.value[2], a: $event.target.value[3] })"></ui-color> |
|
||||||
<span v-else-if="mp.t==='vec4' || mp.t==='vec3' || mp.t==='vec2'" class="vec"> |
|
||||||
<ui-num-input v-for="ax in vecAxes(mp.t)" :key="ax" :value="fmtNum(mp.v[ax])" @confirm="onCompMaterialProp(ci, p.key, undefined, mp.key, { ...mp.v, [ax]: toNum($event.target.value) })"></ui-num-input> |
|
||||||
</span> |
|
||||||
<span v-else-if="mp.t==='texture'" class="asset-pick tex"> |
|
||||||
<ui-icon :value="assetIcon(mp.v.type)"></ui-icon> |
|
||||||
<span class="ref-name">{{ mp.v.name || mp.v.type }}</span> |
|
||||||
<span class="asset-type">{{ mp.v.type }}</span> |
|
||||||
</span> |
|
||||||
</div> |
|
||||||
</span> |
|
||||||
</span> |
|
||||||
<span v-else-if="p.t==='asset'" class="ref-wrap"> |
|
||||||
<span class="asset-pick pick" :class="{ empty: !p.v.name }" @click="p.v.settable && openAssetPicker(ci, p.key, -1, p.v.type)" title="选择资产"> |
|
||||||
<ui-icon :value="assetIcon(p.v.type)"></ui-icon> |
|
||||||
<span class="ref-name">{{ p.v.name || p.v.type }}</span> |
|
||||||
<span class="asset-type">{{ p.v.type }}</span> |
|
||||||
</span> |
|
||||||
<button class="mini" v-if="p.v.name && p.v.settable" @click="onCompNull(ci, p.key)">置空</button> |
|
||||||
</span> |
|
||||||
<span v-else-if="p.t==='ref'" class="ref-wrap"> |
|
||||||
<span class="ref-pick" :class="{ empty: !p.v.name, component: !isNodeRef(p.v) }" @click="openNodePicker('ref', ci, p.key, p.v.componentType || 'cc.Node')"> |
|
||||||
<ui-icon :value="isNodeRef(p.v) ? 'node' : 'component'"></ui-icon> |
|
||||||
<span class="ref-name">{{ p.v.name || '选择节点' }}</span> |
|
||||||
<span class="ref-type" v-if="!isNodeRef(p.v)">{{ p.v.componentType }}</span> |
|
||||||
</span> |
|
||||||
<button class="mini" v-if="p.v.name" @click="onCompNull(ci, p.key)">置空</button> |
|
||||||
</span> |
|
||||||
<span v-else class="v">{{ fmt(p.v) }}</span> |
|
||||||
</div> |
|
||||||
<div v-if="!comp.props.length" class="empty-hint">(无可编辑属性)</div> |
|
||||||
</template> |
|
||||||
</div> |
|
||||||
</template> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="bottom-row"> |
|
||||||
<div class="res-panel" v-if="showRes"> |
|
||||||
<div class="res-head"> |
|
||||||
<b>资源统计</b> |
|
||||||
<span class="res-total" v-if="resStats">已加载 <b>{{ totalAssets() }}</b> 项</span> |
|
||||||
<span class="spacer"></span> |
|
||||||
<button class="mini" @click="showRes = false">关闭</button> |
|
||||||
</div> |
|
||||||
<div class="res-body" v-if="resStats"> |
|
||||||
<div class="res-group"> |
|
||||||
<div class="res-group-title">运行时对象</div> |
|
||||||
<div class="res-grid"> |
|
||||||
<div class="res-item" v-for="it in runItems()" :key="it.label"> |
|
||||||
<span class="res-dot" :class="it.color">{{ it.ico }}</span> |
|
||||||
<span class="res-label">{{ it.label }}</span> |
|
||||||
<b class="res-value">{{ it.value }}</b> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
<div class="res-group"> |
|
||||||
<div class="res-group-title">已加载资源</div> |
|
||||||
<div class="res-grid"> |
|
||||||
<div class="res-item" v-for="it in resItems()" :key="it.label"> |
|
||||||
<span class="res-dot" :class="it.color">{{ it.ico }}</span> |
|
||||||
<span class="res-label">{{ it.label }}</span> |
|
||||||
<b class="res-value">{{ it.value }}</b> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
<div class="empty-hint" v-else>(加载中…)</div> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="logbar-wrap"> |
|
||||||
<div class="logbar-head"> |
|
||||||
<span class="log-title" @click="showLogs = !showLogs" title="展开/收起日志"> |
|
||||||
<svg class="log-fold" :class="{ open: showLogs }" viewBox="0 0 10 10" aria-hidden="true"><path d="M2.5 1.5 7.5 5 2.5 8.5z"/></svg> |
|
||||||
日志<span class="log-count">({{ logs.length }})</span> |
|
||||||
</span> |
|
||||||
<span class="spacer"></span> |
|
||||||
<span class="log-filters" v-if="showLogs"> |
|
||||||
<button class="mini" :class="{ active: logFilter === 'all' }" @click="logFilter = 'all'">全部</button> |
|
||||||
<button class="mini" :class="{ active: logFilter === 'log' }" @click="logFilter = 'log'">log</button> |
|
||||||
<button class="mini" :class="{ active: logFilter === 'warn' }" @click="logFilter = 'warn'">warn</button> |
|
||||||
<button class="mini" :class="{ active: logFilter === 'error' }" @click="logFilter = 'error'">error</button> |
|
||||||
</span> |
|
||||||
<span class="copied-hint" v-if="copyHint">已复制</span> |
|
||||||
<button v-if="showLogs" class="mini" @click="clearLogs">清空</button> |
|
||||||
<button v-if="showLogs" class="mini" @click="copyLogs">复制全部</button> |
|
||||||
<button class="mini" @click="showLogs = !showLogs">{{ showLogs ? '收起' : '展开' }}</button> |
|
||||||
</div> |
|
||||||
<div class="logbar" v-if="showLogs" ref="logList" @scroll="onLogScroll"> |
|
||||||
<div v-for="(l, i) in filteredLogs" :key="i" class="log-item" :class="logClass(l)"> |
|
||||||
<span class="log-badge" :class="'b-' + logLevel(l)">{{ logLevelText(l) }}</span> |
|
||||||
<span class="log-time">{{ logTime(l) }}</span> |
|
||||||
<span class="log-text">{{ logText(l) }}</span> |
|
||||||
<button class="mini copy-line" title="复制此行" @click="copyLogLine(l)">⧉</button> |
|
||||||
</div> |
|
||||||
<div v-if="!filteredLogs.length" class="empty-hint">(暂无日志)</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
|
|
||||||
<!-- 节点选择弹层(Widget 对齐目标 / 组件引用) --> |
|
||||||
<div class="node-picker-mask" v-if="nodePicker.visible" @click.self="closeNodePicker"> |
|
||||||
<div class="node-picker"> |
|
||||||
<div class="np-head"> |
|
||||||
<span class="np-title"> |
|
||||||
{{ nodePicker.mode === 'widget' ? '选择对齐目标节点' : (nodePicker.componentType === 'cc.Node' ? '选择引用节点' : '选择 ' + nodePicker.componentType + ' 组件所在节点') }} |
|
||||||
</span> |
|
||||||
<ui-input class="np-filter" :value="nodePicker.filter" @change="nodePicker.filter = $event.target.value" placeholder="搜索节点…" show-clear></ui-input> |
|
||||||
<button class="mini" @click="closeNodePicker">关闭</button> |
|
||||||
</div> |
|
||||||
<div class="np-list"> |
|
||||||
<div v-for="r in pickerRows" :key="r.id" class="np-row" :class="{ 'has-children': r.hasChildren }" :style="{ paddingLeft: (14 + r.depth * 14) + 'px' }" @click="onNodePick(r)"> |
|
||||||
<ui-icon :value="r.hasChildren ? 'folder' : 'node'"></ui-icon> |
|
||||||
<span class="np-name">{{ r.name }}</span> |
|
||||||
<span class="np-type" v-if="r.type">{{ r.type }}</span> |
|
||||||
</div> |
|
||||||
<div v-if="!pickerRows.length" class="empty-hint">(无匹配节点)</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
|
|
||||||
<!-- 资产选择弹层(spriteFrame / material 等资产引用) --> |
|
||||||
<div class="node-picker-mask" v-if="assetPicker.visible" @click.self="closeAssetPicker"> |
|
||||||
<div class="node-picker"> |
|
||||||
<div class="np-head"> |
|
||||||
<span class="np-title">选择资产{{ assetPicker.type ? '(' + assetPicker.type + ')' : '' }}</span> |
|
||||||
<select class="ap-type" :value="assetPicker.type" @change="assetPicker.type = $event.target.value"> |
|
||||||
<option value="">全部类型</option> |
|
||||||
<option v-for="t in assetPicker.types" :key="t" :value="t">{{ t }}</option> |
|
||||||
</select> |
|
||||||
<ui-input class="np-filter" :value="assetPicker.filter" @change="assetPicker.filter = $event.target.value" placeholder="搜索资产…" show-clear></ui-input> |
|
||||||
<button class="mini" @click="closeAssetPicker">关闭</button> |
|
||||||
</div> |
|
||||||
<div class="np-list"> |
|
||||||
<div v-if="assetPicker.loading" class="empty-hint">(加载中…)</div> |
|
||||||
<div v-else v-for="r in assetRows" :key="r.uuid || r.name" class="np-row" @click="onAssetPick(r)"> |
|
||||||
<ui-icon :value="assetIcon(r.type)"></ui-icon> |
|
||||||
<span class="np-name">{{ r.name || r.uuid }}</span> |
|
||||||
<span class="np-type">{{ r.type }}</span> |
|
||||||
</div> |
|
||||||
<div v-if="!assetPicker.loading && !assetRows.length" class="empty-hint">(无匹配资产)</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
@ -1,3 +0,0 @@ |
|||||||
<div style="height: 100%; overflow: hidden;"> |
|
||||||
<div id="app" style="height: 100%;"></div> |
|
||||||
</div> |
|
||||||
@ -1,38 +0,0 @@ |
|||||||
<div class="preview-panel-root"> |
|
||||||
<div class="preview-toolbar"> |
|
||||||
<span class="p-online" :class="{ ok: !!previewLoaded }"> |
|
||||||
<i class="dot"></i>{{ previewLoaded ? '在线' : (previewOnline === false ? '未启动' : '') }} |
|
||||||
</span> |
|
||||||
<span class="p-sep"></span> |
|
||||||
<span class="p-group" title="设备 / 分辨率"> |
|
||||||
<ui-select class="p-device" :value="previewDevice" @confirm="onPreviewDevice($event.target.value)"> |
|
||||||
<option value="Default">设计分辨率</option> |
|
||||||
<option value="FullScreen">全屏</option> |
|
||||||
<option value="WebpageFullScreen">视口</option> |
|
||||||
<option value="Xiaomi Redmi 8">Redmi8 (360x760)</option> |
|
||||||
</ui-select> |
|
||||||
</span> |
|
||||||
<button class="pbtn" :class="{ on: previewRotated }" @click="onPreviewRotate" title="横竖屏切换"><ui-icon value="rotate"></ui-icon></button> |
|
||||||
<span class="p-group p-fps" title="帧率上限"> |
|
||||||
<ui-icon value="setting"></ui-icon> |
|
||||||
<ui-num-input class="p-fps-in" :value="String(previewFps)" @confirm="onPreviewFps($event.target.value)"></ui-num-input> |
|
||||||
</span> |
|
||||||
<span class="p-sep"></span> |
|
||||||
<button class="pbtn" :class="{ on: previewPaused }" @click="onPreviewPause" :title="previewPaused ? '继续' : '暂停'"><ui-icon :value="previewPaused ? 'play' : 'pause'"></ui-icon></button> |
|
||||||
<button class="pbtn" :class="{ on: previewMuted }" @click="onPreviewMute" title="静音 / 取消静音"><ui-icon value="music"></ui-icon></button> |
|
||||||
<span class="spacer"></span> |
|
||||||
<button class="pbtn major" @click="onLoadPreview" title="重新加载预览"><ui-icon value="refresh"></ui-icon></button> |
|
||||||
</div> |
|
||||||
<div class="preview-frame-wrap"> |
|
||||||
<div v-show="showPreview" ref="gameView" class="game-view"></div> |
|
||||||
<div class="p-zoom" v-show="showPreview && previewZoom !== 1">{{ Math.round(previewZoom * 100) }}%</div> |
|
||||||
<div class="preview-err" v-if="showPreview && previewRenderErr" @click="onLoadPreview" title="点击重新加载">{{ previewRenderErr }}</div> |
|
||||||
<div class="preview-cover" v-if="showPreview && previewOnline === false"> |
|
||||||
<div class="cover-box"> |
|
||||||
<b>预览服务未启动</b> |
|
||||||
<div>请在 Cocos Creator 中点击「浏览器预览」启动预览服务。</div> |
|
||||||
<button @click="onLoadPreview">重试</button> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
@ -1 +0,0 @@ |
|||||||
<div style="height: 100%; overflow: hidden;"><div id="app" style="height: 100%;"></div></div> |
|
||||||
@ -1,5 +0,0 @@ |
|||||||
<div class="counter"> |
|
||||||
<h2>{{counter}}</h2> |
|
||||||
<ui-button class="blue" @click="addition">+</ui-button> |
|
||||||
<ui-button @click="subtraction">-</ui-button> |
|
||||||
</div> |
|
||||||
@ -1,22 +0,0 @@ |
|||||||
{ |
|
||||||
"$schema": "https://schemastore.azurewebsites.net/schemas/json/tsconfig.json", |
|
||||||
"compilerOptions": { |
|
||||||
"target": "ES2017", |
|
||||||
"module": "CommonJS", |
|
||||||
"moduleResolution": "node", |
|
||||||
"inlineSourceMap": true, |
|
||||||
"inlineSources": true, |
|
||||||
"esModuleInterop": true, |
|
||||||
"skipLibCheck": true, |
|
||||||
"strict": false, |
|
||||||
"experimentalDecorators": true, |
|
||||||
"forceConsistentCasingInFileNames": true, |
|
||||||
"resolveJsonModule": true, |
|
||||||
"outDir": "./dist", |
|
||||||
"rootDir": "./source", |
|
||||||
"types": [ |
|
||||||
"node", |
|
||||||
"@cocos/creator-types/editor" |
|
||||||
] |
|
||||||
} |
|
||||||
} |
|
||||||
File diff suppressed because one or more lines are too long
@ -1,36 +0,0 @@ |
|||||||
{ |
|
||||||
"$schema": "./@types/schema/package/index.json", |
|
||||||
"package_version": 2, |
|
||||||
"name": "map-layer-generator", |
|
||||||
"version": "1.0.0", |
|
||||||
"author": "流云", |
|
||||||
"editor": ">=3.8.5", |
|
||||||
"scripts": { |
|
||||||
"preinstall": "node ./scripts/preinstall.js", |
|
||||||
"build": "tsc", |
|
||||||
"watch": "tsc -w" |
|
||||||
}, |
|
||||||
"description": "根据 Excel 自动生成地图 mapLayer 预制体", |
|
||||||
"main": "./dist/main.js", |
|
||||||
"devDependencies": { |
|
||||||
"@cocos/creator-types": "^3.8.5", |
|
||||||
"@types/node": "^18.17.1", |
|
||||||
"typescript": "^4.3.4" |
|
||||||
}, |
|
||||||
"contributions": { |
|
||||||
"menu": [ |
|
||||||
{ |
|
||||||
"path": "扩展", |
|
||||||
"label": "生成地图预制体", |
|
||||||
"message": "generateMapLayers" |
|
||||||
} |
|
||||||
], |
|
||||||
"messages": { |
|
||||||
"generateMapLayers": { |
|
||||||
"methods": [ |
|
||||||
"generateMapLayers" |
|
||||||
] |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,274 +0,0 @@ |
|||||||
import path from 'path'; |
|
||||||
|
|
||||||
const Fs = require('fs'); |
|
||||||
|
|
||||||
// ── 属性映射:标记 → MapGrid 组件全部 8 个属性的完整值 ──
|
|
||||||
const MARKER_PROPS: Record<string, Record<string, boolean>> = { |
|
||||||
'': { _isCanWalk: true, _isCanPutGrid: true, _isBestPlace: false, _isCanBeDig: false, _isCanPutText: false, _isCanRemove: false, _isCanBeThunder: false, _isFreePlace: false }, |
|
||||||
'CanWalk': { _isCanWalk: true, _isCanPutGrid: true, _isBestPlace: true, _isCanBeDig: false, _isCanPutText: false, _isCanRemove: false, _isCanBeThunder: false, _isFreePlace: false }, |
|
||||||
'CanBeDig': { _isCanWalk: false, _isCanPutGrid: false, _isBestPlace: false, _isCanBeDig: true, _isCanPutText: false, _isCanRemove: false, _isCanBeThunder: false, _isFreePlace: false }, |
|
||||||
'CanPut': { _isCanWalk: false, _isCanPutGrid: false, _isBestPlace: false, _isCanBeDig: false, _isCanPutText: true, _isCanRemove: false, _isCanBeThunder: false, _isFreePlace: false }, |
|
||||||
'spawnPoint_1': { _isCanWalk: true, _isCanPutGrid: false, _isBestPlace: false, _isCanBeDig: false, _isCanPutText: false, _isCanRemove: false, _isCanBeThunder: false, _isFreePlace: false }, |
|
||||||
'spawnPoint_2': { _isCanWalk: true, _isCanPutGrid: false, _isBestPlace: false, _isCanBeDig: false, _isCanPutText: false, _isCanRemove: false, _isCanBeThunder: false, _isFreePlace: false }, |
|
||||||
'spawnPoint_3': { _isCanWalk: true, _isCanPutGrid: false, _isBestPlace: false, _isCanBeDig: false, _isCanPutText: false, _isCanRemove: false, _isCanBeThunder: false, _isFreePlace: false }, |
|
||||||
'wallTarget1': { _isCanWalk: true, _isCanPutGrid: false, _isBestPlace: false, _isCanBeDig: false, _isCanPutText: false, _isCanRemove: false, _isCanBeThunder: false, _isFreePlace: false }, |
|
||||||
'wallTarget2': { _isCanWalk: true, _isCanPutGrid: false, _isBestPlace: false, _isCanBeDig: false, _isCanPutText: false, _isCanRemove: false, _isCanBeThunder: false, _isFreePlace: false }, |
|
||||||
'isFreePlace': { _isCanWalk: true, _isCanPutGrid: true, _isBestPlace: false, _isCanBeDig: false, _isCanPutText: false, _isCanRemove: false, _isCanBeThunder: false, _isFreePlace: true }, |
|
||||||
}; |
|
||||||
|
|
||||||
const SPAWN_MARKERS = ['spawnPoint_1', 'spawnPoint_2', 'spawnPoint_3']; |
|
||||||
const MAPGRID_FILE_ID = 'a5Czjqnj1DIa/1qXv6uwzF'; |
|
||||||
|
|
||||||
const CELL_SIZE = 93; |
|
||||||
const ORIGIN_X = -325.5; |
|
||||||
const ORIGIN_Y = 418.5; |
|
||||||
const COLS = 8; |
|
||||||
const ROWS = 10; |
|
||||||
|
|
||||||
function cellToWorld(index: number): { x: number; y: number } { |
|
||||||
const col = index % COLS; |
|
||||||
const row = Math.floor(index / COLS); |
|
||||||
return { x: ORIGIN_X + col * CELL_SIZE, y: ORIGIN_Y - row * CELL_SIZE }; |
|
||||||
} |
|
||||||
|
|
||||||
/** 在 JSON 数组中按 _name 查找节点,返回索引 */ |
|
||||||
function findNodeIndex(data: any[], name: string): number { |
|
||||||
for (let i = 0; i < data.length; i++) { |
|
||||||
if (data[i] && data[i]._name === name) return i; |
|
||||||
} |
|
||||||
return -1; |
|
||||||
} |
|
||||||
|
|
||||||
/** 获取格子的 PrefabInstance 对象 */ |
|
||||||
function getGridInstance(data: any[], geziIdx: number, gridIndex: number): any | null { |
|
||||||
const gezi = data[geziIdx]; |
|
||||||
if (!gezi || !gezi._children || gridIndex >= gezi._children.length) return null; |
|
||||||
const gridRef = gezi._children[gridIndex]; |
|
||||||
const gridNode = data[gridRef.__id__]; |
|
||||||
if (!gridNode || !gridNode._prefab) return null; |
|
||||||
const prefabInfo = data[gridNode._prefab.__id__]; |
|
||||||
if (!prefabInfo || !prefabInfo.instance) return null; |
|
||||||
return data[prefabInfo.instance.__id__]; |
|
||||||
} |
|
||||||
|
|
||||||
/** 清除指定格子中所有 MapGrid 组件的属性覆盖 */ |
|
||||||
function clearGridOverrides(data: any[], geziIdx: number, gridIndex: number): void { |
|
||||||
const instance = getGridInstance(data, geziIdx, gridIndex); |
|
||||||
if (!instance || !instance.propertyOverrides) return; |
|
||||||
instance.propertyOverrides = instance.propertyOverrides.filter((ref: any) => { |
|
||||||
const po = data[ref.__id__]; |
|
||||||
if (!po || !po.targetInfo) return true; // 保留非 targetInfo 的条目
|
|
||||||
const ti = data[po.targetInfo.__id__]; |
|
||||||
if (!ti || !ti.localID) return true; |
|
||||||
// 删除 targetInfo.localID 匹配 MapGrid 组件的覆盖
|
|
||||||
return ti.localID[0] !== MAPGRID_FILE_ID; |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
/** 向指定格子的 PrefabInstance 追加一条 MapGrid 属性覆盖 */ |
|
||||||
function addGridOverride(data: any[], geziIdx: number, gridIndex: number, propName: string, value: any): void { |
|
||||||
const instance = getGridInstance(data, geziIdx, gridIndex); |
|
||||||
if (!instance || !instance.propertyOverrides) return; |
|
||||||
|
|
||||||
const targetInfoId = data.length; |
|
||||||
data.push({ __type__: 'cc.TargetInfo', localID: [MAPGRID_FILE_ID] }); |
|
||||||
|
|
||||||
const overrideId = data.length; |
|
||||||
data.push({ |
|
||||||
__type__: 'CCPropertyOverrideInfo', |
|
||||||
targetInfo: { __id__: targetInfoId }, |
|
||||||
propertyPath: [propName], |
|
||||||
value: value, |
|
||||||
}); |
|
||||||
instance.propertyOverrides.push({ __id__: overrideId }); |
|
||||||
} |
|
||||||
|
|
||||||
/** 移动/设置指定父节点下某个子节点的坐标 */ |
|
||||||
function setChildPos(data: any[], parentName: string, childName: string, x: number, y: number): boolean { |
|
||||||
const parentIdx = findNodeIndex(data, parentName); |
|
||||||
if (parentIdx < 0) return false; |
|
||||||
const parent = data[parentIdx]; |
|
||||||
if (!parent._children) return false; |
|
||||||
for (const ref of parent._children) { |
|
||||||
const child = data[ref.__id__]; |
|
||||||
if (child && child._name === childName) { |
|
||||||
child._lpos = { __type__: 'cc.Vec3', x, y, z: 0 }; |
|
||||||
return true; |
|
||||||
} |
|
||||||
} |
|
||||||
return false; |
|
||||||
} |
|
||||||
|
|
||||||
/** 从父节点中删除指定名称的子节点 */ |
|
||||||
function removeChildByName(data: any[], parentName: string, childName: string): void { |
|
||||||
const parentIdx = findNodeIndex(data, parentName); |
|
||||||
if (parentIdx < 0) return; |
|
||||||
const parent = data[parentIdx]; |
|
||||||
if (!parent._children) return; |
|
||||||
parent._children = parent._children.filter((ref: any) => { |
|
||||||
const child = data[ref.__id__]; |
|
||||||
return !(child && child._name === childName); |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
/** 向父节点添加一个浅拷贝自模板的子节点(用于补充缺失的 spawnPoint) */ |
|
||||||
function copyChildFromTemplate(data: any[], template: any[], parentName: string, childName: string): void { |
|
||||||
const parentIdx = findNodeIndex(data, parentName); |
|
||||||
if (parentIdx < 0) return; |
|
||||||
// 从模板中找到同名子节点引用
|
|
||||||
const tplParentIdx = findNodeIndex(template, parentName); |
|
||||||
if (tplParentIdx < 0) return; |
|
||||||
const tplParent = template[tplParentIdx]; |
|
||||||
for (const ref of tplParent._children || []) { |
|
||||||
const child = template[ref.__id__]; |
|
||||||
if (child && child._name === childName) { |
|
||||||
// 将子节点及其组件深克隆追加到 data 末尾,并在父节点 _children 中引用
|
|
||||||
const cloneStartIdx = data.length; |
|
||||||
const cloned = JSON.parse(JSON.stringify(child)); |
|
||||||
data.push(cloned); |
|
||||||
data[parentIdx]._children.push({ __id__: cloneStartIdx }); |
|
||||||
return; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 应用格子数据到预制体 JSON(新增和更新共用) |
|
||||||
*/ |
|
||||||
function applyGridData(data: any[], template: any[], grids: string[]): void { |
|
||||||
const geziIdx = findNodeIndex(data, '格子'); |
|
||||||
if (geziIdx < 0) return; |
|
||||||
|
|
||||||
// 1. 收集 spawn 位置映射 + 遍历格子清除旧覆盖 / 添加新覆盖
|
|
||||||
const spawnPositions = new Map<string, { x: number; y: number }>(); |
|
||||||
for (let i = 0; i < 80; i++) { |
|
||||||
clearGridOverrides(data, geziIdx, i); |
|
||||||
|
|
||||||
const marker = grids[i] || ''; |
|
||||||
const props = MARKER_PROPS[marker]; |
|
||||||
if (props) { |
|
||||||
for (const [prop, value] of Object.entries(props)) { |
|
||||||
addGridOverride(data, geziIdx, i, prop, value); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if (SPAWN_MARKERS.includes(marker)) { |
|
||||||
spawnPositions.set(marker, cellToWorld(i)); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// 2. wallTarget1/wallTarget2 配对
|
|
||||||
const t1 = grids.indexOf('wallTarget1'); |
|
||||||
const t2 = grids.indexOf('wallTarget2'); |
|
||||||
if (t1 >= 0 && t2 >= 0) { |
|
||||||
const p1 = cellToWorld(t1), p2 = cellToWorld(t2); |
|
||||||
const r1 = Math.floor(t1 / COLS), c1 = t1 % COLS; |
|
||||||
const r2 = Math.floor(t2 / COLS), c2 = t2 % COLS; |
|
||||||
let mx: number, my: number; |
|
||||||
if (r1 === r2) { mx = (p1.x + p2.x) / 2; my = p1.y; } // 横连
|
|
||||||
else if (c1 === c2) { mx = p1.x; my = (p1.y + p2.y) / 2; } // 竖连
|
|
||||||
else { mx = (p1.x + p2.x) / 2; my = (p1.y + p2.y) / 2; } // 斜角
|
|
||||||
setChildPos(data, '目标点', 'wallTarget1', mx, my); |
|
||||||
} else if (t1 >= 0) { |
|
||||||
const pos = cellToWorld(t1); |
|
||||||
setChildPos(data, '目标点', 'wallTarget1', pos.x, pos.y); |
|
||||||
} |
|
||||||
|
|
||||||
// 3. 同步 spawn 节点:删除多余 → 补回缺失 → 设置坐标
|
|
||||||
const allSpawns = ['spawnPoint_1', 'spawnPoint_2', 'spawnPoint_3', 'spawnPoint_4', 'spawnPoint_5', 'spawnPoint_6', 'spawnPoint_7']; |
|
||||||
for (const spName of allSpawns) { |
|
||||||
if (spawnPositions.has(spName)) continue; |
|
||||||
removeChildByName(data, '出怪口', spName); |
|
||||||
} |
|
||||||
for (const [spName, pos] of spawnPositions) { |
|
||||||
const parentIdx = findNodeIndex(data, '出怪口'); |
|
||||||
const exists = parentIdx >= 0 && (data[parentIdx]._children || []).some((ref: any) => { |
|
||||||
const child = data[ref.__id__]; |
|
||||||
return child && child._name === spName; |
|
||||||
}); |
|
||||||
if (!exists) { |
|
||||||
copyChildFromTemplate(data, template, '出怪口', spName); |
|
||||||
} |
|
||||||
setChildPos(data, '出怪口', spName, pos.x, pos.y); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// ── 扩展入口 ──
|
|
||||||
export const methods: { [key: string]: (...args: any[]) => any } = { |
|
||||||
async generateMapLayers() { |
|
||||||
try { |
|
||||||
const projectPath = Editor.Project.path; |
|
||||||
|
|
||||||
// 1. 读取 Excel
|
|
||||||
const excelPath = path.join(projectPath, 'assets/res/地图/地图.xls'); |
|
||||||
if (!Fs.existsSync(excelPath)) { |
|
||||||
console.error('[地图生成] Excel 文件不存在:', excelPath); |
|
||||||
return; |
|
||||||
} |
|
||||||
const xlsxPath = path.join(__dirname, '../../excel-to-json-ts/lib/xlsx.js'); |
|
||||||
const XLSX = require(xlsxPath); |
|
||||||
const buffer = Fs.readFileSync(excelPath); |
|
||||||
const workbook = XLSX.read(buffer, { type: 'buffer' }); |
|
||||||
|
|
||||||
// 2. 读取模板
|
|
||||||
const templatePath = path.join(projectPath, 'assets/bundles/battleMap/prefab/地图/mapLayerTemplate.prefab'); |
|
||||||
if (!Fs.existsSync(templatePath)) { |
|
||||||
console.error('[地图生成] 模板预制体不存在:', templatePath); |
|
||||||
return; |
|
||||||
} |
|
||||||
const templateJson = Fs.readFileSync(templatePath, 'utf-8'); |
|
||||||
const templateData = JSON.parse(templateJson); |
|
||||||
|
|
||||||
const prefabDir = path.join(projectPath, 'assets/bundles/battleMap/prefab/地图'); |
|
||||||
|
|
||||||
console.log(`[地图生成] 开始处理 ${workbook.SheetNames.length} 个工作表...`); |
|
||||||
|
|
||||||
// 3. 遍历每个工作表
|
|
||||||
for (const sheetName of workbook.SheetNames) { |
|
||||||
const worksheet = workbook.Sheets[sheetName]; |
|
||||||
const rows: any[][] = XLSX.utils.sheet_to_json(worksheet, { header: 1 }); |
|
||||||
|
|
||||||
const grids: string[] = []; |
|
||||||
for (let r = 0; r < ROWS; r++) { |
|
||||||
for (let c = 0; c < COLS; c++) { |
|
||||||
const raw = (rows[r] || [])[c]; |
|
||||||
grids.push((raw !== undefined && raw !== null && raw !== '') ? String(raw).trim() : ''); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
const prefabPath = path.join(prefabDir, `${sheetName}.prefab`); |
|
||||||
const exists = Fs.existsSync(prefabPath); |
|
||||||
|
|
||||||
let prefabData: any[]; |
|
||||||
if (exists) { |
|
||||||
// 已有预制体 → 读取后原地修改
|
|
||||||
const existingJson = Fs.readFileSync(prefabPath, 'utf-8'); |
|
||||||
prefabData = JSON.parse(existingJson); |
|
||||||
applyGridData(prefabData, templateData, grids); |
|
||||||
} else { |
|
||||||
// 新预制体 → 从模板克隆生成
|
|
||||||
prefabData = JSON.parse(JSON.stringify(templateData)); |
|
||||||
prefabData[0]._name = sheetName; |
|
||||||
prefabData[1]._name = sheetName; |
|
||||||
applyGridData(prefabData, templateData, grids); |
|
||||||
} |
|
||||||
|
|
||||||
const jsonStr = JSON.stringify(prefabData, null, 2); |
|
||||||
const url = `db://assets/bundles/battleMap/prefab/地图/${sheetName}.prefab`; |
|
||||||
await Editor.Message.request('asset-db', 'create-asset', url, jsonStr, { overwrite: true }); |
|
||||||
|
|
||||||
const stats: Record<string, number> = {}; |
|
||||||
for (const m of grids) { const k = m || '(空白)'; stats[k] = (stats[k] || 0) + 1; } |
|
||||||
const statStr = Object.entries(stats).sort((a, b) => b[1] - a[1]).map(([k, v]) => `${k}:${v}`).join(', '); |
|
||||||
console.log(`[地图生成] ${exists ? '~' : '+'} ${sheetName}.prefab (${statStr})`); |
|
||||||
} |
|
||||||
|
|
||||||
console.log('[地图生成] 全部完成!'); |
|
||||||
} catch (err) { |
|
||||||
console.error('[地图生成] 错误:', err); |
|
||||||
} |
|
||||||
}, |
|
||||||
}; |
|
||||||
|
|
||||||
export function load() { } |
|
||||||
export function unload() { } |
|
||||||
@ -1,11 +0,0 @@ |
|||||||
{ |
|
||||||
"extends": "./base.tsconfig.json", |
|
||||||
"compilerOptions": { |
|
||||||
"outDir": "./dist", |
|
||||||
"rootDir": "./source", |
|
||||||
"types": [ |
|
||||||
"node", |
|
||||||
"@cocos/creator-types/editor" |
|
||||||
] |
|
||||||
} |
|
||||||
} |
|
||||||
Loading…
Reference in new issue