feat: 重构英雄编队逻辑,优化出生与排队规则
1. 移除英雄出生落点偏移,改为按职业优先级从右向左排队 2. 重构英雄卡点击逻辑,支持点击整张卡牌选取 3. 新增英雄血量背景预制体与UI按钮样式 4. 优化AOE技能目标选取逻辑,使用中排基准点
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,8 @@
|
||||
* @description 统一移动组件与系统(英雄 + 怪物)
|
||||
*
|
||||
* 核心规则:
|
||||
* 1. 准备阶段:双方原地待命,不推进不索敌。
|
||||
* 1. 准备阶段:不推进不索敌;英雄按职业优先级(坦克→战士→刺客→射手→法师→辅助)
|
||||
* 从最右迎敌位向左排队移动,防止同类型落点重叠;怪物原地待命。
|
||||
* 2. 战斗阶段:双方向最近敌人推进,进入攻击范围(model.dis)后停止移动并攻击。
|
||||
* 3. 场上无敌人:停止移动(战斗结束,由 MissionComp 进入结算)。
|
||||
* 4. 击退/位移不会把单位推出屏幕:HERO 不越过 BoxSet.LETF_END,MON 不越过 BoxSet.RIGHT_END。
|
||||
@@ -14,6 +15,7 @@ import { HeroViewComp } from "./HeroViewComp";
|
||||
import { HeroAttrsComp } from "./HeroAttrsComp";
|
||||
import { smc } from "../common/SingletonModuleComp";
|
||||
import { BoxSet, FacSet } from "../common/config/GameSet";
|
||||
import { HRole } from "../common/config/heroSet";
|
||||
import { Node } from "cc";
|
||||
|
||||
@ecs.register('MoveComp')
|
||||
@@ -50,6 +52,18 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
||||
private readonly renderEntries: { node: Node; bossPriority: number; frontScore: number; spawnOrder: number; eid: number; laneScore: number }[] = [];
|
||||
private renderEntryCount = 0;
|
||||
|
||||
// ======================== 英雄编队排队 ========================
|
||||
|
||||
/** 编队锚点:最右迎敌位(与 MissionHeroComp.HERO_ROW_FRONT 的 x 对齐,避免循环依赖在此独立定义) */
|
||||
private static readonly QUEUE_ANCHOR_X = -200;
|
||||
/** 编队槽位默认间隔(像素);人数超出边界宽度时自动压缩间距 */
|
||||
private static readonly QUEUE_SPACING = 30;
|
||||
/** 编队槽位重算节流间隔(秒):召唤/死亡后最多延迟一次重算 */
|
||||
private readonly queueRebuildInterval = 0.1;
|
||||
private lastQueueRebuildAt = 0;
|
||||
/** 编队槽位缓存:按职业优先级排序后各英雄的目标 X */
|
||||
private readonly queueSlots: { eid: number; targetX: number }[] = [];
|
||||
|
||||
private getHeroViewMatcher(): ecs.IMatcher {
|
||||
if (!this.heroViewMatcher) {
|
||||
this.heroViewMatcher = ecs.allOf(HeroAttrsComp, HeroViewComp);
|
||||
@@ -104,11 +118,15 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
||||
}
|
||||
}
|
||||
|
||||
/** 准备阶段:双方原地待命,不推进不索敌 */
|
||||
/** 准备阶段:不推进不索敌;英雄向编队槽位排队移动,怪物原地待命 */
|
||||
if (!smc.mission.in_fight) {
|
||||
this.clearCombatTarget(model);
|
||||
model.is_atking = false;
|
||||
if (view.status !== "atk") view.status_change("idle");
|
||||
if (model.fac === FacSet.HERO) {
|
||||
this.processHeroQueue(e, model, move, view);
|
||||
} else if (view.status !== "atk") {
|
||||
view.status_change("idle");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -133,6 +151,90 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
||||
model.enemy_in_cast_range = false;
|
||||
}
|
||||
|
||||
// ======================== 英雄编队排队(准备阶段) ========================
|
||||
|
||||
/**
|
||||
* 英雄编队排队:
|
||||
* 按 坦克→战士→刺客→射手→法师→辅助 的职业优先级(即 HRole 枚举值升序),
|
||||
* 从最右迎敌位(QUEUE_ANCHOR_X)向左依次排开,防止同类型英雄落点重叠。
|
||||
*
|
||||
* @param entity 当前英雄实体
|
||||
* @param model 英雄属性(取 speed / role)
|
||||
* @param move 移动组件(维护 direction)
|
||||
* @param view 视图组件(驱动位移与动画状态)
|
||||
*/
|
||||
private processHeroQueue(entity: ecs.Entity, model: HeroAttrsComp, move: MoveComp, view: HeroViewComp): void {
|
||||
const targetX = this.resolveHeroQueueTargetX(entity);
|
||||
const selfX = view.node.position.x;
|
||||
|
||||
/** 无槽位(未登记)或已到位:贴齐槽位并原地待命,恢复朝右迎敌 */
|
||||
if (targetX === null || Math.abs(targetX - selfX) <= 2) {
|
||||
if (targetX !== null && Math.abs(selfX - targetX) > 0.01) {
|
||||
view.node.setPosition(targetX, view.node.position.y, 0);
|
||||
}
|
||||
view.scale = 1;
|
||||
if (view.status !== "atk") view.status_change("idle");
|
||||
return;
|
||||
}
|
||||
|
||||
/** 未到位:向槽位推进(与战斗推进同速),朝移动方向转身 */
|
||||
const dir = targetX > selfX ? 1 : -1;
|
||||
move.direction = dir;
|
||||
view.scale = dir;
|
||||
const delta = (model.speed / 3) * this.dt * dir;
|
||||
const newX = this.clampByFacBoundary(selfX + delta, model.fac);
|
||||
if (Math.abs(newX - selfX) >= 0.01) {
|
||||
view.node.setPosition(newX, view.node.position.y, 0);
|
||||
view.status_change("move");
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询当前英雄的编队槽位目标 X(无槽位返回 null) */
|
||||
private resolveHeroQueueTargetX(entity: ecs.Entity): number | null {
|
||||
const now = Date.now() / 1000;
|
||||
if (now - this.lastQueueRebuildAt >= this.queueRebuildInterval) {
|
||||
this.lastQueueRebuildAt = now;
|
||||
this.rebuildHeroQueueSlots();
|
||||
}
|
||||
for (const slot of this.queueSlots) {
|
||||
if (slot.eid === entity.eid) return slot.targetX;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重算编队槽位:
|
||||
* 收集全部存活英雄,按 职业优先级 → 出生序 → eid 稳定排序后,
|
||||
* 从 QUEUE_ANCHOR_X 向左按间隔分配;人数过多时压缩间距,保证不越过 LETF_END。
|
||||
*/
|
||||
private rebuildHeroQueueSlots(): void {
|
||||
const heroes: { eid: number; role: number; spawnOrder: number }[] = [];
|
||||
ecs.query(this.getHeroViewMatcher()).forEach(e => {
|
||||
const m = e.get(HeroAttrsComp);
|
||||
if (!m || m.fac !== FacSet.HERO || m.is_dead) return;
|
||||
const v = e.get(HeroViewComp);
|
||||
if (!v?.node) return;
|
||||
const move = e.get(MoveComp);
|
||||
heroes.push({ eid: e.eid, role: m.role ?? HRole.Support, spawnOrder: move?.spawnOrder ?? 0 });
|
||||
});
|
||||
heroes.sort((a, b) =>
|
||||
(a.role - b.role) ||
|
||||
(a.spawnOrder - b.spawnOrder) ||
|
||||
(a.eid - b.eid)
|
||||
);
|
||||
|
||||
const count = heroes.length;
|
||||
const maxWidth = MoveSystem.QUEUE_ANCHOR_X - BoxSet.LETF_END;
|
||||
const spacing = count > 1
|
||||
? Math.min(MoveSystem.QUEUE_SPACING, maxWidth / (count - 1))
|
||||
: 0;
|
||||
|
||||
this.queueSlots.length = 0;
|
||||
for (let i = 0; i < count; i++) {
|
||||
this.queueSlots.push({ eid: heroes[i].eid, targetX: MoveSystem.QUEUE_ANCHOR_X - i * spacing });
|
||||
}
|
||||
}
|
||||
|
||||
private syncCombatTarget(model: HeroAttrsComp, selfView: HeroViewComp, enemyView: HeroViewComp): void {
|
||||
if (!enemyView || !enemyView.node || !enemyView.ent) {
|
||||
this.clearCombatTarget(model);
|
||||
|
||||
@@ -769,11 +769,9 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
||||
}
|
||||
return null;
|
||||
} else {
|
||||
// 怪物打英雄,英雄网格:
|
||||
// 列1: 0,1,2 (X=-210)
|
||||
// 列2: 3,4,5 (X=-300)
|
||||
// 英雄只有2列,第2列中路是 index 4
|
||||
return MissionHeroComp.HERO_POSITIONS[4].clone();
|
||||
// 怪物打英雄:以英雄编队中排基准点为 AOE 目标
|
||||
// (英雄已改为按职业优先级从最右向左排队,中排 -260 即队伍中部)
|
||||
return MissionHeroComp.HERO_ROW_MID.clone();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* 1. 渲染单个英雄卡的显示信息(名称、等级、描述、图标、费用),
|
||||
* 展示方式与 SCardComp 一致(图集静态图标 + ★等级 + 描述词条)。
|
||||
* 同时支持动态升级卡(SpecialUpgrade + target_hero_eid)按英雄卡样式渲染。
|
||||
* 2. 处理购买点击 → UseHeroCard 上限校验(guard) → 扣费 → 派发 CallHero 召唤英雄。
|
||||
* 2. 处理卡牌点击(点击卡牌任意区域即选取) → UseHeroCard 上限校验(guard) → 派发 CallHero 召唤英雄。
|
||||
* 特殊卡(升级/刷新)则派发 UseSpecialCard。
|
||||
* 3. 使用成功后派发 CardUsed 事件,通知 MissionCardComp 刷新英雄卡池。
|
||||
*
|
||||
@@ -33,7 +33,7 @@ const { ccclass, property } = _decorator;
|
||||
* CHeroComp —— 英雄卡池单个英雄卡项
|
||||
*
|
||||
* 由 MissionCardComp.cacheCardComps() 实例化。
|
||||
* 以图标 + 名称 + 描述词条 + 购买按钮的形式呈现。
|
||||
* 以图标 + 名称 + 描述词条 + 费用角标的形式呈现,点击卡牌任意区域即选取。
|
||||
*/
|
||||
@ccclass('CHeroComp')
|
||||
@ecs.register('CHeroComp', false)
|
||||
@@ -72,7 +72,7 @@ export class CHeroComp extends CCComp {
|
||||
@property(Label)
|
||||
private level_label: Label = null;
|
||||
|
||||
/** 购买按钮节点 */
|
||||
/** 费用显示节点(原购买按钮,现仅展示费用,不再承担点击) */
|
||||
@property({ type: Node })
|
||||
private buy_node: Node = null;
|
||||
|
||||
@@ -118,9 +118,10 @@ export class CHeroComp extends CCComp {
|
||||
fied3: !!this.fied3,
|
||||
buy_node: !!this.buy_node
|
||||
});
|
||||
this.buy_node?.on(NodeEventType.TOUCH_START, this.onBuyTouchStart, this);
|
||||
this.buy_node?.on(NodeEventType.TOUCH_END, this.onBuyClick, this);
|
||||
this.buy_node?.on(NodeEventType.TOUCH_CANCEL, this.onBuyTouchCancel, this);
|
||||
// 点击整张卡牌选取英雄:监听挂载在卡牌根节点,命中区域由根节点 UITransformer 决定
|
||||
this.node.on(NodeEventType.TOUCH_START, this.onCardTouchStart, this);
|
||||
this.node.on(NodeEventType.TOUCH_END, this.onCardClick, this);
|
||||
this.node.on(NodeEventType.TOUCH_CANCEL, this.onCardTouchCancel, this);
|
||||
|
||||
// 缓存透明度组件(用于淡入淡出动画)
|
||||
this.opacityComp = this.node.getComponent(UIOpacity) || this.node.addComponent(UIOpacity);
|
||||
@@ -134,10 +135,10 @@ export class CHeroComp extends CCComp {
|
||||
|
||||
onDestroy() {
|
||||
super.onDestroy();
|
||||
if (this.buy_node && this.buy_node.isValid) {
|
||||
this.buy_node.off(NodeEventType.TOUCH_START, this.onBuyTouchStart, this);
|
||||
this.buy_node.off(NodeEventType.TOUCH_END, this.onBuyClick, this);
|
||||
this.buy_node.off(NodeEventType.TOUCH_CANCEL, this.onBuyTouchCancel, this);
|
||||
if (this.node && this.node.isValid) {
|
||||
this.node.off(NodeEventType.TOUCH_START, this.onCardTouchStart, this);
|
||||
this.node.off(NodeEventType.TOUCH_END, this.onCardClick, this);
|
||||
this.node.off(NodeEventType.TOUCH_CANCEL, this.onCardTouchCancel, this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,7 +304,7 @@ export class CHeroComp extends CCComp {
|
||||
this.updateHeroAnimation(this.icon_node, renderUuid, this.iconVisualToken);
|
||||
}
|
||||
|
||||
// 购买按钮费用显示
|
||||
// 费用显示(原购买按钮节点,现仅作费用角标)
|
||||
if (this.buy_node) {
|
||||
this.buy_node.active = true;
|
||||
const costLabel = this.cost_label
|
||||
@@ -372,7 +373,7 @@ export class CHeroComp extends CCComp {
|
||||
|
||||
/**
|
||||
* 播放购买动画:卡牌放大(scale 1→1.1)+ 淡出,结束后隐藏节点。
|
||||
* 动画结束由 onBuyClick 的后续逻辑(CardUsed 刷新卡池)接管。
|
||||
* 动画结束由 onCardClick 的后续逻辑(CardUsed 刷新卡池)接管。
|
||||
*/
|
||||
private playPurchaseAnim() {
|
||||
Tween.stopAllByTarget(this.node);
|
||||
@@ -413,44 +414,44 @@ export class CHeroComp extends CCComp {
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 按钮动画 ========================
|
||||
// ======================== 点击反馈动画 ========================
|
||||
|
||||
/** 按钮正常缩放 */
|
||||
private readonly btnNormalScale: number = 1;
|
||||
/** 按钮按下缩放 */
|
||||
private readonly btnPressScale: number = 0.92;
|
||||
/** 按钮点击回弹峰值缩放 */
|
||||
private readonly btnClickScale: number = 1.08;
|
||||
/** 卡牌正常缩放 */
|
||||
private readonly cardNormalScale: number = 1;
|
||||
/** 卡牌按下缩放 */
|
||||
private readonly cardPressScale: number = 0.92;
|
||||
/** 卡牌点击回弹峰值缩放 */
|
||||
private readonly cardClickScale: number = 1.08;
|
||||
|
||||
/** 购买按钮按下:缩小反馈 */
|
||||
private onBuyTouchStart() {
|
||||
this.playBtnScaleTo(this.btnPressScale, 0.06);
|
||||
/** 卡牌按下:缩小反馈 */
|
||||
private onCardTouchStart() {
|
||||
this.playCardScaleTo(this.cardPressScale, 0.06);
|
||||
}
|
||||
|
||||
/** 购买按钮取消:恢复缩放 */
|
||||
private onBuyTouchCancel() {
|
||||
this.playBtnScaleTo(this.btnNormalScale, 0.08);
|
||||
/** 卡牌取消(滑出卡牌区域):恢复缩放 */
|
||||
private onCardTouchCancel() {
|
||||
this.playCardScaleTo(this.cardNormalScale, 0.08);
|
||||
}
|
||||
|
||||
/** 按钮缩放动画(通用) */
|
||||
private playBtnScaleTo(scale: number, duration: number) {
|
||||
if (!this.buy_node || !this.buy_node.isValid) return;
|
||||
Tween.stopAllByTarget(this.buy_node);
|
||||
tween(this.buy_node)
|
||||
/** 卡牌缩放动画(通用) */
|
||||
private playCardScaleTo(scale: number, duration: number) {
|
||||
if (!this.node || !this.node.isValid) return;
|
||||
Tween.stopAllByTarget(this.node);
|
||||
tween(this.node)
|
||||
.to(duration, { scale: new Vec3(scale, scale, 1) })
|
||||
.start();
|
||||
}
|
||||
|
||||
/** 按钮点击回弹动画:先弹到峰值再回到正常 */
|
||||
private playBtnClickAnim(onComplete?: () => void) {
|
||||
if (!this.buy_node || !this.buy_node.isValid) {
|
||||
/** 卡牌点击回弹动画:先弹到峰值再回到正常 */
|
||||
private playCardClickAnim(onComplete?: () => void) {
|
||||
if (!this.node || !this.node.isValid) {
|
||||
onComplete?.();
|
||||
return;
|
||||
}
|
||||
Tween.stopAllByTarget(this.buy_node);
|
||||
tween(this.buy_node)
|
||||
.to(0.05, { scale: new Vec3(this.btnClickScale, this.btnClickScale, 1) })
|
||||
.to(0.08, { scale: new Vec3(this.btnNormalScale, this.btnNormalScale, 1) })
|
||||
Tween.stopAllByTarget(this.node);
|
||||
tween(this.node)
|
||||
.to(0.05, { scale: new Vec3(this.cardClickScale, this.cardClickScale, 1) })
|
||||
.to(0.08, { scale: new Vec3(this.cardNormalScale, this.cardNormalScale, 1) })
|
||||
.call(() => onComplete?.())
|
||||
.start();
|
||||
}
|
||||
@@ -561,20 +562,20 @@ export class CHeroComp extends CCComp {
|
||||
// ======================== 购买逻辑 ========================
|
||||
|
||||
/**
|
||||
* 购买点击处理:
|
||||
* 1. 播放按钮点击回弹动画。
|
||||
* 卡牌点击处理(点击卡牌任意区域即选取):
|
||||
* 1. 播放卡牌点击回弹动画。
|
||||
* 2. 英雄卡先通过 UseHeroCard 事件做数量上限校验(guard 模式,可被取消)。
|
||||
* 3. 按卡牌类型分发效果:英雄卡 → CallHero 召唤英雄;特殊卡 → UseSpecialCard。
|
||||
* 4. 派发 CardUsed 通知 MissionCardComp 刷新英雄卡池。
|
||||
* 5. 标记已购买,隐藏购买按钮。
|
||||
* 5. 标记已购买,隐藏费用角标。
|
||||
*
|
||||
* 注:购买不花费金币(免费获取),原扣费逻辑已移除。
|
||||
*/
|
||||
private onBuyClick() {
|
||||
private onCardClick() {
|
||||
if (!this.cardData || this.isPurchased) return;
|
||||
|
||||
// 播放按钮点击回弹动画(视觉反馈,不阻塞后续逻辑)
|
||||
this.playBtnClickAnim();
|
||||
// 播放卡牌点击回弹动画(视觉反馈,不阻塞后续逻辑)
|
||||
this.playCardClickAnim();
|
||||
|
||||
oops.audio.playEffect("music/button");
|
||||
|
||||
|
||||
@@ -17,10 +17,11 @@
|
||||
* revive 系技能为"濒死回血"(血量归零时消耗次数回血续命),不触发真死。
|
||||
*
|
||||
* 出生点规则:
|
||||
* 按攻击类型(HType)分三排:近战 → 前排(x=-200,最靠右迎敌),
|
||||
* 按攻击类型(HType)落排:近战 → 前排(x=-200,最靠右迎敌),
|
||||
* 中距 → 中排(x=-260),远程 → 后排(x=-320,最靠左)。
|
||||
* 同一类型重复召唤时,从该类型基准点向两侧 ±SLOT_OFFSET 依次交替展开,避免落点重叠。
|
||||
* posIndex 取 heroGrid 全局空闲下标(0~9)用于网格登记/索敌,落点偏移与 posIndex、职业均解耦。
|
||||
* 同类型落点不再做 ± 偏移展开,落点重叠由 MoveSystem 的编队排队解决
|
||||
* (准备阶段按职业优先级 坦克→战士→刺客→射手→法师→辅助 从最右向左排开)。
|
||||
* posIndex 取 heroGrid 全局空闲下标(0~9)用于网格登记/索敌,与落点解耦。
|
||||
*
|
||||
* 依赖:
|
||||
* - Hero(hero/Hero.ts)—— 英雄 ECS 实体类
|
||||
@@ -39,7 +40,7 @@ import { HeroInfo, HType } from "../common/config/heroSet";
|
||||
import { oops } from "../../../../extensions/oops-plugin-framework/assets/core/Oops";
|
||||
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
|
||||
import { FacSet, BoxSet } from "../common/config/GameSet";
|
||||
import { HeroViewComp } from "../hero/HeroViewComp";
|
||||
import { MoveComp } from "../hero/MoveComp";
|
||||
const { ccclass } = _decorator;
|
||||
|
||||
/**
|
||||
@@ -63,19 +64,9 @@ export class MissionHeroComp extends CCComp {
|
||||
public static readonly HERO_ROW_MID = v3(-260, BoxSet.GAME_LINE, 0)
|
||||
public static readonly HERO_ROW_BACK = v3(-320, BoxSet.GAME_LINE, 0)
|
||||
|
||||
/**
|
||||
* 英雄登场占位点(兼容旧引用,等价于后/中/前三排基准点):
|
||||
* - index 0=后排、1=中排、2=前排。
|
||||
*/
|
||||
public static readonly HERO_POSITIONS: Vec3[] = [
|
||||
MissionHeroComp.HERO_ROW_BACK,
|
||||
MissionHeroComp.HERO_ROW_MID,
|
||||
MissionHeroComp.HERO_ROW_FRONT,
|
||||
];
|
||||
|
||||
/**
|
||||
* 攻击类型 → 出生排映射:
|
||||
* 近战 → 前排,中距 → 中排,远程 → 后排。
|
||||
* 近战 → 前排,中距 → 中排(优先中间位),远程 → 后排。
|
||||
*/
|
||||
private static readonly TYPE_ROW: Record<HType, Vec3> = {
|
||||
[HType.Melee]: MissionHeroComp.HERO_ROW_FRONT,
|
||||
@@ -83,9 +74,6 @@ export class MissionHeroComp extends CCComp {
|
||||
[HType.Long]: MissionHeroComp.HERO_ROW_BACK,
|
||||
};
|
||||
|
||||
/** 同类型重复召唤时的落点横向间隔(像素) */
|
||||
private static readonly SLOT_OFFSET = 30
|
||||
|
||||
/** 英雄出生时的掉落高度(从空中落到地面的像素差) */
|
||||
private static readonly HERO_DROP_HEIGHT = 260
|
||||
|
||||
@@ -103,6 +91,8 @@ export class MissionHeroComp extends CCComp {
|
||||
is_processing_queue: boolean = false
|
||||
/** 召唤请求队列:保证召唤按顺序串行执行 */
|
||||
summon_queue: { uuid: number; hero_lv: number; card_lv: number }[] = []
|
||||
/** 英雄出生序计数器(编队排队/渲染排序的稳定决胜) */
|
||||
private heroSpawnOrder = 0
|
||||
/** 预留英雄列表 */
|
||||
heros: any = []
|
||||
|
||||
@@ -188,42 +178,26 @@ export class MissionHeroComp extends CCComp {
|
||||
|
||||
/**
|
||||
* 为英雄分配出生槽位:
|
||||
* 按攻击类型取三排基准点(近战前排、中距中排、远程后排),
|
||||
* 同类型第 1 个落基准点,之后按 ±SLOT_OFFSET 交替展开(0, +30, -30, +60, -60 …)。
|
||||
* 按攻击类型取三排基准点(近战前排、中距优先中排、远程后排),同类型不再做横向偏移展开;
|
||||
* 落点重叠由 MoveSystem 的编队排队(按职业优先级从最右向左排开)负责分开。
|
||||
*
|
||||
* 槽位编码:posIndex 直接取 heroGrid 的全局空闲下标(0~9),与落点无映射。
|
||||
* 落点偏移由「同攻击类型已占位英雄的 x 偏移」推展开序计算,与 posIndex、职业均解耦。
|
||||
*
|
||||
* @param type 英雄攻击类型(HType,决定所属排)
|
||||
* @param heroes 当前存活英雄列表(spawnHeroAt 在实体创建前快照,避免复用池残留实体干扰槽位判定)
|
||||
* @param heroes 当前存活英雄列表(spawnHeroAt 在实体创建前快照,避免复用池残留实体干扰网格判定)
|
||||
* @returns { posIndex heroGrid 空闲下标(-1 表示满员未登记), landingPos 落地点 }
|
||||
*/
|
||||
private pickSpawnSlotForHero(type: HType, heroes: Hero[]): { posIndex: number; landingPos: Vec3 } {
|
||||
const basePos = MissionHeroComp.TYPE_ROW[type] ?? MissionHeroComp.HERO_ROW_MID;
|
||||
|
||||
// 同攻击类型已占位的 x 偏移集合(依据落点 x 反推,与 posIndex 解耦)
|
||||
const usedOffsets = new Set<number>();
|
||||
// 已登记的网格下标集合(仅用于 posIndex 分配)
|
||||
const occupiedGrid = new Set<number>();
|
||||
for (const h of heroes) {
|
||||
const m = h.get(HeroAttrsComp)!;
|
||||
if (m.posIndex >= 0) occupiedGrid.add(m.posIndex);
|
||||
if (m.type !== type) continue;
|
||||
const view = h.get(HeroViewComp);
|
||||
if (view?.node?.isValid) {
|
||||
usedOffsets.add(Math.round(view.node.position.x - basePos.x));
|
||||
}
|
||||
}
|
||||
|
||||
// 展开序 n:0=居中,1=+30,2=-30,3=+60 …,找到第一个未被同类型占用的偏移
|
||||
// 上限取 heroGrid 容量,同类型超过该数量时复用基准点(不再向外展开)
|
||||
let offsetX = 0;
|
||||
for (let n = 0; n < smc.mission.heroGrid.length; n++) {
|
||||
const step = Math.ceil(n / 2);
|
||||
const dir = n % 2 === 1 ? 1 : -1;
|
||||
const candidate = n === 0 ? 0 : dir * step * MissionHeroComp.SLOT_OFFSET;
|
||||
if (!usedOffsets.has(candidate)) { offsetX = candidate; break; }
|
||||
}
|
||||
const landingPos = v3(basePos.x + offsetX, basePos.y, 0);
|
||||
const landingPos = v3(basePos.x, basePos.y, 0);
|
||||
|
||||
// posIndex 取 heroGrid 全局空闲下标,仅用于网格登记/索敌,与落点无映射
|
||||
let posIndex = -1;
|
||||
@@ -235,8 +209,9 @@ export class MissionHeroComp extends CCComp {
|
||||
|
||||
/**
|
||||
* 生成一个英雄:
|
||||
* - 按攻击类型自动分配三排槽位并展开落点。
|
||||
* - 按攻击类型落排基准点(近战前排 / 中距中排 / 远程后排),不做横向偏移。
|
||||
* - 计算出生点(空中)和落点(地面),播放掉落动画。
|
||||
* - 落地后由 MoveSystem 按职业优先级从最右向左排队分开。
|
||||
*
|
||||
* @param uuid 英雄 UUID
|
||||
* @param hero_lv 英雄等级
|
||||
@@ -259,6 +234,12 @@ export class MissionHeroComp extends CCComp {
|
||||
let spawnPos: Vec3 = v3(landingPos.x, landingPos.y + MissionHeroComp.HERO_DROP_HEIGHT, 0);
|
||||
hero.load(spawnPos, scale, uuid, landingPos.y, hero_lv, card_lv, finalPosIndex);
|
||||
|
||||
// 登记出生序,供编队排队/渲染排序做稳定决胜
|
||||
const move = hero.get(MoveComp);
|
||||
if (move) {
|
||||
move.spawnOrder = ++this.heroSpawnOrder;
|
||||
}
|
||||
|
||||
// 召唤完成后,派发事件以更新英雄面板
|
||||
const model = hero.get(HeroAttrsComp);
|
||||
if (model) {
|
||||
|
||||
Reference in New Issue
Block a user