diff --git a/src/engine/MixamoAnimator.js b/src/engine/MixamoAnimator.js index ec9cea0..2b537f3 100644 --- a/src/engine/MixamoAnimator.js +++ b/src/engine/MixamoAnimator.js @@ -52,6 +52,7 @@ const BASE_STATES = ["idle", "walk", "run", "jump", "fall"]; // Дополнительные движения (грузятся лениво при первом setState): const EXTRA_STATES = [ "jump_anticipate", "jump_air", "jump_land", + "jump_fwd_anticipate", "jump_fwd_air", "jump_fwd_land", "walk_backward", "run_backward", "run_to_stop", "run_slide", "jump_forward", "jump_backward", "jump_down", "crouch_enter", "crouch_idle", "crouch_walk", "crouch_to_stand", @@ -274,7 +275,10 @@ export class MixamoAnimator { // (только что приземлились, ноги пружинят к bind), // середина = 0 (присед на полу), конец = выпрямление. // Для всех остальных — фильтруем (физика двигает _modelRoot). - const PHASES = new Set(['jump_anticipate', 'jump_land']); + const PHASES = new Set([ + 'jump_anticipate', 'jump_land', + 'jump_fwd_anticipate', 'jump_fwd_land', + ]); if (!PHASES.has(state)) { continue; } @@ -329,6 +333,7 @@ export class MixamoAnimator { const ONE_SHOT = new Set([ "jump", "jump_forward", "jump_backward", "jump_down", "jump_anticipate", "jump_land", + "jump_fwd_anticipate", "jump_fwd_air", "jump_fwd_land", "crouch_enter", "crouch_to_stand", "hit_react", "die_forward", "die_back", "throw_action", "pickup", "push_button", "open_door", @@ -374,7 +379,10 @@ export class MixamoAnimator { // Сброс Hips.position в bind-pose при выходе из jump-фаз. // Иначе последний keyframe анимации остаётся на Hips и idle/walk // подхватывает смещённую позицию → персонаж проседает. - const JUMP_STATES = new Set(['jump_air', 'jump_land', 'jump_in_place']); + const JUMP_STATES = new Set([ + 'jump_air', 'jump_land', 'jump_in_place', 'jump_anticipate', + 'jump_fwd_anticipate', 'jump_fwd_air', 'jump_fwd_land', + ]); if (JUMP_STATES.has(this._currentState) && !JUMP_STATES.has(state) && this._restPositions) { const rest = this._restPositions.get('Hips'); @@ -391,10 +399,12 @@ export class MixamoAnimator { // Anti-flicker debounce: не даём переключать состояние чаще чем раз в 120мс, // КРОМЕ переходов с/на воздушные состояния (jump/fall) — там важна скорость // и one-shot crouch_enter/crouch_to_stand (они короткие). - const isVitalSwitch = state === 'jump' || state === 'fall' - || state === 'jump_air' || state === 'jump_land' - || this._currentState === 'jump' || this._currentState === 'fall' - || this._currentState === 'jump_air' || this._currentState === 'jump_land' + const JUMP_VITAL = new Set([ + 'jump', 'fall', 'jump_air', 'jump_land', 'jump_anticipate', + 'jump_fwd_anticipate', 'jump_fwd_air', 'jump_fwd_land', + ]); + const isVitalSwitch = JUMP_VITAL.has(state) + || JUMP_VITAL.has(this._currentState) || state === 'crouch_enter' || state === 'crouch_to_stand'; if (!isVitalSwitch && this._lastSwitchAt && (now - this._lastSwitchAt) < 120) { // Запомним последний запрошенный state — если он не изменится за @@ -435,12 +445,19 @@ export class MixamoAnimator { // eslint-disable-next-line no-console console.log(`[MixamoAnimator] setState: ${this._currentState || 'none'} → ${state} (loop=${loop})`); + // Per-state speedRatio: подгоняем длительность под физику. + // jump_fwd_air: Mixamo Jump полёт = 0.43с, физика = 0.73с + // → speedRatio = 0.59 (замедлить чтобы клип не зациклился). + const SPEED_RATIO = { + jump_fwd_air: 0.59, + }; + const speedRatio = SPEED_RATIO[state] || 1.0; // Запустить новую анимацию. Babylon 7 ВНИМАНИЕ: параметр loop // в start() иногда игнорится — дублируем через loopAnimation // (выставлен в _ensureGroup). try { next.reset(); - next.start(loop, 1.0, next.from, next.to, false); + next.start(loop, speedRatio, next.from, next.to, false); } catch (e) { try { next.play(loop); } catch (_) {} } diff --git a/src/engine/PlayerController.js b/src/engine/PlayerController.js index 6a50de1..78d4cd1 100644 --- a/src/engine/PlayerController.js +++ b/src/engine/PlayerController.js @@ -3092,12 +3092,21 @@ export class PlayerController { } else if (canJump && c.has('Space') && !inWater && !this._shipMode && !this._ufoMode) { if (!this._jumpHeld) { - // 3-фазная модель: при первом нажатии Space запускаем - // jump_anticipate (присед перед прыжком) на 0.375с. - // Физика прыжка стартует ПОСЛЕ окончания anticipate-фазы. + // 3-фазная модель прыжка. + // _jumpKind определяется по нажатым клавишам в момент Space: + // in_place — нет WASD (анимация Mixamo Jumping) + // forward — есть WASD (анимация Mixamo Jump = прыжок вперёд) + const cc = this._codes; + const wasdHeld = cc && (cc.has('KeyW') || cc.has('KeyS') + || cc.has('KeyA') || cc.has('KeyD') + || cc.has('ArrowUp') || cc.has('ArrowDown') + || cc.has('ArrowLeft') || cc.has('ArrowRight')); + this._jumpKind = wasdHeld ? 'forward' : 'in_place'; + // anticipate-фаза разной длительности. + const antDuration = this._jumpKind === 'forward' ? 170 : 375; this._jumpHeld = true; this._coyoteLeft = 0; - this._jumpAnticipateUntil = Date.now() + 375; + this._jumpAnticipateUntil = Date.now() + antDuration; this._jumpPendingImpulse = true; // Robot: запускаем boost-фазу на 0.45с if (this._robotMode) { @@ -3341,30 +3350,35 @@ export class PlayerController { const now = Date.now(); const inCrouchTransition = this._crouchTransitionUntil && now < this._crouchTransitionUntil; - // jump_anticipate — присед перед прыжком (0.375с), физика заморожена. + // 3-фазная анимация прыжка. Выбираем семейство фаз по _jumpKind: + // in_place: jump_anticipate/jump_air/jump_land (Mixamo Jumping) + // forward: jump_fwd_anticipate/jump_fwd_air/jump_fwd_land (Mixamo Jump) + const isForward = this._jumpKind === 'forward'; + const stAnticipate = isForward ? 'jump_fwd_anticipate' : 'jump_anticipate'; + const stAir = isForward ? 'jump_fwd_air' : 'jump_air'; + const stLand = isForward ? 'jump_fwd_land' : 'jump_land'; + const landDuration = isForward ? 142 : 570; const inAnticipate = this._jumpAnticipateUntil && now < this._jumpAnticipateUntil && this._jumpPendingImpulse; - // jump_land — постприземление (присед→встать) 0.57с. const inJumpLand = this._jumpLandUntil && now < this._jumpLandUntil; if (inAnticipate) { - mState = 'jump_anticipate'; + mState = stAnticipate; } else if (!result.onGround) { - // В воздухе: jump_air (только в полёте, без приседа/landing). - mState = 'jump_air'; + mState = stAir; this._wasAirborne = true; this._crouchEnterPending = false; this._crouchExitPending = false; this._crouchTransitionUntil = 0; - // Сбрасываем anticipate-окно — мы уже в воздухе this._jumpAnticipateUntil = 0; } else if (this._wasAirborne) { - // Только что приземлились — запустим jump_land - this._jumpLandUntil = now + 570; + this._jumpLandUntil = now + landDuration; this._wasAirborne = false; - mState = 'jump_land'; - } else if (inJumpLand && !isMoving) { - mState = 'jump_land'; + mState = stLand; + } else if (inJumpLand) { + // Для forward — доигрываем land даже при движении + // (там короткая фаза 142мс) + if (isForward || !isMoving) mState = stLand; } else if (this._crouchEnterPending && inCrouchTransition && !isMoving) { mState = 'crouch_enter'; } else if (this._crouchExitPending && inCrouchTransition && !isMoving) {