2 Commits

Author SHA1 Message Date
panFD
67c2a53211 fix(map): 统一英雄相关提示的弹窗逻辑与文案
1. 调整出售英雄时的提示方式,复用smalltip气泡弹窗而非独立toast
2. 优化MissionCardComp的smalltip提示方法,支持自定义文案和多负载格式
3. 补充英雄满员提示的自定义文案,与出售限制提示保持统一提示样式
2026-08-02 10:17:10 +08:00
panFD
401a5daf77 refactor(common prompt,hero box): 优化弹窗绑定逻辑与英雄出售限制
1. 重构CommonPrompt的编辑器属性绑定,添加按钮节点序列化引用并优化兜底查找逻辑
2. 为英雄出售操作添加存活数量校验,场上仅1个英雄时禁止出售
3. 新增统一的存活英雄统计工具方法
4. 补充Claude工具命令配置
2026-08-02 09:42:30 +08:00
8 changed files with 9367 additions and 2942 deletions

View File

@@ -16,7 +16,9 @@
"Bash(git add *)",
"Bash(git commit *)",
"Bash(npm install *)",
"Bash(npx tsc *)"
"Bash(npx tsc *)",
"Bash(d:/game/pixelheros/extensions/pixelhero-config-editor/node_modules/.bin/tsx --version)",
"Bash(d:/game/pixelheros/extensions/pixelhero-config-editor/node_modules/.bin/tsx --tsconfig scripts/tsconfig.eval.json scripts/eval-heroes.ts --lv=1 --base=5011)"
],
"deny": [
"Bash(rm -rf *)",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -59,40 +59,47 @@ export interface WindowPromptParams {
@ccclass("CommonPrompt")
export class CommonPrompt extends Component {
/** 窗口标题 */
@property(Label)
@property({ type: Label, displayName: "标题文本" })
private lab_title: Label | null = null;
/** 窗口内容(富文本,需在 prefab 中将 Label 换为 RichText 后重新拖入) */
@property(RichText)
@property({ type: RichText, displayName: "内容富文本" })
private lab_content: RichText | null = null;
/** 确定按钮文本 */
@property(Label)
@property({ type: Label, displayName: "确定按钮文本" })
private lab_ok: Label | null = null;
/** 取消按钮文本 */
@property(Label)
@property({ type: Label, displayName: "取消按钮文本" })
private lab_cancel: Label | null = null;
/** 确定按钮节点 */
@property({ type: Node, displayName: "确定按钮" })
private btn_ok: Node | null = null;
/** 取消按钮节点 */
@property({ type: Node, displayName: "取消按钮" })
private btn_cancel: Node | null = null;
/** 运行时参数 */
private config: WindowPromptParams = {};
/** 弹窗打开动画已播放标记(防止 onAdded 重复触发) */
private animPlayed: boolean = false;
/** 代码动态绑定的确定按钮prefab clickEvents 失效时的兜底) */
private btnOk: Node | null = null;
/** 代码动态绑定的取消按钮 */
private btnCancel: Node | null = null;
onLoad() {
// 兜底prefab 中 clickEvents 引用的旧插件脚本被移除后,代码动态绑定保证按钮可用
this.btnOk = this.node.getChildByName("Node")?.getChildByName("btn_ok") ?? null;
this.btnCancel = this.node.getChildByName("Node")?.getChildByName("btn_cancel") ?? null;
this.btnOk?.on(Button.EventType.CLICK, this.onOk, this);
this.btnCancel?.on(Button.EventType.CLICK, this.onCancel, this);
// 优先使用编辑器绑定的按钮节点;未绑定时按节点名查找兜底
if (!this.btn_ok) {
this.btn_ok = this.node.getChildByName("Node")?.getChildByName("btn_ok") ?? null;
}
if (!this.btn_cancel) {
this.btn_cancel = this.node.getChildByName("Node")?.getChildByName("btn_cancel") ?? null;
}
this.btn_ok?.on(Button.EventType.CLICK, this.onOk, this);
this.btn_cancel?.on(Button.EventType.CLICK, this.onCancel, this);
// 遮罩点击关闭btn_sky 为 btn_ok 内嵌子节点,无 Button 组件,直接监听触摸事件)
this.btnOk?.getChildByName("btn_sky")?.on(NodeEventType.TOUCH_END, this.onMaskClick, this);
this.btn_ok?.getChildByName("btn_sky")?.on(NodeEventType.TOUCH_END, this.onMaskClick, this);
// 序列化属性槽引用的是旧插件 LanguageLabel 组件实例,脚本替换后已失效;
// 这里按节点名查找 Label/RichText 作为兜底,保证文字一定写入
@@ -100,10 +107,10 @@ export class CommonPrompt extends Component {
this.lab_title = this.node.getChildByName("lab_title")?.getComponent(Label) ?? null;
}
if (!this.lab_ok) {
this.lab_ok = this.btnOk?.getChildByName("lab_ok")?.getComponent(Label) ?? null;
this.lab_ok = this.btn_ok?.getChildByName("lab_ok")?.getComponent(Label) ?? null;
}
if (!this.lab_cancel) {
this.lab_cancel = this.btnCancel?.getChildByName("lab_cancel")?.getComponent(Label) ?? null;
this.lab_cancel = this.btn_cancel?.getChildByName("lab_cancel")?.getComponent(Label) ?? null;
}
if (!this.lab_content) {
this.lab_content = this.node.getChildByName("lab_content")?.getComponent(RichText) ?? null;
@@ -127,8 +134,8 @@ export class CommonPrompt extends Component {
}
onDestroy() {
this.btnOk?.off(Button.EventType.CLICK, this.onOk, this);
this.btnCancel?.off(Button.EventType.CLICK, this.onCancel, this);
this.btn_ok?.off(Button.EventType.CLICK, this.onOk, this);
this.btn_cancel?.off(Button.EventType.CLICK, this.onCancel, this);
Tween.stopAllByTarget(this.node);
this.config = {};
}

View File

@@ -20,7 +20,7 @@ import { GameEvent } from "../common/config/GameEvent";
import { HeroInfo } from "../common/config/heroSet";
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
import { Hero } from "../hero/Hero";
import { getLvColor } from "../common/config/GameSet";
import { FacSet, getLvColor } from "../common/config/GameSet";
import { buildSkillDescRich } from "../common/config/HeroSkillDesc";
import { MissionEconomy } from "./MissionEconomy";
import { UIID } from "../common/config/GameUIConfig";
@@ -189,6 +189,11 @@ export class HeroBoxComp extends CCComp {
*/
private onSellHeroClick() {
if (!this.eid || !this.model) return;
// 场上仅剩 1 个英雄时不允许出售:复用英雄栏 smalltip 气泡提示(与满员同一挂载点)
if (HeroBoxComp.getAliveHeroCount() <= 1) {
oops.message.dispatchEvent(GameEvent.ShowSmallTip, { type: "hero_full", text: "至少保留 1 个英雄" });
return;
}
oops.audio.playEffect("music/button");
const heroName = this.model.hero_name ?? "";
@@ -453,6 +458,20 @@ export class HeroBoxComp extends CCComp {
// ======================== 内部工具 ========================
/**
* 统计场上存活英雄数量(与 MissionCardComp.getAliveHeroCount 口径一致)。
*/
private static getAliveHeroCount(): number {
let count = 0;
ecs.query(ecs.allOf(HeroAttrsComp)).forEach((entity: ecs.Entity) => {
const model = entity.get(HeroAttrsComp);
if (model && model.fac === FacSet.HERO) {
count++;
}
});
return count;
}
/**
* 写入一行战斗信息(总值 + 附加值)。
* 总值始终显示0 不隐藏);附加值为 0 时隐藏,正/负分别显示 (+x) / (x)。

View File

@@ -455,11 +455,17 @@ export class MissionCardComp extends CCComp {
}
private onShowSmallTip(event: string, args: any) {
const type = args as string;
this.showSmallTip(type as any);
// 支持字符串(仅类型)或 { type, text }(类型 + 自定义文案)两种负载
const payload = typeof args === "string" ? { type: args } : (args ?? {});
this.showSmallTip(payload.type, payload.text);
}
public showSmallTip(type: "refresh_coin" | "buy_coin" | "hero_full") {
/**
* 在目标节点上弹出 smalltip 气泡(缩放动画)。
* @param type 提示类型(决定挂载到哪个节点)
* @param text 可选:覆盖气泡文字(默认读 prefab 中配置的文本)
*/
public showSmallTip(type: "refresh_coin" | "buy_coin" | "hero_full", text?: string) {
let targetNode: Node | null = null;
switch (type) {
case "refresh_coin":
@@ -475,6 +481,11 @@ export class MissionCardComp extends CCComp {
if (targetNode && targetNode.isValid) {
const tipNode = targetNode.getChildByName("smalltip");
if (tipNode) {
// 动态覆盖气泡文字box/Label用于同节点多文案场景如英雄满员 / 至少保留 1 人)
if (text) {
const label = tipNode.getComponentInChildren(Label);
if (label) label.string = text;
}
tipNode.active = true;
Tween.stopAllByTarget(tipNode);
@@ -740,7 +751,7 @@ export class MissionCardComp extends CCComp {
if (current >= heroMax) {
payload.cancel = true;
payload.reason = "hero_limit";
this.showSmallTip("hero_full");
this.showSmallTip("hero_full", "英雄队伍已满");
this.playHeroNumDeniedAnim();
}
}