像素大勇者游戏软件 V1.0            第 1 页 共 60 页 ============================================================================ // ===================== 文件: Main.ts ===================== import { profiler, _decorator ,PhysicsSystem2D,EPhysics2DDrawFlags} from 'cc'; import { DEBUG } from 'cc/env'; import { oops } from '../../extensions/oops-plugin-framework/assets/core/Oops'; import { Root } from '../../extensions/oops-plugin-framework/assets/core/Root'; import { ecs } from '../../extensions/oops-plugin-framework/assets/libs/ecs/ECS'; import { UIConfigData } from './game/common/config/GameUIConfig'; import { smc } from './game/common/SingletonModuleComp'; import { Initialize } from './game/initialize/Initialize'; const { ccclass, property } = _decorator; @ccclass('Main') export class Main extends Root { start() { // PhysicsSystem2D.instance.debugDrawFlags = EPhysics2DDrawFlags.Aabb // |EPhysics2DDrawFlags.Pair // |EPhysics2DDrawFlags.CenterOfMass // |EPhysics2DDrawFlags.Joint // |EPhysics2DDrawFlags.Shape; } protected async run() { smc.initialize = ecs.getEntity(Initialize); smc.vmAdd() } protected initGui() { oops.gui.init(UIConfigData); } protected async initEcsSystem() { // oops.ecs.add(new EcsPositionSystem()); 手动控制用,没有特殊 ,只需要使用 @ecs.register('SMoveSystem') 装饰器注册就可以了 } } // ===================== 文件: game/initialize/Initialize.ts ===================== /* */ import { Node, Button } from "cc"; import { UICallbacks } from "../../../../extensions/oops-plugin-framework/assets/core/gui/layer/Defines"; import { oops } from "../../../../extensions/oops-plugin-framework/assets/core/Oops"; import { AsyncQueue, NextFunction } from "../../../../extensions/oops-plugin-framework/assets/libs/collection/AsyncQueue"; import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; import { UIID } from "../common/config/GameUIConfig"; import { LoadingViewComp } from "./view/LoadingViewComp"; import { smc } from "../common/SingletonModuleComp"; import { WxCloudApi } from "../wx_clound_client_api/WxCloudApi"; import { mLogger } from "../common/Logger"; import { gameDataSync } from "../common/GameDataSync"; /** * 游戏进入初始化模块 * 1、热更新 * 2、加载默认资源 */ @ecs.register(`Initialize`) export class Initialize extends ecs.Entity { LoadingView!: LoadingViewComp; debugMode: boolean = false; // 是否启用调试模式 protected init() { var queue: AsyncQueue = new AsyncQueue(); 像素大勇者游戏软件 V1.0            第 2 页 共 60 页 ============================================================================ // --- 全局注入:为所有 Button 组件增加点击音效 --- const originalOnTouchEnded = Button.prototype["_onTouchEnded"]; if (originalOnTouchEnded) { Button.prototype["_onTouchEnded"] = function (event) { if (this.interactable && this.node && this.node.activeInHierarchy) { oops.audio.playEffect("music/button"); } originalOnTouchEnded.call(this, event); }; } // --------------------------------------------- // 加载自定义资 this.loadCustom(queue); // 加载多语言包 this.loadLanguage(queue); // 加载公共资源 this.loadCommon(queue); // 加载游戏内容加载进度提示界面 this.onComplete(queue); queue.play(); } /** 加载自定义内容(可选) */ private loadCustom(queue: AsyncQueue) { queue.push(async (next: NextFunction, params: any, args: any) => { try { // 加载多语言对应字体 oops.res.load("language/font/" + oops.language.current, async () => { // 统一的数据加载流程 await this.loadGameDataUnified(); next(); }); //加载精灵配置表 // oops.res.load("config/game/heros", next); } catch (error) { mLogger.error(this.debugMode, 'Initialize', "[Initialize]: 自定义内容加载失败:", error); next(); // 即使失败也要继续,不阻塞游戏启动 } }); } /** 加载化语言包(可选) */ private loadLanguage(queue: AsyncQueue) { queue.push((next: NextFunction, params: any, args: any) => { // 设置默认语言 let lan = oops.storage.get("language"); // if (lan == null) { if (lan == null || lan == "") { lan = "zh"; oops.storage.set("language", lan); } // 设置语言包路径 oops.language.pack.json = oops.config.game.languagePathJson; oops.language.pack.texture = oops.config.game.languagePathTexture; 像素大勇者游戏软件 V1.0            第 3 页 共 60 页 ============================================================================ // 加载语言包资源 oops.language.setLanguage(lan, next); }); } /** 加载公共资源(必备) */ private loadCommon(queue: AsyncQueue) { queue.push((next: NextFunction, params: any, args: any) => { // 顺便在这里预加载 SMC 需要的公共图集 smc.preloadCommonAssets(); oops.res.loadDir("common", next); }); } /** * 统一的游戏数据加载流程 * 微信客户端:使用云端数据 * 非微信客户端:使用本地调试数据 */ private async loadGameDataUnified() { try { mLogger.log(this.debugMode, 'Initialize', "[Initialize]: 开始统一数据加载流程..."); if (gameDataSync.isWxClient()) { // 微信客户端:加载云端数据 mLogger.log(this.debugMode, 'Initialize', "[Initialize]: 检测到微信客户端,使用云端数据"); await this.loadFromCloud(); } else { // 非微信客户端:使用本地调试数据 mLogger.log(this.debugMode, 'Initialize', "[Initialize]: 非微信客户端,使用本地调试数据"); await this.loadFromLocalDebug(); } } catch (error) { mLogger.log(this.debugMode, 'Initialize', "[Initialize]: 统一数据加载失败:", error); // 失败时使用默认数据 游戏需要退出 } } /** * 从云端加载数据(微信客户端,sync 模式) */ private async loadFromCloud() { try { // 1. 初始化微信云环境 this.initWxCloudEnv(); // 2. 一次调用完成登录与双向数据同步(时间戳对比在云端完成) const cloudNewer = await gameDataSync.syncFromCloud(); mLogger.log(this.debugMode, 'Initialize', `[Initialize]: 云端同步完成,云端数据较新=${cloudNewer}`); } catch (error) { mLogger.log(this.debugMode, 'Initialize', "[Initialize]: 云端数据加载异常:", error); // 异常时使用本地数据兜底 await gameDataSync.syncFromCloud(); } } /** 像素大勇者游戏软件 V1.0            第 4 页 共 60 页 ============================================================================ * 从本地调试数据加载(非微信客户端) */ private async loadFromLocalDebug() { try { // 非微信客户端仅使用本地缓存数据 await gameDataSync.syncFromCloud(); } catch (error) { mLogger.log(this.debugMode, 'Initialize', "[Initialize]: 本地调试数据加载异常:", error); } } /** * 初始化微信云环境 */ private initWxCloudEnv() { try { // 请替换为您的实际云环境ID const cloudEnvId = "cloud1-6gknw0qk911036d8"; // TODO: 配置您的云环境ID WxCloudApi.init(cloudEnvId); mLogger.log(this.debugMode, 'Initialize', "[Initialize]: 微信云环境初始化完成"); } catch (error) { mLogger.log(this.debugMode, 'Initialize', "[Initialize]: 微信云环境初始化失败:", error); } } /** 加载完成进入游戏内容加载界面 */ private onComplete(queue: AsyncQueue) { queue.complete = () => { var uic: UICallbacks = { onAdded: (node: Node, params: any) => { var comp = node.getComponent(LoadingViewComp) as ecs.Comp; this.add(comp); } }; // 界面管理 - 打开游戏内容资源加载进度提示界面 oops.gui.open(UIID.Loading, null, uic); }; } } // ===================== 文件: game/initialize/view/LoadingViewComp.ts ===================== /* */ import { _decorator ,v3,Vec3} from "cc"; import { oops } from "../../../../../extensions/oops-plugin-framework/assets/core/Oops"; import { ecs } from "../../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; import { CCVMParentComp } from "../../../../../extensions/oops-plugin-framework/assets/module/common/CCVMParentComp"; import { UIID } from "../../common/config/GameUIConfig"; import { smc } from "../../common/SingletonModuleComp"; import { GameMap } from "../../map/GameMap"; const { ccclass, property } = _decorator; /** 游戏资源加载 */ @ccclass('LoadingViewComp') @ecs.register('LoadingView', false) export class LoadingViewComp extends CCVMParentComp { /** VM 组件绑定数据 */ data: any = { /** 加载资源当前进度 */ finished: 0, 像素大勇者游戏软件 V1.0            第 5 页 共 60 页 ============================================================================ /** 加载资源最大进度 */ total: 0, /** 加载资源进度比例值 */ progress: "", /** 加载流程中提示文本 */ prompt: "" }; private progress: number = 0; reset(): void { // 获取用户信息的多语言提示文本 this.data.prompt = oops.language.getLangByID("loading_load_player"); // 关闭加载界面 oops.gui.remove(UIID.Loading); // 释放加载界面资源 oops.res.releaseDir("loading"); // 加载地图 var map = ecs.getEntity(GameMap); map.load(); smc.map = map; oops.gui.open(UIID.Role_Controller) } start() { this.loadRes(); } /** 加载资源 */ private async loadRes() { this.data.progress = 0; this.loadCustom(); this.loadGameRes(); } /** 加载游戏本地JSON数据(自定义内容) */ private loadCustom() { // 加载游戏本地JSON数据的多语言提示文本 this.data.prompt = oops.language.getLangByID("loading_load_json"); } /** 加载初始游戏内容资源 */ private loadGameRes() { // 加载初始游戏内容资源的多语言提示文本 this.data.prompt = oops.language.getLangByID("loading_load_game"); oops.res.loadDir("game", this.onProgressCallback.bind(this), this.onCompleteCallback.bind(this)); } /** 加载进度事件 */ private onProgressCallback(finished: number, total: number, item: any) { this.data.finished = finished; this.data.total = total; var progress = finished / total; if (progress > this.progress) { this.progress = progress; this.data.progress = (progress * 100).toFixed(2); 像素大勇者游戏软件 V1.0            第 6 页 共 60 页 ============================================================================ } } /** 加载完成事件 */ private onCompleteCallback() { this.ent.remove(LoadingViewComp); } } // ===================== 文件: game/common/CameraFX.ts ===================== /** * @file CameraFX.ts * @description 大招镜头聚焦工具(表现层) * * 项目内场景 Camera 从未启用(MapViewScene.camera 可能为 null), * 与 ScreenShake 同理改为操作地图根节点 smc.map.MapView.node: * 以施法者位置为锚点做 scale + 位移 tween,实现"画面瞬间拉近再回弹"的镜头突进感, * UI 层(oops.gui 挂在独立根节点)天然不受影响。 * * 与 ScreenShake 的区别:本工具是"有明确锚点的缩放聚焦", * ScreenShake 是"无锚点的随机抖动",两者可叠加(大招命中瞬间聚焦回弹 + 震屏)。 * * 用法: * CameraFX.focusOn(heroView.node, 1.2, 0.5); */ import { Node, tween, Tween, v3, Vec3, view } from "cc"; import { smc } from "./SingletonModuleComp"; export class CameraFX { /** 根节点基准状态(首次聚焦时快照:位置 + 缩放,用于回弹复位,防多次触发漂移) */ private static basePos: Vec3 | null = null; private static baseScale: Vec3 | null = null; /** * 镜头聚焦:画面瞬间向锚点拉近,随后回弹复位 * @param anchor 聚焦锚点节点(通常是施法者节点),以其世界坐标为画面中心 * @param zoom 拉近倍率(建议 1.15~1.25,过大容易出戏) * @param holdTime 聚焦保持到回弹完成的总时长(秒,建议 0.4~0.6) */ static focusOn(anchor: Node | null | undefined, zoom: number = 1.2, holdTime: number = 0.5) { const root = smc.map?.MapView?.node; if (!root || !root.isValid || !anchor || !anchor.isValid) return; if (!this.basePos) { this.basePos = root.position.clone(); this.baseScale = root.scale.clone(); } // 打断进行中的聚焦(新大招覆盖旧大招,避免多英雄连续放大时 tween 叠加失控) Tween.stopAllByTarget(root); // 锚点世界坐标 -> 根节点本地坐标,作为缩放后的画面中心 const worldPos = anchor.worldPosition; const localPos = v3(); root.inverseTransformPoint(localPos, worldPos); const designSize = view.getDesignResolutionSize(); const focusScale = this.baseScale.x * zoom; // 缩放会围绕根节点原点(0,0)放大画面,需反向平移让锚点保持在屏幕中心附近: // 目标位置 = 基准位置 - 锚点本地坐标 * (zoom - 1) * 基准缩放 像素大勇者游戏软件 V1.0            第 7 页 共 60 页 ============================================================================ // 同时按设计分辨率的一半作为屏幕中心基准,避免锚点在屏幕边缘时拉出黑边 const offsetX = localPos.x * (zoom - 1) * this.baseScale.x; const offsetY = localPos.y * (zoom - 1) * this.baseScale.y; const clampX = designSize.width * 0.25; // 平移量限幅:最多偏移 1/4 屏,防边缘英雄把画面拉飞 const clampY = designSize.height * 0.25; const targetPos = v3( this.basePos.x - Math.max(-clampX, Math.min(clampX, offsetX)), this.basePos.y - Math.max(-clampY, Math.min(clampY, offsetY)), this.basePos.z ); const focusScaleV3 = v3(focusScale, focusScale, this.baseScale.z); // 三段式节奏:快速突进(0.12s) -> 短暂停留 -> 慢速回弹(holdTime) tween(root) .to(0.12, { position: targetPos, scale: focusScaleV3 }, { easing: "sineOut" }) .delay(Math.max(0.05, holdTime * 0.3)) .to(holdTime, { position: this.basePos, scale: this.baseScale }, { easing: "sineInOut" }) .start(); } /** * 复位镜头状态(关卡切换/战斗结束时调用,防止状态残留) */ static reset() { const root = smc.map?.MapView?.node; if (root && root.isValid && this.basePos && this.baseScale) { Tween.stopAllByTarget(root); root.setPosition(this.basePos); root.setScale(this.baseScale); } this.basePos = null; this.baseScale = null; } } // ===================== 文件: game/common/CardIconHelper.ts ===================== /** * @file CardIconHelper.ts * @description 卡牌图标解析与设置共享工具 * * 职责: * 统一处理技能卡/装备卡/商品卡的图标 ID 解析与 SpriteFrame 设置, * 避免 SkillBoxComp / SCardComp / ItemListComp / EquipListComp 重复维护相同逻辑。 * * 解析优先级: * 1. cardData.icon(卡牌自定义图标,最高优先级) * 2. 装备卡(trigger_type=Field 且有 field):取第一个 field 词条的 FieldSkillSet.icon * 3. 技能卡(有 skill):取 SkillSet.icon * 4. 兜底:skill uuid 或 card uuid 的字符串形式 */ import { Node, Sprite } from "cc"; import { CardConfig, CardTriggerType } from "./config/CardSet"; import { FieldSkillSet, SkillSet } from "./config/SkillSet"; import { smc } from "./SingletonModuleComp"; /** * 解析卡牌图标 ID * @param cardData 卡牌配置 * @returns 图标 ID(对应图集 frame 名),空字符串表示无法解析 */ 像素大勇者游戏软件 V1.0            第 8 页 共 60 页 ============================================================================ export function getCardIconId(cardData: CardConfig | null): string { if (!cardData) return ""; // 1. 卡牌自定义 icon 优先 if (cardData.icon) return cardData.icon; // 2. 装备卡:取第一个 field 词条图标 if (cardData.field && cardData.field.length > 0) { const fieldUuid = cardData.field[0].uuid; return FieldSkillSet[fieldUuid]?.icon || `${fieldUuid}`; } // 3. 技能卡:取 SkillSet.icon if (cardData.skill) { return SkillSet[cardData.skill]?.icon || `${cardData.skill}`; } // 4. 兜底:uuid 字符串 return `${cardData.uuid}`; } /** * 设置节点 Sprite 的图标 * @param node 目标节点(自动查找自身或子节点的 Sprite) * @param iconId 图标 ID(对应 smc.uiconsAtlas 中的 frame 名) */ export function setSpriteFrame(node: Node, iconId: string): void { if (!node || !iconId) return; const sprite = node.getComponent(Sprite) || node.getComponentInChildren(Sprite); if (!sprite) return; if (smc.uiconsAtlas) { const frame = smc.uiconsAtlas.getSpriteFrame(iconId); sprite.spriteFrame = frame || null; } } // ===================== 文件: game/common/CommonPrompt.ts ===================== /** * @file CommonPrompt.ts * @description 自定义公共确认窗口组件(UI 视图层) * * 职责: * 1. 替换框架插件 CommonPrompt(extensions/oops-plugin-framework/.../CommonPrompt.ts), * 脚本类名、onTouchEnd 处理器与 4 个属性名保持同名,window.prefab 无需改动即可自动重链。 * 2. 内容区由 cc.Label 升级为 cc.RichText,支持 // 等富文本标签。 * 3. 扩展标题样式、窗体缩放动画、遮罩点击关闭等自定义能力。 * * 使用(oops.gui 模块): * oops.gui.open(UIID.Window, { * title: "标题", * content: "支持 富文本", * okWord: "确定", cancelWord: "取消", needCancel: true, * okFunc: () => {}, cancelFunc: () => {}, * titleColor: "#FF0000", // 可选:标题颜色 * maskClose: true, // 可选:点击空白遮罩关闭(等同取消) * closeFunc: () => {} // 可选:任意方式关闭后的统一回调 * }); */ import { _decorator, Button, Color, Component, EventTouch, Label, Node, NodeEventType, RichText, Tween, tween, Vec3 } from "cc"; import { oops } from "db://oops-framework/core/Oops"; 像素大勇者游戏软件 V1.0            第 9 页 共 60 页 ============================================================================ import { RICH_OUTLINE_COLOR, RICH_OUTLINE_WIDTH } from "./RichOutline"; const { ccclass, property } = _decorator; /** 确认窗口参数 */ export interface WindowPromptParams { /** 标题(普通文本) */ title?: string; /** 内容(RichText 富文本,支持 //// 等标签) */ content?: string; /** 确定按钮文字 */ okWord?: string; /** 确定回调 */ okFunc?: () => void; /** 取消按钮文字 */ cancelWord?: string; /** 取消回调 */ cancelFunc?: () => void; /** 是否显示取消按钮 */ needCancel?: boolean; /** 标题颜色(如 "#FF0000",不传用默认色) */ titleColor?: string; /** 点击窗体外空白遮罩是否关闭(等同取消,默认 false) */ maskClose?: boolean; /** 任意方式关闭后的统一回调(在 okFunc/cancelFunc 之后触发) */ closeFunc?: () => void; } /** * CommonPrompt —— 自定义公共确认窗口 * * 与 window.prefab 的绑定约定: * - 根节点按钮事件 handler:onTouchEnd(btn_ok / btn_cancel 已绑定) * - 属性名:lab_title / lab_content / lab_ok / lab_cancel * - lab_content 需将 prefab 中的 cc.Label 替换为 cc.RichText 后拖入 */ @ccclass("CommonPrompt") export class CommonPrompt extends Component { /** 窗口标题 */ @property({ type: Label, displayName: "标题文本" }) private lab_title: Label | null = null; /** 窗口内容(富文本,需在 prefab 中将 Label 换为 RichText 后重新拖入) */ @property({ type: RichText, displayName: "内容富文本" }) private lab_content: RichText | null = null; /** 确定按钮文本 */ @property({ type: Label, displayName: "确定按钮文本" }) private lab_ok: Label | null = null; /** 取消按钮文本 */ @property({ type: Label, displayName: "取消按钮文本" }) private lab_cancel: Label | null = null; /** 确定按钮节点 */ @property({ type: Node, displayName: "确定按钮" }) private btn_ok: Node | null = null; /** 取消按钮节点 */ @property({ type: Node, displayName: "取消按钮" }) 像素大勇者游戏软件 V1.0            第 10 页 共 60 页 ============================================================================ private btn_cancel: Node | null = null; /** 运行时参数 */ private config: WindowPromptParams = {}; /** 弹窗打开动画已播放标记(防止 onAdded 重复触发) */ private animPlayed: boolean = false; onLoad() { // 优先使用编辑器绑定的按钮节点;未绑定时按节点名查找兜底 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.btn_ok?.getChildByName("btn_sky")?.on(NodeEventType.TOUCH_END, this.onMaskClick, this); // 序列化属性槽引用的是旧插件 LanguageLabel 组件实例,脚本替换后已失效; // 这里按节点名查找 Label/RichText 作为兜底,保证文字一定写入 if (!this.lab_title) { this.lab_title = this.node.getChildByName("lab_title")?.getComponent(Label) ?? null; } if (!this.lab_ok) { this.lab_ok = this.btn_ok?.getChildByName("lab_ok")?.getComponent(Label) ?? null; } if (!this.lab_cancel) { 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; } // 禁用旧插件残留的 LanguageLabel 组件:其 update 会用空 dataID 每帧覆盖 Label 文本 this.disableLegacyLanguageLabels(this.lab_title?.node); this.disableLegacyLanguageLabels(this.lab_ok?.node); this.disableLegacyLanguageLabels(this.lab_cancel?.node); this.disableLegacyLanguageLabels(this.lab_content?.node); } /** 禁用节点上残留的 LanguageLabel(旧插件组件,类名带 LanguageLabel 后缀) */ private disableLegacyLanguageLabels(node: Node | null | undefined) { if (!node || !node.isValid) return; for (const comp of node.components) { if (comp.constructor.name === "LanguageLabel") { comp.enabled = false; } } } onDestroy() { 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 = {}; } 像素大勇者游戏软件 V1.0            第 11 页 共 60 页 ============================================================================ /** * oops.gui 添加到舞台回调(框架约定入口) * @param params 见 WindowPromptParams */ onAdded(params: WindowPromptParams = {}) { this.config = params || {}; this.setTitle(); this.setContent(); this.setBtnOkLabel(); this.setBtnCancelLabel(); this.node.active = true; this.playOpenAnim(); } /** 按钮点击处理器(window.prefab 中 btn_ok / btn_cancel 的 clickEvents 绑定此方法) */ private onTouchEnd(event: EventTouch) { switch (event.target.name) { case "btn_ok": this.onOk(); break; case "btn_cancel": this.onCancel(); break; default: break; } } /** 遮罩(btn_sky)点击:仅当 maskClose 开启时按取消处理 */ private onMaskClick() { if (this.config.maskClose) { this.onCancel(); } } private setTitle() { if (!this.lab_title || !this.lab_title.isValid) return; this.lab_title.string = this.config.title ?? ""; if (this.config.titleColor) { this.lab_title.color = new Color().fromHEX(this.config.titleColor); } this.ensureTitleOutline(); } /** 标题描边:Cocos 3.8+ 废弃 LabelOutline 组件,改用 Label 内建 outlineColor/outlineWidth */ private ensureTitleOutline() { if (!this.lab_title || !this.lab_title.isValid) return; this.lab_title.outlineColor = new Color().fromHEX(RICH_OUTLINE_COLOR); this.lab_title.outlineWidth = RICH_OUTLINE_WIDTH; } private setContent() { if (!this.lab_content || !this.lab_content.isValid) return; this.lab_content.string = this.config.content ?? ""; } private setBtnOkLabel() { if (!this.lab_ok || !this.lab_ok.isValid) return; this.lab_ok.string = this.config.okWord ?? "确定"; } 像素大勇者游戏软件 V1.0            第 12 页 共 60 页 ============================================================================ private setBtnCancelLabel() { if (!this.lab_cancel || !this.lab_cancel.isValid) return; this.lab_cancel.string = this.config.cancelWord ?? "取消"; // 取消按钮挂在 lab_cancel 的父节点上 const cancelBtn = this.lab_cancel.node?.parent; if (cancelBtn && cancelBtn.isValid) { cancelBtn.active = this.config.needCancel || false; } } /** 打开动画:缩放 0.8 → 1 回弹 */ private playOpenAnim() { if (this.animPlayed) return; this.animPlayed = true; Tween.stopAllByTarget(this.node); this.node.setScale(0.8, 0.8, 1); tween(this.node) .to(0.15, { scale: new Vec3(1, 1, 1) }, { easing: "backOut" }) .start(); } private onOk() { oops.audio.playEffect("music/button"); this.config.okFunc?.(); this.close(); } private onCancel() { oops.audio.playEffect("music/button"); this.config.cancelFunc?.(); this.close(); } private close() { this.config.closeFunc?.(); oops.gui.removeByNode(this.node); } } // ===================== 文件: game/common/config/BuffSet.ts ===================== // ========== Buff/Debuff 配置定义 ========== import { Attrs } from "./HeroAttrs"; /** Buff 类别 */ export enum BuffCategory { Buff = 0, Debuff = 1, Control = 2, } /** 修饰器运算类型 */ export enum ModOp { Flat = 0, PercentAdd = 1, PercentMul = 2, } /** 单条属性修饰器 */ export interface BuffModifier { 像素大勇者游戏软件 V1.0            第 13 页 共 60 页 ============================================================================ attr: Attrs; op: ModOp; value: number; } /** 周期效果(DoT/HoT) */ export interface BuffTickEffect { interval: number; damage_or_heal: number; // 正=治疗,负=伤害 scale_by_ap_pct: number; // 按施法者 ap 缩放百分比,0=不吃加成 } /** * debuff 图标节点名枚举。 * 取值即 prefab 节点名:top.prefab 的 top/debuff 容器 与 tooltip.prefab 的 loss_life/hp/debuff 容器 * 下存在同名子节点(两处共用一套图标)。 * 注意:LightningDamag 为 prefab 原始拼写(少个 e),勿"修正"。 */ export enum DebuffIcon { AttackDebuff = "AttackDebuff", // 攻击类 debuff IceDamage = "IceDamage", // 冰系伤害 FireDamage = "FireDamage", // 火系伤害(灼烧) LightningDamag = "LightningDamag", // 雷系伤害 Penetration = "Penetration", // 穿透 Stun = "Stun", // 击晕 Poison = "Poison", // 中毒 Blood = "Blood", // 出血 } /** * buffId → debuff 图标映射。 * 仅伤害类/控制类 debuff 需要图标;未映射的 buff 不显示。 * 新增 debuff 时:1) BuffList 加配置 2) 此处加一行映射。 */ export const DEBUFF_ICON_MAP: Record = { 7002: DebuffIcon.FireDamage, // 易伤(灼热火球附加) 7003: DebuffIcon.IceDamage, // 冰冻 7004: DebuffIcon.Stun, // 击晕 7005: DebuffIcon.Poison, // 中毒 7006: DebuffIcon.AttackDebuff, // 减攻 7007: DebuffIcon.Blood, // 出血 7008: DebuffIcon.Penetration, // 破甲 7025: DebuffIcon.LightningDamag, // 麻痹(雷系图标,注意 prefab 原始拼写少个 e) }; /** 易伤 buff 的 id(HeroAtkSystem 结算时按此 id 汇总易伤值) */ export const VULNERABLE_BUFF_ID = 7002; /** 出血 buff 的 id(每层使目标被暴击率 +1%) */ export const BLEED_BUFF_ID = 7007; /** 出血每层附加的被暴击率(百分点) */ export const BLEED_CRIT_PER_STACK = 1; /** Buff 静态定义 */ export interface BuffConfig { id: number; name: string; icon: string; category: BuffCategory; duration: number; // 单层持续时间(秒),0=永久 max_stack: number; // 默认 0=无限叠加;1=不可叠层;N=最多N层 像素大勇者游戏软件 V1.0            第 14 页 共 60 页 ============================================================================ modifiers?: BuffModifier[]; tick?: BuffTickEffect; is_control?: boolean; control_kind?: 'frost' | 'stun' | 'paralyze' | 'none'; info: string; } /** Buff 配置表 */ export const BuffList: Record = { // 攻击强化(限时版) 7001: { id: 7001, name: "攻击强化", icon: "3-Aggression", category: BuffCategory.Buff, duration: 10, max_stack: 0, modifiers: [{ attr: Attrs.ap, op: ModOp.PercentAdd, value: 5 }], info: "攻击力提升 5%,持续 10 秒", }, // 易伤(灼热火球附加;受到伤害时附加固定值,可被易伤抗性百分比减免) // 注:modifiers.attr 用 hp 仅为占位以通过 aggregateTimedMods 结构校验,实际易伤值由 HeroAtkSystem 按 VULNERABLE_BUFF_ID 直接汇总 7002: { id: 7002, name: "易伤", icon: "5-Apply-Burn", category: BuffCategory.Debuff, duration: 5, max_stack: 5, modifiers: [{ attr: Attrs.hp, op: ModOp.Flat, value: 10 }], info: "受到的伤害附加 10 点易伤,可叠加,持续 5 秒", }, // 冰冻(控制,走统一框架,双写兼容) 7003: { id: 7003, name: "冰冻", icon: "21-Deep-Frezze", category: BuffCategory.Control, duration: 2, max_stack: 1, is_control: true, control_kind: 'frost', info: "冰冻目标,无法行动", }, // 击晕(短控:施加瞬间技能 CD 削减 20%,持续期间 CD 停止增长) 7004: { id: 7004, name: "击晕", icon: "13-Concussion", category: BuffCategory.Control, duration: 1, max_stack: 1, is_control: true, control_kind: 'stun', info: "击晕目标 1 秒,无法行动,技能 CD 削减 20% 并停止增长", }, // 中毒(纯伤害 DoT,可叠层) 7005: { id: 7005, name: "中毒", icon: "4-Apply-Poison", category: BuffCategory.Debuff, duration: 5, max_stack: 5, tick: { interval: 0.5, damage_or_heal: -10, scale_by_ap_pct: 30 }, info: "每 0.5 秒受到 10 点中毒伤害,可叠加,持续 5 秒", }, // 减攻(降低目标攻击力) 7006: { id: 7006, name: "减攻", icon: "16-Cripple", category: BuffCategory.Debuff, duration: 5, max_stack: 1, modifiers: [{ attr: Attrs.ap, op: ModOp.PercentAdd, value: -10 }], info: "攻击力降低 10%,持续 5 秒", }, // 破甲(降低目标防御值,防御按加法模型累加,由 getFinalDefense 结算) 7008: { id: 7008, name: "破甲", icon: "76-Shove-Stab", category: BuffCategory.Debuff, duration: 5, max_stack: 1, modifiers: [{ attr: Attrs.defense, op: ModOp.PercentAdd, value: -50 }], info: "防御降低 50,持续 5 秒", }, // 出血(每层使目标被暴击率 +1%,微量伤害 DoT;暴击率加成由 HeroAtkSystem 按 BLEED_BUFF_ID 层数结算) 像素大勇者游戏软件 V1.0            第 15 页 共 60 页 ============================================================================ 7007: { id: 7007, name: "出血", icon: "7-Apply-Bleed", category: BuffCategory.Debuff, duration: 5, max_stack: 5, tick: { interval: 0.5, damage_or_heal: -1, scale_by_ap_pct: 0 }, info: "微量出血,每层使自身被暴击率提升 1%,可叠加,持续 5 秒", }, // ==================== 玩家计时性强化(药水/技能用,默认 5s) ==================== // 计时性回血(HoT) 7010: { id: 7010, name: "再生", icon: "38-Healing-Light", category: BuffCategory.Buff, duration: 5, max_stack: 1, tick: { interval: 0.5, damage_or_heal: 10, scale_by_ap_pct: 0 }, info: "每秒回复生命,持续 5 秒", }, // 暴击强化(限时) 7012: { id: 7012, name: "暴击爆发", icon: "27-Hawkeye", category: BuffCategory.Buff, duration: 5, max_stack: 1, modifiers: [{ attr: Attrs.critical, op: ModOp.PercentAdd, value: 30 }], info: "暴击率提升 30%,持续 5 秒", }, // 击退距离强化(限时) 7013: { id: 7013, name: "击退爆发", icon: "34-Force-Push", category: BuffCategory.Buff, duration: 5, max_stack: 1, modifiers: [{ attr: Attrs.knockback_chance, op: ModOp.PercentAdd, value: 30 }], info: "击退距离提升 30%,持续 5 秒", }, // 穿透强化(限时) 7014: { id: 7014, name: "穿透爆发", icon: "56-Pierce-Shot", category: BuffCategory.Buff, duration: 5, max_stack: 1, modifiers: [{ attr: Attrs.puncture_chance, op: ModOp.PercentAdd, value: 30 }], info: "穿透概率提升 30%,持续 5 秒", }, // 攻速强化(限时,走 getEffectiveSkillCd 计时修饰) 7015: { id: 7015, name: "攻速爆发", icon: "131-Galloop", category: BuffCategory.Buff, duration: 5, max_stack: 1, modifiers: [{ attr: Attrs.speed, op: ModOp.PercentAdd, value: 40 }], info: "攻击速度提升 40%,持续 5 秒", }, // 风怒机制已停用: 7016 风怒爆发 buff 已移除 /* // 风怒强化(限时,按概率百分点) 7016: { id: 7016, name: "风怒爆发", icon: "98-Enchant-Wind", category: BuffCategory.Buff, duration: 5, max_stack: 1, modifiers: [{ attr: Attrs.wfuny, op: ModOp.PercentAdd, value: 20 }], info: "风怒概率提升 20%,持续 5 秒", }, */ // 击晕概率强化(限时) 7017: { id: 7017, name: "击晕爆发", icon: "8-Bash", category: BuffCategory.Buff, duration: 5, max_stack: 1, modifiers: [{ attr: Attrs.stun_chance, op: ModOp.PercentAdd, value: 30 }], info: "击晕概率提升 30%,持续 5 秒", }, 像素大勇者游戏软件 V1.0            第 16 页 共 60 页 ============================================================================ // 击晕抗性强化(限时) 7018: { id: 7018, name: "击晕抗性", icon: "46-Iron-Clad", category: BuffCategory.Buff, duration: 5, max_stack: 1, modifiers: [{ attr: Attrs.stun_res, op: ModOp.PercentAdd, value: 50 }], info: "击晕抗性提升 50%,持续 5 秒", }, // 冰冻抗性强化(限时) 7019: { id: 7019, name: "冰冻抗性", icon: "91-Ice-Nova", category: BuffCategory.Buff, duration: 5, max_stack: 1, modifiers: [{ attr: Attrs.freeze_res, op: ModOp.PercentAdd, value: 50 }], info: "冰冻抗性提升 50%,持续 5 秒", }, // 暴伤强化(限时) 7020: { id: 7020, name: "暴伤爆发", icon: "63-Power-Strike", category: BuffCategory.Buff, duration: 5, max_stack: 1, modifiers: [{ attr: Attrs.critical_damage, op: ModOp.PercentAdd, value: 30 }], info: "暴击伤害提升 30%,持续 5 秒", }, // 冰冻概率强化(限时) 7021: { id: 7021, name: "冰冻概率爆发", icon: "90-Frost-Spike", category: BuffCategory.Buff, duration: 5, max_stack: 1, modifiers: [{ attr: Attrs.freeze_chance, op: ModOp.PercentAdd, value: 5 }], info: "冰冻概率提升 5%,持续 5 秒", }, // 穿透伤害强化(限时) 7022: { id: 7022, name: "穿透伤害爆发", icon: "59-Sparking-Arrow", category: BuffCategory.Buff, duration: 5, max_stack: 1, modifiers: [{ attr: Attrs.puncture_dmg_bonus, op: ModOp.PercentAdd, value: 30 }], info: "穿透后伤害提升 30%,持续 5 秒", }, // 防御强化(限时,按加法模型累加固定值,由 getFinalDefense 结算) 7023: { id: 7023, name: "防御强化", icon: "36-Guardian-Stance", category: BuffCategory.Buff, duration: 10, max_stack: 0, modifiers: [{ attr: Attrs.defense, op: ModOp.PercentAdd, value: 50 }], info: "防御提升 50,持续 10 秒", }, // 防御爆发(限时,药水用,统一 5s) 7024: { id: 7024, name: "防御爆发", icon: "135-Bulletproof", category: BuffCategory.Buff, duration: 5, max_stack: 1, modifiers: [{ attr: Attrs.defense, op: ModOp.PercentAdd, value: 100 }], info: "防御提升 100,持续 5 秒", }, // ==================== 控制 / 麻痹系 ==================== // 麻痹(控制:技能 CD 增长减速 50%,可被麻痹强化 buff 提升,累计达 100% 则停止增长) 7025: { id: 7025, name: "麻痹", icon: "6-Apply-Paralysis", category: BuffCategory.Control, duration: 5, max_stack: 1, is_control: true, control_kind: 'paralyze', info: "麻痹目标,技能 CD 增长减慢 50%,持续 5 秒", }, // 麻痹强化(提升麻痹对技能 CD 的减速量,与麻痹基础减速按百分点叠加) 7026: { 像素大勇者游戏软件 V1.0            第 17 页 共 60 页 ============================================================================ id: 7026, name: "麻痹强化", icon: "47-Lightning", category: BuffCategory.Buff, duration: 5, max_stack: 1, modifiers: [{ attr: Attrs.paralyze_power, op: ModOp.PercentAdd, value: 30 }], info: "麻痹对技能 CD 的减速量额外提升 30%,持续 5 秒", }, // ==================== 反伤系 ==================== // 荆棘(固定值反伤,可叠加) 7027: { id: 7027, name: "荆棘", icon: "61-Poison-Ivy", category: BuffCategory.Buff, duration: 5, max_stack: 5, modifiers: [{ attr: Attrs.thorns_dmg, op: ModOp.Flat, value: 10 }], info: "受到攻击时反弹 10 点固定伤害,可叠加,持续 5 秒", }, // 荆棘比例(百分比反伤,可叠加) 7028: { id: 7028, name: "荆棘比例", icon: "61-Poison-Ivy", category: BuffCategory.Buff, duration: 5, max_stack: 3, modifiers: [{ attr: Attrs.thorns_rate, op: ModOp.PercentAdd, value: 10 }], info: "受到攻击时反弹 10% 所受伤害,可叠加,持续 5 秒", }, // ==================== 生命上限系 ==================== // 生命爆发(限时提升最大生命;生效时按增量等量回血,到期钳回,由 HeroAttrsComp.syncHpWithFinalMax 统一结算) 7029: { id: 7029, name: "生命爆发", icon: "29-Endure", category: BuffCategory.Buff, duration: 5, max_stack: 1, modifiers: [{ attr: Attrs.hp_max, op: ModOp.PercentAdd, value: 30 }], info: "最大生命值提升 30%,并立即回复等量生命,持续 5 秒", }, // ==================== 闪避 / 命中系 ==================== // 闪避爆发(限时提升闪避概率,百分点累加) 7030: { id: 7030, name: "闪避爆发", icon: "98-Enchant-Wind", category: BuffCategory.Buff, duration: 5, max_stack: 1, modifiers: [{ attr: Attrs.dodge, op: ModOp.PercentAdd, value: 20 }], info: "闪避概率提升 20%,持续 5 秒", }, // 命中强化(限时提升命中概率,百分点累加) 7031: { id: 7031, name: "命中强化", icon: "27-Hawkeye", category: BuffCategory.Buff, duration: 5, max_stack: 1, modifiers: [{ attr: Attrs.accuracy, op: ModOp.PercentAdd, value: 20 }], info: "命中概率提升 20%,持续 5 秒", }, }; // ===================== 文件: game/common/config/CardSet.ts ===================== import * as exp from "constants" import { HeroInfo, HeroList, HType, FieldEntry } from "./heroSet" import { oops } from "db://oops-framework/core/Oops" import { SkillOverrides, TGroup } from "./SkillSet" class I18nString { constructor(private key: string, private params?: any[]) { } private getTranslated(): string { let str = oops.language.getLangByID(this.key) || this.key; if (this.params && this.params.length > 0) { for (let i = 0; i < this.params.length; i++) { 像素大勇者游戏软件 V1.0            第 18 页 共 60 页 ============================================================================ str = str.replace(`{${i}}`, String(this.params[i])); } } return str; } toString() { return this.getTranslated(); } valueOf() { return this.getTranslated(); } toJSON() { return this.getTranslated(); } get length() { return this.toString().length; } } const t = (key: string, ...params: any[]) => new I18nString(key, params) as unknown as string; /** 卡牌大类定义 */ export enum CardType { Hero = 1, Skill = 2, SpecialUpgrade = 3, SpecialRefresh = 4, } /** 卡牌大类定义 */ export enum CKind { Hero = 1, //英雄 Skill = 2, //技能 Card = 3, //卡牌 Potion = 4, //药水 } /** * 卡牌技能触发类型 * - 命名对齐英雄侧 SkillTriggerType,便于跨模块认知统一 * - 枚举值从 1 开始,避免 0 的 falsy 坑(if (trigger_type) 判断出错) */ export enum CardTriggerType { Instant = 1, // 即时触发:使用后立即生效一次 Interval = 2, // 定时循环:战斗中按 t_inv 间隔重复触发 Field = 3, // 驻场光环(俗称「装备」):被动生效,由 field 数组驱动,支持单卡多词条 FightStart = 4, // 战斗开始时触发 HeroDead = 6, // 场上己方英雄死亡时触发 HeroCall = 7, // 英雄上场时触发(主角召唤 + 技能召唤 + 复活) } /** 通用卡牌配置 */ export interface CardConfig { uuid: number type: CardType cost: number weight: number kind: CKind rarity: number // 卡牌等级(驱动 bg_node 颜色:1=green 2=blue 3=purple 4=red 5=yellow) wave?: number // 针对技能卡:仅在指定回合(wave)才能抽到 hero_lv?: number base_rarity?: number // 技能卡扩展属性 skill?: number // 关联的技能 UUID icon?: string // 图标ID(可选,优先使用;未设置时按 trigger_type 从 SkillSet/FieldSkillSet 自动取) name?: string // 卡牌名称 info?: string // 卡牌描述信息 is_inst?: boolean // 是否即时起效 t_times?: number // 触发次数 像素大勇者游戏软件 V1.0            第 19 页 共 60 页 ============================================================================ t_inv?: number // 触发间隔(秒) keep_waves?: number // 维持的回合数(-1表示持续到战斗结束,0或undefined表示仅本回合) overrides?: SkillOverrides // 技能参数覆写(如自定义伤害ap、buff值、金币数等) field?: FieldEntry[] // 装备词条数组:驻场光环卡(trigger_type=Field)的属性词条,支持单卡多词条;value 为实际生效数值(如 [{uuid:7002,value:0.1},{uuid:7004,value:0.1}] 同时提供攻击/暴击) /** 触发类型(必填,技能卡专用;功能卡/英雄卡可缺省) */ trigger_type?: CardTriggerType; /** * 事件型触发的全局次数上限(仅 FightStart/HeroDead/HeroCall 有效) * 默认 Infinity;达到上限后销毁节点 * 注意:与 t_times 语义不同——t_times 控制每回合内 Interval 的次数 */ trigger_limit?: number; /** * 动态升级卡专属:目标英雄实体 eid(运行时唯一)。 * 仅当 type === CardType.SpecialUpgrade 且该卡由 buildDrawCards 动态生成时使用。 * 使用该卡时精确升级场上对应 eid 的英雄实体。 */ target_hero_eid?: number; } /** 基础卡池(技能、功能卡;英雄卡池已迁移至 heroSet.ts HeroCardPoolList) */ export const CardPoolList: CardConfig[] = []; export enum SpecialRefreshHeroType { Any = 0, Melee = 1, Ranged = 2, } /** 升级功能卡完整配置(作为动态升级卡的基础模板) */ export interface SpecialUpgradeCardConfig extends CardConfig { name: string info: string /** 模板字段:currentLv=0 表示动态卡(实际值由 target_hero_eid 对应的英雄决定) */ currentLv: number /** 模板字段:targetLv=0 表示"+1 级"(实际目标等级 = 当前等级 + 1) */ targetLv: number } /** 刷新功能卡完整配置 */ export interface SpecialRefreshCardConfig extends CardConfig { name: string info: string refreshLv: number refreshHeroType: SpecialRefreshHeroType } /** * 升级卡模板。 * 仅有 7001 一条记录,作为动态升级卡的基础配置(cost/weight/name/info)。 * 实际使用时由 MissionCardComp.buildDrawCards 根据场上英雄动态生成 CardConfig, * 通过 target_hero_eid 字段绑定具体英雄实体。 */ export const SpecialUpgradeCardList: Record = { 7001: { uuid: 7001, type: CardType.SpecialUpgrade, cost: 10, weight: 18, rarity: 1, kind: CKind.Card, name: t("scard_name_7001"), info: t("scard_info_7001"), currentLv: 0, targetLv: 0, }, 像素大勇者游戏软件 V1.0            第 20 页 共 60 页 ============================================================================ } export const SpecialRefreshCardList: Record = { 7101: { uuid: 7101, type: CardType.SpecialRefresh, cost: 3, weight: 14, rarity: 1, kind: CKind.Card, name: t("scard_name_7101"), info: t("scard_info_7101"), refreshLv: 0, refreshHeroType: SpecialRefreshHeroType.Melee, }, 7102: { uuid: 7102, type: CardType.SpecialRefresh, cost: 3, weight: 14, rarity: 1, kind: CKind.Card, name: t("scard_name_7102"), info: t("scard_info_7102"), refreshLv: 0, refreshHeroType: SpecialRefreshHeroType.Ranged, }, 7103: { uuid: 7103, type: CardType.SpecialRefresh, cost: 4, weight: 12, rarity: 1, kind: CKind.Card, name: t("scard_name_7103"), info: t("scard_info_7103"), refreshLv: 0, refreshHeroType: SpecialRefreshHeroType.Any, }, } // ============ 装备相关语义已迁移至 EquipSet.ts ============ // 装备卡池(EquipPoolList)独立管理,不再混入 CardPoolList /** 单次按权重抽取一张卡 */ const weightedPick = (cards: CardConfig[]): CardConfig | null => { if (cards.length === 0) return null const totalWeight = cards.reduce((total, card) => total + card.weight, 0) let random = Math.random() * totalWeight for (const card of cards) { random -= card.weight if (random <= 0) return card } return cards[cards.length - 1] } /** 连续抽取 count 张卡,允许重复或通过 unique 剔除重复 */ const pickCards = (cards: CardConfig[], count: number, unique: boolean = false): CardConfig[] => { if (cards.length === 0 || count <= 0) return [] const selected: CardConfig[] = [] let available = [...cards] while (selected.length < count) { if (available.length === 0) break const pick = weightedPick(available) if (!pick) break selected.push(pick) if (unique) { available = available.filter(c => c.uuid !== pick.uuid) } } return selected } const normalizeTypeFilter = (type: CardType | CardType[]): Set => { const list = Array.isArray(type) ? type : [type] return new Set(list) } /** * 通用按规则抽卡。 * @param options 抽卡选项 */ export const drawCardsByRule = ( 像素大勇者游戏软件 V1.0            第 21 页 共 60 页 ============================================================================ options: { count?: number type?: CardType | CardType[] heroType?: HType heroLv?: number wave?: number unique?: boolean } = {} ): CardConfig[] => { const count = Math.max(0, Math.floor(options.count ?? 4)) let pool = CardPoolList if (options.type !== undefined) { const typeSet = normalizeTypeFilter(options.type) pool = pool.filter(card => typeSet.has(card.type)) } if (options.heroType !== undefined || options.heroLv !== undefined) { pool = pool.filter(card => { if (card.type !== CardType.Hero) return false const hero = HeroInfo[card.uuid] if (!hero) return false if (options.heroType !== undefined && hero.type !== options.heroType) return false if (options.heroLv !== undefined && card.hero_lv !== options.heroLv) return false return true }) } // 如果传入了回合并且是技能卡,则根据 wave 过滤 if (options.wave !== undefined) { pool = pool.filter(card => { if (card.type === CardType.Skill) { return card.wave === options.wave; } return true; }) } const picked = pickCards(pool, count, options.unique) return picked } // ===================== 文件: game/common/config/CardSkillText.ts ===================== /** * @file CardSkillText.ts * @description 卡牌/道具语境技能描述文案层(从已弃用的 HeroSkillDesc.ts 迁出) * * 职责:不依赖英雄语境的卡牌/道具描述富文本生成,数值永远来自 * mergeSkillParams 合并后的实时配置,杜绝"改数值忘改文案"的漂移。 * * 输出(均为富文本 BBCode,RichText 直接可用): * - buildCardDescRich(card) —— 商店卡片描述(技能卡/药品卡/装备卡统一入口) * - buildEquipRich(entries, showName) —— 装备/驻场光环词条详情 * - buildSingleSkillRich(uuid, ov, lv) —— 单技能详情(CD/持续 + 效果,IBox 弹窗用) * * 英雄语境描述请使用 HeroSkillText.ts(heroInfo 内联模板驱动)。 * * 依赖: * - SkillSet / FieldSkillSet(技能/光环静态配置与合并) * - SkillDescSet / FieldDescSet(结构化描述模板) * - BuffList(计时 buff 默认值兜底) * - CardConfig(卡牌统一配置结构) 像素大勇者游戏软件 V1.0            第 22 页 共 60 页 ============================================================================ */ import { CardConfig } from "./CardSet"; import { FieldSkillSet, mergeSkillParams, SkillConfig, SkillKind, SkillOverrides, SkillSet, TGroup } from "./SkillSet"; import { FieldDescSet, FieldValueFmt, SkillDescSet, SkillDescConfig } from "./SkillDescSet"; import { BuffList } from "./BuffSet"; import { FieldEntry } from "./heroSet"; /** * 一条能力的结构化行:数值片段单独抽出,供富文本高亮与纯文本复用。 * 渲染结果 = prefix + value(可高亮) + suffix。 */ interface ISkillLine { /** 数值前的文本(含触发条件、技能名、目标、动词等) */ prefix: string; /** 需要高亮的核心数值文本(如 "15%"、"4次"、"+30"),无数值时为 "" */ value: string; /** 数值后的文本(如 ",持续5秒" 之类尾部补充) */ suffix: string; } /** 富文本数值高亮色(鲜亮绿色,深色面板上高亮醒目) */ const RICH_VALUE_COLOR = "#3CE66E"; /** 数值描边色(深绿,衬托亮绿填充,与技能名橙填充+深褐描边同一设计思路) */ const RICH_VALUE_OUTLINE = "#145A32"; /** * 数值富文本片段:亮绿填充 + 深绿描边。 * 高亮数值明度高,深色描边增强边缘对比、提升深色面板上的锐度。 * @param text 数值文本(含单位,如 "15%"、"4次") */ function valueRich(text: string | number): string { return `${text}`; } /** 富文本技能名高亮色(蜜桃橙,暖色系,与面板绿/蓝/红/黄/紫均不冲突) */ const RICH_NAME_COLOR = "#F8C471"; /** 技能名描边色(深褐,衬托蜜桃橙填充,深色面板上温馨醒目) */ const RICH_NAME_OUTLINE = "#6E2C00"; /** * 技能名富文本片段:[技能名]整体蜜桃橙填充 + 深褐描边(含方括号)。 * 括号与技能名同色同描边,正文/数值不描边,突出技能名视觉层级。 * @param name 技能名(纯文本,不含括号) * @returns [技能名] 富文本片段 */ function skillNameRich(name: string): string { return `[${name}]`; } /** 技能名「」匹配模式(仅本模块行首/行中技能名片段使用) */ const SKILL_NAME_PATTERN = /「([^」]*)」/g; /** "持续X秒"匹配模式(卡片描述后处理:把持续时间数值也高亮) */ const DURATION_PATTERN = /持续(\d+)秒/g; /** 解析目标描述文本(与战斗 SCastSystem 目标选取语义严格一致) */ function buildTargetDesc(skill: SkillConfig): string { switch (skill.TGroup) { 像素大勇者游戏软件 V1.0            第 23 页 共 60 页 ============================================================================ case TGroup.Self: return "自身"; case TGroup.Enemy: return "敌方"; case TGroup.Ally: case TGroup.All: return "全体友方"; case TGroup.Team: { const n = Math.max(1, Math.floor(skill.hit_count ?? 1)); // 治疗类命中低血量队友(SCastSystem.pickHealTargetsByMostMissingHp) if (skill.kind === SkillKind.Heal) { return n > 1 ? `${n}名低血量队友` : "低血量队友"; } return `${n}名队友`; } default: return "全体友方"; } } /** * 占位符模板替换,并把"核心数值片段"用 \x01...\x02 标记,便于上层拆分高亮。 * * Why: 模板中 {value}{unit} 是玩家最关心的数值。这里在渲染时将其单独包裹, * 使富文本可对其单独上色,保证高亮片段与模板数值严格一致。 * * @returns 含 \x01hlValue\x02 标记的完整文本 */ function renderTemplate(templ: string, vars: Record, hlValue: string): string { let out = templ; for (const key of Object.keys(vars)) { out = out.split(`{${key}}`).join(String(vars[key])); } // 用 \x01...\x02 包裹数值片段作为标记,便于上层拆分为 prefix/value/suffix return out.split(hlValue).join(`\x01${hlValue}\x02`); } /** * 由一行模板渲染结果构造结构化行(剥离 \x01\x02 标记,把数值片段拆到 value 字段) * @param head 行首(触发条件:技能名 ) * @param rendered 已用 \x01...\x02 标记数值片段的效果文本 */ function makeLine(head: string, rendered: string): ISkillLine { const start = rendered.indexOf("\x01"); const end = rendered.indexOf("\x02"); if (start >= 0 && end > start) { // 恰好含一个数值片段:prefix=行首+数值前文本,value=数值,suffix=数值后文本 return { prefix: `${head}${rendered.slice(0, start)}`, value: rendered.slice(start + 1, end), suffix: rendered.slice(end + 1), }; } // 无数值片段(未命中模板或兜底 info):剥离残留标记,整行作为 prefix,不高亮 return { prefix: `${head}${rendered.replace(/[\x01\x02]/g, "")}`, value: "", suffix: "" }; } /** * 根据合并后的技能配置构造结构化效果行(查 SkillDescSet 模板渲染 + 数值高亮抽取) * 像素大勇者游戏软件 V1.0            第 24 页 共 60 页 ============================================================================ * 计时模式默认值兜底: * overrides 仅给 timed_buff_id 而未覆写 buff_value / buff_duration 时, * 从 BuffList[timed_buff_id] 取默认值,保证 1 级基础档文案完整。 */ function buildEffectLine(head: string, skill: SkillConfig, descOverride?: SkillDescConfig): ISkillLine { const desc = descOverride ?? SkillDescSet[skill.uuid]; // 未入表技能回退基础备注 info,无法抽出数值,整行不高亮 if (!desc) return { prefix: `${head}${String(skill.info ?? "")}`, value: "", suffix: "" }; const buff = skill.timed_buff_id !== undefined ? BuffList[skill.timed_buff_id] : undefined; const isTimed = buff !== undefined && !!desc.templTimed; const templ = isTimed ? desc.templTimed! : desc.templPerm; const valueField = isTimed ? (desc.timedField ?? desc.permField) : desc.permField; // 计时模式默认值兜底:buff_value 未覆写时用 BuffList 默认修饰值 let value = (skill[valueField as keyof SkillConfig] as number | undefined); if (value === undefined && isTimed) { value = buff!.modifiers?.[0]?.value ?? buff!.tick?.damage_or_heal ?? 0; } value = value ?? 0; const dur = skill.buff_duration ?? buff?.duration ?? 0; // 高亮片段 = 数值 + 单位(如 "15%"、"4次"、"+30"),是玩家视觉焦点 const hlValue = `${value}${desc.unit}`; const rendered = renderTemplate(templ, { target: buildTargetDesc(skill), value: value, unit: desc.unit, verb: desc.verb, dur: dur, }, hlValue); return makeLine(head, rendered); } /** * 驻场光环(field)结构化行:查 FieldDescSet 按 type 渲染,value 实时取数值并高亮。 * * Why: FieldSkillConfig.info 是手写文案,改 value 易漂移。这里以 FieldSkillType * 为 key 查标准动词,数值按 fmt(比例/整数/减免)从 value 实时渲染。 * * @param showName 是否在行中包含「技能名」片段(英雄面板需要,装备商店不需要) */ function buildFieldLine(head: string, entry: FieldEntry, showName: boolean = true): ISkillLine | null { const fs = FieldSkillSet[entry.uuid]; if (!fs) return null; const desc = FieldDescSet[fs.type]; const nameSeg = showName ? `「${fs.name}」 ` : ""; // 未入表类型回退基础备注 info,整行不高亮 if (!desc) return { prefix: `${head}${nameSeg}${fs.info}`, value: "", suffix: "" }; // 数值按格式渲染为高亮片段(数值取自词条自身 value) let valueText: string; switch (desc.fmt) { case FieldValueFmt.Percent: valueText = `+${Math.round(entry.value * 100)}%`; break; case FieldValueFmt.IntMinus: valueText = `-${entry.value}`; break; case FieldValueFmt.Int: 像素大勇者游戏软件 V1.0            第 25 页 共 60 页 ============================================================================ default: valueText = `+${entry.value}`; break; } return { prefix: `${head}${nameSeg}${desc.verb}`, value: valueText, suffix: "" }; } /** * 把"持续X秒"中的 X 也包裹绿色高亮(卡片描述后处理)。 * Why: 计时模板 {dur} 不在 \x01\x02 高亮标记内,结构化行仅高亮主数值; * 持续时间对药水/计时 buff 同样关键,这里补充高亮,保证视觉一致。 */ function highlightDuration(text: string): string { return text.replace(DURATION_PATTERN, `持续${valueRich("$1")}秒`); } /** * 结构化行 → 富文本串(技能名橙色、数值绿色)。 * buildEquipRich / buildCardDescRich 共用,保证渲染规则唯一。 */ function renderLineRich(line: ISkillLine): string { const prefix = line.prefix.replace(SKILL_NAME_PATTERN, (m, g1) => skillNameRich(g1)); return line.value ? `${prefix}${valueRich(line.value)}${line.suffix}` : `${prefix}${line.suffix}`; } /** * 扫描合并后技能的附加概率字段(crt/frz/stun/bck),非零的拼为高亮后缀。 * Why: 模板不再硬编码"有概率击晕"等文案,由实际配置驱动,避免描述与数值漂移。 * @returns 形如 ",30%击晕,20%冰冻";全为 0 时返回 "" */ function buildChanceSuffix(skill: SkillConfig): string { const items: string[] = []; const push = (val: number | undefined, label: string) => { if (val && val > 0) items.push(`附加${valueRich(val)}%${label}`); }; push(skill.crt, "暴击"); push(skill.frz, "冰冻"); push(skill.stun, "击晕"); push(skill.bck, "击退"); return items.length > 0 ? `,${items.join(",")}` : ""; } /** * 单个技能详情富文本(IBox 弹窗用):展示 CD/持续时间 + 结构化效果描述。 * @param skillUuid SkillSet 技能 UUID * @param overrides 可选覆写参数(如药水卡自定义档位) * @param lv 技能等级(仅用于展示,不影响数值计算) * @returns 多行富文本串:第一行名称/等级/持续时间,第二行效果描述 */ export function buildSingleSkillRich(skillUuid: number, overrides?: SkillOverrides, lv: number = 1): string { const base = SkillSet[skillUuid]; if (!base) return "未知技能"; const skill = mergeSkillParams(base, overrides); // SkillConfig 未定义 cd 字段,冷却时间由外部运行时数据传入;计时模式展示持续时间 const dur = skill.buff_duration ?? (skill.timed_buff_id !== undefined ? BuffList[skill.timed_buff_id]?.duration : undefined) ?? 0; const cdTag = dur > 0 ? valueRich(`持续:${dur}s`) : ""; const nameTag = `${skillNameRich(skill.name)} Lv.${lv}`; 像素大勇者游戏软件 V1.0            第 26 页 共 60 页 ============================================================================ // 复用结构化效果行,head 为空避免重复触发类型前缀 const line = buildEffectLine("", skill); const prefix = line.prefix.replace(SKILL_NAME_PATTERN, (m, g1) => skillNameRich(g1)); const effect = line.value ? `${prefix}${valueRich(line.value)}${line.suffix}` : `${prefix}${line.suffix}`; return cdTag ? `${nameTag} ${cdTag}\n${effect}` : `${nameTag}\n${effect}`; } /** * 装备/驻场光环详情富文本:展示全部词条,数值绿色高亮。 * @param entries 驻场光环词条数组(uuid 指向 FieldSkillSet 底座,value 为生效数值) * @param showName 是否包含「技能名」片段(英雄面板 true,装备商店 false) * @returns 多行富文本串,每行一个词条 */ export function buildEquipRich(entries: FieldEntry[], showName: boolean = false): string { if (!entries?.length) return "无词条"; const lines: string[] = []; for (const entry of entries) { const line = buildFieldLine("", entry, showName); if (!line) continue; const prefix = line.prefix.replace(SKILL_NAME_PATTERN, (m, g1) => skillNameRich(g1)); const text = line.value ? `${prefix}${valueRich(line.value)}${line.suffix}` : `${prefix}${line.suffix}`; lines.push(text); } return lines.length > 0 ? lines.join("\n") : "无词条"; } /** * 商店卡片描述富文本(SCardComp / ItemListComp 用,BBCode)。 * * Why: 卡片 info 为手写文案,与 SkillSet/BuffList 数值易漂移。 * 本函数从 CardConfig.skill + overrides 合并后的实时配置生成描述, * 数值随档位(如高级药水 buff_value)自动同步,数值绿色高亮、技能名橙色。 * * 描述规则: * - 药品/技能卡(card.skill 有效)→ 查 SkillDescSet 模板渲染(计时模式取 BuffList 默认值兜底); * 未入表技能回退 SkillSet.info / card.info,整行不高亮。 * - 装备卡(card.field 有效)→ 复用 buildEquipRich 逐词条渲染。 * - 触发节奏(t_times/t_inv)融入单行描述:定时触发"每M秒召唤K个「技能名」攻击敌人,xxx,共N次"(K=overrides.num+1);即时触发仅显示效果。 * * @param card 卡片配置(SCardSet 技能卡 / ICardSet 药品卡 / EquipSet 装备卡) * @returns 多行富文本串;无可描述内容时回退 card.info */ export function buildCardDescRich(card: CardConfig): string { // 装备卡:逐词条渲染驻场光环 if (card.field && card.field.length > 0) { return buildEquipRich(card.field); } // 药品/技能卡:合并覆写后按 SkillDescSet 模板渲染 const base = card.skill ? SkillSet[card.skill] : undefined; if (base) { const skill = mergeSkillParams(base, card.overrides); const line = buildEffectLine("", skill); // 未入表技能回退链:SkillSet.info → card.info if (!line.value && !SkillDescSet[skill.uuid]) { 像素大勇者游戏软件 V1.0            第 27 页 共 60 页 ============================================================================ return String(skill.info || card.info || ""); } // 触发节奏融入描述:定时触发"每M秒释放一次「技能名」,xxx,共N次";即时触发直接描述 const tTimes = card.t_times ?? 1; const tInv = card.t_inv ?? 0; const isInstant = card.is_inst || tInv <= 0; // 附加概率后缀(暴击/冰冻/击晕/击退),由实际配置驱动 const suffix = buildChanceSuffix(skill); const rich = highlightDuration(renderLineRich(line)) + suffix; if (isInstant) { return rich; } const rhythm = `每${valueRich(tInv)}秒召唤${valueRich((card.overrides?.num ?? 0) + 1)}个${skillNameRich(skill.name)}攻击敌人`; const times = `, 共${valueRich(tTimes)}次`; return `${rhythm}, ${rich}${times}`; } return card.info || ""; } // ===================== 文件: game/common/config/EquipSet.ts ===================== /** * @file EquipSet.ts * @description 装备卡池独立配置 * * 职责: * 定义所有装备卡的静态数据,与技能卡池(CardSet.CardPoolList)彻底分离。 * 装备卡不再参与抽卡,改为商店直购,任务开始时一次性全部展示。 * * 装备生效机制: * 购买 → 派发 GameEvent.UseEquipCard → MissEquipComp 接收 → EBox ECS 实体 * → EquipBoxComp(trigger_type=Field,被动驻场)→ FieldSkillHelper 全局聚合属性加成 * * 依赖: * - CardConfig / CardTriggerType / CardType / CKind(CardSet) * - FieldSkillSet(SkillSet)—— 属性词条配置 */ import { CardConfig, CardType, CKind, CardTriggerType } from "./CardSet"; import { FieldEntry } from "./heroSet"; /** 装备卡原始数据(仅装备相关字段,由 EquipSet 内部转换为 CardConfig) */ interface EquipRawData { uuid: number; rarity: number; name: string; info: string; icon?: string; // 图标ID(对应 ui6 图集 frame 名) field: FieldEntry[]; // 驻场光环词条:uuid 指向 FieldSkillSet 底座,value 为实际生效数值 cost?: number; } /** 装备卡原始数据表 */ const EquipRawList: EquipRawData[] = [ // ==================== rarity 1 档(基础强度) ==================== { uuid: 8701, rarity: 1, icon: "Helmet-FA_Helmet_021_Gray", name: "战后恢复", info: "战斗结束生命回复量+10%", field: [{ uuid: 7001, value: 0.1 }] }, { uuid: 8702, rarity: 1, icon: "Sword-FA_WP_Main_Sword_003_GrayGold", name: "攻击加成", info: "全体攻击力+10%", field: [{ uuid: 7002, value: 0.1 }] }, { uuid: 8703, rarity: 1, icon: "Blunt-FA_WP_Main_Blunt_002_Wood", name: "击晕加成", info: "全体击晕概率+10%", field: [{ uuid: 7003, value: 0.1 }] }, 像素大勇者游戏软件 V1.0            第 28 页 共 60 页 ============================================================================ { uuid: 8704, rarity: 1, icon: "Axe-FA_WP_Main_Axe_001_WoodGray", name: "暴击加成", info: "全体暴击率+10%", field: [{ uuid: 7004, value: 0.1 }] }, { uuid: 8705, rarity: 1, icon: "Bow-FA_WP_Main_Bow_003_WoodGreen", name: "暴伤加成", info: "全体暴击伤害+20%", field: [{ uuid: 7005, value: 0.2 }] }, { uuid: 8706, rarity: 1, icon: "Crossbow-FA_WP_Main_Crossbow_001_WoodWhite", name: "攻速加成", info: "全体攻击速度+10%", field: [{ uuid: 7006, value: 0.1 }] }, { uuid: 8707, rarity: 1, icon: "Chest-FA_Chest_014_Gray", name: "生命加成", info: "全体最大生命+10%", field: [{ uuid: 7007, value: 0.1 }] }, // 风怒机制已停用: 8708 风怒加成卡已移除 // { uuid: 8708, rarity: 1, icon: "Wand-FA_WP_Main_Wand_001_GrayGreen", name: "风怒加成", info: "全体风怒概率+10%", field: [{ uuid: 7008, value: 0.1 }] }, { uuid: 8709, rarity: 1, icon: "Spear-FA_WP_Main_Spear_004_WoodBlue", name: "穿刺加成", info: "全体穿刺概率+10%", field: [{ uuid: 7009, value: 0.1 }] }, { uuid: 8712, rarity: 1, icon: "Spear-FA_WP_Main_Spear_004_WoodBlue", name: "破甲加成", info: "全体穿透后伤害+10%", field: [{ uuid: 7020, value: 0.1 }] }, // 多词条装备 { uuid: 8720, rarity: 1, icon: "Shield-FA_WP_Sub_Shield_007_GoldGray", name: "勇者护符", info: "全体攻击力+10%, 暴击率+10%, 暴击伤害+20%", field: [{ uuid: 7002, value: 0.1 }, { uuid: 7004, value: 0.1 }, { uuid: 7005, value: 0.2 }] }, // ==================== rarity 2 档(强度 ×2) ==================== { uuid: 8751, rarity: 2, icon: "Helmet-FA_Helmet_022_DarkBlue", name: "战后恢复", info: "战斗结束生命回复量+20%", field: [{ uuid: 7001, value: 0.2 }] }, { uuid: 8752, rarity: 2, icon: "Sword-FA_WP_Main_Sword_007_Blue", name: "攻击加成", info: "全体攻击力+20%", field: [{ uuid: 7002, value: 0.2 }] }, { uuid: 8753, rarity: 2, icon: "Blunt-FA_WP_Main_Blunt_004_Gray", name: "击晕加成", info: "全体击晕概率+20%", field: [{ uuid: 7003, value: 0.2 }] }, { uuid: 8754, rarity: 2, icon: "Axe-FA_WP_Main_Axe_006_WoodBronze", name: "暴击加成", info: "全体暴击率+20%", field: [{ uuid: 7004, value: 0.2 }] }, { uuid: 8755, rarity: 2, icon: "Bow-FA_WP_Main_Bow_010_Blue", name: "暴伤加成", info: "全体暴击伤害+40%", field: [{ uuid: 7005, value: 0.4 }] }, { uuid: 8756, rarity: 2, icon: "Crossbow-FA_WP_Main_Crossbow_004_Gray", name: "攻速加成", info: "全体攻击速度+20%", field: [{ uuid: 7006, value: 0.2 }] }, { uuid: 8757, rarity: 2, icon: "Chest-FA_Chest_014_BlueGold", name: "生命加成", info: "全体最大生命+20%", field: [{ uuid: 7007, value: 0.2 }] }, // 风怒机制已停用: 8758 风怒加成卡已移除 // { uuid: 8758, rarity: 2, icon: "Wand-FA_WP_Main_Wand_003_BlueWood", name: "风怒加成", info: "全体风怒概率+20%", field: [{ uuid: 7008, value: 0.2 }] }, { uuid: 8759, rarity: 2, icon: "Spear-FA_WP_Main_Spear_007_GraySkyblue", name: "穿刺加成", info: "全体穿刺概率+20%", field: [{ uuid: 7009, value: 0.2 }] }, { uuid: 8763, rarity: 2, icon: "Spear-FA_WP_Main_Spear_007_GraySkyblue", name: "破甲加成", info: "全体穿透后伤害+20%", field: [{ uuid: 7020, value: 0.2 }] }, // 多词条装备 { uuid: 8770, rarity: 2, icon: "Chest-FA_Chest_014_BlueGold", name: "守卫护甲", info: "全体最大生命+20%, 战后生命回复+20%, 攻速+20%", field: [{ uuid: 7007, value: 0.2 }, { uuid: 7001, value: 0.2 }, { uuid: 7006, value: 0.2 }] }, // ==================== rarity 3 档(强度 ×3) ==================== { uuid: 8801, rarity: 3, icon: "Helmet-FA_Helmet_021_RedGold", name: "战后恢复", info: "战斗结束生命回复量+30%", field: [{ uuid: 7001, value: 0.3 }] }, { uuid: 8802, rarity: 3, icon: "Sword-FA_WP_Main_Sword_001_Gold", name: "攻击加成", info: "全体攻击力+30%", field: [{ uuid: 7002, value: 0.3 }] }, { uuid: 8803, rarity: 3, icon: "Blunt-FA_WP_Main_Blunt_006_OrangeGray", name: "击晕加成", info: "全体击晕概率+30%", field: [{ uuid: 7003, value: 0.3 }] }, { uuid: 8804, rarity: 3, icon: "Axe-FA_WP_Main_Axe_012_WoodGold", name: "暴击加成", info: "全体暴击率+30%", field: [{ uuid: 7004, value: 0.3 }] }, { uuid: 8805, rarity: 3, icon: "Bow-FA_WP_Main_Bow_010_Gold", name: "暴伤加成", info: "全体暴击伤害+60%", field: [{ uuid: 7005, value: 0.6 }] }, { uuid: 8806, rarity: 3, icon: "Crossbow-FA_WP_Main_Crossbow_005_WoodGold", name: "攻速加成", info: "全体攻击速度+30%", field: [{ uuid: 7006, value: 0.3 }] }, { uuid: 8807, rarity: 3, icon: "Chest-FA_Chest_015_RedGold", name: "生命加成", info: "全体最大生命+30%", field: [{ uuid: 7007, value: 0.3 }] }, // 风怒机制已停用: 8808 风怒加成卡已移除 // { uuid: 8808, rarity: 3, icon: "Wand-FA_WP_Main_Wand_004_WoodGold", name: "风怒加成", info: "全体风怒概率+30%", field: [{ uuid: 7008, value: 0.3 }] }, { uuid: 8809, rarity: 3, icon: "Spear-FA_WP_Main_Spear_008_GrayGold", name: "穿刺加成", info: "全体穿刺概率+30%", field: [{ uuid: 7009, value: 0.3 }] }, { uuid: 8817, rarity: 3, icon: "Spear-FA_WP_Main_Spear_008_GrayGold", name: "破甲加成", info: "全体穿透后伤害+30%", field: [{ uuid: 7020, value: 0.3 }] }, // 触发次数强化 { uuid: 8811, rarity: 3, icon: "Wand-FA_WP_Main_Wand_004_WoodGold", name: "召唤强化", info: "召唤触发技能次数+1", field: [{ uuid: 7014, value: 1 }] }, { uuid: 8812, rarity: 3, icon: "Staff-FA_WP_Main_Staff_005_WoodGold", name: "死亡强化", info: "死亡触发技能次数+1", field: [{ uuid: 7015, value: 1 }] }, { uuid: 8813, rarity: 3, icon: "Spear-FA_WP_Main_Spear_008_GrayGold", name: "开场强化", info: "战斗开始触发技能次数+1", field: [{ uuid: 7016, value: 1 }] }, { uuid: 8815, rarity: 3, icon: "Sword-FA_WP_Main_Sword_001_Gold", name: "攻击强化", info: "攻击触发技能次数+1", field: [{ uuid: 7018, value: 1 }] }, { uuid: 8816, rarity: 3, icon: "Shield-FA_WP_Sub_Shield_009_RedGold", name: "受击强化", info: "被攻击触发技能次数+1", field: [{ uuid: 7019, value: 1 }] }, // 多词条装备 { uuid: 8820, rarity: 3, icon: "Sword-FA_WP_Main_Sword_001_Gold", name: "神圣之刃", info: "全体攻击力+30%, 暴击率+30%, 暴击伤害+60%, 攻速+30%", field: [{ uuid: 7002, value: 0.3 }, { uuid: 7004, value: 0.3 }, { uuid: 7005, value: 0.6 }, { uuid: 7006, value: 0.3 }] }, ]; /** * 装备卡池列表(转换为 CardConfig 格式) * * 所有装备卡统一配置: * - type: Skill(复用卡牌体系) * - trigger_type: Field(被动驻场) * - keep_waves: -1(持续到战斗结束) * - is_inst: false(不主动施法) * - cost: 由原始数据指定,默认 0 */ export const EquipPoolList: CardConfig[] = EquipRawList.map(data => ({ uuid: data.uuid, 像素大勇者游戏软件 V1.0            第 29 页 共 60 页 ============================================================================ type: CardType.Skill, cost: data.cost ?? 0, weight: 10, kind: CKind.Skill, rarity: data.rarity, icon: data.icon, name: data.name, info: data.info, is_inst: false, t_times: 999, t_inv: 0, keep_waves: -1, field: data.field, trigger_type: CardTriggerType.Field, })); /** * 按权重抽取 N 张装备卡(unique 保证一次刷新内不重复,不足时允许重复补齐)。 * @param count 需要抽取的数量 */ export function drawEquipCards(count: number): CardConfig[] { const safeCount = Math.max(0, Math.floor(count)); if (EquipPoolList.length === 0 || safeCount <= 0) return []; const picked: CardConfig[] = []; let available = [...EquipPoolList]; while (picked.length < safeCount) { if (available.length === 0) break; const pick = weightedPick(available); if (!pick) break; picked.push(pick); available = available.filter(c => c.uuid !== pick.uuid); } // 不足时允许重复补齐 const filled = [...picked]; while (filled.length < safeCount) { const fallback = weightedPick(EquipPoolList); if (!fallback) break; filled.push(fallback); } return filled; } /** 单次按权重抽取一张卡 */ function weightedPick(cards: CardConfig[]): CardConfig | null { if (cards.length === 0) return null; const totalWeight = cards.reduce((total, card) => total + (card.weight ?? 0), 0); let random = Math.random() * totalWeight; for (const card of cards) { random -= (card.weight ?? 0); if (random <= 0) return card; } return cards[cards.length - 1]; } /** * 按 UUID 查找装备卡配置 * @param uuid 装备卡 UUID */ 像素大勇者游戏软件 V1.0            第 30 页 共 60 页 ============================================================================ export function findEquipByUuid(uuid: number): CardConfig | undefined { return EquipPoolList.find(e => e.uuid === uuid); } // ===================== 文件: game/common/config/GameEvent.ts ===================== /* */ /** 游戏事件 */ export enum GameEvent { /** 游戏服务器连接成功 */ GameServerConnected = "GameServerConnected", /** 登陆成功 */ LoginSuccess = "LoginSuccess", MAP_MOVE_END_LEFT = "MAP_MOVE_END_LEFT", MAP_MOVE_END_RIGHT = "MAP_MOVE_END_RIGHT", GuideStart = "GuideStart", GuideEnd = "GuideEnd", GuideComplete = "GuideComplete", CastSkill = "CastSkill", CardsClose = "CardsClose", CardRefresh = "CardRefresh", /** 单张卡牌被点击选中(payload 为被点击的 CHeroComp 实例,用于其他卡牌联动隐藏 call_btn) */ CardSelected = "CardSelected", /** 单张卡牌被成功使用(payload 为该卡槽组件实例 CHeroComp / SCardComp,用于 MissionCardComp 补卡) */ CardUsed = "CardUsed", UseHeroCard = "UseHeroCard", UseSkillCard = "UseSkillCard", UseSpecialCard = "UseSpecialCard", CallHero = "CallHero", Zhaohuan = "Zhaohuan", MissionLoss = "MissionLoss", MissionWin = "MissionWin", MissionStart = "MissionStart", FightReady = "FightReady", FightStart = "FightStart", FightPause = "FightPause", FightResume = "FightResume", MissionEnd = "MissionEnd", MissionComplete = "MissionComplete",//战斗结算完成 HeroSkillSelect = "HeroSkillSelect", HeroSkillSelectEnd = "HeroSkillSelectEnd", HeroSelect = "HeroSelect", EnhancementSelect = "EnhancementSelect", CanUpdateLv = "CanUpdateLv", UseEnhancement = "UseEnhancement", MasterCalled = "MasterCalled", CastHeroSkill = "CastHeroSkill", EquipAdd = "EquipAdd", EquipSelect = "EquipSelect", FuncSelect = "FuncSelect", TalentSelect = "TalentSelect", UseTalentCard = "UseTalentCard", UseItemCard = "UseItemCard", UseAttrCard = "UseAttrCard", AttrSelect = "AttrSelect", NewWave = "NewWave", AD_BACK_TRUE = "AD_BACK_TRUE", AD_BACK_FALSE = "AD_BACK_FALSE", 像素大勇者游戏软件 V1.0            第 31 页 共 60 页 ============================================================================ if (this.remainLab) this.remainLab.string = `剩余 ${remain}`; if (this.progress) { this.progress.progress = this.waveTotal > 0 ? Math.min(1, Math.max(0, (this.waveTotal - remain) / this.waveTotal)) : 0; } } } // ===================== 文件: game/skill/oncend.ts ===================== import { _decorator, Component, Animation } from 'cc'; const { ccclass } = _decorator; @ccclass('oneCom') export class oneCom extends Component { private anim: Animation | null = null; start() { this.anim = this.node.getComponent(Animation); if (!this.anim) { this.node.destroy(); return; } this.anim.off(Animation.EventType.FINISHED, this.onAnimationFinished, this); this.anim.on(Animation.EventType.FINISHED, this.onAnimationFinished, this); } onDestroy() { if (this.anim && this.anim.isValid) { this.anim.off(Animation.EventType.FINISHED, this.onAnimationFinished, this); } this.anim = null; } onAnimationFinished() { this.node.destroy(); } } // ===================== 文件: game/skill/SDataCom.ts ===================== import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; import { BoxSet } from "../common/config/GameSet"; import { TimedDebuffSnapshot } from "../hero/DamageQueueComp"; /** 业务层对象 */ //技能数据 @ecs.register('SDataCom') export class SDataCom extends ecs.Comp { /** 业务层组件移除时,重置所有数据为默认值 */ Attrs:any=null casterEid: number = -1; // 施法者实体ID,用于安全校验 group:BoxSet=BoxSet.HERO fac: number = 0; // 0:hero 1:monster s_uuid:number=0 skill_lv:number=0 ext_dmg:number=0 //额外伤害 dmg_ratio:number=1 //伤害比例 hit_count:number=0 //击中数量 像素大勇者游戏软件 V1.0            第 32 页 共 60 页 ============================================================================ max_hit_count:number=0 //最大可命中次数 /** 命中后对敌 debuff(由 Skill.load 从合并后配置快照;DamageEvent 透传给 HeroAtkSystem 结算) */ timed_debuff?: TimedDebuffSnapshot; reset() { this.Attrs=null this.group=BoxSet.HERO this.fac=0 this.s_uuid=0 this.skill_lv=0 this.casterEid = -1; this.hit_count=0 this.max_hit_count=0 this.ext_dmg=0 this.dmg_ratio=1 this.timed_debuff=undefined } } // ===================== 文件: game/skill/Skill.ts ===================== import { BoxCollider2D, instantiate, Node, Prefab, v3, Vec3, NodePool } from "cc"; import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; import { EType, SkillSet, SkillOverrides, mergeSkillParams } from "../common/config/SkillSet"; import { oops } from "db://oops-framework/core/Oops"; import { HeroAttrsComp } from "../hero/HeroAttrsComp"; import { Attrs } from "../common/config/HeroAttrs"; import { SkillView } from "./SkillView"; import { SDataCom } from "./SDataCom"; import { SMoveDataComp } from "../skill/SMoveComp"; import { StimeDataComp } from "./STimeComp"; import { HeroViewComp } from "../hero/HeroViewComp"; import { smc } from "../common/SingletonModuleComp"; import { mLogger } from "../common/Logger"; /** Skill 模块 */ @ecs.register(`Skill`) export class Skill extends ecs.Entity { private debugMode: boolean = false; /** 多键对象池:Map */ static pools: Map = new Map(); static readonly MAX_POOL_SIZE: number = 48; static readonly MAX_POOL_TOTAL: number = 192; private static totalPoolSize(): number { let total = 0; this.pools.forEach((pool) => { total += pool.size(); }); return total; } static getFromPool(path: string): Node | null { if (this.pools.has(path)) { const pool = this.pools.get(path)!; while (pool.size() > 0) { const node = pool.get(); if (node && node.isValid) { return node; } } 像素大勇者游戏软件 V1.0            第 33 页 共 60 页 ============================================================================ } return null; } static putToPool(path: string, node: Node) { if (!node || !node.isValid) return; if (!this.pools.has(path)) { this.pools.set(path, new NodePool()); } const pool = this.pools.get(path)!; if (pool.size() >= this.MAX_POOL_SIZE || this.totalPoolSize() >= this.MAX_POOL_TOTAL) { node.destroy(); return; } pool.put(node); } static clearPools() { this.pools.forEach((pool) => { while (pool.size() > 0) { const node = pool.get(); if (node && node.isValid) { node.destroy(); } } pool.clear(); }); this.pools.clear(); } static getPoolStats() { let total = 0; this.pools.forEach((pool) => { total += pool.size(); }); return { paths: this.pools.size, total, maxPerPath: this.MAX_POOL_SIZE, maxTotal: this.MAX_POOL_TOTAL }; } /** ---------- 数据层 ---------- */ SDataCom!: SDataCom; SMoveCom!: SMoveDataComp /** 当前技能的预制体路径(用于对象池回收) */ private prefabPath: string = ""; private skillNode: Node | null = null; // 持有节点引用以便销毁时回收 /** ---------- 业务层 ---------- */ // SkillBll!: SkillBllComp; /** ---------- 视图层 ---------- */ SView!: SkillView; /** 实始添加的数据层组件 */ protected init() { this.addComponents(SDataCom); 像素大勇者游戏软件 V1.0            第 34 页 共 60 页 ============================================================================ this.addComponents(SMoveDataComp); } load(startPos: Vec3, parent: Node, s_uuid: number, targetPos: Vec3, caster: HeroViewComp, cAttrsComp: HeroAttrsComp, skill_lv: number = 0, ext_dmg: number = 0, overrides?: SkillOverrides) { let config = SkillSet[s_uuid]; if (!config) { mLogger.error(this.debugMode, 'Skill', "[Skill] 技能配置不存在:", s_uuid); return; } config = mergeSkillParams(config, overrides); const path = `game/skill/atk/${config.sp_name}`; const initSkillNode = (prefab: Prefab) => { const node: Node = Skill.getFromPool(path) || instantiate(prefab); if (!node || !node.isValid) { mLogger.error(this.debugMode, 'Skill', "[Skill] 节点无效:", path); return; } this.prefabPath = path; this.skillNode = node; let skillParent: Node | null = null; if (smc.map && smc.map.MapView && smc.map.MapView.scene && smc.map.MapView.scene.entityLayer && smc.map.MapView.scene.entityLayer.node) { skillParent = smc.map.MapView.scene.entityLayer.node.getChildByName("SKILL"); } if (!skillParent || !skillParent.isValid) { skillParent = parent; } if (!skillParent || !skillParent.isValid) { mLogger.error(this.debugMode, 'Skill', "[Skill] 父节点无效"); if (node.isValid) node.destroy(); return; } node.parent = skillParent; node.active = true; // 设置节点属性 let face = caster.node.scale.x < 0 ? -1 : 1 node.setScale(v3(Math.abs(node.scale.x) * face, node.scale.y, 1)) // 初始视图 const SView = node.getComponent(SkillView); if (!SView) { mLogger.error(this.debugMode, 'Skill', "[Skill] SkillView 组件缺失:", path); if (node.isValid) node.destroy(); return; } if (SView.endType != EType.collision) { const collider = node.getComponent(BoxCollider2D); if (collider) { collider.enabled = false } } // 只设置必要的运行时属性,配置信息通过 SkillSet[uuid] 访问 // 核心标识 SView.s_uuid = s_uuid SView.group = caster.box_group 像素大勇者游戏软件 V1.0            第 35 页 共 60 页 ============================================================================ this.add(SView); // 强制出生点:-1=不强制使用 atk_x/atk_y 偏移,>=0=强制世界坐标 if (SView.forceStartX >= 0) { startPos.x = SView.forceStartX; } else { startPos.x = startPos.x + SView.atk_x * face; } if (SView.forceStartY >= 0) { startPos.y = SView.forceStartY; } else { startPos.y = startPos.y + SView.atk_y; } node.setPosition(startPos); // 初始化移动组件 - 从SkillView获取移动参数 let sMoveCom = this.get(SMoveDataComp); if (!sMoveCom) { sMoveCom = this.add(SMoveDataComp); } sMoveCom.reset(); // 复用组件时重置状态 sMoveCom.startPos.set(startPos); sMoveCom.targetPos.set(targetPos); sMoveCom.s_uuid = s_uuid; sMoveCom.scale = caster.node.scale.x < 0 ? -1 : 1; // runType/endType/speed/time 由 prefab 的 SkillView @property 决定,配置表同名字段仅作兜底 sMoveCom.runType = SView.runType; sMoveCom.endType = SView.endType; sMoveCom.speed = SView.speed > 0 ? SView.speed : config.speed; sMoveCom.bezierStartHeight = config.bezier_start_y ?? sMoveCom.bezierStartHeight; // 抛射物高度增量:叠加视图层 height_offset,抬高贝塞尔弹道顶点 sMoveCom.bezierMidHeight = (config.bezier_mid_y ?? sMoveCom.bezierMidHeight) + Math.max(0, SView.height_offset); sMoveCom.bezierArc = config.bezier_arc ?? sMoveCom.bezierArc; // 从SkillView获取移动参数,位置初始化由SMoveSystem统一处理 sMoveCom.atk_x = SView.atk_x; sMoveCom.atk_y = SView.atk_y; sMoveCom.distance = SView.distance; if (SView.endType === EType.timeEnd) { let sTimeCom = this.get(StimeDataComp); if (!sTimeCom) sTimeCom = this.add(StimeDataComp); sTimeCom.reset(); sTimeCom.s_uuid = s_uuid; sTimeCom.totalTime = Math.max(1, SView.time > 0 ? SView.time : (config.time ?? 0)); sTimeCom.hitInterval = Math.max(0.5, config.hitcd || 0); } else { const sTimeCom = this.get(StimeDataComp); if (sTimeCom) this.remove(StimeDataComp); } // 初始化数据组件 let sDataCom = this.get(SDataCom); if (!sDataCom) { sDataCom = this.add(SDataCom); } sDataCom.reset(); sDataCom.group = caster.box_group sDataCom.casterEid = caster.ent.eid sDataCom.Attrs = {}; // 技能成长数值已由 SkillConfig/SkillOverrides 提供(crt/frz/stun/bck/ap/hit_count),无需再叠加 SkillUpList const sCrt = config.crt ?? 0; 像素大勇者游戏软件 V1.0            第 36 页 共 60 页 ============================================================================ const sFrz = config.frz ?? 0; const sStun = config.stun ?? 0; const sPara = config.para ?? 0; const sBck = config.bck ?? 0; const sAp = config.ap; const sHit = config.hit_count; sDataCom.Attrs[Attrs.ap] = Math.floor(cAttrsComp.getFinalAp() * sAp / 100); //技能的ap是百分值 需要/100 而且需要再最终计算总ap时再/100,不然会出现ap为90%变0 sDataCom.Attrs[Attrs.critical] = cAttrsComp.getFinalCritical() + sCrt; sDataCom.Attrs[Attrs.critical_damage] = cAttrsComp.getFinalCritDamage(); sDataCom.Attrs[Attrs.freeze_chance] = cAttrsComp.getFinalFreezeChance() + sFrz; sDataCom.Attrs[Attrs.stun_chance] = cAttrsComp.getFinalStunChance() + sStun; sDataCom.Attrs[Attrs.paralyze_chance] = cAttrsComp.getFinalParalyzeChance() + sPara; sDataCom.Attrs[Attrs.knockback_chance] = cAttrsComp.getFinalKnockbackChance() + sBck; sDataCom.Attrs[Attrs.knockback_distance] = cAttrsComp.getFinalKnockbackDistance(); // 技能击退概率:取合并后配置的 bck_chance(heroSet overrides 可覆写),未配置视为 0 不击退 sDataCom.Attrs[Attrs.knockback_skill_chance] = config.bck_chance ?? 0; sDataCom.Attrs[Attrs.puncture_chance] = cAttrsComp.getFinalPunctureChance(); // 初始化携带施法者的穿透概率 sDataCom.Attrs[Attrs.puncture_dmg_bonus] = cAttrsComp.getFinalPunctureDmgBonus(); // 初始化携带施法者的穿透伤害加成 sDataCom.Attrs[Attrs.accuracy] = cAttrsComp.getFinalAccuracy(); // 初始化携带施法者的命中率(含驻场光环与计时 buff) sDataCom.s_uuid = s_uuid sDataCom.skill_lv = Math.max(0, skill_lv); sDataCom.fac = cAttrsComp.fac sDataCom.ext_dmg = ext_dmg sDataCom.hit_count = 0 sDataCom.max_hit_count = Math.max(1, sHit) // proc_skill 已改为施法后触发(SCastSystem 判定),此处不再快照到伤害链 // 快照合并后的对敌 debuff 配置,命中时由 HeroAtkSystem 结算(heroSet overrides/lv_entries 注入的 debuff 依赖此透传) sDataCom.timed_debuff = config.timed_debuff_id ? { id: config.timed_debuff_id, value: config.debuff_value, duration: config.debuff_duration, chance: config.debuff_chance } : undefined; SView.init(); }; let prefab: Prefab = oops.res.get(path, Prefab); if (prefab) { initSkillNode(prefab); } else { oops.res.loadAsync(path, Prefab).then((res: Prefab) => { if (res) { initSkillNode(res); } else { mLogger.error(this.debugMode, 'Skill', "[Skill] 预制体加载失败:", path); } }).catch((err) => { mLogger.error(this.debugMode, 'Skill', "[Skill] 预制体加载异常:", path, err); }); } } /** 模块资源释放 */ destroy() { // 注: 自定义释放逻辑,视图层实现 ecs.IComp 接口的 ecs 组件需要手动释放 this.remove(SDataCom); this.remove(SMoveDataComp) this.remove(StimeDataComp) // 移除组件会触发 reset,但我们在 reset 中移除了 destroy this.remove(SkillView) // 回收节点到对象池 像素大勇者游戏软件 V1.0            第 37 页 共 60 页 ============================================================================ if (this.skillNode && this.skillNode.isValid && this.prefabPath) { Skill.putToPool(this.prefabPath, this.skillNode); this.skillNode = null; } this.prefabPath = ""; super.destroy(); } } // ===================== 文件: game/skill/SkillView.ts ===================== import { _decorator, Animation, CCFloat, CCInteger, Collider2D, Contact2DType, Enum, UITransform, v3, Vec3 } from "cc"; import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp"; import { HeroViewComp } from "../hero/HeroViewComp"; import { DTType, EType, RType, SkillConfig, SkillSet } from "../common/config/SkillSet"; import { SDataCom } from "./SDataCom"; import { HeroAttrsComp } from "../hero/HeroAttrsComp"; import { DamageQueueHelper } from "../hero/DamageQueueComp"; import { mLogger } from "../common/Logger"; import { FightSet } from "../common/config/GameSet"; import { Attrs } from "../common/config/HeroAttrs"; const { ccclass, property } = _decorator; /** 技能视图组件:负责碰撞窗口、碰撞回调与伤害派发 */ @ccclass('SkillView') @ecs.register('SkillView', false) export class SkillView extends CCComp { /** 视图层逻辑代码分离演示 */ @property({ type: CCInteger }) atk_x: number = 0 @property({ type: CCInteger }) atk_y: number = 0 /** 运动类型:直线/贝塞尔/固定起点/固定终点(由 prefab 配置,优先级高于 SkillSet) */ @property({ type: Enum(RType), tooltip: "技能特效运动类型" }) runType: RType = RType.linear; /** 消亡条件:动画结束/计时结束/碰撞次数结束(由 prefab 配置,优先级高于 SkillSet) */ @property({ type: Enum(EType), tooltip: "技能特效消亡条件" }) endType: EType = EType.collision; /** 移动速度(由 prefab 配置,<=0 时回退 SkillSet.speed) */ @property({ type: CCFloat, slide: true, range: [0, 2000, 10], tooltip: "技能特效移动速度" }) speed: number = 0; /** timeEnd 持续时间(秒),仅 endType=timeEnd 时生效(由 prefab 配置,<=0 时回退 SkillSet.time) */ @property({ type: CCFloat, slide: true, range: [0, 30, 0.1], tooltip: "timeEnd 持续时间(秒)" }) time: number = 0; /** 直线飞行距离(像素),仅 runType=linear 时生效(由 prefab 配置,<=0 时保持默认飞出屏幕的行为) */ @property({ type: CCFloat, slide: true, range: [0, 2000, 10], tooltip: "直线飞行距离(仅 linear 生效)" }) distance: number = 0; /** 强制出生点世界坐标X(-1=不强制,使用 atk_x 偏移;>=0 时强制使用该值,如 0=屏幕中心) */ @property({ type: CCFloat, tooltip: "强制出生点X:-1=不强制,>=0=强制世界坐标X" }) forceStartX: number = -1; 像素大勇者游戏软件 V1.0            第 38 页 共 60 页 ============================================================================ /** 强制出生点世界坐标Y(-1=不强制,使用 atk_y 偏移;>=0 时强制使用该值) */ @property({ type: CCFloat, tooltip: "强制出生点Y:-1=不强制,>=0=强制世界坐标Y" }) forceStartY: number = -1; /** 召唤数量:SCastSystem 单次施法实例化的技能实体个数(<=1 为单体;>1 时按召唤物语义,不再参与风怒等额外触发) */ @property({ type: CCInteger, slide: true, range: [1, 8, 1], tooltip: "单次施法召唤的技能实体个数" }) summon_count: number = 1; /** 抛射物高度增量(像素),贝塞尔弹道时叠加到 bezierMidHeight,控制抛物线顶点抬高 */ @property({ type: CCFloat, slide: true, range: [0, 500, 5], tooltip: "抛射物高度增量(叠加到贝塞尔弹道中点高度)" }) height_offset: number = 0; private debugMode: boolean = true; anim: Animation = null; group: number = 0; SConf: SkillConfig = null; sData: SDataCom = null; s_uuid: number = 1001 private collider: Collider2D = null; // 缓存碰撞体引用 private pendingDisableCollider: boolean = false; private isDisposing: boolean = false; // 生命周期保护标记,避免在销毁阶段重复处理碰撞 init() { this.SConf = SkillSet[this.s_uuid] this.sData = this.ent.get(SDataCom) this.anim = this.node.getComponent(Animation) this.node.active = true; this.pendingDisableCollider = false; this.isDisposing = false; this.collider = this.getComponent(Collider2D); if (this.collider) { this.collider.group = this.group; this.collider.off(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this); this.collider.on(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this); this.collider.enabled = this.endType === EType.collision; } if (this.node.getComponent(Animation)) { let anim = this.node.getComponent(Animation); mLogger.log(this.debugMode, 'SkillView', "[SkillCom]:has anim", anim) anim.off(Animation.EventType.FINISHED, this.onAnimationFinished, this); anim.on(Animation.EventType.FINISHED, this.onAnimationFinished, this); // 对象池复用时,需要手动播放默认动画(因为 Play On Load 只在首次生效) if (anim.defaultClip) { anim.play(anim.defaultClip.name); } } } onBeginContact(seCol: Collider2D, oCol: Collider2D) { if (!this.sData || !this.SConf) { mLogger.warn(this.debugMode, 'SkillView', '[SkillView] onBeginContact 缺少 sData 或 SConf,忽略此次碰撞'); return; } if (this.isDisposing) return; if (!this.node || !this.node.activeInHierarchy) return; const targetView = oCol.getComponent(HeroViewComp); // 不是 HeroViewComp,直接忽略 if (!targetView) return; 像素大勇者游戏软件 V1.0            第 39 页 共 60 页 ============================================================================ // 🔥 方案A:防御性检查 - 在获取model前强制检查ent是否存在 if (!targetView.ent) { mLogger.warn(this.debugMode, 'SkillView', '[SkillView] onBeginContact targetView.ent为空,实体已销毁,忽略此次碰撞'); return; } if (this.debugMode) { const casterEid = this.sData.casterEid; const targetName = targetView?.ent?.get(HeroAttrsComp)?.hero_name ?? '非英雄对象'; const targetEid = targetView?.ent?.eid ?? '未知EID'; mLogger.log(this.debugMode, 'SkillView', `[skillView] 碰撞1 [${this.sData.group}][eid:${casterEid}]的[${seCol.group}]:[${this.SConf.name}][${this.ent.eid}]碰撞了 [${oCol.group}]:[${targetName}][${targetEid}]`); } if (oCol.group === seCol.group) return; if (this.pendingDisableCollider) return; if (this.sData.hit_count >= this.sData.max_hit_count) { this.handle_collision_limit(); return; } this.sData.hit_count++; if (this.sData.hit_count >= this.sData.max_hit_count) { // == 新增:穿透概率判定 == // 仅对单体伤害技能生效(DTType.single)且存在穿透概率 if (this.SConf.DTType === DTType.single) { const punctureChance = this.sData.Attrs[Attrs.puncture_chance] || 0; if (punctureChance > 0) { const rand = Math.random() * 100; if (rand < punctureChance) { // 触发穿透:不销毁,增加 max_hit_count 允许继续穿透 this.sData.max_hit_count++; // 概率衰减(减去固定的 PUNCTURE_DOWN 值,比如 50) this.sData.Attrs[Attrs.puncture_chance] = Math.max(0, punctureChance - FightSet.PUNCTURE_DOWN); mLogger.log(this.debugMode, 'SkillView', `[概率判定] 穿透 概率:${punctureChance.toFixed(1)}% 随机:${rand.toFixed(1)} -> ✔成功(衰减后剩余:${this.sData.Attrs[Attrs.puncture_chance]}%)`); } else { mLogger.log(this.debugMode, 'SkillView', `[概率判定] 穿透 概率:${punctureChance.toFixed(1)}% 随机:${rand.toFixed(1)} -> ✘失败,投射物销毁`); // 未触发穿透,正常销毁 this.handle_collision_limit(); } } else { this.handle_collision_limit(); } } else { this.handle_collision_limit(); } } // 命中次数按碰撞事件统计:不依赖是否最终造成伤害 let model = targetView.ent.get(HeroAttrsComp); if (this.debugMode) { mLogger.log(this.debugMode, 'SkillView', `[skillView] 碰撞3`, oCol.group, seCol.group, model); } if (!model) return; if (model.is_dead) return; if (this.sData.fac == model.fac) return; // 检查是否已经命中过这个目标(日志安全输出) this.apply_damage(targetView, this.sData.hit_count) } onAnimationFinished() { // animationEnd 类型:动画结束后再关闭碰撞并销毁 if (this.endType == EType.animationEnd) { this.disable_collider_now(); 像素大勇者游戏软件 V1.0            第 40 页 共 60 页 ============================================================================ this.ent.destroy() return; } // fixed 类型不移动,无法通过移动完成销毁;动画播完作为兜底销毁时机 if (this.runType == RType.fixed) { this.disable_collider_now(); this.ent.destroy() } } // 动画帧事件 atk:仅负责开启一帧碰撞窗口,不负责伤害与计数 public atk(args: any) { if (!this.SConf) return; if (this.endType == EType.collision) return if (this.enable_collider_safely()) { mLogger.log(this.debugMode, 'SkillView', `[SkillView] [${this.SConf?.name}] 开启碰撞检测`); this.scheduleOnce(() => { if (!this.node || !this.node.isValid || this.isDisposing) return; this.close_collider(); mLogger.log(this.debugMode, 'SkillView', `[SkillView] [${this.SConf?.name}] 关闭碰撞检测`); }, 0); } } /** * 仅负责伤害派发,不处理命中次数与实体销毁 * @param target 目标英雄视图 * @param punctureIndex 当前是第几个被命中的目标(1=首目标;>=2 视为穿透目标,单体技能按 FightSet.PUNCTURE_DMG_RATIO 折算伤害,范围技能不衰减) */ apply_damage(target: HeroViewComp, punctureIndex: number = 1) { if (target == null) return; // 安全检查:如果目标实体已不存在,直接返回 if (!target.ent) return; if (!this.sData) return; if (!this.SConf) return; if (this.debugMode) { const targetName = target.ent.get(HeroAttrsComp)?.hero_name ?? "未知目标"; mLogger.log(this.debugMode, 'SkillView', `[skillView] 伤害 [${this.group}][eid:${this.sData.casterEid}]的 [${this.SConf.name}]对 [${target.box_group}][${targetName}][${target.ent.eid}] 第${punctureIndex}目标`); } // 穿透伤害衰减:仅单体伤害技能(DTType.single)且命中第2个及以后目标时按比例折算 // 乘法模型:最终穿透系数 = PUNCTURE_DMG_RATIO * (1 + puncture_dmg_bonus/100) // 注:puncture_dmg_bonus 单位为百分点(30 = +30%),与项目其他概率/加成属性保持一致 const punctureDmgBonus = this.sData.Attrs[Attrs.puncture_dmg_bonus] || 0; const decayRatio = (this.SConf.DTType === DTType.single && punctureIndex >= 2) ? FightSet.PUNCTURE_DMG_RATIO * (1 + punctureDmgBonus / 100) : 1; const finalRatio = this.sData.dmg_ratio * decayRatio; // 使用伤害队列系统处理伤害 DamageQueueHelper.addDamageToEntity( target.ent, this.sData.Attrs, this.sData.casterEid, this.sData.s_uuid, this.sData.ext_dmg, finalRatio, 0, this.sData.timed_debuff, ); } close_collider() { if (!this.collider) return; if (this.pendingDisableCollider && !this.collider.enabled) return; 像素大勇者游戏软件 V1.0            第 41 页 共 60 页 ============================================================================ this.pendingDisableCollider = true; if (this.collider.isValid) { this.collider.enabled = false; } this.pendingDisableCollider = false; } private disable_collider_now() { this.isDisposing = true; this.close_collider(); } // 碰撞上限收口:先关碰撞;collision 类型再延迟销毁实体 private handle_collision_limit() { this.close_collider(); if (this.SConf?.EType !== EType.collision) return; if (this.isDisposing) return; this.isDisposing = true; this.scheduleOnce(() => { if (this.ent) { this.ent.destroy(); } }, 0); } private enable_collider_safely(): boolean { if (!this.collider || !this.collider.isValid) return false; if (this.isDisposing) return false; if (!this.node || !this.node.isValid || !this.node.activeInHierarchy) return false; this.pendingDisableCollider = false; this.collider.group = this.group; this.collider.off(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this); this.collider.on(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this); this.collider.enabled = true; return true; } /** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */ reset() { // 清理碰撞体事件监听 if (this.collider && this.collider.isValid) { this.collider.off(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this); this.collider.enabled = false; } this.pendingDisableCollider = false; this.isDisposing = false; if (this.anim && this.anim.isValid) { this.anim.off(Animation.EventType.FINISHED, this.onAnimationFinished, this); } // 取消所有定时器 this.unscheduleAllCallbacks(); if (this.node && this.node.isValid) { this.node.active = false; } } } // ===================== 文件: game/skill/SMoveComp.ts ===================== import { Vec3, v3, Node } from "cc"; import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; import { RType, EType, SkillSet } from "../common/config/SkillSet"; import { BoxSet } from "../common/config/GameSet"; import { SkillView } from "./SkillView"; import { smc } from "../common/SingletonModuleComp"; 像素大勇者游戏软件 V1.0            第 42 页 共 60 页 ============================================================================ /** * ==================== 技能移动数据组件 ==================== * * 用途: * - 存储技能实体的移动相关数据 * - 管理移动状态和参数 * - 支持多种移动类型(线性、贝塞尔、固定位置等) */ @ecs.register('SMoveDataComp') export class SMoveDataComp extends ecs.Comp { /** 起始位置 */ startPos: Vec3 = v3(0, 0, 0); /** 目标位置 */ targetPos: Vec3 = v3(0, 0, 0); /** 当前位置 */ currentPos: Vec3 = v3(0, 0, 0); /** 移动速度 */ speed: number = 500; /** 移动进度 (0-1) */ progress: number = 0; /** 移动方向缩放 */ scale: number = 1; /** 技能UUID */ s_uuid: number = 0; /** 运行类型 */ runType: RType = RType.linear; /** 结束类型 */ endType: EType = EType.collision; /** 是否强制水平移动 */ isHorizontal: boolean = true; /** 攻击偏移X - 从视图层传递 */ atk_x: number = 0; /** 攻击偏移Y - 从视图层传递 */ atk_y: number = 0; /** 是否正在移动 */ isMoving: boolean = false; /** 是否已完成移动 */ isCompleted: boolean = false; /** 移动开始时间 */ startTime: number = 0; /** 移动总时间 */ totalTime: number = 0; /** 贝塞尔曲线控制点 */ 像素大勇者游戏软件 V1.0            第 43 页 共 60 页 ============================================================================ controlPoint: Vec3 = v3(0, 0, 0); bezierStartHeight: number = 30; bezierMidHeight: number = 200; bezierArc: number = 1; /** 是否逐渐加速 (ease-in) */ isAccelerate: boolean = false; /** 是否自动销毁(到达目标后) */ autoDestroy: boolean = true; /** 直线飞行距离(仅 runType=linear 生效,<=0 时延长终点飞出屏幕) */ distance: number = 0; reset() { this.startPos.set(0, 0, 0); this.targetPos.set(0, 0, 0); this.currentPos.set(0, 0, 0); this.controlPoint.set(0, 0, 0); this.bezierStartHeight = 30; this.bezierMidHeight = 200; this.bezierArc = 1; this.isAccelerate = false; this.speed = 500; this.progress = 0; this.scale = 1; this.s_uuid = 0; this.runType = RType.linear; this.endType = EType.collision; this.isHorizontal = true; this.atk_x = 0; this.atk_y = 0; this.isMoving = false; this.isCompleted = false; this.startTime = 0; this.totalTime = 0; this.autoDestroy = true; this.distance = 0; } /** * 开始移动 */ startMove() { this.isMoving = true; this.isCompleted = false; this.progress = 0; this.startTime = Date.now(); this.currentPos.set(this.startPos); // 根据移动类型计算总时间和控制点 this.calculateMoveParameters(); } /** * 计算移动参数 */ private calculateMoveParameters() { const distance = Vec3.distance(this.startPos, this.targetPos); const safeSpeed = Math.max(1, this.speed); 像素大勇者游戏软件 V1.0            第 44 页 共 60 页 ============================================================================ this.totalTime = Math.max(0.05, distance / safeSpeed); // 为贝塞尔移动生成控制点 if (this.runType === RType.bezier) { this.generateBezierControlPoint(); } } /** * 生成贝塞尔曲线控制点 */ private generateBezierControlPoint() { const midPoint = new Vec3(); Vec3.lerp(midPoint, this.startPos, this.targetPos, 0.5); // 计算垂直方向 const direction = new Vec3(); Vec3.subtract(direction, this.targetPos, this.startPos); if (direction.length() < 0.0001) { this.controlPoint.set(midPoint.x, midPoint.y + this.bezierMidHeight, midPoint.z); return; } const perpendicular = v3(-direction.y, direction.x, 0); perpendicular.normalize(); const distance = Vec3.distance(this.startPos, this.targetPos); const baseOffset = Math.max(120, Math.min(220, distance * 0.35)); const offset = baseOffset * Math.max(0.2, this.bezierArc); if (perpendicular.y < 0) { perpendicular.x *= -1; perpendicular.y *= -1; } Vec3.scaleAndAdd(this.controlPoint, midPoint, perpendicular, offset); const minPeakY = midPoint.y + Math.max(0, this.bezierMidHeight); if (this.controlPoint.y < minPeakY) { this.controlPoint.y = minPeakY; } } /** * 更新移动进度 */ updateProgress(deltaTime: number): boolean { if (!this.isMoving || this.isCompleted) { return false; } this.progress += deltaTime / this.totalTime; if (this.progress >= 1) { this.progress = 1; this.isCompleted = true; this.isMoving = false; } // 根据移动类型计算当前位置 this.calculateCurrentPosition(); return true; } 像素大勇者游戏软件 V1.0            第 45 页 共 60 页 ============================================================================ /** * 根据移动类型计算当前位置 */ private calculateCurrentPosition() { // 如果开启了逐渐加速,混合线性与二次方曲线 (如 0.7 * t^2 + 0.3 * t) // 这样起步拥有 30% 的基础速度,不会显得完全静止,随后逐渐加速 const t = this.isAccelerate ? (this.progress * this.progress * 0.7 + this.progress * 0.3) : this.progress; switch (this.runType) { case RType.linear: // 直线运动 Vec3.lerp(this.currentPos, this.startPos, this.targetPos, t); break; case RType.bezier: this.calculateBezierPosition(t); break; case RType.fixed: case RType.fixedEnd: // 固定位置类型不需要移动 this.currentPos.set(this.startPos); break; default: Vec3.lerp(this.currentPos, this.startPos, this.targetPos, t); break; } } /** * 计算贝塞尔曲线位置 */ private calculateBezierPosition(t: number) { // 二次贝塞尔曲线公式: B(t) = (1-t)²P0 + 2(1-t)tP1 + t²P2 const oneMinusT = 1 - t; const oneMinusTSquared = oneMinusT * oneMinusT; const tSquared = t * t; const twoOneMinusTt = 2 * oneMinusT * t; this.currentPos.x = oneMinusTSquared * this.startPos.x + twoOneMinusTt * this.controlPoint.x + tSquared * this.targetPos.x; this.currentPos.y = oneMinusTSquared * this.startPos.y + twoOneMinusTt * this.controlPoint.y + tSquared * this.targetPos.y; this.currentPos.z = oneMinusTSquared * this.startPos.z + twoOneMinusTt * this.controlPoint.z + tSquared * this.targetPos.z; } /** * 停止移动 */ stopMove() { 像素大勇者游戏软件 V1.0            第 46 页 共 60 页 ============================================================================ this.isMoving = false; this.isCompleted = true; } /** * 获取移动统计信息(用于调试) */ getMoveStats(): { isMoving: boolean; isCompleted: boolean; progress: number; runType: RType; endType: EType; totalTime: number; elapsedTime: number; } { const currentTime = Date.now(); return { isMoving: this.isMoving, isCompleted: this.isCompleted, progress: this.progress, runType: this.runType, endType: this.endType, totalTime: this.totalTime, elapsedTime: (currentTime - this.startTime) / 1000 }; } } // ===================== 文件: game/skill/SMoveSystem.ts ===================== import { v3, Vec3, Node } from "cc"; import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; import { EType, RType, SkillSet } from "../common/config/SkillSet"; import { SMoveDataComp } from "./SMoveComp"; import { smc } from "../common/SingletonModuleComp"; import { SkillView } from "./SkillView"; import { mLogger } from "../common/Logger"; /** * ==================== 技能移动系统 ==================== * * 职责: * 1. 处理技能实体的移动逻辑 * 2. 更新技能位置 * 3. 管理移动生命周期 * 4. 支持多种移动类型 * * 设计理念: * - 参考DamageQueueComp的实现方式 * - 独立的移动处理系统 * - 支持从视图层获取的参数 */ @ecs.register('SMoveSystem') export class SMoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate { debugMode: boolean = false; filter(): ecs.IMatcher { return ecs.allOf(SMoveDataComp, SkillView); } 像素大勇者游戏软件 V1.0            第 47 页 共 60 页 ============================================================================ entityEnter(entity: ecs.Entity): void { const moveComp = entity.get(SMoveDataComp); const skillView = entity.get(SkillView); if (!moveComp || !skillView || !skillView.node) return; // 获取技能配置 const skillConfig = SkillSet[moveComp.s_uuid]; if (!skillConfig) { mLogger.warn(this.debugMode, 'SMoveSystem', `[SMoveSystem] 技能配置不存在: ${moveComp.s_uuid}`); return; } // 移动参数由 prefab 的 SkillView @property 决定(speed<=0 时 Skill.load 已回退配置表值) if (moveComp.speed <= 0) { moveComp.speed = skillConfig.speed; } if (skillConfig.is_accel) { moveComp.isAccelerate = true; } // 根据runType设置初始位置 this.initializePosition(moveComp, skillView); // 开始移动(除了固定位置类型) if (moveComp.runType !== RType.fixed && moveComp.runType !== RType.fixedEnd) { moveComp.startMove(); } mLogger.log(this.debugMode, 'SMoveSystem', `[SMoveSystem] 技能 ${skillConfig.name} 开始移动,类型: ${moveComp.runType}`); } /** * 根据runType初始化技能位置 */ private initializePosition(moveComp: SMoveDataComp, skillView: SkillView): void { const node = skillView.node; switch (moveComp.runType) { case RType.linear: // linear类型:根据atk_x和atk_y调整起始位置 const adjustedStartPos = v3( moveComp.startPos.x + moveComp.atk_x * moveComp.scale, moveComp.startPos.y + moveComp.atk_y, moveComp.startPos.z ); // 水平向前飞行:方向仅由朝向(scale)决定,忽略目标点的高度差 const forwardX = moveComp.scale > 0 ? 1 : -1; let adjustedTargetPos: Vec3; if (moveComp.distance > 0) { // 距离模式:沿水平方向飞行固定距离 adjustedTargetPos = v3( adjustedStartPos.x + forwardX * moveComp.distance, adjustedStartPos.y, adjustedStartPos.z ); } else { // 延长终止点,统一消亡点的x坐标为 +-500 像素大勇者游戏软件 V1.0            第 48 页 共 60 页 ============================================================================ const targetX = moveComp.scale > 0 ? 500 : -500; adjustedTargetPos = v3( targetX, adjustedStartPos.y, adjustedStartPos.z ); } moveComp.startPos.set(adjustedStartPos); moveComp.targetPos.set(adjustedTargetPos); node.setPosition(adjustedStartPos); // 设置旋转角度 const dirForAngle = new Vec3(); Vec3.subtract(dirForAngle, adjustedTargetPos, adjustedStartPos); if (dirForAngle.length() > 0.01) { let angle = Math.atan2(dirForAngle.y, dirForAngle.x) * (180 / Math.PI); if (moveComp.scale < 0) { angle += 180; } node.angle = angle; } break; case RType.fixed: // 固定起始位置 node.setPosition(moveComp.startPos.x, node.position.y, 0); break; case RType.fixedEnd: // 固定结束位置 node.setPosition(moveComp.targetPos.x > 360 ? 300 : moveComp.targetPos.x, moveComp.targetPos.y, 0); break; case RType.bezier: const bezierStartPos = v3( moveComp.startPos.x, moveComp.startPos.y + moveComp.bezierStartHeight, moveComp.startPos.z ); const bezierTargetPos = v3( moveComp.targetPos.x, moveComp.targetPos.y + moveComp.bezierStartHeight, moveComp.targetPos.z ); moveComp.startPos.set(bezierStartPos); moveComp.targetPos.set(bezierTargetPos); node.setPosition(bezierStartPos); break; default: // 其他类型(包括bezier):根据atk_x和atk_y调整起始位置 if (moveComp.atk_x !== 0 || moveComp.atk_y !== 0) { const adjustedStartPos = v3( moveComp.startPos.x + moveComp.atk_x * moveComp.scale, moveComp.startPos.y + moveComp.atk_y, moveComp.startPos.z ); moveComp.startPos.set(adjustedStartPos); node.setPosition(adjustedStartPos); } break; 像素大勇者游戏软件 V1.0            第 49 页 共 60 页 ============================================================================ } } update(entity: ecs.Entity): void { if (!smc.mission.play) return; if (smc.mission.pause) return const moveComp = entity.get(SMoveDataComp); const skillView = entity.get(SkillView); if (!moveComp || !skillView || !skillView.node) return; // 更新移动进度 const isMoving = moveComp.updateProgress(this.dt); if (isMoving) { // 更新节点位置 skillView.node.setPosition(moveComp.currentPos); // 处理自动旋转(贝塞尔移动) if (moveComp.runType === RType.bezier) { this.updateRotation(skillView.node, moveComp); } } // 检查移动完成 if (moveComp.isCompleted && moveComp.autoDestroy) { // 根据结束类型决定是否销毁 if ( moveComp.endType === EType.collision ) { skillView.close_collider(); entity.destroy(); } } } /** * 更新节点旋转(用于贝塞尔移动) */ private updateRotation(node: Node, moveComp: SMoveDataComp) { if (moveComp.progress < 1) { // 计算下一帧的位置来确定方向 const nextProgress = Math.min(moveComp.progress + 0.01, 1); const t = moveComp.isAccelerate ? (nextProgress * nextProgress * 0.7 + nextProgress * 0.3) : nextProgress; const nextPos = v3(0, 0, 0); // 计算下一个位置 const oneMinusT = 1 - t; const oneMinusTSquared = oneMinusT * oneMinusT; const tSquared = t * t; const twoOneMinusTt = 2 * oneMinusT * t; nextPos.x = oneMinusTSquared * moveComp.startPos.x + twoOneMinusTt * moveComp.controlPoint.x + tSquared * moveComp.targetPos.x; nextPos.y = oneMinusTSquared * moveComp.startPos.y + twoOneMinusTt * moveComp.controlPoint.y + 像素大勇者游戏软件 V1.0            第 50 页 共 60 页 ============================================================================ tSquared * moveComp.targetPos.y; // 计算方向角度 const direction = new Vec3(); Vec3.subtract(direction, nextPos, moveComp.currentPos); if (direction.length() > 0.01) { let angle = Math.atan2(direction.y, direction.x) * (180 / Math.PI); if (moveComp.scale < 0) { angle += 180; } node.angle = angle; } } } } /** * ==================== 技能移动辅助工具 ==================== */ export class SMoveHelper { /** * 为技能实体设置移动参数 */ static setupMoveForSkill(entity: ecs.Entity, startPos: Vec3, targetPos: Vec3, s_uuid: number): void { let moveComp = entity.get(SMoveDataComp); if (!moveComp) { moveComp = entity.add(SMoveDataComp); } moveComp.startPos.set(startPos); moveComp.targetPos.set(targetPos); moveComp.s_uuid = s_uuid; // 从技能配置获取默认参数 const skillConfig = SkillSet[s_uuid]; if (skillConfig) { moveComp.runType = skillConfig.RType || RType.linear; moveComp.speed = skillConfig.speed || 500; if (skillConfig.is_accel) moveComp.isAccelerate = true; } } /** * 检查技能是否正在移动 */ static isSkillMoving(entity: ecs.Entity): boolean { const moveComp = entity.get(SMoveDataComp); return moveComp ? moveComp.isMoving : false; } /** * 获取技能移动统计信息 */ static getSkillMoveStats(entity: ecs.Entity): any { const moveComp = entity.get(SMoveDataComp); return moveComp ? moveComp.getMoveStats() : null; } 像素大勇者游戏软件 V1.0            第 51 页 共 60 页 ============================================================================ /** * 停止技能移动 */ static stopSkillMove(entity: ecs.Entity): void { const moveComp = entity.get(SMoveDataComp); if (moveComp) { moveComp.stopMove(); } } } // ===================== 文件: game/skill/STimeComp.ts ===================== import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; import { EType, SkillSet } from "../common/config/SkillSet"; import { SkillView } from "./SkillView"; import { smc } from "../common/SingletonModuleComp"; @ecs.register('StimeDataComp') export class StimeDataComp extends ecs.Comp { s_uuid: number = 0; totalTime: number = 0; elapsedTime: number = 0; hitInterval: number = 0; hitElapsed: number = 0; reset() { this.s_uuid = 0; this.totalTime = 0; this.elapsedTime = 0; this.hitInterval = 0; this.hitElapsed = 0; } } @ecs.register('STimeSystem') export class STimeSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate { filter(): ecs.IMatcher { return ecs.allOf(StimeDataComp, SkillView); } update(entity: ecs.Entity): void { if(!smc.mission.play ) return; if(smc.mission.pause) return const timeComp = entity.get(StimeDataComp); const skillView = entity.get(SkillView); if (!timeComp || !skillView) return; const conf = SkillSet[timeComp.s_uuid]; if (!conf || conf.EType !== EType.timeEnd) return; if (timeComp.totalTime <= 0) { skillView.close_collider(); entity.destroy(); return; } timeComp.elapsedTime += this.dt; timeComp.hitElapsed += this.dt; if (timeComp.hitInterval > 0 && timeComp.hitElapsed >= timeComp.hitInterval) { timeComp.hitElapsed -= timeComp.hitInterval; skillView.atk(null); } if (timeComp.elapsedTime >= timeComp.totalTime) { skillView.close_collider(); 像素大勇者游戏软件 V1.0            第 52 页 共 60 页 ============================================================================ entity.destroy(); } } } // ===================== 文件: game/skill/timedCom.ts ===================== import { _decorator, Component, Node } from 'cc'; import { SkillSet } from '../common/config/SkillSet'; import { smc } from '../common/SingletonModuleComp'; const { ccclass, property } = _decorator; @ccclass('timedCom') export class timedCom extends Component { time = 0.3; start() { } update(deltaTime: number) { this.time -= deltaTime; if(this.time <= 0) { this.node.destroy() } } } // ===================== 文件: game/skill/Tooltip.ts ===================== import { oops } from "../../../../extensions/oops-plugin-framework/assets/core/Oops"; import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; import { BoxSet, FacSet } from "../common/config/GameSet"; import { SkillSet } from "../common/config/SkillSet"; import { smc } from "../common/SingletonModuleComp"; import { TooltipCom } from "./TooltipCom"; import { instantiate, Node, Prefab, Vec3 ,tween, v3,animation,Label,resources,SpriteFrame,Sprite, NodePool, Color} from "cc"; /** Skill 模块 */ @ecs.register(`Tooltip`) export class Tooltip extends ecs.Entity { /** 对象池 */ static pool: NodePool = new NodePool(); static readonly MAX_POOL_SIZE: number = 80; /** 回收节点 */ static put(node: Node) { if (!node || !node.isValid) return; if (this.pool.size() >= this.MAX_POOL_SIZE) { node.destroy(); return; } node.active = false; node.parent = null; this.pool.put(node); } static clearPool() { while (this.pool.size() > 0) { const node = this.pool.get(); 像素大勇者游戏软件 V1.0            第 53 页 共 60 页 ============================================================================ if (node && node.isValid) { node.destroy(); } } this.pool.clear(); } static getPoolStats() { return { total: this.pool.size(), max: this.MAX_POOL_SIZE }; } /** ---------- 数据层 ---------- */ // SkillModel!: SkillModelComp; /** ---------- 业务层 ---------- */ // SkillBll!: SkillBllComp; /** ---------- 视图层 ---------- */ TooltipView!: TooltipCom; /** 实始添加的数据层组件 */ protected init() { } /** 模块资源释放 */ destroy() { // 注: 自定义释放逻辑,视图层实现 ecs.IComp 接口的 ecs 组件需要手动释放 this.remove(TooltipCom); super.destroy(); } static load(pos: Vec3 = Vec3.ZERO,type:number=1,vaule:string="",s_uuid:number=1001,parent:any=null,cd:number=1,fac:number=FacSet.MON,debuffIcon:string="",tipColor:Color|null=null) { let node: Node; if (Tooltip.pool.size() > 0) { node = Tooltip.pool.get()!; } else { var path = "game/skill/buff/tooltip"; var prefab: Prefab = oops.res.get(path, Prefab)!; node = instantiate(prefab); } node.parent = parent; node.setPosition(pos); node.active = true; var sv = node.getComponent(TooltipCom)!; sv.init(type, vaule, s_uuid, fac, debuffIcon, tipColor); // this.add(sv); // 不要添加到单例实体上,否则会覆盖或导致单例被销毁 } } // ===================== 文件: game/skill/TooltipCom.ts ===================== import { _decorator, Collider2D, Contact2DType, v3, IPhysics2DContact, Vec3, tween, Label, resources, SpriteFrame, Sprite, UIOpacity, Color, math, Tween, Node } from "cc"; import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp"; import { SkillSet } from "../common/config/SkillSet"; import { FacSet, TooltipTypes } from "../common/config/GameSet"; 像素大勇者游戏软件 V1.0            第 54 页 共 60 页 ============================================================================ import { Tooltip } from "./Tooltip"; const { ccclass, property } = _decorator; /** 视图层对象 */ @ccclass('TooltipCom') @ecs.register('TooltipView', false) export class TooltipCom extends CCComp { stype: number = 1; // 1:减少生命值,2:增加生命值,3:技能图标 value: string = ""; s_uuid: number = 1001; fac: number = FacSet.MON; /** DoT 飘字需激活的 debuff 图标节点名(loss_life/hp/debuff 下的子节点,如 FireDamage);空串=普通伤害 */ debuffIcon: string = ""; /** 通用文字飘字(TooltipTypes.tip)的自定义颜色,null 表示使用预制体默认色 */ tipColor: Color | null = null; // 动画参数配置 private readonly popDuration = 0.15; private readonly driftDuration = 0.5; private readonly fadeDuration = 0.2; private _uiOpacity: UIOpacity | null = null; start() { // 首次加载时如果未手动调用 init,则自动初始化(防止直接挂载场景的情况) // 但在当前架构下,Tooltip 都是通过 Tooltip.load 创建的,所以 start 可以留空或仅做基本检查 } /** 初始化并播放动画 */ init(type: number, value: string, uuid: number, fac: number = FacSet.MON, debuffIcon: string = "", tipColor: Color | null = null) { this.stype = type; this.value = value; this.s_uuid = uuid; this.fac = fac; this.debuffIcon = debuffIcon; this.tipColor = tipColor; // 初始化或获取 UIOpacity 组件 this._uiOpacity = this.node.getComponent(UIOpacity); if (!this._uiOpacity) { this._uiOpacity = this.node.addComponent(UIOpacity); } this._uiOpacity.opacity = 255; // 检测父节点翻转 let sx = 1; if (this.node.parent && this.node.parent.scale.x < 0) { sx = -1; } // 🔥 关键修复:停止当前节点和 UIOpacity 上的所有缓动 Tween.stopAllByTarget(this.node); if (this._uiOpacity) { Tween.stopAllByTarget(this._uiOpacity); this._uiOpacity.opacity = 255; } this.node.setScale(v3(0, 0, 1)); // 初始缩放为0 this.node.active = true; // 确保节点激活 像素大勇者游戏软件 V1.0            第 55 页 共 60 页 ============================================================================ // 重置所有子节点状态 this.node.children.forEach(child => child.active = false); let scaleMax = 1.5; let isCrit = false; let isHeal = false; // 初始随机偏移,防止完全重叠 const offsetX = (Math.random() - 0.5) * 40; // 设置极高的渲染层级,防止被怪物遮挡 const topSiblingIndex = 9999; // 临时变量,存储当前Y坐标,方便统一处理。增加60的Y轴基础偏移,防止伤害飘字等遮挡血条 let currentY = this.node.position.y + 60; switch (this.stype) { case TooltipTypes.life: // 普通伤害 this.node.setPosition(v3(this.node.position.x + offsetX, currentY)); this.node.setSiblingIndex(topSiblingIndex); this.setupLabel(this.fac === FacSet.HERO ? "hloss" : "loss_life", "hp", this.value); this.setupDebuffIcon(); // DoT 伤害时点亮 loss_life/hp/debuff 下的对应图标 scaleMax = 1.5; break; case TooltipTypes.health: // 治疗 this.node.setPosition(v3(this.node.position.x + offsetX, currentY)); this.node.setSiblingIndex(topSiblingIndex); this.setupLabel("add_life", "hp", this.value); isHeal = true; break; case TooltipTypes.addmp: // 回蓝 this.node.setPosition(v3(this.node.position.x + offsetX, currentY)); this.node.setSiblingIndex(topSiblingIndex); this.setupLabel("add_mp", "mp", this.value); isHeal = true; break; case TooltipTypes.crit: // 暴击 this.node.setPosition(v3(this.node.position.x + offsetX, currentY)); this.node.setSiblingIndex(topSiblingIndex + 100); // 暴击层级更高 this.setupLabel("bloss", "hp", this.value); scaleMax = 3.0; // 暴击放大 isCrit = true; break; case TooltipTypes.skill: const skillConfig = SkillSet[this.s_uuid]; const skillName = skillConfig ? skillConfig.name : ""; this.setupLabel("skill", "name", skillName + this.value); // 技能喊话基于 load 传入位置再抬高 50,避免遮挡角色 this.node.setPosition(v3(this.node.position.x, this.node.position.y + 50)); this.node.setSiblingIndex(topSiblingIndex); break; case TooltipTypes.uskill: this.setupLabel("uskill", "name", this.value); this.node.setPosition(v3(this.node.position.x, currentY + 30)); this.node.setSiblingIndex(topSiblingIndex); break; case TooltipTypes.lvup: this.node.getChildByName("lvup").active = true; this.node.setPosition(v3(this.node.position.x, currentY - 30)); 像素大勇者游戏软件 V1.0            第 56 页 共 60 页 ============================================================================ this.node.setSiblingIndex(topSiblingIndex); break; case TooltipTypes.apup: this.setupLabel("apup", "num", "+" + this.value); this.node.setPosition(v3(this.node.position.x, currentY - 60)); this.node.setSiblingIndex(topSiblingIndex); break; case TooltipTypes.hpup: this.setupLabel("hpup", "num", "+" + this.value); this.node.setPosition(v3(this.node.position.x, currentY - 60)); this.node.setSiblingIndex(topSiblingIndex); break; case TooltipTypes.shield: // 护盾吸收伤害:显示 数值 this.node.setPosition(v3(this.node.position.x + offsetX, currentY)); this.node.setSiblingIndex(topSiblingIndex); this.setupTipLabel(`${this.value}`, this.tipColor ?? new Color(79, 195, 247, 255)); // 默认蓝 isHeal = true; // 垂直上飘,区别于伤害随机横飘 break; case TooltipTypes.miss: // 闪避 this.node.setPosition(v3(this.node.position.x + offsetX, currentY)); this.node.setSiblingIndex(topSiblingIndex); this.setupTipLabel(this.value || "闪避", this.tipColor ?? new Color(255, 255, 255, 255)); // 默认白 isHeal = true; break; case TooltipTypes.tip: // 通用文字飘字 this.node.setPosition(v3(this.node.position.x + offsetX, currentY)); this.node.setSiblingIndex(topSiblingIndex); this.setupTipLabel(this.value, this.tipColor); isHeal = true; break; } this.playAnimation(scaleMax, isCrit, isHeal, sx); } private setupLabel(nodeName: string, labelNodeName: string, text: string) { const node = this.node.getChildByName(nodeName); if (node) { node.active = true; const label = node.getChildByName(labelNodeName)?.getComponent(Label); if (label) label.string = text; } } /** * 通用文字飘字:激活 words 节点并设置文字与颜色 * 预制体需包含 words/name 结构(Label);与 skill 节点区分,走漂移上飘动画而非原地淡出 * 对象池复用安全:未传色时恢复默认白,避免上次自定义色残留 */ private setupTipLabel(text: string, color: Color | null = null) { const words = this.node.getChildByName("words"); if (!words) return; words.active = true; const label = words.getChildByName("name")?.getComponent(Label); if (label) { label.string = text; label.color = color ?? Color.WHITE.clone(); } } 像素大勇者游戏软件 V1.0            第 57 页 共 60 页 ============================================================================ /** * DoT 伤害飘字:激活 loss_life/hp/debuff 下指定的 debuff 图标子节点。 * 仅怪物普通伤害(loss_life)路径调用;debuffIcon 为空或未找到节点时静默跳过。 */ private setupDebuffIcon() { if (!this.debuffIcon) return; const debuffNode = this.node.getChildByName("loss_life")?.getChildByName("hp")?.getChildByName("debuff"); if (!debuffNode) return; // 先全部隐藏(对象池复用防残留),再点亮目标图标 for (const child of debuffNode.children) { child.active = child.name === this.debuffIcon; } } playAnimation(scaleMax: number, isCrit: boolean, isHeal: boolean, sx: number = 1) { // 随机 X 轴偏移 (防止重叠) const randomX = (Math.random() - 0.5) * 60; const moveY = 80; const t = tween(this.node); // 1. 爆发阶段 (Pop) t.to(this.popDuration, { scale: v3(sx * scaleMax, scaleMax, 1) }, { easing: 'backOut' }); const isSkill = this.stype === TooltipTypes.skill; if (isSkill) { // 技能类型的提示:回弹后稍作停留,然后原地快速淡出 t.to(this.popDuration, { scale: v3(sx * 1.2, 1.2, 1) }); t.delay(0.2); t.parallel( tween(this._uiOpacity).to(this.fadeDuration, { opacity: 0 }) ); } else { // 2. 漂移阶段 (Drift) & 3. 淡出 (Fade) // 其他类型提示:并行执行恢复缩放、位移和淡出 t.parallel( tween(this.node).to(this.popDuration, { scale: v3(sx * 1.2, 1.2, 1) }), // 回弹到正常大小 (稍微放大) tween(this.node).by(this.driftDuration, { position: v3(isHeal ? 0 : randomX, moveY, 0) }, { easing: 'sineOut' }), // 治疗垂直飘,其他随机飘 tween(this._uiOpacity).delay(this.driftDuration - this.fadeDuration).to(this.fadeDuration, { opacity: 0 }) // 最后阶段淡出 ); } // 结束销毁 t.call(() => { // this.ent.destroy(); // 不要销毁实体,而是直接回收节点 Tooltip.put(this.node); }).start(); } update(deltaTime: number) { } /** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */ reset() { // 使用对象池回收 Tooltip.put(this.node); } } 像素大勇者游戏软件 V1.0            第 58 页 共 60 页 ============================================================================ // ===================== 文件: game/wx_clound_client_api/WxCloudApi.ts ===================== import { get } from "http"; export type CloudReturnType = { code: number,// 200成功 msg?:string, data?:T } export class WxCloudApi{ /** * @en init the cloud * @zh 初始化云客户端(只调用一次即可) * @param {string} env 云环境名称 */ public static init(env:string){ wx?.cloud.init({ env: env }); } /** * @en Login to the cloud * @zh 登录云 * @return 返回结果 * 参考: * result.result = { * code: number, // 200成功,其他都是失败 * msg: string, // 如果失败,这里是失败原因等信息 * data: { // 成功才有 * openid: string, // 用户微信平台openid * regist_time: number, // 时间戳,用户注册时间 * game_data: object, // 开发者自己保存的数据 * } * } * 如果这个泛型令你报错(一般是因为你删了wx.api.d.ts导致),请使用以下签名: * login():Promise<{result: CloudReturnType<{openid: string, regist_time: number, game_data: object}>}> * 或者你是个“不拘小节”的老哥,可以用以下简洁版签名(参考上方的数据结构例子使用即可) * login():Promise */ public static async login(): Promise>>{ return await wx?.cloud.callFunction({ name: 'cocos_cloud', data: { cmd: "login" } }); } /** * @en Save game data to the cloud * @zh 把客户端数据写入云,此为全覆盖写入,请自行管理完整数据 * @return 返回结果 * 参考: * result.result = { 像素大勇者游戏软件 V1.0            第 59 页 共 60 页 ============================================================================ * code: number, // 200成功,其他都是失败 * msg: string, // 如果失败,这里是失败原因等信息 * data: { * errMsg: "document.update:ok", // 数据库返回结果 * stats: { * updated: number, // 更新了几条数据(正常是1) * } * } * 如果这个泛型令你报错(一般是因为你删了wx.api.d.ts导致),请使用以下签名: * save(gameData: any): Promise<{resoult:CloudReturnType}> * 或者你是个“不拘小节”的老哥,可以用以下简洁版签名(参考上方的数据结构例子使用即可) * login():Promise */ public static async save(gameData: any): Promise>> { return await wx?.cloud.callFunction({ name: 'cocos_cloud', data: { cmd: 'save', data: gameData } }); } public static async get(): Promise>> { return await wx?.cloud.callFunction({ name: 'cocos_cloud', data: { cmd: 'get', } }); } /** * @en Sync game data with the cloud (two-way, timestamp-based) * @zh 与云端双向同步游戏数据:客户端较新则上传覆盖云端,云端较新则返回云端数据 * @param gameData 本地游戏数据(需包含 timestamp 字段) * @return 返回结果 * result.result = { * code: number, // 200成功,其他都是失败 * msg?: string, // 失败原因 * updated: boolean, // true=云端已被本地数据覆盖;false=客户端应使用返回的云端数据 * data: { * openid: string, * game_data: object, // 同步后的最终数据 * } * } */ public static async sync(gameData: any): Promise & { updated: boolean }>> { return await wx?.cloud.callFunction({ name: 'cocos_cloud', data: { cmd: 'sync', data: gameData 像素大勇者游戏软件 V1.0            第 60 页 共 60 页 ============================================================================ } }); } }