Files
pixelheros/docs/软著/像素大勇者游戏软件_V1.0_源程序.txt
panFD 2dc44b2393 feat(软著): 新增软著登记全套文档与资源整理
1. 新增软著登记所需的源程序生成脚本、docx生成脚本与依赖配置
2. 新增像素大勇者游戏软件V1.0设计说明书与填表指引文档
3. 更新技能配置相关的prefab与meta文件,清理冗余的up状态预制件
4. 修复部分技能buff的颜色与精灵帧引用,更新动画资源引用
5. 新增两个技能图标资源文件
2026-09-14 20:46:31 +08:00

3724 lines
153 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
像素大勇者游戏软件 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>(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>(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,支持 <color>/<size>/<b> 等富文本标签。
* 3. 扩展标题样式、窗体缩放动画、遮罩点击关闭等自定义能力。
*
* 使用(oops.gui 模块):
* oops.gui.open(UIID.Window, {
* title: "标题",
* content: "支持 <color=#ff0000>富文本</color>",
* 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 富文本,支持 <color>/<size>/<b>/<i>/<outline> 等标签) */
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<number, DebuffIcon> = {
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<number, BuffConfig> = {
// 攻击强化(限时版)
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<number, SpecialUpgradeCardConfig> = {
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<number, SpecialRefreshCardConfig> = {
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<CardType> => {
const list = Array.isArray(type) ? type : [type]
return new Set<CardType>(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 `<outline color=${RICH_VALUE_OUTLINE} width=1><color=${RICH_VALUE_COLOR}>${text}</color></outline>`;
}
/** 富文本技能名高亮色(蜜桃橙,暖色系,与面板绿/蓝/红/黄/紫均不冲突) */
const RICH_NAME_COLOR = "#F8C471";
/** 技能名描边色(深褐,衬托蜜桃橙填充,深色面板上温馨醒目) */
const RICH_NAME_OUTLINE = "#6E2C00";
/**
* 技能名富文本片段:[技能名]整体蜜桃橙填充 + 深褐描边(含方括号)。
* 括号与技能名同色同描边,正文/数值不描边,突出技能名视觉层级。
* @param name 技能名(纯文本,不含括号)
* @returns <outline><color>[技能名]</color></outline> 富文本片段
*/
function skillNameRich(name: string): string {
return `<outline color=${RICH_NAME_OUTLINE} width=1><color=${RICH_NAME_COLOR}>[${name}]</color></outline>`;
}
/** 技能名「」匹配模式(仅本模块行首/行中技能名片段使用) */
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<string, string | number>, 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<prefabPath, NodePool> */
static pools: Map<string, NodePool> = 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>(SDataCom);
像素大勇者游戏软件 V1.0            第 34 页 共 60 页
============================================================================
this.addComponents<SMoveDataComp>(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<T = any> = {
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<any>
*/
public static async login(): Promise<CloudCallFunctionResult<CloudReturnType<{
openid:string,
regist_time:number,
game_data:any
}>>>{
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<any>
*/
public static async save(gameData: any): Promise<CloudCallFunctionResult<CloudReturnType<{
errMsg: string,
status:{
updated: number
}
}>>> {
return await wx?.cloud.callFunction({
name: 'cocos_cloud',
data: {
cmd: 'save',
data: gameData
}
});
}
public static async get(): Promise<CloudCallFunctionResult<CloudReturnType<{}>>> {
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<CloudCallFunctionResult<CloudReturnType<{
openid: string,
game_data: any
}> & { updated: boolean }>> {
return await wx?.cloud.callFunction({
name: 'cocos_cloud',
data: {
cmd: 'sync',
data: gameData
像素大勇者游戏软件 V1.0            第 60 页 共 60 页
============================================================================
}
});
}
}