refactor(mission): 优化节间过场表现与关卡流程

1. 新增地图滚动速度重置与背景动画加速逻辑,强化过场推进感
2. 重构节间过场流程,同步启动英雄归位与地图滚动
3. 调整关卡文本显示格式与字体大小,优化视觉体验
4. 新增全局移动锁控制英雄过场动画状态,修复动画残留问题
This commit is contained in:
panFD
2026-09-12 22:41:51 +08:00
parent 1b2545deec
commit 10e2c0d817
5 changed files with 74 additions and 28 deletions

View File

@@ -45,6 +45,13 @@ export class MoveComp extends ecs.Comp {
@ecs.register('MoveSystem')
export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate {
/**
* 节间过场移动动画锁(全局):
* 置位期间,存活英雄即使已归位到编队槽位也保持"move"移动动画(配合地图滚动,表现行军推进)。
* 由 MissionComp 在节间过场开始(背景滚动启动)时置 true、过场结束(背景滚动停止)时置 false。
*/
public static transitionMoveLock = false;
/** 渲染层级重排节流,避免每帧排序 */
private readonly renderSortInterval = 0.05;
private lastRenderSortAt = 0;
@@ -65,6 +72,8 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
private static readonly QUEUE_LEFT_LIMIT = FightSet.HERO_ROW_BACK_X - 120;
/** 编队槽位默认间隔(像素);后排人数过多时自动压缩间距 */
private static readonly QUEUE_SPACING = 30;
/** 编队归位目标时长(秒):任意距离动态提速,保证约 1s 到位 */
private static readonly QUEUE_ARRIVE_TIME = 1;
/** 编队槽位重算节流间隔(秒):召唤/死亡后最多延迟一次重算 */
private readonly queueRebuildInterval = 0.1;
private lastQueueRebuildAt = 0;
@@ -181,18 +190,30 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
view.node.setPosition(targetX, view.node.position.y, 0);
}
view.scale = 1;
view.status_change("idle");
// 节间过场锁定期:即使已归位也保持移动动画(配合地图滚动表现行军推进),过场结束才回待机
if (MoveSystem.transitionMoveLock) {
view.status_change("move");
} else {
view.status_change("idle");
}
return;
}
/** 未到位:向槽位推进(与战斗推进同速),朝移动方向转身 */
/** 未到位:向槽位推进,朝移动方向转身 */
const dir = targetX > selfX ? 1 : -1;
move.direction = dir;
view.scale = dir;
/** 编队排队允许突破战斗左边界(LETF_END=后排锚点),扩展至 QUEUE_LEFT_LIMIT */
const queueClamp = (x: number): number => Math.max(MoveSystem.QUEUE_LEFT_LIMIT, x);
const delta = (model.speed / 3) * this.dt * dir;
const newX = queueClamp(selfX + delta);
/** 归位速度:按剩余距离动态计算,保证任意距离约 1s 到位;不低于基础移速,避免近距离僵住 */
const dist = Math.abs(targetX - selfX);
const speed = Math.max(model.speed / 3, dist / MoveSystem.QUEUE_ARRIVE_TIME);
const delta = speed * this.dt * dir;
let newX = queueClamp(selfX + delta);
/** 防过冲:本帧会越过目标则直接贴齐 */
if ((dir > 0 && newX > targetX) || (dir < 0 && newX < targetX)) {
newX = targetX;
}
if (Math.abs(newX - selfX) >= 0.01) {
view.node.setPosition(newX, view.node.position.y, 0);
view.status_change("move");

View File

@@ -23,6 +23,7 @@ import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/modu
import { smc } from "../common/SingletonModuleComp";
import { oops } from "../../../../extensions/oops-plugin-framework/assets/core/Oops";
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
import { MoveSystem } from "../hero/MoveComp";
import { GameEvent } from "../common/config/GameEvent";
import { SkillTriggerType } from "../common/config/heroSet";
import { HeroViewComp } from "../hero/HeroViewComp";
@@ -223,6 +224,8 @@ export class MissionComp extends CCComp {
}
onDestroy() {
// 解除节间过场移动动画锁,避免泄漏到下一关
MoveSystem.transitionMoveLock = false;
// 退出关卡时恢复地图背景动画
smc.map.MapView.scene.mapLayer.playAnimations()
super.onDestroy();
@@ -296,12 +299,10 @@ export class MissionComp extends CCComp {
// ======================== 回合倒计时 ========================
/** 进入节间过场:恢复背景滚动 + 英雄归位(in_fight=false 触发编队排队),倒计时结束自动开下一节 */
/** 进入节间过场:停止倒计时状态(背景滚动与英雄归位已在节间过场启动时同步触发) */
private startWaveCountdown() {
this.waveCountdown = MissionComp.COUNTDOWN_TRANSITION;
this.isWaveCountdown = true;
// 节间过场:恢复地图背景滚动动画,表现玩家推进
smc.map?.MapView?.scene?.mapLayer?.playAnimations();
this.updateCountdownUI(true);
}
@@ -319,6 +320,8 @@ export class MissionComp extends CCComp {
this.lastTimeSecond = -1;
// 过场结束,停掉背景滚动(战斗中保持静止)
smc.map?.MapView?.scene?.mapLayer?.stopAnimations();
// 解除英雄移动动画锁:归位后恢复待机
MoveSystem.transitionMoveLock = false;
}
// ======================== 奖励与广告 ========================
@@ -1001,8 +1004,8 @@ export class MissionComp extends CCComp {
// Boss 节(末节)清场 → 本关通关,延迟弹结算(等死亡动画播完)
this.scheduleSettle(false, true);
} else {
// 非末节清场 → 不切暂停,延迟切 BattleEnd,由 autoNextPhase 进入节间过场自动推进下一节
this.scheduleSectionTransition();
// 非末节清场 → 立即启动节间过场(背景滚动 + 英雄归位同帧启动),自动连战下一节
this.beginSectionTransition();
}
return;
}
@@ -1065,19 +1068,21 @@ export class MissionComp extends CCComp {
}
/**
* 预约节间过场(非末节清场):延迟 SETTLE_DELAY 秒等死亡动画播完,再切 BattleEnd。
* 与 scheduleSettle 的区别:不置 pause、不弹结算,由 autoNextPhase 进入 PrepareStart 过场自动连战下一节。
* 启动节间过场(非末节清场):
* 1. 立即切出战斗(in_fight=false)→ 英雄触发编队排队、自动归位到左侧站位。
* 2. 同时恢复地图背景滚动,表现玩家推进(背景与英雄归位同帧启动,避免一先一后)。
* 3. 切 BattleEnd,2s 后由 autoNextPhase 进入 PrepareStart 过场倒计时,倒计时结束自动连战下一节。
*
* Why: 不等 SETTLE_DELAY 死亡动画,让"归位 + 背景滚动"在清场瞬间就启动,过渡更连贯。
*/
private scheduleSectionTransition() {
if (this._settleScheduled) return;
this._settleScheduled = true;
this.scheduleOnce(() => {
this._settleScheduled = false;
// 延迟期间局内状态可能已变化(如英雄刚好全灭、任务结束),兜底校验
if (!smc.mission.play || smc.mission.pause) return;
if (this.currentPhase !== MissionPhase.Battle) return;
this.changePhase(MissionPhase.BattleEnd);
}, MissionComp.SETTLE_DELAY);
private beginSectionTransition() {
smc.mission.in_fight = false;
smc.vmdata.mission_data.in_fight = false;
// 恢复地图背景滚动(与英雄归位同步启动,2 倍速表现推进)
smc.map?.MapView?.scene?.mapLayer?.playAnimations();
// 过场期间英雄保持移动动画:即使已归位也持续行军,地图停才回待机
MoveSystem.transitionMoveLock = true;
this.changePhase(MissionPhase.BattleEnd);
}
/**

View File

@@ -290,6 +290,8 @@ export class MissionHomeComp extends CCComp {
home_active() {
// 任务结束回到主页:确保指引动画与气泡无残留
this.stopStartGuide();
// 大厅地图恢复正常滚动速度(清除战斗节间过场的 4x 加速残留)
smc.map?.MapView?.scene?.mapLayer?.resetSpeed();
this.uodate_data()
this.hideAllPages()
this.refreshRedDots()

View File

@@ -86,14 +86,35 @@ export default class MapLayer extends Component {
return [this.bg_anm, this.bg_anm1, this.bg_anm2, this.bg_anm3, this.bg_anm4].filter((a): a is Animation => a !== null);
}
/** 背景滚动播放速度倍率(节间过场加快推进感) */
private static readonly SCROLL_SPEED = 4;
public playAnimations(name?: string): void {
this.animations.forEach(a => name ? a.play(name) : a.play());
this.animations.forEach(a => {
a.play(name);
// 加快背景滚动速度,强化节间"向前推进"的观感(AnimationState.speed 属性)
const clip = a.defaultClip;
if (clip) {
const state = a.getState(clip.name);
if (state) state.speed = MapLayer.SCROLL_SPEED;
}
});
}
public stopAnimations(): void {
this.animations.forEach(a => a.stop());
}
/** 恢复背景滚动为正常速度(1x):大厅/非过场时调用,清除过场加速残留 */
public resetSpeed(): void {
this.animations.forEach(a => {
const clip = a.defaultClip;
if (!clip) return;
const state = a.getState(clip.name);
if (state) state.speed = 1;
});
}
public pauseAnimations(): void {
this.animations.forEach(a => a.pause());
}