4 Commits

Author SHA1 Message Date
walkpan
95f4209201 fix(技能系统): 调整强制释放技能的战斗状态检查逻辑
将无条件检查是否在战斗中改为仅对敌方目标技能进行战斗状态检查。友方增益和护盾技能现在可以在非战斗状态下释放,提高游戏体验的流畅性。
2026-04-05 22:10:25 +08:00
walkpan
ef07982645 feat(技能): 新增触发技能机制并调整死亡特效颜色
- 在 GameEvent 枚举中添加 TriggerSkill 事件用于技能触发
- 为 Hero 和 Monster 实体添加召唤入场时的 call 技能触发
- 在 HeroAtkSystem 中实现死亡时的 dead 技能触发
- 扩展 SCastSystem 支持强制触发技能(忽略CD和动画前摇)
- 将死亡技能特效颜色从灰色调整为白色以提升视觉效果
2026-04-05 22:09:16 +08:00
walkpan
5a5d849c0b feat(英雄配置): 为英雄数据添加召唤和死亡技能字段
在 heroInfo 接口中添加 call 和 dead 可选字段,用于存储英雄召唤后和死亡后触发的技能ID。同时为盾战士(5001)配置了召唤和死亡技能(6305)。
2026-04-05 21:23:19 +08:00
walkpan
660fa8be7b feat(skill): 新增死亡技能预制体并调整现有技能颜色
- 新增 dead.prefab 预制体,包含动画和灰阶效果
- 调整 blues.prefab 透明度为完全不透明
- 修改 reds.prefab 为暗红色调并提高透明度
- 更新 yellow.prefab 为橙色并启用灰阶效果
2026-04-05 21:23:00 +08:00
11 changed files with 402 additions and 10 deletions

View File

@@ -161,7 +161,7 @@
"r": 255,
"g": 255,
"b": 255,
"a": 206
"a": 255
},
"_spriteFrame": {
"__uuid__": "2423272e-e63b-4736-b15b-30b40cf98a23@d58f7",

View File

@@ -0,0 +1,274 @@
[
{
"__type__": "cc.Prefab",
"_name": "dead",
"_objFlags": 0,
"__editorExtras__": {},
"_native": "",
"data": {
"__id__": 1
},
"optimizationPolicy": 0,
"persistent": false
},
{
"__type__": "cc.Node",
"_name": "dead",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": null,
"_children": [
{
"__id__": 2
}
],
"_active": true,
"_components": [
{
"__id__": 8
},
{
"__id__": 10
}
],
"_prefab": {
"__id__": 12
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 1,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": ""
},
{
"__type__": "cc.Node",
"_name": "skill",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 3
},
{
"__id__": 5
}
],
"_prefab": {
"__id__": 7
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": -35.811,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 0.7,
"y": 0.7,
"z": 1
},
"_mobility": 0,
"_layer": 1,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": ""
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2
},
"_enabled": true,
"__prefab": {
"__id__": 4
},
"_contentSize": {
"__type__": "cc.Size",
"width": 228,
"height": 207
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "b4sNQPJWFKha7x75SWXmRj"
},
{
"__type__": "cc.Sprite",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2
},
"_enabled": true,
"__prefab": {
"__id__": 6
},
"_customMaterial": null,
"_srcBlendFactor": 2,
"_dstBlendFactor": 4,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_spriteFrame": {
"__uuid__": "2423272e-e63b-4736-b15b-30b40cf98a23@a16b8",
"__expectedType__": "cc.SpriteFrame"
},
"_type": 1,
"_fillType": 1,
"_sizeMode": 1,
"_fillCenter": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0
},
"_fillStart": 0.2,
"_fillRange": 1,
"_isTrimmedMode": true,
"_useGrayscale": true,
"_atlas": {
"__uuid__": "2423272e-e63b-4736-b15b-30b40cf98a23",
"__expectedType__": "cc.SpriteAtlas"
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "9a+65VIghBm4HQxuHPQ/mg"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "7a5LvbdlxEc6FKa7cun2oB",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 1
},
"_enabled": true,
"__prefab": {
"__id__": 9
},
"_contentSize": {
"__type__": "cc.Size",
"width": 80,
"height": 110
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "63NP9yq3hEUKD/OZZZ5t7x"
},
{
"__type__": "cc.Animation",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 1
},
"_enabled": true,
"__prefab": {
"__id__": 11
},
"playOnLoad": true,
"_clips": [
{
"__uuid__": "11f930a0-c09e-48e3-9616-c209909363a2",
"__expectedType__": "cc.AnimationClip"
}
],
"_defaultClip": {
"__uuid__": "11f930a0-c09e-48e3-9616-c209909363a2",
"__expectedType__": "cc.AnimationClip"
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "c6LOemuvJKyYCqlF/yUJcr"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "c46/YsCPVOJYA4mWEpNYRx",
"instance": null,
"targetOverrides": null
}
]

View File

