6 Commits

Author SHA1 Message Date
pan
2c306ff21a feat(技能槽UI): 调整技能槽布局并更新预制件样式
清理了无用的导入语句,调整了MissSkillsComp中的技能槽坐标位置,更新了sbox预制件的图标、文本样式与节点配置,优化界面显示效果。
2026-06-04 15:49:16 +08:00
pan
0c9818ca27 fix(cast&skill): 修复技能预制体加载问题,重构代码并添加调试日志
为SCastSystem添加多处调试日志,便于排查技能施法相关问题
重构Skill类的load方法,将同步预制体获取改为异步加载逻辑
封装重复的技能节点初始化逻辑为内部函数,提升代码可读性
修复预制体未预加载时无法创建技能实体的问题
2026-06-04 15:25:43 +08:00
pan
efe6cc0dd7 refactor(skill): 重构技能盒子管理为ECS实体架构
新增SBox ECS实体,统一管理技能盒子的创建、挂载与销毁
重构MissSkillsComp,改用SBox实体替代直接实例化技能节点
更新SkillBoxComp,新增实体引用以通过ECS生命周期销毁节点
临时调整SCastSystem的索敌范围为全屏级,方便测试
2026-06-04 14:41:27 +08:00
pan
c5d521136d 修复(卡牌&施法系统): 修正卡牌消耗与施法目标选择逻辑
调整“持续天降火球”技能卡牌的法力消耗从5改为0,修复错误配置;优化敌方技能的施法目标选择逻辑,通过索敌范围获取真实敌人位置,替代原有的固定偏移位置,提升技能释放准确性
2026-06-04 14:14:02 +08:00
pan
27cd20c70d feat(card): add wave filter for skill card draws
1. 新增卡牌配置wave字段,标记技能卡可抽取的波次
2. 重构抽卡逻辑,新增drawCardsByRule规则支持按波次过滤技能卡
3. 优化任务面板的技能卡抽取逻辑,使用新的抽卡规则获取对应波次的技能卡
4. 更新示例技能卡牌配置,添加wave和overrides配置示例
2026-06-04 11:02:19 +08:00
pan
1855bcec4c feat(card skill): add skill parameter override support
1. 新增卡牌技能参数覆写配置项,支持自定义伤害、buff数值等
2. 调整UI布局的上下边框参数,适配技能框显示
3. 完整打通技能覆写参数从配置到技能释放的全链路
2026-06-04 10:43:53 +08:00
12 changed files with 3514 additions and 2711 deletions

View File

