refactor(hero): 统一英雄攻击距离默认值并优化队友间距逻辑
1. 将英雄/怪物默认攻击距离调整为近战150、中程300、远程450,通过HeroDisVal统一管理 2. 新增同阵营单位移动和待机时的间距避让逻辑,避免单位重叠穿模,添加自然蠕动效果
This commit is contained in:
@@ -85,10 +85,11 @@ export const FormationPointX = {
|
|||||||
[HType.Long]: 100,
|
[HType.Long]: 100,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
/** 各攻击定位的默认攻击距离(像素):近战150 / 中程300 / 远程450;英雄配置了 dis 时优先取配置值 */
|
||||||
export const HeroDisVal: Record<HType.Melee | HType.Mid | HType.Long, number> = {
|
export const HeroDisVal: Record<HType.Melee | HType.Mid | HType.Long, number> = {
|
||||||
[HType.Melee]: 120,
|
[HType.Melee]: 150,
|
||||||
[HType.Mid]: 720,
|
[HType.Mid]: 300,
|
||||||
[HType.Long]: 720,
|
[HType.Long]: 450,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const resolveFormationTargetX = (fac: FacSet, type: HType): number => {
|
export const resolveFormationTargetX = (fac: FacSet, type: HType): number => {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { smc } from "../common/SingletonModuleComp";
|
|||||||
import { HeroAttrsComp } from "./HeroAttrsComp";
|
import { HeroAttrsComp } from "./HeroAttrsComp";
|
||||||
import { HeroViewComp } from "./HeroViewComp";
|
import { HeroViewComp } from "./HeroViewComp";
|
||||||
import { BoxSet, FacSet, FightSet, IndexSet } from "../common/config/GameSet";
|
import { BoxSet, FacSet, FightSet, IndexSet } from "../common/config/GameSet";
|
||||||
import { HeroInfo, resolveFormationTargetX, resolveTriggerByLv, resolveFieldByLv, resolveReviveByLv, calcGrowBonus } from "../common/config/heroSet";
|
import { HeroInfo, resolveFormationTargetX, resolveTriggerByLv, resolveFieldByLv, resolveReviveByLv, calcGrowBonus, HeroDisVal } from "../common/config/heroSet";
|
||||||
import { GameEvent } from "../common/config/GameEvent";
|
import { GameEvent } from "../common/config/GameEvent";
|
||||||
import { SkillTriggerType, HType } from "../common/config/heroSet";
|
import { SkillTriggerType, HType } from "../common/config/heroSet";
|
||||||
import { SkillTriggerHelper } from "./SkillTriggerHelper";
|
import { SkillTriggerHelper } from "./SkillTriggerHelper";
|
||||||
@@ -122,8 +122,8 @@ export class Hero extends ecs.Entity {
|
|||||||
model.grow_type = hero.grow_type;
|
model.grow_type = hero.grow_type;
|
||||||
model.role = hero.role;
|
model.role = hero.role;
|
||||||
model.fac = FacSet.HERO;
|
model.fac = FacSet.HERO;
|
||||||
// 攻击距离:配置优先;未配置按攻击定位给默认值(近战180 / 远程600)
|
// 攻击距离:配置优先;未配置按攻击定位取默认值(近战150 / 中程300 / 远程450)
|
||||||
model.dis = hero.dis ?? (hero.type === HType.Melee ? 180 : 600);
|
model.dis = hero.dis ?? HeroDisVal[hero.type as HType.Melee | HType.Mid | HType.Long];
|
||||||
model.posIndex = posIndex;
|
model.posIndex = posIndex;
|
||||||
if (posIndex >= 0) {
|
if (posIndex >= 0) {
|
||||||
smc.mission.heroGrid[posIndex] = this.eid;
|
smc.mission.heroGrid[posIndex] = this.eid;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { oops } from "../../../../extensions/oops-plugin-framework/assets/core/O
|
|||||||
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
|
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
|
||||||
import { smc } from "../common/SingletonModuleComp";
|
import { smc } from "../common/SingletonModuleComp";
|
||||||
import { BoxSet, FacSet, FightSet, IndexSet } from "../common/config/GameSet";
|
import { BoxSet, FacSet, FightSet, IndexSet } from "../common/config/GameSet";
|
||||||
import { HeroInfo, resolveTriggerByLv, resolveReviveByLv } from "../common/config/heroSet";
|
import { HeroInfo, resolveTriggerByLv, resolveReviveByLv, HeroDisVal } from "../common/config/heroSet";
|
||||||
import { HeroAttrsComp } from "./HeroAttrsComp";
|
import { HeroAttrsComp } from "./HeroAttrsComp";
|
||||||
import { HeroViewComp } from "./HeroViewComp";
|
import { HeroViewComp } from "./HeroViewComp";
|
||||||
import { MoveComp } from "./MoveComp";
|
import { MoveComp } from "./MoveComp";
|
||||||
@@ -174,8 +174,8 @@ export class Monster extends ecs.Entity {
|
|||||||
model.speed = hero.speed ?? 800;
|
model.speed = hero.speed ?? 800;
|
||||||
model.type = hero.type;
|
model.type = hero.type;
|
||||||
model.fac = FacSet.MON;
|
model.fac = FacSet.MON;
|
||||||
// 攻击距离:配置优先;未配置按攻击定位给默认值(近战180 / 远程600)
|
// 攻击距离:配置优先;未配置按攻击定位取默认值(近战150 / 中程300 / 远程450)
|
||||||
model.dis = hero.dis ?? (hero.type === HType.Melee ? 180 : 600);
|
model.dis = hero.dis ?? HeroDisVal[hero.type as HType.Melee | HType.Mid | HType.Long];
|
||||||
model.posIndex = laneIndex;
|
model.posIndex = laneIndex;
|
||||||
if (laneIndex >= 0) {
|
if (laneIndex >= 0) {
|
||||||
smc.mission.monGrid[laneIndex] = this.eid;
|
smc.mission.monGrid[laneIndex] = this.eid;
|
||||||
|
|||||||
@@ -136,9 +136,14 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
|||||||
return dist <= attackRange;
|
return dist <= attackRange;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 同阵营横向最小间距,防止停止移动时完全重叠 */
|
||||||
|
private static readonly ALLY_MIN_GAP_X = 10;
|
||||||
|
/** 挤位时的蠕动幅度(像素) */
|
||||||
|
private static readonly JOSTLE_AMPLITUDE = 2;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 战斗位移:向最近敌人推进,进入攻击范围后停止。
|
* 战斗位移:向最近敌人推进,进入攻击范围后停止。
|
||||||
* 推进无上限限制,但击退/位移受阵营后退边界保护。
|
* 推进无上限限制,可穿越队友;停止移动后受同阵营最小间距约束。
|
||||||
*/
|
*/
|
||||||
private processCombatLogic(_e: ecs.Entity, move: MoveComp, view: HeroViewComp, model: HeroAttrsComp, enemy: HeroViewComp) {
|
private processCombatLogic(_e: ecs.Entity, move: MoveComp, view: HeroViewComp, model: HeroAttrsComp, enemy: HeroViewComp) {
|
||||||
const selfX = view.node.position.x;
|
const selfX = view.node.position.x;
|
||||||
@@ -155,8 +160,10 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
|||||||
if (view.status === "move") {
|
if (view.status === "move") {
|
||||||
view.status_change("idle");
|
view.status_change("idle");
|
||||||
}
|
}
|
||||||
|
/** 停止时若与队友重叠,则向后微调保持最小间距,并加入蠕动避免死板 */
|
||||||
|
this.adjustSpacingWhenIdle(_e, view, move, model);
|
||||||
} else {
|
} else {
|
||||||
/** 未接触:向敌人方向推进 */
|
/** 未接触:向敌人方向推进(可穿越队友,但同点时后来者稍停) */
|
||||||
model.is_atking = false;
|
model.is_atking = false;
|
||||||
const dir = enemyX > selfX ? 1 : -1;
|
const dir = enemyX > selfX ? 1 : -1;
|
||||||
move.direction = dir;
|
move.direction = dir;
|
||||||
@@ -164,6 +171,9 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
|||||||
const delta = speed * this.dt * dir;
|
const delta = speed * this.dt * dir;
|
||||||
let newX = selfX + delta;
|
let newX = selfX + delta;
|
||||||
|
|
||||||
|
/** 推进时若与队友同点,后来者稍停避让 */
|
||||||
|
newX = this.adjustSpacingWhileMoving(_e, view, move, model, newX, dir);
|
||||||
|
|
||||||
/** 后退边界钳制:防止被击退/位移推出屏幕 */
|
/** 后退边界钳制:防止被击退/位移推出屏幕 */
|
||||||
newX = this.clampByFacBoundary(newX, model.fac);
|
newX = this.clampByFacBoundary(newX, model.fac);
|
||||||
|
|
||||||
@@ -174,6 +184,93 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 推进时的间距避让。
|
||||||
|
* Why: 允许穿越队友,但若与队友完全同点(重叠),后来者稍停一拍,
|
||||||
|
* 避免视觉上的"穿模"过于生硬,营造"挤过去"的自然感。
|
||||||
|
* @returns 调整后的目标 X(被阻挡时返回 selfX,即本帧不移动)
|
||||||
|
*/
|
||||||
|
private adjustSpacingWhileMoving(self: ecs.Entity, view: HeroViewComp, selfMove: MoveComp, model: HeroAttrsComp, targetX: number, _dir: number): number {
|
||||||
|
const selfX = view.node.position.x;
|
||||||
|
const selfY = view.node.position.y;
|
||||||
|
|
||||||
|
let blocked = false;
|
||||||
|
ecs.query(this.getHeroViewMatcher()).forEach(e => {
|
||||||
|
if (e === self) return;
|
||||||
|
const attrs = e.get(HeroAttrsComp);
|
||||||
|
const v = e.get(HeroViewComp);
|
||||||
|
const mv = e.get(MoveComp);
|
||||||
|
if (!attrs || !v?.node || !mv) return;
|
||||||
|
if (attrs.is_dead || attrs.is_reviving) return;
|
||||||
|
if (attrs.fac !== model.fac) return;
|
||||||
|
if (Math.abs(v.node.position.y - selfY) > 30) return;
|
||||||
|
|
||||||
|
const otherX = v.node.position.x;
|
||||||
|
/** 同点判定:间距小于最小间距的一半视为完全重叠 */
|
||||||
|
if (Math.abs(selfX - otherX) < MoveSystem.ALLY_MIN_GAP_X * 0.5) {
|
||||||
|
/** spawnOrder 大者(后来者)被阻挡 */
|
||||||
|
if (selfMove.spawnOrder > mv.spawnOrder) {
|
||||||
|
blocked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return blocked ? selfX : targetX;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止移动时的间距微调 + 蠕动效果。
|
||||||
|
* Why: 允许推进时穿越队友,但进入攻击范围停下后若与队友重叠,
|
||||||
|
* 则向后微调保持 ALLY_MIN_GAP_X 间距,避免视觉完全重叠。
|
||||||
|
* 同时加入基于时间的随机蠕动,营造"挤到前方攻击"的生动感。
|
||||||
|
* Y 轴不变,仅约束 X。
|
||||||
|
* 优先级:spawnOrder 小者(先出生)不动,大者向后让位。
|
||||||
|
*/
|
||||||
|
private adjustSpacingWhenIdle(self: ecs.Entity, view: HeroViewComp, selfMove: MoveComp, model: HeroAttrsComp): void {
|
||||||
|
const selfX = view.node.position.x;
|
||||||
|
const selfY = view.node.position.y;
|
||||||
|
let adjustX = selfX;
|
||||||
|
let isCrowded = false;
|
||||||
|
|
||||||
|
ecs.query(this.getHeroViewMatcher()).forEach(e => {
|
||||||
|
if (e === self) return;
|
||||||
|
const attrs = e.get(HeroAttrsComp);
|
||||||
|
const v = e.get(HeroViewComp);
|
||||||
|
const mv = e.get(MoveComp);
|
||||||
|
if (!attrs || !v?.node || !mv) return;
|
||||||
|
if (attrs.is_dead || attrs.is_reviving) return;
|
||||||
|
if (attrs.fac !== model.fac) return;
|
||||||
|
/** 只约束同排(Y 差在阈值内)的队友 */
|
||||||
|
if (Math.abs(v.node.position.y - selfY) > 30) return;
|
||||||
|
|
||||||
|
const otherX = v.node.position.x;
|
||||||
|
const gap = Math.abs(adjustX - otherX);
|
||||||
|
if (gap >= MoveSystem.ALLY_MIN_GAP_X) return;
|
||||||
|
|
||||||
|
isCrowded = true;
|
||||||
|
/** spawnOrder 小者不动,大者向后让位 */
|
||||||
|
if (selfMove.spawnOrder > mv.spawnOrder) {
|
||||||
|
/** 自己靠后:向远离对方的方向后退到最小间距处 */
|
||||||
|
const dir = adjustX >= otherX ? 1 : -1;
|
||||||
|
adjustX = otherX + dir * MoveSystem.ALLY_MIN_GAP_X;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 拥挤时加入蠕动:基于时间+出生序的伪随机偏移,避免多个单位同步抖动 */
|
||||||
|
if (isCrowded) {
|
||||||
|
const time = Date.now() / 1000;
|
||||||
|
const jostlePhase = time * 3 + selfMove.spawnOrder * 0.7; // 不同单位错开相位
|
||||||
|
const jostleOffset = Math.sin(jostlePhase) * MoveSystem.JOSTLE_AMPLITUDE;
|
||||||
|
adjustX += jostleOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.abs(adjustX - selfX) >= 0.01) {
|
||||||
|
/** 后退边界钳制 */
|
||||||
|
adjustX = this.clampByFacBoundary(adjustX, model.fac);
|
||||||
|
view.node.setPosition(adjustX, view.node.position.y, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按阵营裁剪 X 坐标,防止被击退/位移推出屏幕。
|
* 按阵营裁剪 X 坐标,防止被击退/位移推出屏幕。
|
||||||
* HERO 后退不越过 BoxSet.LETF_END;MON 后退不越过 BoxSet.RIGHT_END。
|
* HERO 后退不越过 BoxSet.LETF_END;MON 后退不越过 BoxSet.RIGHT_END。
|
||||||
|
|||||||
Reference in New Issue
Block a user