diff --git a/assets/script/game/common/config/heroSet.ts b/assets/script/game/common/config/heroSet.ts index 91b2562e..d409fa44 100644 --- a/assets/script/game/common/config/heroSet.ts +++ b/assets/script/game/common/config/heroSet.ts @@ -85,10 +85,11 @@ export const FormationPointX = { [HType.Long]: 100, } as const; +/** 各攻击定位的默认攻击距离(像素):近战150 / 中程300 / 远程450;英雄配置了 dis 时优先取配置值 */ export const HeroDisVal: Record = { - [HType.Melee]: 120, - [HType.Mid]: 720, - [HType.Long]: 720, + [HType.Melee]: 150, + [HType.Mid]: 300, + [HType.Long]: 450, } export const resolveFormationTargetX = (fac: FacSet, type: HType): number => { diff --git a/assets/script/game/hero/Hero.ts b/assets/script/game/hero/Hero.ts index e7eb3bcd..d253589d 100644 --- a/assets/script/game/hero/Hero.ts +++ b/assets/script/game/hero/Hero.ts @@ -5,7 +5,7 @@ import { smc } from "../common/SingletonModuleComp"; import { HeroAttrsComp } from "./HeroAttrsComp"; import { HeroViewComp } from "./HeroViewComp"; 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 { SkillTriggerType, HType } from "../common/config/heroSet"; import { SkillTriggerHelper } from "./SkillTriggerHelper"; @@ -122,8 +122,8 @@ export class Hero extends ecs.Entity { model.grow_type = hero.grow_type; model.role = hero.role; model.fac = FacSet.HERO; - // 攻击距离:配置优先;未配置按攻击定位给默认值(近战180 / 远程600) - model.dis = hero.dis ?? (hero.type === HType.Melee ? 180 : 600); + // 攻击距离:配置优先;未配置按攻击定位取默认值(近战150 / 中程300 / 远程450) + model.dis = hero.dis ?? HeroDisVal[hero.type as HType.Melee | HType.Mid | HType.Long]; model.posIndex = posIndex; if (posIndex >= 0) { smc.mission.heroGrid[posIndex] = this.eid; diff --git a/assets/script/game/hero/Mon.ts b/assets/script/game/hero/Mon.ts index 6821159b..492c2fd7 100644 --- a/assets/script/game/hero/Mon.ts +++ b/assets/script/game/hero/Mon.ts @@ -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 { smc } from "../common/SingletonModuleComp"; 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 { HeroViewComp } from "./HeroViewComp"; import { MoveComp } from "./MoveComp"; @@ -174,8 +174,8 @@ export class Monster extends ecs.Entity { model.speed = hero.speed ?? 800; model.type = hero.type; model.fac = FacSet.MON; - // 攻击距离:配置优先;未配置按攻击定位给默认值(近战180 / 远程600) - model.dis = hero.dis ?? (hero.type === HType.Melee ? 180 : 600); + // 攻击距离:配置优先;未配置按攻击定位取默认值(近战150 / 中程300 / 远程450) + model.dis = hero.dis ?? HeroDisVal[hero.type as HType.Melee | HType.Mid | HType.Long]; model.posIndex = laneIndex; if (laneIndex >= 0) { smc.mission.monGrid[laneIndex] = this.eid; diff --git a/assets/script/game/hero/MoveComp.ts b/assets/script/game/hero/MoveComp.ts index 3da80f12..bdfc6cbf 100644 --- a/assets/script/game/hero/MoveComp.ts +++ b/assets/script/game/hero/MoveComp.ts @@ -136,9 +136,14 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate 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) { const selfX = view.node.position.x; @@ -155,8 +160,10 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate if (view.status === "move") { view.status_change("idle"); } + /** 停止时若与队友重叠,则向后微调保持最小间距,并加入蠕动避免死板 */ + this.adjustSpacingWhenIdle(_e, view, move, model); } else { - /** 未接触:向敌人方向推进 */ + /** 未接触:向敌人方向推进(可穿越队友,但同点时后来者稍停) */ model.is_atking = false; const dir = enemyX > selfX ? 1 : -1; move.direction = dir; @@ -164,6 +171,9 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate const delta = speed * this.dt * dir; let newX = selfX + delta; + /** 推进时若与队友同点,后来者稍停避让 */ + newX = this.adjustSpacingWhileMoving(_e, view, move, model, newX, dir); + /** 后退边界钳制:防止被击退/位移推出屏幕 */ 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 坐标,防止被击退/位移推出屏幕。 * HERO 后退不越过 BoxSet.LETF_END;MON 后退不越过 BoxSet.RIGHT_END。