@@ -2692,8 +2692,8 @@
"height": 81,
"rawWidth": 185,
"rawHeight": 81,
"borderTop": 0,
"borderBottom": 0,
"borderTop": 40,
"borderBottom": 35,
"borderLeft": 92,
"borderRight": 92,
"packable": true,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -35,19 +35,22 @@
},
{
"__id__": 50
},
{
"__id__": 56
}
],
"_active": true,
"_components": [
{
"__id__": 56
"__id__": 62
},
{
"__id__": 58
"__id__": 64
}
],
"_prefab": {
"__id__": 60
"__id__": 66
},
"_lpos": {
"__type__": "cc.Vec3",
@@ -87,7 +90,7 @@
"__id__": 1
},
"_children": [],
"_active": true,
"_active": false,
"_components": [
{
"__id__": 3
@@ -266,7 +269,7 @@
"__id__": 11
}
],
"_active": true,
"_active": false,
"_components": [
{
"__id__": 17
@@ -608,7 +611,7 @@
"__id__": 1
},
"_children": [],
"_active": true,
"_active": false,
"_components": [
{
"__id__": 27
@@ -783,7 +786,7 @@
"__id__": 1
},
"_children": [],
"_active": true,
"_active": false,
"_components": [
{
"__id__": 35
@@ -958,7 +961,7 @@
"__id__": 1
},
"_children": [],
"_active": true,
"_active": false,
"_components": [
{
"__id__": 43
@@ -1126,7 +1129,7 @@
},
{
"__type__": "cc.Node",
"_name": "Label",
"_name": "icon",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
@@ -1145,6 +1148,145 @@
"_prefab": {
"__id__": 55
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 35,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 0.9,
"y": 0.9,
"z": 1
},
"_mobility": 0,
"_layer": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": ""
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 50
},
"_enabled": true,
"__prefab": {
"__id__": 52
},
"_contentSize": {
"__type__": "cc.Size",
"width": 78,
"height": 78
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "0elb3r0slJwbY2BNC1zTKn"
},
{
"__type__": "cc.Sprite",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 50
},
"_enabled": true,
"__prefab": {
"__id__": 54
},
"_customMaterial": null,
"_srcBlendFactor": 2,
"_dstBlendFactor": 4,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_spriteFrame": {
"__uuid__": "031877cb-0f3d-4e92-bc5d-e492a0d95a08@d17da",
"__expectedType__": "cc.SpriteFrame"
},
"_type": 0,
"_fillType": 0,
"_sizeMode": 0,
"_fillCenter": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0
},
"_fillStart": 0,
"_fillRange": 0,
"_isTrimmedMode": true,
"_useGrayscale": false,
"_atlas": {
"__uuid__": "031877cb-0f3d-4e92-bc5d-e492a0d95a08",
"__expectedType__": "cc.SpriteAtlas"
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "075wC9BqZNpKBHWkkHGa0w"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "a33ReyWcpAoZY5ieotS6Ir",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.Node",
"_name": "Label",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 57
},
{
"__id__": 59
}
],
"_prefab": {
"__id__": 61
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
@@ -1180,16 +1322,16 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 50
"__id__": 56
},
"_enabled": true,
"__prefab": {
"__id__": 52
"__id__": 58
},
"_contentSize": {
"__type__": "cc.Size",
"width": 21.90380859375,
"height": 58.4
"width": 15.123046875,
"height": 54.4
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -1208,11 +1350,11 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 50
"__id__": 56
},
"_enabled": true,
"__prefab": {
"__id__": 54
"__id__": 60
},
"_customMaterial": null,
"_srcBlendFactor": 2,
@@ -1227,8 +1369,8 @@
"_string": "9",
"_horizontalAlign": 1,
"_verticalAlign": 1,
"_actualFontSize": 25,
"_fontSize": 25,
"_actualFontSize": 20,
"_fontSize": 20,
"_fontFamily": "Arial",
"_lineHeight": 40,
"_overflow": 0,
@@ -1237,7 +1379,7 @@
"_isSystemFontUsed": true,
"_spacingX": 0,
"_isItalic": false,
"_isBold": true,
"_isBold": false,
"_isUnderline": false,
"_underlineHeight": 2,
"_cacheMode": 0,
@@ -1249,7 +1391,7 @@
"b": 0,
"a": 255
},
"_outlineWidth": 4,
"_outlineWidth": 2,
"_enableShadow": false,
"_shadowColor": {
"__type__": "cc.Color",
@@ -1293,7 +1435,7 @@
},
"_enabled": true,
"__prefab": {
"__id__": 57
"__id__": 63
},
"_contentSize": {
"__type__": "cc.Size",
@@ -1321,13 +1463,13 @@
},
"_enabled": true,
"__prefab": {
"__id__": 59
"__id__": 65
},
"icon_node": {
"__id__": 11
"__id__": 50
},
"info_label": {
"__id__": 53
"__id__": 59
},
"_id": ""
},

View File