@@ -0,0 +1,13 @@
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "c180d1d8-387a-4cfa-a53f-9c2eed4ec93f",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "dead"
}
}

View File

@@ -159,9 +159,9 @@
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 206
"g": 122,
"b": 122,
"a": 255
},
"_spriteFrame": {
"__uuid__": "2423272e-e63b-4736-b15b-30b40cf98a23@07243",

View File

@@ -159,9 +159,9 @@
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 206
"g": 173,
"b": 0,
"a": 255
},
"_spriteFrame": {
"__uuid__": "2423272e-e63b-4736-b15b-30b40cf98a23@a16b8",
@@ -178,7 +178,7 @@
"_fillStart": 0.2,
"_fillRange": 1,
"_isTrimmedMode": true,
"_useGrayscale": false,
"_useGrayscale": true,
"_atlas": {
"__uuid__": "2423272e-e63b-4736-b15b-30b40cf98a23",
"__expectedType__": "cc.SpriteAtlas"

View File

@@ -73,4 +73,5 @@ export enum GameEvent {
UpdateMissionGet = "UpdateMissionGet",
GlobalAttrChange = "GlobalAttrChange",
CoinAdd = "CoinAdd",
TriggerSkill = "TriggerSkill", // 瞬间触发施法事件
}

View File

@@ -63,6 +63,8 @@ export interface heroInfo {
type: HType; // 攻击定位(近战/中程/远程)
hp: number; // 生命值上限
ap: number; // 攻击力
call?:number; // 召唤后出发的技能uuid
dead?:number; // 死亡后出发的技能uuid
// dis: number; // 攻击距离(像素)
speed: number; // 移动速度(像素/秒)
skills: Record<number, HSkillInfo> ; // 携带技能ID列表
@@ -94,7 +96,7 @@ export interface HSkillInfo {
export const HeroInfo: Record<number, heroInfo> = {
// ========== 近战英雄 ==========
5001:{uuid:5001,name:"盾战士",path:"hk1", fac:FacSet.HERO,cards_lv:1,lv:1,type:HType.Melee,hp:450,ap:25,speed:480,
5001:{uuid:5001,name:"盾战士",path:"hk1", fac:FacSet.HERO,cards_lv:1,lv:1,type:HType.Melee,hp:450,ap:25,call:6305,dead:6305,speed:480,
skills:{6001:{uuid:6001,lv:1,cd:0.75,ccd:0},6301:{uuid:6301,lv:1,cd:5,ccd:0}},info:"近战,魔法盾 坦克"},
5002:{uuid:5002,name:"圣骑士",path:"hk3", fac:FacSet.HERO,cards_lv:3,lv:1,type:HType.Melee,hp:1350,ap:75,speed:480,
skills:{6001:{uuid:6001,lv:1,cd:0.75,ccd:0},6305:{uuid:6305,lv:1,cd:5,ccd:0}},info:"近战,群体护盾 坦克"},

View File

@@ -143,6 +143,15 @@ export class Hero extends ecs.Entity {
move.moving = false;
hv.as.idle();
// 触发 call 技能
if (hero.call) {
oops.message.dispatchEvent(GameEvent.TriggerSkill, {
s_uuid: hero.call,
heroAttrs: model,
heroView: hv
});
}
// 依据下落距离自适应入场时长,保证手感稳定
const dropDistance = Math.abs(pos.y - dropToY);
const dropDuration = Math.max(0.18, Math.min(0.38, dropDistance / 1200));

View File

@@ -7,7 +7,9 @@ import { HeroAttrsComp } from "./HeroAttrsComp";
import { HeroViewComp } from "./HeroViewComp";
import { DamageQueueComp, DamageEvent } from "./DamageQueueComp";
import { smc } from "../common/SingletonModuleComp";
import { HeroInfo } from "../common/config/heroSet";
import { oops } from "db://oops-framework/core/Oops";
import { GameEvent } from "../common/config/GameEvent";
import { mLogger } from "../common/Logger";
@@ -250,6 +252,19 @@ export class HeroAtkSystem extends ecs.ComblockSystem implements ecs.ISystemUpd
if (!TAttrsComp || TAttrsComp.is_dead) return;
TAttrsComp.is_dead = true;
// 触发死亡技能
const heroInfo = HeroInfo[TAttrsComp.hero_uuid];
if (heroInfo && heroInfo.dead) {
const view = entity.get(HeroViewComp);
if (view) {
oops.message.dispatchEvent(GameEvent.TriggerSkill, {
s_uuid: heroInfo.dead,
heroAttrs: TAttrsComp,
heroView: view
});
}
}
// 触发死亡事件
this.onDeath(entity);

View File

@@ -7,6 +7,7 @@ import { HeroInfo } from "../common/config/heroSet";
import { HeroAttrsComp } from "./HeroAttrsComp";
import { HeroViewComp } from "./HeroViewComp";
import { MoveComp } from "./MoveComp";
import { GameEvent } from "../common/config/GameEvent";
/** 怪物实体:负责怪物对象池复用、属性初始化、入场动画与回收 */
@ecs.register(`Monster`)
export class Monster extends ecs.Entity {
@@ -193,6 +194,15 @@ export class Monster extends ecs.Entity {
move.baseY = dropToY;
move.moving = false;
// 触发 call 技能
if (hero.call) {
oops.message.dispatchEvent(GameEvent.TriggerSkill, {
s_uuid: hero.call,
heroAttrs: model,
heroView: view
});
}
// 依据下落距离自适应入场时长,确保观感一致
const dropDistance = Math.abs(pos.y - dropToY);
const dropDuration = Math.max(0.18, Math.min(0.38, dropDistance / 1200));

View File

@@ -8,6 +8,8 @@ import { smc } from "../common/SingletonModuleComp";
import { HType } from "../common/config/heroSet";
import { Attrs } from "../common/config/HeroAttrs";
import { BoxSet, FightSet } from "../common/config/GameSet";
import { oops } from "db://oops-framework/core/Oops";
import { GameEvent } from "../common/config/GameEvent";
/**
* ==================== 自动施法系统 ====================
@@ -23,7 +25,20 @@ import { BoxSet, FightSet } from "../common/config/GameSet";
*/
@ecs.register('SCastSystem')
export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate {
static instance: SCastSystem | null = null;
debugMode: boolean = false; // 是否启用调试模式
constructor() {
super();
SCastSystem.instance = this;
// 监听触发技能事件
oops.message.on(GameEvent.TriggerSkill, this.onTriggerSkill, this);
}
private onTriggerSkill(event: string, args: { s_uuid: number, heroAttrs: HeroAttrsComp, heroView: HeroViewComp }) {
if (!args || !args.s_uuid || !args.heroAttrs || !args.heroView) return;
this.forceCastTriggerSkill(args.s_uuid, args.heroAttrs, args.heroView);
}
/** 空施法计划:用于“当前无可施法技能”时的统一返回 */
private readonly emptyCastPlan = { skillId: 0, skillLv: 1, isFriendly: false, targetPos: null as Vec3 | null, targetEids: [] as number[] };
/** 近战英雄默认施法射程 */
@@ -71,6 +86,59 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
this.castSkill(castPlan, heroAttrs, heroView);
}
/**
* 强制执行触发技能(召唤/死亡触发)
* 忽略CD、状态、动画前摇直接生效
*/
public forceCastTriggerSkill(s_uuid: number, heroAttrs: HeroAttrsComp, heroView: HeroViewComp) {
// 如果是敌方攻击技能,必须在战斗中才能释放;友方增益/护盾则允许在非战斗中释放
const config = SkillSet[s_uuid];
if (!config) return;
const isEnemyTarget = !this.isSelfSkill(config.TGroup) && !this.isFriendlySkill(config.TGroup);
if (isEnemyTarget && !smc.mission.in_fight) return;
const skillLv = heroAttrs.getSkillLevel(s_uuid) || 1;
let isFriendly = false;
let targetPos: Vec3 | null = null;
let targetEids: number[] = [];
const selfEid = heroView.ent?.eid;
const type = heroAttrs.type as HType;
const maxRange = this.resolveMaxCastRange(heroAttrs, type);
if (this.isSelfSkill(config.TGroup)) {
isFriendly = true;
if (typeof selfEid === "number") targetEids = [selfEid];
} else if (this.isFriendlySkill(config.TGroup)) {
isFriendly = true;
const includeSelf = config.TGroup === TGroup.Ally;
targetEids = this.collectFriendlyTargetEids(heroAttrs.fac, selfEid, includeSelf);
} else {
const target = this.findNearestEnemyInRange(heroAttrs, heroView, maxRange);
if (target && target.node) {
targetPos = this.resolveEnemyCastTargetPos(config, heroAttrs, heroView, target, maxRange);
}
}
const sUp = SkillUpList[s_uuid] ? SkillUpList[s_uuid] : SkillUpList[1001];
const cNum = Math.min(2, Math.max(0, Math.floor(sUp.num ?? 0)));
const castTimes = 1 + cNum;
for (let i = 0; i < castTimes; i++) {
if (!heroView.node || !heroView.node.isValid) return;
if (isFriendly) {
const friendlyTargets = this.resolveFriendlyTargets(targetEids, heroAttrs.fac);
if (friendlyTargets.length === 0) continue;
this.applyFriendlySkillEffects(s_uuid, skillLv, config, heroView, heroAttrs, friendlyTargets, null);
} else {
const enemyTargetPos = this.resolveRepeatCastTargetPos(targetPos, i);
this.applyEnemySkillEffects(s_uuid, skillLv, config, heroView, heroAttrs, enemyTargetPos, i);
}
}
}
/**
* 选择当前应释放的技能。
* 选择顺序:技能候选列表顺序 + 条件过滤CD、目标可达、目标类型匹配