1. 新增软著登记所需的源程序生成脚本、docx生成脚本与依赖配置 2. 新增像素大勇者游戏软件V1.0设计说明书与填表指引文档 3. 更新技能配置相关的prefab与meta文件,清理冗余的up状态预制件 4. 修复部分技能buff的颜色与精灵帧引用,更新动画资源引用 5. 新增两个技能图标资源文件
3724 lines
153 KiB
Plaintext
3724 lines
153 KiB
Plaintext
像素大勇者游戏软件 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 页
|
||
============================================================================
|
||
}
|
||
});
|
||
}
|
||
}
|