@@ -2,6 +2,7 @@ import * as exp from "constants"
import { HeroInfo, HeroList, HType } from "./heroSet"
import { FightSet } from "./GameSet"
import { oops } from "db://oops-framework/core/Oops"
import { SkillOverrides, TGroup } from "./SkillSet"
class I18nString {
constructor(private key: string, private params?: any[]) { }
@@ -51,6 +52,7 @@ export interface CardConfig {
weight: number
kind: CKind
pool_lv: CardLV
wave?: number // 针对技能卡:仅在指定波次(wave)才能抽到
hero_lv?: number
card_lv?: number
base_pool_lv?: number
@@ -62,6 +64,7 @@ export interface CardConfig {
t_times?: number // 触发次数
t_inv?: number // 触发间隔(秒)
keep_waves?: number // 维持的波次数(-1表示持续到战斗结束0或undefined表示仅本波次
overrides?: SkillOverrides // 技能参数覆写如自定义伤害ap、buff值、金币数等
}
export const CardsUpSet: Record<number, number> = {
1: 50,
@@ -127,14 +130,19 @@ HeroList.forEach(uuid => {
// 添加非英雄卡牌 (技能、功能卡)
CardPoolList.push(
// 技能卡牌 (以增益/辅助为主,因为在备战期没有敌人)
{ uuid: 6401, type: CardType.Skill, cost: 0, weight: 20, pool_lv: 1, kind: CKind.Skill, card_lv: 1, name: t("skill_name_6401"), info: t("skill_info_6401"), is_inst: true, t_times: 1, t_inv: 0, keep_waves:15},
{ uuid: 6402, type: CardType.Skill, cost: 0, weight: 20, pool_lv: 1, kind: CKind.Skill, card_lv: 1, name: t("skill_name_6402"), info: t("skill_info_6402"), is_inst: true, t_times: 1, t_inv: 0, keep_waves:15 },
{ uuid: 6403, type: CardType.Skill, cost: 0, weight: 20, pool_lv: 1, kind: CKind.Skill, card_lv: 1, name: t("skill_name_6403"), info: t("skill_info_6403"), is_inst: true, t_times: 1, t_inv: 0, keep_waves:15 },
{ uuid: 6404, type: CardType.Skill, cost: 0, weight: 20, pool_lv: 1, kind: CKind.Skill, card_lv: 1, name: t("skill_name_6404"), info: t("skill_info_6404"), is_inst: true, t_times: 1, t_inv: 0, keep_waves:15 },
{ uuid: 6405, type: CardType.Skill, cost: 0, weight: 20, pool_lv: 2, kind: CKind.Skill, card_lv: 1, name: t("skill_name_6405"), info: t("skill_info_6405"), is_inst: true, t_times: 1, t_inv: 0, keep_waves:15 },
{ uuid: 6406, type: CardType.Skill, cost: 0, weight: 20, pool_lv: 2, kind: CKind.Skill, card_lv: 1, name: t("skill_name_6406"), info: t("skill_info_6406"), is_inst: true, t_times: 1, t_inv: 0, keep_waves:15 },
{ uuid: 6304, type: CardType.Skill, cost: 0, weight: 20, pool_lv: 3, kind: CKind.Skill, card_lv: 1, name: t("skill_name_6304"), info: t("skill_info_6304"), is_inst: true, t_times: 1, t_inv: 0, keep_waves:15 },
{ uuid: 6305, type: CardType.Skill, cost: 0, weight: 20, pool_lv: 3, kind: CKind.Skill, card_lv: 1, name: t("skill_name_6305"), info: t("skill_info_6305"), is_inst: true, t_times: 1, t_inv: 0, keep_waves:15 },
{ uuid: 6304, type: CardType.Skill, cost: 0, weight: 20, pool_lv: 1, wave: 1, kind: CKind.Skill, card_lv: 1, name: t("skill_name_6304"), info: t("skill_info_6304"), is_inst: true, t_times: 1, t_inv: 0, keep_waves: 15 },
{ uuid: 6305, type: CardType.Skill, cost: 0, weight: 20, pool_lv: 1, wave: 1, kind: CKind.Skill, card_lv: 1, name: t("skill_name_6305"), info: t("skill_info_6305"), is_inst: true, t_times: 1, t_inv: 0, keep_waves: 15 },
// 自定义 overrides 示例卡牌
{
uuid: 6401, type: CardType.Skill, cost: 0, weight: 10, pool_lv: 1, wave: 1, kind: CKind.Skill, card_lv: 1,
name: "超强攻击强化", info: "使场上英雄增加50点攻击力",
is_inst: true, t_times: 1, t_inv: 0, keep_waves: 15, overrides: { ap: 50 }
},
{
uuid: 6101, type: CardType.Skill, cost: 0, weight: 10, pool_lv: 1, wave: 1, kind: CKind.Skill, card_lv: 1,
name: "持续天降火球", info: "战斗中每隔3秒释放一个火球造成300%伤害持续2波次",
is_inst: false, t_times: 999, t_inv: 3, keep_waves: 15, overrides: { TGroup: TGroup.Enemy, ap: 300, hit_count: 2 }
}
);
@@ -266,6 +274,7 @@ export const drawCardsByRule = (
heroType?: HType
heroLv?: number
targetPoolLv?: number
wave?: number
} = {}
): CardConfig[] => {
const count = Math.max(0, Math.floor(options.count ?? 4))
@@ -293,6 +302,18 @@ export const drawCardsByRule = (
return true
})
}
// 如果传入了波次并且是技能卡,则根据 wave 过滤
if (options.wave !== undefined) {
pool = pool.filter(card => {
if (card.type === CardType.Skill) {
// 只有 wave 值严格等于当前 wave 的技能卡才会留在池中
return card.wave === options.wave;
}
return true;
})
}
const picked = pickCards(pool, count)
return picked
}

View File

@@ -57,7 +57,7 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
// 卡牌技能直接触发
if (args.isCardSkill) {
this.forceCastCardSkill(args.s_uuid, args.card_lv || 1, args.targetPos || new Vec3(FightSet.CSKILL_START_X, FightSet.CSKILL_START_Y, 0));
this.forceCastCardSkill(args.s_uuid, args.card_lv || 1, args.targetPos || new Vec3(FightSet.CSKILL_START_X, FightSet.CSKILL_START_Y, 0), args.overrides);
return;
}
@@ -70,9 +70,10 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
* 强制执行卡牌技能
* 卡牌技能没有施法者主体,直接从指定坐标释放,或者对全体/随机友方生效
*/
public forceCastCardSkill(s_uuid: number, cardLv: number, spawnPos: Vec3) {
const config = SkillSet[s_uuid];
public forceCastCardSkill(s_uuid: number, cardLv: number, spawnPos: Vec3, overrides?: SkillOverrides) {
let config = SkillSet[s_uuid];
if (!config) return;
config = mergeSkillParams(config, overrides);
// 如果是敌方目标,没有战斗时不释放
const isEnemyTarget = !this.isSelfSkill(config.TGroup) && !this.isFriendlySkill(config.TGroup);
@@ -111,24 +112,49 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
mockAttrs.freeze_chance = 0;
mockAttrs.puncture_chance = 0;
mockAttrs.fac = FacSet.HERO;
mockAttrs.type = HType.Long; // 假定为远程,拥有较长索敌范围
mockAttrs.dis = 2000; // 给予全屏以上的索敌范围
let targetPos: Vec3 | null = null;
if (!isFriendly) {
// 伪造一个 view 供找敌逻辑使用,位置为 spawnPos
const mockView = {
node: { position: spawnPos }
} as any;
// 获取全屏索敌范围
const maxRange = this.resolveMaxCastRange(mockAttrs, mockAttrs.type as HType);
const target = this.findNearestEnemyInRange(mockAttrs, mockView, maxRange);
if (target && target.node) {
targetPos = this.resolveEnemyCastTargetPos(config, mockAttrs, mockView, target, maxRange);
}
// 如果全屏都没找到敌人,直接放弃释放伤害技能
if (!targetPos) {
console.log("[SCastSystem] forceCastCardSkill: no enemy found for skill", s_uuid);
return;
}
}
console.log("[SCastSystem] forceCastCardSkill: casting skill", s_uuid, "castTimes", castTimes, "targetPos", targetPos);
for (let i = 0; i < castTimes; i++) {
if (isFriendly) {
const friendlyTargets = this.resolveFriendlyTargets(targetEids, FacSet.HERO);
if (friendlyTargets.length === 0) continue;
this.applyFriendlySkillEffects(s_uuid, cardLv, config, null as any, mockAttrs, friendlyTargets, spawnPos);
} else {
const enemyTargetPos = this.resolveRepeatCastTargetPos(new Vec3(spawnPos.x + 300, spawnPos.y, spawnPos.z), i);
this.createSkillEntityForCard(s_uuid, cardLv, mockAttrs, spawnPos, enemyTargetPos, i);
const enemyTargetPos = this.resolveRepeatCastTargetPos(targetPos, i);
this.createSkillEntityForCard(s_uuid, cardLv, mockAttrs, spawnPos, enemyTargetPos, i, overrides);
}
}
}
/** 专用于卡牌施放的技能实体生成 */
private createSkillEntityForCard(s_uuid: number, skillLv: number, mockAttrs: HeroAttrsComp, startPos: Vec3, targetPos: Vec3 | null, castIndex: number = 0) {
private createSkillEntityForCard(s_uuid: number, skillLv: number, mockAttrs: HeroAttrsComp, startPos: Vec3, targetPos: Vec3 | null, castIndex: number = 0, overrides?: SkillOverrides) {
const scene = smc.map.MapView.scene;
const parent = scene.entityLayer?.node?.getChildByName("SKILL");
if (!parent || !targetPos) return;
if (!parent || !targetPos) {
console.log("[SCastSystem] createSkillEntityForCard failed: parent or targetPos missing", !!parent, !!targetPos);
return;
}
const skill = ecs.getEntity<Skill>(Skill);
const actualStartPos = this.resolveRepeatCastStartPos(startPos, castIndex);
@@ -140,7 +166,8 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
box_group: BoxSet.HERO
} as any;
skill.load(actualStartPos, parent, s_uuid, targetPos.clone(), mockView, mockAttrs, skillLv, 0);
skill.load(actualStartPos, parent, s_uuid, targetPos.clone(), mockView, mockAttrs, skillLv, 0, overrides);
console.log("[SCastSystem] createSkillEntityForCard success for skill", s_uuid);
}
/** 空施法计划:用于“当前无可施法技能”时的统一返回 */
private readonly emptyCastPlan = { skillId: 0, skillLv: 1, isFriendly: false, targetPos: null as Vec3 | null, targetEids: [] as number[], overrides: undefined as SkillOverrides | undefined };

View File

@@ -24,10 +24,9 @@ import { mLogger } from "../common/Logger";
import { _decorator, Node, Prefab, instantiate, 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 { SkillBoxComp } from "./SkillBoxComp";
import { SBox } from "./SBox";
import { oops } from "db://oops-framework/core/Oops";
import { GameEvent } from "../common/config/GameEvent";
import { smc } from "../common/SingletonModuleComp";
const { ccclass, property } = _decorator;
/** 技能槽位数据结构 */
@@ -64,16 +63,16 @@ export class MissSkillsComp extends CCComp {
* 第 2 行 y=320x = -320, -240, -160, -80, 0
*/
private slots: SkillBoxSlot[] = [
{ x: -320, y: 240, used: false, node: null },
{ x: -240, y: 240, used: false, node: null },
{ x: -160, y: 240, used: false, node: null },
{ x: -80, y: 240, used: false, node: null },
{ x: 0, y: 240, used: false, node: null },
{ x: -320, y: 320, used: false, node: null },
{ x: -240, y: 320, used: false, node: null },
{ x: -160, y: 320, used: false, node: null },
{ x: -80, y: 320, used: false, node: null },
{ x: 0, y: 320, used: false, node: null },
{ x: -320, y: 340, used: false, node: null },
{ x: -240, y: 340, used: false, node: null },
{ x: -160, y: 340, used: false, node: null },
{ x: -80, y: 340, used: false, node: null },
{ x: 0, y: 340, used: false, node: null },
{ x: -320, y: 420, used: false, node: null },
{ x: -240, y: 420, used: false, node: null },
{ x: -160, y: 420, used: false, node: null },
{ x: -80, y: 420, used: false, node: null },
{ x: 0, y: 420, used: false, node: null },
];
/** 注册事件监听 */
@@ -164,9 +163,6 @@ export class MissSkillsComp extends CCComp {
* @param card_lv 技能卡等级
*/
addSkill(uuid: number, card_lv: number) {
// 技能节点的父容器
var parent = smc.map.MapView.scene.entityLayer!.node!.getChildByName("SKILL")!;
if (!this.skill_box) {
mLogger.error(this.debugMode, "MissSkillsComp", "skill_box prefab not set");
return;
@@ -179,17 +175,13 @@ export class MissSkillsComp extends CCComp {
return;
}
// 实例化并放入槽位
const node = instantiate(this.skill_box);
node.parent = parent;
node.setPosition(new Vec3(this.slots[emptyIndex].x, this.slots[emptyIndex].y, 0));
// 使用 ECS 实体创建技能节点
let sbox = ecs.getEntity<SBox>(SBox);
let pos = new Vec3(this.slots[emptyIndex].x, this.slots[emptyIndex].y, 0);
let node = sbox.load(uuid, card_lv, pos, this.skill_box);
this.slots[emptyIndex].used = true;
this.slots[emptyIndex].node = node;
// 初始化技能效果组件
const comp = node.getComponent(SkillBoxComp) || node.addComponent(SkillBoxComp);
comp.init(uuid, card_lv);
}
/** ECS 组件移除时销毁节点 */

View File

@@ -778,12 +778,22 @@ export class MissionCardComp extends CCComp {
private buildSkillDrawCards(): CardConfig[] {
const targetType = CardType.Skill;
const cards = getCardsByLv(this.poolLv, targetType);
const currentWave = this.getCurrentWave();
// 使用明确规则的 drawCardsByRule指定只要 3 张技能卡,并且过滤对应 wave
const cards = drawCardsByRule(this.poolLv, {
count: 3,
type: targetType,
wave: currentWave
});
if (cards.length >= 3) return cards.slice(0, 3);
const filled = [...cards];
while (filled.length < 3) {
const fallback = getCardsByLv(this.poolLv, targetType);
const fallback = drawCardsByRule(this.poolLv, {
count: 3,
type: targetType,
wave: currentWave
});
if (fallback.length === 0) break;
filled.push(fallback[filled.length % fallback.length]);
}

View File

@@ -0,0 +1,52 @@
import { instantiate, Prefab, Vec3, Node } from "cc";
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { smc } from "../common/SingletonModuleComp";
import { SkillBoxComp } from "./SkillBoxComp";
/** SBox 实体:负责技能卡效果节点创建、初始化与销毁流程 */
@ecs.register(`SBox`)
export class SBox extends ecs.Entity {
/** 技能盒子视图和逻辑组件引用 */
SkillBox!: SkillBoxComp;
protected init() {
// 如果有纯逻辑数据组件可以在这里 addComponents
}
/** 销毁实体并释放视图节点,防止残留 */
destroy(): void {
const view = this.get(SkillBoxComp);
if (view && view.node && view.node.isValid) {
view.node.destroy();
}
this.remove(SkillBoxComp);
super.destroy();
}
/**
* 加载并初始化技能盒子
* 1) 创建节点并挂到 SKILL 层
* 2) 初始化表现与属性数据
*/
load(uuid: number, card_lv: number, pos: Vec3, prefab: Prefab): Node {
let node = instantiate(prefab);
let scene = smc.map.MapView.scene;
// 统一挂到实体显示层 SKILL 节点下
let parent = scene.entityLayer!.node!.getChildByName("SKILL");
if (parent) {
node.parent = parent;
}
node.setPosition(pos);
// 获取并注册组件
let sboxComp = node.getComponent(SkillBoxComp) || node.addComponent(SkillBoxComp);
this.add(sboxComp);
// 初始化业务逻辑
sboxComp.init(uuid, card_lv);
return node;
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "10240197-8f36-4350-88cb-e3eae9567f12",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -30,7 +30,7 @@ import { _decorator, Node, Prefab, Sprite, Label, Vec3, resources, SpriteAtlas }
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
import { CardPoolList } from "../common/config/CardSet";
import { SkillSet } from "../common/config/SkillSet";
import { SkillSet, SkillOverrides } from "../common/config/SkillSet";
import { oops } from "db://oops-framework/core/Oops";
import { GameEvent } from "../common/config/GameEvent";
import { smc } from "../common/SingletonModuleComp";
@@ -70,6 +70,8 @@ export class SkillBoxComp extends CCComp {
private trigger_interval: number = 0;
/** 维持的波次数(-1表示直到战斗结束0表示不跨波次>0表示维持的具体波次数 */
private keep_waves: number = 0;
/** 技能覆写参数自定义伤害、Buff等 */
private overrides?: SkillOverrides;
// ======================== 运行时状态 ========================
@@ -125,6 +127,7 @@ export class SkillBoxComp extends CCComp {
this.trigger_times = config.t_times ?? 1;
this.trigger_interval = config.t_inv ?? 0;
this.keep_waves = config.keep_waves ?? 0;
this.overrides = config.overrides;
}
this.current_trigger_times = 0;
@@ -140,7 +143,11 @@ export class SkillBoxComp extends CCComp {
if (this.keep_waves === 0 && this.current_trigger_times >= this.trigger_times) {
// 次数已满且不跨波次维持 → 延迟 1 秒后销毁(保留短暂视觉反馈)
this.scheduleOnce(() => {
if (this.node && this.node.isValid) this.node.destroy();
if (this.ent) {
(this.ent as ecs.Entity).destroy();
} else if (this.node && this.node.isValid) {
this.node.destroy();
}
}, 1.0);
}
}
@@ -209,7 +216,11 @@ export class SkillBoxComp extends CCComp {
if (this.keep_waves > 0) {
this.keep_waves--;
if (this.keep_waves <= 0) {
if (this.node && this.node.isValid) this.node.destroy();
if (this.ent) {
(this.ent as ecs.Entity).destroy();
} else if (this.node && this.node.isValid) {
this.node.destroy();
}
return;
}
}
@@ -227,7 +238,11 @@ export class SkillBoxComp extends CCComp {
// 默认逻辑:不跨波次维持
if (!this.is_instant) {
if (this.current_trigger_times >= this.trigger_times) {
if (this.node && this.node.isValid) this.node.destroy();
if (this.ent) {
(this.ent as ecs.Entity).destroy();
} else if (this.node && this.node.isValid) {
this.node.destroy();
}
}
}
}
@@ -235,7 +250,11 @@ export class SkillBoxComp extends CCComp {
/** 任务结束:强制销毁 */
private onMissionEnd() {
this.node.destroy();
if (this.ent) {
(this.ent as ecs.Entity).destroy();
} else if (this.node && this.node.isValid) {
this.node.destroy();
}
}
// ======================== 帧更新 ========================
@@ -261,7 +280,11 @@ export class SkillBoxComp extends CCComp {
// 次数用完且不跨波次维持 → 延迟销毁
if (this.keep_waves === 0 && this.current_trigger_times >= this.trigger_times) {
this.scheduleOnce(() => {
if (this.node && this.node.isValid) this.node.destroy();
if (this.ent) {
(this.ent as ecs.Entity).destroy();
} else if (this.node && this.node.isValid) {
this.node.destroy();
}
}, 0.5);
}
}
@@ -285,12 +308,15 @@ export class SkillBoxComp extends CCComp {
s_uuid: this.s_uuid,
isCardSkill: true, // 标记为卡牌技能(区别于英雄自身技能)
card_lv: this.card_lv,
targetPos: targetPos
targetPos: targetPos,
overrides: this.overrides
});
}
/** ECS 组件移除时销毁节点 */
reset() {
this.node.destroy();
if (this.node && this.node.isValid) {
this.node.destroy();
}
}
}

View File

@@ -109,124 +109,135 @@ export class Skill extends ecs.Entity {
}
config = mergeSkillParams(config, overrides);
// 加载预制体
const path = `game/skill/atk/${config.sp_name}`;
const prefab:Prefab = oops.res.get(path, Prefab);
if (!prefab) {
mLogger.error(this.debugMode, 'Skill', "[Skill] 预制体加载失败:", path);
return;
}
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(config.EType!=EType.collision){
const collider=node.getComponent(BoxCollider2D);
if(collider){
collider.enabled=false
const initSkillNode = (prefab: Prefab) => {
const node: Node = Skill.getFromPool(path) || instantiate(prefab);
if (!node || !node.isValid) {
mLogger.error(this.debugMode, 'Skill', "[Skill] 节点无效:", path);
return;
}
}
// 只设置必要的运行时属性,配置信息通过 SkillSet[uuid] 访问
// 核心标识
SView.s_uuid= s_uuid
SView.group= caster.box_group
this.add(SView);
startPos.x=startPos.x+SView.atk_x*face
startPos.y=startPos.y+SView.atk_y
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.setPosition(startPos);
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(config.EType!=EType.collision){
const collider=node.getComponent(BoxCollider2D);
if(collider){
collider.enabled=false
}
}
// 只设置必要的运行时属性,配置信息通过 SkillSet[uuid] 访问
// 核心标识
SView.s_uuid= s_uuid
SView.group= caster.box_group
// 初始化移动组件 - 从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;
sMoveCom.runType = config.RType;
sMoveCom.endType = config.EType;
sMoveCom.bezierStartHeight = config.bezier_start_y ?? sMoveCom.bezierStartHeight;
sMoveCom.bezierMidHeight = config.bezier_mid_y ?? sMoveCom.bezierMidHeight;
sMoveCom.bezierArc = config.bezier_arc ?? sMoveCom.bezierArc;
// 从SkillView获取移动参数位置初始化由SMoveSystem统一处理
sMoveCom.atk_x = SView.atk_x;
sMoveCom.atk_y = SView.atk_y;
this.add(SView);
startPos.x=startPos.x+SView.atk_x*face
startPos.y=startPos.y+SView.atk_y
if (config.EType === 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, config.time ?? 0);
sTimeCom.hitInterval = Math.max(0.5, config.hitcd || 0);
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;
sMoveCom.runType = config.RType;
sMoveCom.endType = config.EType;
sMoveCom.bezierStartHeight = config.bezier_start_y ?? sMoveCom.bezierStartHeight;
sMoveCom.bezierMidHeight = config.bezier_mid_y ?? sMoveCom.bezierMidHeight;
sMoveCom.bezierArc = config.bezier_arc ?? sMoveCom.bezierArc;
// 从SkillView获取移动参数位置初始化由SMoveSystem统一处理
sMoveCom.atk_x = SView.atk_x;
sMoveCom.atk_y = SView.atk_y;
if (config.EType === 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, 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 = {};
const SUp=SkillUpList[s_uuid] ? SkillUpList[s_uuid]:SkillUpList[1001];
const sCrt = (config.crt ?? 0)+(SUp.crt*skill_lv);
const sFrz = (config.frz ?? 0)+(SUp.frz*skill_lv);
const sAp =config.ap+(SUp.ap*skill_lv);
const sHit=config.hit_count+(SUp.hit_count*skill_lv);
sDataCom.Attrs[Attrs.ap] = Math.floor(cAttrsComp.ap*sAp/100); //技能的ap是百分值 需要/100 而且需要再最终计算总ap时再/100不然会出现ap为90%变0
sDataCom.Attrs[Attrs.critical] = cAttrsComp.getRuntimeCritical() + sCrt;
sDataCom.Attrs[Attrs.critical_damage] = cAttrsComp.getRuntimeCritDamageBonus();
sDataCom.Attrs[Attrs.freeze_chance] = cAttrsComp.getRuntimeFreezeChance() + sFrz;
sDataCom.Attrs[Attrs.knockback_chance] = cAttrsComp.knockback_chance || 0;
sDataCom.Attrs[Attrs.knockback_distance] = cAttrsComp.knockback_distance || 0;
sDataCom.Attrs[Attrs.puncture_chance] = cAttrsComp.getRuntimePunctureChance(); // 初始化携带施法者的穿透概率
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)
SView.init();
};
let prefab: Prefab = oops.res.get(path, Prefab);
if (prefab) {
initSkillNode(prefab);
} else {
const sTimeCom = this.get(StimeDataComp);
if (sTimeCom) this.remove(StimeDataComp);
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);
});
}
// 初始化数据组件
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 = {};
const SUp=SkillUpList[s_uuid] ? SkillUpList[s_uuid]:SkillUpList[1001];
const sCrt = (config.crt ?? 0)+(SUp.crt*skill_lv);
const sFrz = (config.frz ?? 0)+(SUp.frz*skill_lv);
const sAp =config.ap+(SUp.ap*skill_lv);
const sHit=config.hit_count+(SUp.hit_count*skill_lv);
sDataCom.Attrs[Attrs.ap] = Math.floor(cAttrsComp.ap*sAp/100); //技能的ap是百分值 需要/100 而且需要再最终计算总ap时再/100不然会出现ap为90%变0
sDataCom.Attrs[Attrs.critical] = cAttrsComp.getRuntimeCritical() + sCrt;
sDataCom.Attrs[Attrs.critical_damage] = cAttrsComp.getRuntimeCritDamageBonus();
sDataCom.Attrs[Attrs.freeze_chance] = cAttrsComp.getRuntimeFreezeChance() + sFrz;
sDataCom.Attrs[Attrs.knockback_chance] = cAttrsComp.knockback_chance || 0;
sDataCom.Attrs[Attrs.knockback_distance] = cAttrsComp.knockback_distance || 0;
sDataCom.Attrs[Attrs.puncture_chance] = cAttrsComp.getRuntimePunctureChance(); // 初始化携带施法者的穿透概率
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)
SView.init();
}
/** 模块资源释放 */