From f50e2499ee4e950f43e203699e40dcec6d438912 Mon Sep 17 00:00:00 2001 From: marcostocco <26927387+marcostocco@users.noreply.github.com> Date: Thu, 14 May 2026 21:41:23 +0200 Subject: [PATCH 1/2] new Bird and BirdYellow type player, new sugar rush animation for frog, --- Content/Metadata/Interactive/PlayerBird.res | 177 ++++++ .../Metadata/Interactive/PlayerBirdYellow.res | 158 +++++ Content/Metadata/Interactive/PlayerFrog.res | 20 +- Content/Metadata/UI/HUD.res | 11 +- Sources/Jazz2/Actors/Player.cpp | 564 ++++++++++++++---- Sources/Jazz2/Actors/Player.h | 7 + Sources/Jazz2/Actors/PlayerCorpse.cpp | 9 + .../Actors/Solid/PowerUpMorphMonitor.cpp | 14 +- Sources/Jazz2/LevelHandler.cpp | 4 + Sources/Jazz2/PlayerType.h | 31 + Sources/Jazz2/UI/HUD.cpp | 6 +- 11 files changed, 859 insertions(+), 142 deletions(-) create mode 100644 Content/Metadata/Interactive/PlayerBird.res create mode 100644 Content/Metadata/Interactive/PlayerBirdYellow.res diff --git a/Content/Metadata/Interactive/PlayerBird.res b/Content/Metadata/Interactive/PlayerBird.res new file mode 100644 index 000000000..20fcb4c89 --- /dev/null +++ b/Content/Metadata/Interactive/PlayerBird.res @@ -0,0 +1,177 @@ +{ + "Version": { + "Target": "Jazz² Resurrection" + }, + + "Animations": { + "Idle": { + "Path": "Birdy/idle.aura", + "States": [ 0 ] + }, + "Walk": { + "Path": "Birdy/fly.aura", + "States": [ 1 ], + "FrameRate": 7 + }, + "IdleBored1": { + "Path": "Birdy/idle_worm.aura", + "FrameRate": 5, + "States": [ 536870944 ] + }, + "IdleBored2": { + "Path": "Birdy/idle_turn_head_left.aura", + "FrameRate": 5, + "States": [ 536870945 ] + }, + "IdleBored3": { + "Path": "Birdy/idle_look_left.aura", + "FrameRate": 5, + "States": [ 536870946 ] + }, + "IdleBored4": { + "Path": "Birdy/idle_turn_head_left_back.aura", + "FrameRate": 5, + "States": [ 536870947 ] + }, + "IdleBored5": { + "Path": "Birdy/idle_turn_head_right.aura", + "FrameRate": 5, + "States": [ 536870948 ] + }, + "IdleBored6": { + "Path": "Birdy/idle_look_right.aura", + "FrameRate": 5, + "States": [ 536870949 ] + }, + "IdleBored7": { + "Path": "Birdy/idle_turn_head_right_back.aura", + "FrameRate": 5, + "States": [ 536870950 ] + }, + "Shoot": { + "Path": "Birdy/fly.aura", + "States": [ 16, 144 ], + "FrameRate": 7 + }, + "Hurt": { + "Path": "Birdy/hurt.aura", + "FrameRate": 6, + "States": [ 2048 ] + }, + "Death": { + "Path": "Birdy/die.aura", + "FrameRate": 3, + "States": [ 1073741839 ] + }, + "Corpse": { + "Path": "Birdy/corpse.aura", + "States": [ 536870912 ] + }, + "SugarRush": { + "Path": "Common/sugar_rush_stars.aura", + "States": [ 536870913 ] + }, + "Shield": { + "Path": "Common/player_shield.aura", + "States": [ 536870928 ] + }, + "ShieldFire": { + "Path": "Common/shield_fire.aura", + "States": [ 536870929 ] + }, + "ShieldWater": { + "Path": "Common/shield_water.aura", + "States": [ 536870930 ] + }, + "ShieldLightning": { + "Path": "Common/shield_lightning.aura", + "States": [ 536870931 ] + }, + "WarpIn": { + "Path": "Jazz/warp_in.aura", + "States": [ 1073741843 ], + "FrameOffset": 2 + }, + "WarpOut": { + "Path": "Jazz/warp_out.aura", + "States": [ 1073741844 ], + "FrameCount": 5 + }, + "WarpInFreefall": { + "Path": "Jazz/warp_in.aura", + "States": [ 1073741847 ], + "FrameOffset": 2 + }, + "WarpOutFreefall": { + "Path": "Jazz/warp_out.aura", + "States": [ 1073741848 ], + "FrameCount": 5 + } + }, + + "Sounds": { + "Fly": { + "Paths": [ "Birdy/fly_1.wav", "Birdy/fly_2.wav" ] + }, + "ChangeWeapon": { + "Paths": [ "UI/weapon_change.wav" ] + }, + "EndOfLevel": { + "Paths": [ "Jazz/level_complete.wav" ] + }, + "WarpIn": { + "Paths": [ "Common/warp_in.wav" ] + }, + "WarpOut": { + "Paths": [ "Common/warp_out.wav" ] + }, + "Die": { + "Paths": [ "Common/gunsm1.wav" ] + }, + "PickupAmmo": { + "Paths": [ "Pickup/ammo.wav" ] + }, + "PickupCoin": { + "Paths": [ "Pickup/coin.wav" ] + }, + "PickupGem": { + "Paths": [ "Pickup/gem.wav" ] + }, + "PickupOneUp": { + "Paths": [ "Pickup/1up.wav" ] + }, + "PickupDrink": { + "Paths": [ "Pickup/food_drink_1.wav", "Pickup/food_drink_2.wav", "Pickup/food_drink_3.wav", "Pickup/food_drink_4.wav" ] + }, + "PickupFood": { + "Paths": [ "Pickup/food_edible_1.wav", "Pickup/food_edible_2.wav", "Pickup/food_edible_3.wav", "Pickup/food_edible_4.wav" ] + }, + "PickupMaxCarrot": { + "Paths": [ "Jazz/carrot.wav" ] + }, + "WeaponBlaster": { + "Paths": [ "Weapon/bullet_blaster_jazz_4.wav" ] + }, + "WeaponToaster": { + "Paths": [ "Weapon/toaster.wav" ] + }, + "WeaponThunderbolt": { + "Paths": [ "Unknown/unknown_bonus1.wav" ] + }, + "WeaponThunderboltStart": { + "Paths": [ "Cinematics/opening_shot.wav" ] + }, + "WeaponThunderboltEnd": { + "Paths": [ "Pickup/shield_lightning_bullet_1.wav" ] + }, + "EndOfLevel1": { + "Paths": [ "Common/char_revup.wav" ] + }, + "EndOfLevel2": { + "Paths": [ "Weapon/ricochet_bullet_3.wav" ] + }, + "BonusWarpNotEnoughCoins": { + "Paths": [ "Object/bonus_not_enough_coins.wav" ] + } + } +} diff --git a/Content/Metadata/Interactive/PlayerBirdYellow.res b/Content/Metadata/Interactive/PlayerBirdYellow.res new file mode 100644 index 000000000..9779801dc --- /dev/null +++ b/Content/Metadata/Interactive/PlayerBirdYellow.res @@ -0,0 +1,158 @@ +{ + "Version": { + "Target": "Jazz² Resurrection" + }, + + "Animations": { + "Walk": { + "Path": "BirdyYellow/fly.aura", + "States": [ 1 ], + "FrameRate": 7 + }, + "Idle": { + "Path": "BirdyYellow/idle.aura", + "States": [ 0 ] + }, + "IdleBored1": { + "Path": "BirdyYellow/idle_worm.aura", + "FrameRate": 5, + "States": [ 536870944 ] + }, + "IdleBored2": { + "Path": "BirdyYellow/idle_turn_head_left.aura", + "FrameRate": 5, + "States": [ 536870945 ] + }, + "IdleBored3": { + "Path": "BirdyYellow/idle_look_left.aura", + "FrameRate": 5, + "States": [ 536870946 ] + }, + "IdleBored4": { + "Path": "BirdyYellow/idle_turn_head_left_back.aura", + "FrameRate": 5, + "States": [ 536870947 ] + }, + "IdleBored5": { + "Path": "BirdyYellow/idle_turn_head_right.aura", + "FrameRate": 5, + "States": [ 536870948 ] + }, + "IdleBored6": { + "Path": "BirdyYellow/idle_look_right.aura", + "FrameRate": 5, + "States": [ 536870949 ] + }, + "IdleBored7": { + "Path": "BirdyYellow/idle_turn_head_right_back.aura", + "FrameRate": 5, + "States": [ 536870950 ] + }, + "Shoot": { + "Path": "BirdyYellow/charge_ver.aura", + "States": [ 16, 144 ], + }, + "Hurt": { + "Path": "BirdyYellow/hurt.aura", + "FrameRate": 6, + "States": [ 2048 ] + }, + "Death": { + "Path": "BirdyYellow/die.aura", + "FrameRate": 3, + "States": [ 1073741839 ] + }, + "Corpse": { + "Path": "BirdyYellow/corpse.aura", + "States": [ 536870912 ] + }, + "SugarRush": { + "Path": "Common/sugar_rush_stars.aura", + "States": [ 536870913 ] + }, + "Shield": { + "Path": "Common/player_shield.aura", + "States": [ 536870928 ] + }, + "ShieldFire": { + "Path": "Common/shield_fire.aura", + "States": [ 536870929 ] + }, + "ShieldWater": { + "Path": "Common/shield_water.aura", + "States": [ 536870930 ] + }, + "ShieldLightning": { + "Path": "Common/shield_lightning.aura", + "States": [ 536870931 ] + }, + "WarpIn": { + "Path": "Spaz/warp_in.aura", + "States": [ 1073741843 ], + "FrameOffset": 2 + }, + "WarpOut": { + "Path": "Spaz/warp_out.aura", + "States": [ 1073741844 ], + "FrameCount": 5 + }, + "WarpInFreefall": { + "Path": "Spaz/warp_in.aura", + "States": [ 1073741847 ], + "FrameOffset": 2 + }, + "WarpOutFreefall": { + "Path": "Spaz/warp_out.aura", + "States": [ 1073741848 ], + "FrameCount": 5 + } + }, + + "Sounds": { + "Fly": { + "Paths": [ "Birdy/fly_1.wav", "Birdy/fly_2.wav" ] + }, + "EndOfLevel": { + "Paths": [ "Jazz/level_complete.wav" ] + }, + "WarpIn": { + "Paths": [ "Common/warp_in.wav" ] + }, + "WarpOut": { + "Paths": [ "Common/warp_out.wav" ] + }, + "Die": { + "Paths": [ "Common/gunsm1.wav" ] + }, + "PickupAmmo": { + "Paths": [ "Pickup/ammo.wav" ] + }, + "PickupCoin": { + "Paths": [ "Pickup/coin.wav" ] + }, + "PickupGem": { + "Paths": [ "Pickup/gem.wav" ] + }, + "PickupOneUp": { + "Paths": [ "Pickup/1up.wav" ] + }, + "PickupDrink": { + "Paths": [ "Pickup/food_drink_1.wav", "Pickup/food_drink_2.wav", "Pickup/food_drink_3.wav", "Pickup/food_drink_4.wav" ] + }, + "PickupFood": { + "Paths": [ "Pickup/food_edible_1.wav", "Pickup/food_edible_2.wav", "Pickup/food_edible_3.wav", "Pickup/food_edible_4.wav" ] + }, + "PickupMaxCarrot": { + "Paths": [ "Jazz/carrot.wav" ] + }, + "EndOfLevel1": { + "Paths": [ "Common/char_revup.wav" ] + }, + "EndOfLevel2": { + "Paths": [ "Weapon/ricochet_bullet_3.wav" ] + }, + "BonusWarpNotEnoughCoins": { + "Paths": [ "Object/bonus_not_enough_coins.wav" ] + } + } +} \ No newline at end of file diff --git a/Content/Metadata/Interactive/PlayerFrog.res b/Content/Metadata/Interactive/PlayerFrog.res index 43b8369bb..638a012f1 100644 --- a/Content/Metadata/Interactive/PlayerFrog.res +++ b/Content/Metadata/Interactive/PlayerFrog.res @@ -50,13 +50,18 @@ "FrameRate": 6, "States": [ 2048, 1073741839 ] }, - + "FallToIdle": { "Path": "Frog/fall_land.aura", "States": [ 1073741826 ], "FrameRate": 28 }, + "SugarRush": { + "Path": "Common/sugar_rush_stars.aura", + "States": [ 536870913 ] + }, + "Shield": { "Path": "Common/player_shield.aura", "States": [ 536870928 ] @@ -126,6 +131,12 @@ "HurtSoft": { "Paths": [ "Frog/noise_4.wav" ] }, + "EndOfLevel": { + "Paths": [ "Jazz/level_complete.wav" ] + }, + "EndOfLevel2": { + "Paths": [ "Weapon/ricochet_bullet_3.wav" ] + }, "PickupAmmo": { "Paths": [ "Pickup/ammo.wav" ] }, @@ -147,7 +158,12 @@ "PickupMaxCarrot": { "Paths": [ "Pickup/food_edible_1.wav" ] }, - + "WarpIn": { + "Paths": [ "Common/warp_in.wav" ] + }, + "WarpOut": { + "Paths": [ "Common/warp_out.wav" ] + }, "BonusWarpNotEnoughCoins": { "Paths": [ "Object/bonus_not_enough_coins.wav" ] } diff --git a/Content/Metadata/UI/HUD.res b/Content/Metadata/UI/HUD.res index 240e0a9a6..1106f4b28 100644 --- a/Content/Metadata/UI/HUD.res +++ b/Content/Metadata/UI/HUD.res @@ -128,7 +128,16 @@ "FrameRate": 2, "States": [ 63 ] }, - + "CharacterBird": { + "Path": "UI/icon_birdy.aura", + "FrameRate": 2, + "States": [ 64 ] + }, + "CharacterBirdYellow": { + "Path": "UI/icon_birdy_yellow.aura", + "FrameRate": 2, + "States": [ 65 ] + }, "Heart": { "Path": "UI/heart.aura", "States": [ 70 ] diff --git a/Sources/Jazz2/Actors/Player.cpp b/Sources/Jazz2/Actors/Player.cpp index 0415ec7fa..bedc6b045 100644 --- a/Sources/Jazz2/Actors/Player.cpp +++ b/Sources/Jazz2/Actors/Player.cpp @@ -51,6 +51,33 @@ namespace Jazz2::Actors static constexpr AnimState TransformFrogFromSpaz = (AnimState)0x60000001; static constexpr AnimState TransformFrogFromLori = (AnimState)0x60000002; + static constexpr float GroundedCheckpointYOffset = 20.0f; + static constexpr float BirdFlySpeedX = 2.8f; + static constexpr float BirdFlySpeedYDown = 2.4f; + static constexpr float BirdFlySpeedYUp = 1.4f; + static constexpr float BirdSinkSpeed = 0.6f; + static constexpr float BirdHorizontalResponse = 0.08f; + static constexpr float BirdVerticalResponse = 0.04f; + static constexpr float BirdChargeSpeedMultiplier = 1.4f; + static constexpr float BirdChargeCardinalEqualizer = 1.4f; + static constexpr std::uint32_t IdleBoredBase = 536870944u; + + static float GetCheckpointYOffsetForPlayerType(PlayerType playerType) + { + return (IsBirdMorphType(playerType) ? 0.0f : GroundedCheckpointYOffset); + } + + static bool IsPlayerMovingNow(ILevelHandler* levelHandler, Player* player) + { + return ( + levelHandler->PlayerActionPressed(player, PlayerAction::Left) || + levelHandler->PlayerActionPressed(player, PlayerAction::Right) || + levelHandler->PlayerActionPressed(player, PlayerAction::Up) || + levelHandler->PlayerActionPressed(player, PlayerAction::Down) || + levelHandler->PlayerActionPressed(player, PlayerAction::Jump) + ); + } + Player::Player() : _playerIndex(0), @@ -71,7 +98,7 @@ namespace Jazz2::Actors _activeModifier(Modifier::None), _externalForceCooldown(0.0f), _springCooldown(0.0f), - _inIdleTransition(false), _inLedgeTransition(false), _canDoubleJump(true), + _inIdleTransition(false), _inLedgeTransition(false), _canDoubleJump(true), _pendingCopterFramesLeft(0.0f), _carryingObject(nullptr), _lives(0), _coins(0), _coinsCheckpoint(0), _foodEaten(0), _foodEatenCheckpoint(0), _score(0), _checkpointLight(1.0f), @@ -91,6 +118,7 @@ namespace Jazz2::Actors _lastPoleTime(0.0f), _inTubeTime(0.0f), _dizzyTime(0.0f), + _birdIdleBaseY(0.0f), _activeShield(ShieldType::None), _activeShieldTime(0.0f), _weaponFlareTime(0.0f), @@ -129,13 +157,20 @@ namespace Jazz2::Actors case PlayerType::Spaz: async_await RequestMetadataAsync("Interactive/PlayerSpaz"_s); break; case PlayerType::Lori: async_await RequestMetadataAsync("Interactive/PlayerLori"_s); break; case PlayerType::Frog: async_await RequestMetadataAsync("Interactive/PlayerFrog"_s); break; + case PlayerType::Bird: async_await RequestMetadataAsync("Interactive/PlayerBird"_s); break; + case PlayerType::BirdYellow: async_await RequestMetadataAsync("Interactive/PlayerBirdYellow"_s); break; case PlayerType::Spectate: // TODO: Spectate mode - load minimal metadata for fallback, but player will be invisible async_await RequestMetadataAsync("Interactive/PlayerJazz"_s); break; } - SetAnimation(AnimState::Fall); + if (IsBirdMorphType(_playerType)) { + SetState(ActorState::ApplyGravitation, false); + SetAnimation(AnimState::Idle); + } else { + SetAnimation(AnimState::Fall); + } std::memset(_weaponAmmo, 0, sizeof(_weaponAmmo)); std::memset(_weaponAmmoCheckpoint, 0, sizeof(_weaponAmmoCheckpoint)); @@ -152,7 +187,7 @@ namespace Jazz2::Actors _health = 0; _maxHealth = 0; _renderer.setDrawEnabled(false); - + // Empty hitbox for spectate mode AABB = {}; AABBInner = {}; @@ -272,6 +307,7 @@ namespace Jazz2::Actors OnUpdatePhysics(timeMult); UpdateAnimation(timeMult); + CheckSuspendState(timeMult); CheckEndOfSpecialMoves(timeMult); @@ -298,7 +334,7 @@ namespace Jazz2::Actors OnHandleMovement(timeMult, areaWeaponAllowed, canJumpPrev); // Handle weapon switching - if (((_controllable && _controllableExternal) || !_levelHandler->IsReforged()) && _playerType != PlayerType::Frog) { + if (((_controllable && _controllableExternal) || !_levelHandler->IsReforged()) && _playerType != PlayerType::Frog && _playerType != PlayerType::BirdYellow) { bool isGamepad; if (_levelHandler->PlayerActionHit(this, PlayerAction::ChangeWeapon, true, isGamepad)) { if (!isGamepad || PreferencesCache::WeaponWheel == WeaponWheelStyle::Disabled) { @@ -485,6 +521,9 @@ namespace Jazz2::Actors if (_jumpTime > 0.0f) { _jumpTime -= timeMult; } + if (_pendingCopterFramesLeft > 0.0f) { + _pendingCopterFramesLeft = std::max(0.0f, _pendingCopterFramesLeft - timeMult); + } if (_externalForceCooldown > 0.0f) { _externalForceCooldown -= timeMult; } @@ -695,6 +734,13 @@ namespace Jazz2::Actors #endif } } + +#if defined(WITH_AUDIO) + // Cleanup bird fly sound if it has stopped playing + if (_birdFlySound != nullptr && _birdFlySound->isStopped()) { + _birdFlySound = nullptr; + } +#endif } void Player::OnHandleMovement(float timeMult, bool areaWeaponAllowed, bool canJumpPrev) @@ -714,6 +760,7 @@ namespace Jazz2::Actors if (_keepRunningTime <= 0.0f) { bool canWalk = (_controllable && _controllableExternal && !_isLifting && _suspendType != SuspendType::SwingingVine && + !IsBirdMorphType(_playerType) && (_playerType != PlayerType::Frog || !_levelHandler->PlayerActionPressed(this, PlayerAction::Fire))); float playerMovement = _levelHandler->PlayerHorizontalMovement(this); @@ -770,7 +817,7 @@ namespace Jazz2::Actors if (CanJump()) { _wasUpPressed = _wasDownPressed = false; } - } else if (_inTubeTime <= 0.0f) { + } else if (_inTubeTime <= 0.0f && !IsBirdMorphType(_playerType)) { _speed.X = std::max((std::abs(_speed.X) - Deceleration * timeMult), 0.0f) * (_speed.X < 0.0f ? -1.0f : 1.0f); _isActivelyPushing = false; @@ -781,7 +828,7 @@ namespace Jazz2::Actors _wasActivelyPushing = false; } } - } else { + } else if (!IsBirdMorphType(_playerType)) { _keepRunningTime -= timeMult; _isActivelyPushing = _wasActivelyPushing = true; @@ -811,7 +858,92 @@ namespace Jazz2::Actors return; } - if (_inWater || _activeModifier != Modifier::None) { + if (IsBirdMorphType(_playerType)) { + float playerMovement = _levelHandler->PlayerHorizontalMovement(this); + float playerMovementVert = _levelHandler->PlayerVerticalMovement(this); + + if (std::abs(playerMovement) <= 0.3f) { + playerMovement = 0.0f; + } + if (std::abs(playerMovementVert) <= 0.3f) { + playerMovementVert = 0.0f; + } + + // Jump button acts as Up arrow for Bird + if (_levelHandler->PlayerActionPressed(this, PlayerAction::Jump)) { + playerMovementVert = std::min(playerMovementVert, -1.0f); + } + + bool birdYellowHasDirectionalInput = IsPlayerMovingNow(_levelHandler, this); + bool birdYellowChargingActive = ( + _playerType == PlayerType::BirdYellow && + _levelHandler->PlayerActionPressed(this, PlayerAction::Fire) && + birdYellowHasDirectionalInput + ); + float chargeSpeedMultiplier = (birdYellowChargingActive ? BirdChargeSpeedMultiplier : 1.0f); + bool hasHorizontalInput = (std::abs(playerMovement) > 0.0f); + bool hasVerticalInput = (std::abs(playerMovementVert) > 0.0f); + float chargeDirectionEqualizer = ( + birdYellowChargingActive && hasHorizontalInput != hasVerticalInput + ? BirdChargeCardinalEqualizer + : 1.0f + ); + + bool hasGroundBelow = HasGroundBelowForBird(); + + float targetSpeedX = playerMovement * BirdFlySpeedX * chargeSpeedMultiplier * chargeDirectionEqualizer; + float horizontalLerp = std::min(1.0f, BirdHorizontalResponse * timeMult); + float verticalLerp = std::min(1.0f, BirdVerticalResponse * timeMult); + + _speed.X = lerp(_speed.X, targetSpeedX, horizontalLerp); + + if (birdYellowChargingActive) { + // Move like in water during charge + float playerMovementVelocity = std::abs(playerMovementVert); + if (playerMovementVelocity > 0.3f) { + _speed.Y = std::clamp(_speed.Y + Acceleration * chargeSpeedMultiplier * chargeDirectionEqualizer * timeMult * (playerMovementVert > 0.0f ? 1.0f : -1.0f), -MaxRunningSpeed * chargeSpeedMultiplier * chargeDirectionEqualizer * playerMovementVelocity, MaxRunningSpeed * chargeSpeedMultiplier * chargeDirectionEqualizer * playerMovementVelocity); + } else { + _speed.Y = std::max((std::abs(_speed.Y) - Deceleration * timeMult), 0.0f) * (_speed.Y < 0.0f ? -1.0f : 1.0f); + } + UpdateSwimmingRotationAndAnimation(); + + // Match animation speed to when moving moderately + if (_currentTransition == nullptr && std::abs(_speed.Y) < 0.5f && std::abs(_speed.X) < 0.5f) { + // Simulate moderate movement speed for animation when stationary + _renderer.AnimDuration = std::max(_currentAnimation->AnimDuration + 1.0f - 2.5f * 0.26f, 0.4f); + } + } else { + _renderer.setRotation(0.0f); + + // sink slowly when no vertical input and not on ground + float targetSpeedY; + if (playerMovementVert < 0.0f) { + targetSpeedY = playerMovementVert * BirdFlySpeedYUp; + } else if (playerMovementVert > 0.0f) { + targetSpeedY = playerMovementVert * BirdFlySpeedYDown; + } else if (hasGroundBelow) { + targetSpeedY = 0.0f; + } else { + targetSpeedY = BirdSinkSpeed; + } + _speed.Y = lerp(_speed.Y, targetSpeedY, verticalLerp); + } + + // Update facing based on movement direction + if (std::abs(_speed.X) > 0.3f) { + SetFacingLeft(_speed.X < 0.0f); + } + + #if defined(WITH_AUDIO) + if (hasGroundBelow && std::abs(_speed.X) < 0.1f && std::abs(_speed.Y) < 0.1f && _birdFlySound != nullptr) { + _birdFlySound->stop(); + _birdFlySound = nullptr; + } + #endif + + _isFreefall = false; + SetState(ActorState::ApplyGravitation, false); + } else if ((_inWater && _playerType != PlayerType::Frog) || _activeModifier != Modifier::None) { float playerMovement = _levelHandler->PlayerVerticalMovement(this); float playerMovementVelocity = std::abs(playerMovement); if (playerMovementVelocity > 0.3f) { @@ -878,7 +1010,7 @@ namespace Jazz2::Actors SetAnimation(AnimState::Crouch); } } - } else if (!CanJump() && !_wasDownPressed && _playerType != PlayerType::Frog) { + } else if (!CanJump() && !_wasDownPressed && _playerType != PlayerType::Frog && !IsBirdMorphType(_playerType)) { _wasDownPressed = true; _speed.X = 0.0f; @@ -929,33 +1061,30 @@ namespace Jazz2::Actors }); } else { switch (_playerType) { - case PlayerType::Jazz: { + case PlayerType::Jazz: + case PlayerType::Lori: { if ((_currentAnimation->State & AnimState::Crouch) == AnimState::Crouch) { _controllable = false; SetAnimation(AnimState::Uppercut); - SetPlayerTransition(AnimState::TransitionUppercutA, true, true, SpecialMoveType::Uppercut, [this]() { - _externalForce.Y = (_levelHandler->IsReforged() ? -1.4f : -1.2f); - _speed.Y = -2.0f; - SetState(ActorState::CanJump, false); - SetPlayerTransition(AnimState::TransitionUppercutB, true, true, SpecialMoveType::Uppercut); - }); + + if (_playerType == PlayerType::Jazz) { + SetPlayerTransition(AnimState::TransitionUppercutA, true, true, SpecialMoveType::Uppercut, [this]() { + _externalForce.Y = (_levelHandler->IsReforged() ? -1.4f : -1.2f); + _speed.Y = -2.0f; + SetState(ActorState::CanJump, false); + SetPlayerTransition(AnimState::TransitionUppercutB, true, true, SpecialMoveType::Uppercut); + }); + } else { + _controllableTimeout = 40.0f; + SetPlayerTransition(AnimState::TransitionUppercutA, true, false, SpecialMoveType::Sidekick, [this]() { + _externalForce.X = 4.0f * (IsFacingLeft() ? -1.0f : 1.0f); + _speed.X = 9.3f * (IsFacingLeft() ? -1.0f : 1.0f); + SetState(ActorState::ApplyGravitation, false); + }); + } } else { - if (_speed.Y > 0.01f && !CanJump() && (_currentAnimation->State & (AnimState::Fall | AnimState::Copter)) != AnimState::Idle) { - SetState(ActorState::ApplyGravitation, false); - _speed.Y = 1.5f; - _externalForce.Y = 0.0f; - if ((_currentAnimation->State & AnimState::Copter) != AnimState::Copter) { - SetAnimation(AnimState::Copter); - } - _copterFramesLeft = 70.0f; -#if defined(WITH_AUDIO) - if (_copterSound == nullptr) { - _copterSound = PlaySfx("Copter"_s, 0.6f, 1.5f); - if (_copterSound != nullptr) { - _copterSound->setLooping(true); - } - } -#endif + if (!CanJump() && _canDoubleJump) { + _pendingCopterFramesLeft = 32.0f; } } break; @@ -973,6 +1102,7 @@ namespace Jazz2::Actors }); PlayPlayerSfx("Sidekick"_s); + _pendingCopterFramesLeft = 0.0f; } else { if (!CanJump() && _canDoubleJump) { _canDoubleJump = false; @@ -989,37 +1119,6 @@ namespace Jazz2::Actors } break; } - case PlayerType::Lori: { - if ((_currentAnimation->State & AnimState::Crouch) == AnimState::Crouch) { - _controllable = false; - _controllableTimeout = 40.0f; - SetAnimation(AnimState::Uppercut); - SetPlayerTransition(AnimState::TransitionUppercutA, true, false, SpecialMoveType::Sidekick, [this]() { - _externalForce.X = 4.0f * (IsFacingLeft() ? -1.0f : 1.0f); - _speed.X = 9.3f * (IsFacingLeft() ? -1.0f : 1.0f); - SetState(ActorState::ApplyGravitation, false); - }); - } else { - if (_speed.Y > 0.01f && !CanJump() && (_currentAnimation->State & (AnimState::Fall | AnimState::Copter)) != AnimState::Idle) { - SetState(ActorState::ApplyGravitation, false); - _speed.Y = 1.5f; - _externalForce.Y = 0.0f; - if ((_currentAnimation->State & AnimState::Copter) != AnimState::Copter) { - SetAnimation(AnimState::Copter); - } - _copterFramesLeft = 70.0f; -#if defined(WITH_AUDIO) - if (_copterSound == nullptr) { - _copterSound = PlaySfx("Copter"_s, 0.6f, 1.5f); - if (_copterSound != nullptr) { - _copterSound->setLooping(true); - } - } -#endif - } - } - break; - } } } } @@ -1085,6 +1184,27 @@ namespace Jazz2::Actors } } + if (!CanJump() && _pendingCopterFramesLeft > 0.0f && _canDoubleJump && (_playerType == PlayerType::Jazz || _playerType == PlayerType::Lori) && + _speed.Y > 0.1f && (_currentAnimation->State & (AnimState::Fall | AnimState::Copter)) != AnimState::Idle) { + _pendingCopterFramesLeft = 0.0f; + _canDoubleJump = false; + SetState(ActorState::ApplyGravitation, false); + _speed.Y = 1.0f; + _externalForce.Y = 0.0f; + if ((_currentAnimation->State & AnimState::Copter) != AnimState::Copter) { + SetAnimation(AnimState::Copter); + } + _copterFramesLeft = 70.0f; +#if defined(WITH_AUDIO) + if (_copterSound == nullptr) { + _copterSound = PlaySfx("Copter"_s, 0.6f, 1.5f); + if (_copterSound != nullptr) { + _copterSound->setLooping(true); + } + } +#endif + } + // Fire bool weaponInUse = false; if (_weaponAllowed && areaWeaponAllowed && _levelHandler->PlayerActionPressed(this, PlayerAction::Fire)) { @@ -1101,20 +1221,30 @@ namespace Jazz2::Actors _controllableTimeout = 0.0f; }); } - } else if (_weaponAmmo[(std::int32_t)_currentWeapon] != 0) { - _wasFirePressed = true; + } + else if (_weaponAmmo[(std::int32_t)_currentWeapon] != 0) { + bool birdYellowDirectionalPressed = IsPlayerMovingNow(_levelHandler, this); + if (_playerType == PlayerType::BirdYellow && !birdYellowDirectionalPressed) { + _wasFirePressed = false; + } else { + _wasFirePressed = true; // Shooting has higher priority than pushing if object can't be moved further anymore _pushFramesLeft = 0.0f; bool weaponCooledDown = (_weaponCooldown <= 0.0f); weaponInUse = FireCurrentWeapon(_currentWeapon); + if (weaponInUse) { if (_currentTransition != nullptr && (_currentTransition->State == AnimState::Spring || _currentTransition->State == AnimState::TransitionShootToIdle)) { ForceCancelTransition(); } + if (_playerType != PlayerType::BirdYellow) { + SetAnimation(_currentAnimation->State | AnimState::Shoot); + } else { + SetAnimation(AnimState::Shoot); + } - SetAnimation(_currentAnimation->State | AnimState::Shoot); // Rewind the animation, if it should be played only once if (weaponCooledDown) { if (_currentAnimation->LoopMode == AnimationLoopMode::Once) { @@ -1126,6 +1256,7 @@ namespace Jazz2::Actors _fireFramesLeft = 20.0f; } + } } } } else if (_wasFirePressed) { @@ -1153,7 +1284,7 @@ namespace Jazz2::Actors bool Player::OnDraw(RenderQueue& renderQueue) { - if (_weaponFlareTime > 0.0f && !_inWater && _currentTransition == nullptr) { + if (_weaponFlareTime > 0.0f && !_inWater && _currentTransition == nullptr && !IsBirdMorphType(_playerType)) { auto* res = _metadata->FindAnimation(WeaponFlare); if (res != nullptr && res->Base->TextureDiffuse != nullptr) { auto& command = _weaponFlareCommand; @@ -1492,6 +1623,8 @@ namespace Jazz2::Actors case PlayerType::Jazz: RequestMetadata("Interactive/PlayerJazz"_s); break; case PlayerType::Spaz: RequestMetadata("Interactive/PlayerSpaz"_s); break; case PlayerType::Lori: RequestMetadata("Interactive/PlayerLori"_s); break; + case PlayerType::Bird: RequestMetadata("Interactive/PlayerBird"_s); break; + case PlayerType::BirdYellow: RequestMetadata("Interactive/PlayerBirdYellow"_s); break; case PlayerType::Frog: RequestMetadata("Interactive/PlayerFrog"_s); break; } @@ -1705,6 +1838,7 @@ namespace Jazz2::Actors } _canDoubleJump = true; + _pendingCopterFramesLeft = 0.0f; _isFreefall = false; SetState(ActorState::IsSolidObject, true); @@ -1951,7 +2085,8 @@ namespace Jazz2::Actors AnimState oldState = _currentAnimation->State; AnimState newState; - if (_inWater) { + bool isBirdMorph = IsBirdMorphType(_playerType); + if (_inWater && _playerType != PlayerType::Frog && !isBirdMorph) { newState = AnimState::Swim; } else if (_activeModifier == Modifier::Airboard) { newState = AnimState::Airboard; @@ -1963,7 +2098,7 @@ namespace Jazz2::Actors newState = AnimState::Swing; } else if (_isLifting) { newState = AnimState::Lift; - } else if (CanJump() && _isActivelyPushing && _pushFramesLeft > 0.0f && _keepRunningTime <= 0.0f && _fireFramesLeft <= 0.0f) { + } else if (!isBirdMorph && CanJump() && _isActivelyPushing && _pushFramesLeft > 0.0f && _keepRunningTime <= 0.0f && _fireFramesLeft <= 0.0f) { newState = AnimState::Push; if (_inIdleTransition) { @@ -1974,7 +2109,7 @@ namespace Jazz2::Actors // Only certain ones don't need to be preserved from earlier state, others should be set as expected AnimState composite = (_currentAnimation->State & CompositeAnimMask); - if (_isActivelyPushing == _wasActivelyPushing) { + if (!isBirdMorph && _isActivelyPushing == _wasActivelyPushing) { float absSpeedX = std::abs(_speed.X); if (absSpeedX > MaxRunningSpeed) { composite |= AnimState::Dash; @@ -1990,14 +2125,35 @@ namespace Jazz2::Actors } } - if (_fireFramesLeft > 0.0f) { + if (isBirdMorph) { + composite &= ~(AnimState::Walk | AnimState::Run | AnimState::Dash | AnimState::Jump | AnimState::Fall | AnimState::Freefall | AnimState::Spring | AnimState::Dizzy | AnimState::Swim | AnimState::Shoot); + } else if (_fireFramesLeft > 0.0f) { composite |= AnimState::Shoot; } if (_suspendType != SuspendType::None) { composite |= AnimState::Hook; } else { - if (CanJump()) { + if (isBirdMorph) { + bool birdDirectionalPressed = IsPlayerMovingNow(_levelHandler, this); + bool birdYellowChargingActive = ( + _playerType == PlayerType::BirdYellow && + _levelHandler->PlayerActionPressed(this, PlayerAction::Fire) && + birdDirectionalPressed + ); + bool birdStationaryOnGround = (HasGroundBelowForBird() && !birdDirectionalPressed && std::abs(_speed.X) < 0.3f && std::abs(_speed.Y) < 0.3f); + + if (birdYellowChargingActive) { + composite |= AnimState::Swim | AnimState::Shoot; + } else { + if (!birdStationaryOnGround) { + composite |= AnimState::Walk; + } + if (_fireFramesLeft > 0.0f && (_playerType != PlayerType::BirdYellow || birdDirectionalPressed)) { + composite |= AnimState::Shoot; + } + } + } else if (CanJump()) { // Grounded, no vertical speed if (_dizzyTime > 0.0f) { composite |= AnimState::Dizzy; @@ -2030,18 +2186,21 @@ namespace Jazz2::Actors if (_currentTransition == nullptr) { constexpr StringView IdleBored[] = { - "IdleBored1"_s, "IdleBored2"_s, "IdleBored3"_s, "IdleBored4"_s, "IdleBored5"_s + "IdleBored1"_s, "IdleBored2"_s, "IdleBored3"_s, "IdleBored4"_s, "IdleBored5"_s, + "IdleBored6"_s, "IdleBored7"_s }; std::int32_t maxIdx; switch (_playerType) { case PlayerType::Jazz: maxIdx = 5; break; case PlayerType::Spaz: maxIdx = 4; break; case PlayerType::Lori: maxIdx = 3; break; + case PlayerType::Bird: + case PlayerType::BirdYellow: maxIdx = 7; break; default: maxIdx = 0; break; } if (maxIdx > 0) { std::int32_t selectedIdx = Random().Fast(0, maxIdx); - if (SetTransition((AnimState)(536870944 + selectedIdx), true)) { + if (SetTransition((AnimState)(IdleBoredBase + selectedIdx), true)) { PlayPlayerSfx(IdleBored[selectedIdx]); } } @@ -2243,6 +2402,14 @@ namespace Jazz2::Actors void Player::CheckSuspendState(float timeMult) { + if (IsBirdMorphType(_playerType)) { + if (_suspendType != SuspendType::None) { + _suspendType = SuspendType::None; + _suspendTime = 8.0f; + } + return; + } + if (_suspendTime > 0.0f) { _suspendTime -= timeMult; return; @@ -2298,7 +2465,7 @@ namespace Jazz2::Actors } } - if (newSuspendState != SuspendType::None && _playerType != PlayerType::Frog && _frozenTimeLeft <= 0.0f) { + if (newSuspendState != SuspendType::None && _playerType != PlayerType::Frog && !IsBirdMorphType(_playerType) && _frozenTimeLeft <= 0.0f) { if (_currentSpecialMove == SpecialMoveType::None) { _suspendType = newSuspendState; SetState(ActorState::ApplyGravitation, false); @@ -2335,48 +2502,94 @@ namespace Jazz2::Actors } } - void Player::OnHandleWater() + bool Player::HasGroundBelowForBird() { - if (_inWater) { - if (_pos.Y >= _levelHandler->GetWaterLevel()) { - SetState(ActorState::ApplyGravitation, false); + TileCollisionParams params = { TileDestructType::None, true }; + AABBf groundProbe = AABBf(AABBInner.L + 2.0f, AABBInner.B + 1.0f, AABBInner.R - 2.0f, AABBInner.B + 3.0f); + return !_levelHandler->IsPositionEmpty(this, groundProbe, params); + } - if (std::abs(_speed.X) > 1.0f || std::abs(_speed.Y) > 1.0f) { - float angle; - if (_speed.X == 0.0f) { - if (IsFacingLeft()) { - angle = atan2(-_speed.Y, -std::numeric_limits::epsilon()); - } else { - angle = atan2(_speed.Y, std::numeric_limits::epsilon()); - } - } else if (_speed.X < 0.0f) { - angle = atan2(-_speed.Y, -_speed.X); - } else { - angle = atan2(_speed.Y, _speed.X); - } + void Player::UpdateSwimmingRotationAndAnimation() + { + // Update rotation based on velocity + if (std::abs(_speed.X) > 1.0f || std::abs(_speed.Y) > 1.0f) { + float angle; + if (_speed.X == 0.0f) { + if (IsFacingLeft()) { + angle = atan2(-_speed.Y, -std::numeric_limits::epsilon()); + } else { + angle = atan2(_speed.Y, std::numeric_limits::epsilon()); + } + } else if (_speed.X < 0.0f) { + angle = atan2(-_speed.Y, -_speed.X); + } else { + angle = atan2(_speed.Y, _speed.X); + } - if (angle > fPi) { - angle = angle - fTwoPi; - } + if (angle > fPi) { + angle = angle - fTwoPi; + } - _renderer.setRotation(std::clamp(angle, -fPiOver3, fPiOver3)); - } + _renderer.setRotation(std::clamp(angle, -fPiOver3, fPiOver3)); + } else { + _renderer.setRotation(0.0f); + } - // Adjust swimming animation speed - if (_currentTransition == nullptr) { - _renderer.AnimDuration = std::max(_currentAnimation->AnimDuration + 1.0f - Vector2f(_speed.X, _speed.Y).Length() * 0.26f, 0.4f); + // Adjust animation speed + if (_currentTransition == nullptr) { + _renderer.AnimDuration = std::max(_currentAnimation->AnimDuration + 1.0f - Vector2f(_speed.X, _speed.Y).Length() * 0.26f, 0.4f); + } + } + + void Player::OnHandleWater() + { + if (_playerType == PlayerType::Frog) { + // Frog has no swim animation or physics: gravity stays on, only play splash on entry/exit + if (_inWater) { + if (_pos.Y < _levelHandler->GetWaterLevel() && _waterCooldownLeft <= 0.0f) { + // Frog exited water + _inWater = false; + _waterCooldownLeft = 20.0f; + OnWaterSplash(Vector2f(_pos.X, _levelHandler->GetWaterLevel()), false); + } else { + // Cap upward speed to prevent exaggerated jumps while in water + if (_speed.Y < -2.5f) { + _speed.Y = -2.5f; + } + } + } else { + if (_pos.Y >= _levelHandler->GetWaterLevel() && _waterCooldownLeft <= 0.0f) { + // Frog entered water + _inWater = true; + _waterCooldownLeft = 20.0f; + _controllable = true; + EndDamagingMove(); + OnWaterSplash(Vector2f(_pos.X, _levelHandler->GetWaterLevel()), true); } + } + // Always keep gravity and normal movement active for Frog in water + SetState(ActorState::ApplyGravitation, true); + _renderer.setRotation(0.0f); + return; + } + + if (_inWater) { + if (_pos.Y >= _levelHandler->GetWaterLevel()) { + SetState(ActorState::ApplyGravitation, false); + + UpdateSwimmingRotationAndAnimation(); } else if (_waterCooldownLeft <= 0.0f) { _inWater = false; _waterCooldownLeft = 20.0f; SetState(ActorState::ApplyGravitation | ActorState::CanJump, true); - _externalForce.Y = -0.6f; + if (!IsBirdMorphType(_playerType)) { + _externalForce.Y = -0.6f; + SetAnimation(AnimState::Jump); + } _renderer.setRotation(0.0f); - SetAnimation(AnimState::Jump); - OnWaterSplash(Vector2f(_pos.X, _levelHandler->GetWaterLevel()), false); } } else { @@ -2695,6 +2908,22 @@ namespace Jazz2::Actors _pos.Y = std::clamp(_pos.Y + speed.Y * timeMult, float(levelBounds.Y), float(levelBounds.Y + levelBounds.H)); } + void Player::OnAnimationFinished() + { + ActorBase::OnAnimationFinished(); + + if (IsBirdMorphType(_playerType)) { + bool birdFiringActive = (_fireFramesLeft > 0.0f); + if ((!HasGroundBelowForBird() || birdFiringActive) && (_birdFlySound == nullptr)) { +#if defined(WITH_AUDIO) + _birdFlySound = PlayPlayerSfx("Fly"_s, 0.5f); +#else + PlayPlayerSfx("Fly"_s, 0.5f); +#endif + } + } + } + std::shared_ptr Player::PlayPlayerSfx(StringView identifier, float gain, float pitch) { auto it = _metadata->Sounds.find(String::nullTerminatedView(identifier)); @@ -2706,7 +2935,7 @@ namespace Jazz2::Actors } else { buffer = nullptr; } - + return _levelHandler->PlaySfx(this, identifier, buffer, Vector3f::Zero, true, gain, pitch); } @@ -2740,6 +2969,10 @@ namespace Jazz2::Actors _copterSound->stop(); _copterSound = nullptr; } + if (_birdFlySound != nullptr) { + _birdFlySound->stop(); + _birdFlySound = nullptr; + } if (_weaponSound != nullptr) { _weaponSound->stop(); _weaponSound = nullptr; @@ -2906,6 +3139,24 @@ namespace Jazz2::Actors shot->OnFire(shared_from_this(), gunspotPos, _speed, angle, IsFacingLeft()); _levelHandler->AddActor(shot); + if (_playerType == PlayerType::Bird) { + bool birdInFlight = !HasGroundBelowForBird(); + bool birdDescending = (_speed.Y > 0.1f || _levelHandler->PlayerVerticalMovement(this) > 0.0f); + float diagonalOffset = (IsFacingLeft() ? -0.18f : 0.18f); + if (birdInFlight && birdDescending) { + diagonalOffset = -diagonalOffset; + } + + std::shared_ptr shot2 = std::make_shared(); + shot2->OnActivated(ActorActivationDetails( + _levelHandler, + initialPos, + shotParams + )); + shot2->OnFire(shared_from_this(), gunspotPos, _speed, angle + diagonalOffset, IsFacingLeft()); + _levelHandler->AddActor(shot2); + } + std::int32_t fastFire = (_weaponUpgrades[(std::int32_t)WeaponType::Blaster] >> 1); _weaponCooldown = cooldownBase - (fastFire * cooldownUpgrade); @@ -3180,7 +3431,7 @@ namespace Jazz2::Actors ForceCancelTransition(); } - SetAnimation(_currentAnimation->State | AnimState::Shoot); + SetAnimation( _currentAnimation->State | AnimState::Shoot); initialPos = Vector3i((std::int32_t)_pos.X, (std::int32_t)_pos.Y, _renderer.layer() - 2); gunspotPos = _pos; @@ -3338,7 +3589,10 @@ namespace Jazz2::Actors } _controllable = false; - SetState(ActorState::IsInvulnerable | ActorState::ApplyGravitation, true); + SetState(ActorState::IsInvulnerable, true); + if (!IsBirdMorphType(_playerType)) { + SetState(ActorState::ApplyGravitation, true); + } _fireFramesLeft = 0.0f; _copterFramesLeft = 0.0f; _pushFramesLeft = 0.0f; @@ -3454,9 +3708,25 @@ namespace Jazz2::Actors Vector3i((std::int32_t)checkpointPosX, (std::int32_t)checkpointPosY, ILevelHandler::PlayerZ - playerIndex), playerParams )); - _playerTypeOriginal = playerTypeOriginal; + if (IsBirdMorphType(_playerType)) { + // Bird can be restored slightly inside geometry from resumable saves, + // so nudge it upward until the hitbox no longer overlaps solid tiles. + TileCollisionParams params = { TileDestructType::None, true }; + constexpr std::int32_t MaxLiftPixels = 32; + std::int32_t movedUp = 0; + OnUpdateHitbox(); + while (!_levelHandler->IsPositionEmpty(this, AABBInner, params) && movedUp < MaxLiftPixels) { + MoveInstantly(Vector2f(0.0f, -1.0f), MoveType::Relative | MoveType::Force); + movedUp++; + OnUpdateHitbox(); + } + if (movedUp > 0) { + _checkpointPos.Y -= (float)movedUp; + } + } + _checkpointLight = src.ReadValueAsLE(); _lives = src.ReadVariableInt32(); _coins = src.ReadVariableInt32(); @@ -3595,8 +3865,8 @@ namespace Jazz2::Actors _isFreefall |= CanFreefall(); SetPlayerTransition(_isFreefall ? AnimState::TransitionWarpOutFreefall : AnimState::TransitionWarpOut, false, true, SpecialMoveType::None, [this, flags]() { SetState(ActorState::IsInvulnerable, false); - // Don't re-enable gravity if any modifier is active - if (_activeModifier == Modifier::None) { + // Don't re-enable gravity if any modifier is active, and keep Bird in flying mode + if (_activeModifier == Modifier::None && !IsBirdMorphType(_playerType)) { SetState(ActorState::ApplyGravitation, true); } @@ -3618,10 +3888,10 @@ namespace Jazz2::Actors void Player::InitialPoleStage(bool horizontal) { - if (_isAttachedToPole || _playerType == PlayerType::Frog) { + if (_isAttachedToPole || _playerType == PlayerType::Frog || IsBirdMorphType(_playerType)) { return; } - + std::int32_t x = (std::int32_t)_pos.X / Tiles::TileSet::DefaultTileSize; std::int32_t y = (std::int32_t)_pos.Y / Tiles::TileSet::DefaultTileSize; @@ -3787,14 +4057,14 @@ namespace Jazz2::Actors _copterFramesLeft = 10.0f * FrameTimer::FramesPerSecond; -#if defined(WITH_AUDIO) - if (_copterSound == nullptr) { - _copterSound = PlaySfx("Copter"_s, 0.6f, 1.5f); - if (_copterSound != nullptr) { - _copterSound->setLooping(true); + #if defined(WITH_AUDIO) + if (_copterSound == nullptr) { + _copterSound = PlaySfx("Copter"_s, 0.6f, 1.5f); + if (_copterSound != nullptr) { + _copterSound->setLooping(true); + } } - } -#endif + #endif break; } case Modifier::LizardCopter: { @@ -4157,6 +4427,13 @@ namespace Jazz2::Actors PlayerType playerTypePrevious = _playerType; + float prevCheckpointOffset = GetCheckpointYOffsetForPlayerType(playerTypePrevious); + float newCheckpointOffset = GetCheckpointYOffsetForPlayerType(type); + if (prevCheckpointOffset != newCheckpointOffset) { + // Keep checkpoint world position stable across morphs with different Y-offset rules. + _checkpointPos.Y += (prevCheckpointOffset - newCheckpointOffset); + } + _playerType = type; // Load new metadata @@ -4165,6 +4442,22 @@ namespace Jazz2::Actors case PlayerType::Spaz: RequestMetadata("Interactive/PlayerSpaz"); break; case PlayerType::Lori: RequestMetadata("Interactive/PlayerLori"); break; case PlayerType::Frog: RequestMetadata("Interactive/PlayerFrog"); break; + case PlayerType::Bird: RequestMetadata("Interactive/PlayerBird"); break; + case PlayerType::BirdYellow: RequestMetadata("Interactive/PlayerBirdYellow"); break; + break; + } + + if (IsBirdMorphType(type)) { + _suspendType = SuspendType::None; + _suspendTime = 8.0f; + _carryingObject = nullptr; + _isFreefall = false; + _speed.Y = 0.0f; + _externalForce.Y = 0.0f; + _internalForceY = 0.0f; + SetState(ActorState::ApplyGravitation, false); + } else { + SetState(ActorState::ApplyGravitation, true); } // Refresh animation state @@ -4194,33 +4487,47 @@ namespace Jazz2::Actors } // Set transition + auto emitMorphSmoke = [this]() { + Explosion::Create(_levelHandler, Vector3i((std::int32_t)(_pos.X - 12.0f), (std::int32_t)(_pos.Y - 6.0f), _renderer.layer() + 4), Explosion::Type::SmokeBrown); + Explosion::Create(_levelHandler, Vector3i((std::int32_t)(_pos.X - 8.0f), (std::int32_t)(_pos.Y + 28.0f), _renderer.layer() + 4), Explosion::Type::SmokeBrown); + Explosion::Create(_levelHandler, Vector3i((std::int32_t)(_pos.X + 12.0f), (std::int32_t)(_pos.Y + 10.0f), _renderer.layer() + 4), Explosion::Type::SmokeBrown); + + Explosion::Create(_levelHandler, Vector3i((std::int32_t)_pos.X, (std::int32_t)(_pos.Y + 12.0f), _renderer.layer() + 6), Explosion::Type::SmokeBrown); + }; + if (type == PlayerType::Frog) { PlayPlayerSfx("Transform"); - _controllable = false; - _controllableTimeout = 120.0f; - switch (playerTypePrevious) { case PlayerType::Jazz: + _controllable = false; + _controllableTimeout = 120.0f; SetTransition(TransformFrogFromJazz, false, [this]() { _controllable = true; _controllableTimeout = 0.0f; }); break; case PlayerType::Spaz: + _controllable = false; + _controllableTimeout = 120.0f; SetTransition(TransformFrogFromSpaz, false, [this]() { _controllable = true; _controllableTimeout = 0.0f; }); break; case PlayerType::Lori: + _controllable = false; + _controllableTimeout = 120.0f; SetTransition(TransformFrogFromLori, false, [this]() { _controllable = true; _controllableTimeout = 0.0f; }); break; + default: + emitMorphSmoke(); + break; } - } else if (playerTypePrevious == PlayerType::Frog) { + } else if (playerTypePrevious == PlayerType::Frog && (type == PlayerType::Jazz || type == PlayerType::Spaz || type == PlayerType::Lori)) { _controllable = false; _controllableTimeout = 120.0f; @@ -4229,11 +4536,7 @@ namespace Jazz2::Actors _controllableTimeout = 0.0f; }); } else { - Explosion::Create(_levelHandler, Vector3i((std::int32_t)(_pos.X - 12.0f), (std::int32_t)(_pos.Y - 6.0f), _renderer.layer() + 4), Explosion::Type::SmokeBrown); - Explosion::Create(_levelHandler, Vector3i((std::int32_t)(_pos.X - 8.0f), (std::int32_t)(_pos.Y + 28.0f), _renderer.layer() + 4), Explosion::Type::SmokeBrown); - Explosion::Create(_levelHandler, Vector3i((std::int32_t)(_pos.X + 12.0f), (std::int32_t)(_pos.Y + 10.0f), _renderer.layer() + 4), Explosion::Type::SmokeBrown); - - Explosion::Create(_levelHandler, Vector3i((std::int32_t)_pos.X, (std::int32_t)(_pos.Y + 12.0f), _renderer.layer() + 6), Explosion::Type::SmokeBrown); + emitMorphSmoke(); } return true; @@ -4322,9 +4625,9 @@ namespace Jazz2::Actors void Player::SetCheckpoint(Vector2f pos, float ambientLight) { - _checkpointPos = Vector2f(pos.X, pos.Y - 20.0f); + _checkpointPos = Vector2f(pos.X, pos.Y - GetCheckpointYOffsetForPlayerType(_playerType)); _checkpointLight = ambientLight; - + _foodEatenCheckpoint = _foodEaten; _coinsCheckpoint = _coins; std::memcpy(_gemsCheckpoint, _gems, sizeof(_gems)); @@ -4358,8 +4661,9 @@ namespace Jazz2::Actors _carryingObject = actor; _canDoubleJump = true; + _pendingCopterFramesLeft = 0.0f; - if (suspendType == SuspendType::SwingingVine) { + if (suspendType == SuspendType::SwingingVine && _playerType != PlayerType::Frog && !IsBirdMorphType(_playerType)) { _suspendType = suspendType; SetState(ActorState::ApplyGravitation, false); } else if (_suspendType == SuspendType::SwingingVine) { @@ -4368,4 +4672,4 @@ namespace Jazz2::Actors _renderer.setRotation(0.0f); } } -} \ No newline at end of file +} diff --git a/Sources/Jazz2/Actors/Player.h b/Sources/Jazz2/Actors/Player.h index 9d938cc9f..9bf2a2f95 100644 --- a/Sources/Jazz2/Actors/Player.h +++ b/Sources/Jazz2/Actors/Player.h @@ -312,11 +312,13 @@ namespace Jazz2::Actors Modifier _activeModifier; bool _inIdleTransition, _inLedgeTransition; bool _canDoubleJump; + float _pendingCopterFramesLeft; ActorBase* _carryingObject; float _externalForceCooldown; float _springCooldown; #if defined(WITH_AUDIO) std::shared_ptr _copterSound; + std::shared_ptr _birdFlySound; #endif std::int32_t _lives, _coins, _coinsCheckpoint, _foodEaten, _foodEatenCheckpoint, _score; @@ -345,6 +347,8 @@ namespace Jazz2::Actors float _inTubeTime; float _dizzyTime; std::shared_ptr _spawnedBird; + AnimState _birdPreviousAnimState; + float _birdIdleBaseY; std::shared_ptr _activeModifierDecor; SmallVector _trail; Vector2f _trailLastPos; @@ -372,6 +376,7 @@ namespace Jazz2::Actors bool OnTileDeactivated() override; bool OnPerish(ActorBase* collider) override; void OnUpdate(float timeMult) override; + void OnAnimationFinished() override; void OnUpdateHitbox() override; bool OnDraw(RenderQueue& renderQueue) override; void OnEmitLights(SmallVectorImpl& lights) override; @@ -411,6 +416,8 @@ namespace Jazz2::Actors void PushSolidObjects(float timeMult); void CheckEndOfSpecialMoves(float timeMult); void CheckSuspendState(float timeMult); + bool HasGroundBelowForBird(); + void UpdateSwimmingRotationAndAnimation(); void OnUpdatePhysics(float timeMult); void OnUpdateTimers(float timeMult); void OnHandleMovement(float timeMult, bool areaWeaponAllowed, bool canJumpPrev); diff --git a/Sources/Jazz2/Actors/PlayerCorpse.cpp b/Sources/Jazz2/Actors/PlayerCorpse.cpp index fb4ebd436..231c98f8c 100644 --- a/Sources/Jazz2/Actors/PlayerCorpse.cpp +++ b/Sources/Jazz2/Actors/PlayerCorpse.cpp @@ -26,6 +26,15 @@ namespace Jazz2::Actors case PlayerType::Lori: async_await RequestMetadataAsync("Interactive/PlayerLori"_s); break; + case PlayerType::Frog: + async_await RequestMetadataAsync("Interactive/PlayerFrog"_s); + break; + case PlayerType::Bird: + async_await RequestMetadataAsync("Interactive/PlayerBird"_s); + break; + case PlayerType::BirdYellow: + async_await RequestMetadataAsync("Interactive/PlayerBirdYellow"_s); + break; } SetAnimation((AnimState)536870912); diff --git a/Sources/Jazz2/Actors/Solid/PowerUpMorphMonitor.cpp b/Sources/Jazz2/Actors/Solid/PowerUpMorphMonitor.cpp index c9a1d6104..b870b4633 100644 --- a/Sources/Jazz2/Actors/Solid/PowerUpMorphMonitor.cpp +++ b/Sources/Jazz2/Actors/Solid/PowerUpMorphMonitor.cpp @@ -48,6 +48,8 @@ namespace Jazz2::Actors::Solid case PlayerType::Jazz: PreloadMetadataAsync("Interactive/PlayerJazz"_s); break; case PlayerType::Spaz: PreloadMetadataAsync("Interactive/PlayerSpaz"_s); break; case PlayerType::Lori: PreloadMetadataAsync("Interactive/PlayerLori"_s); break; + case PlayerType::Bird: PreloadMetadataAsync("Interactive/PlayerBird"_s); break; + case PlayerType::BirdYellow: PreloadMetadataAsync("Interactive/PlayerBirdYellow"_s); break; } } } @@ -132,16 +134,12 @@ namespace Jazz2::Actors::Solid return PlayerType::Spaz; } case MorphType::Swap3: - if (currentType == PlayerType::Spaz) { - return PlayerType::Lori; - } else if (currentType == PlayerType::Lori) { - return PlayerType::Jazz; - } else { - return PlayerType::Spaz; - } + return GetNextBaseMorphType(currentType); case MorphType::ToBird: - // TODO: Implement Birds + if (!IsBirdMorphType(currentType)) { + return PlayerType::Bird; + } return std::nullopt; default: diff --git a/Sources/Jazz2/LevelHandler.cpp b/Sources/Jazz2/LevelHandler.cpp index bef2bf52a..a95f98405 100644 --- a/Sources/Jazz2/LevelHandler.cpp +++ b/Sources/Jazz2/LevelHandler.cpp @@ -2487,6 +2487,10 @@ namespace Jazz2 switch (_players[0]->GetPlayerType()) { case PlayerType::Jazz: newType = PlayerType::Spaz; break; case PlayerType::Spaz: newType = PlayerType::Lori; break; + case PlayerType::Lori: newType = PlayerType::BirdYellow; break; + case PlayerType::BirdYellow: newType = PlayerType::Frog; break; + case PlayerType::Frog: newType = PlayerType::Bird; break; + case PlayerType::Bird: newType = PlayerType::Jazz; break; default: newType = PlayerType::Jazz; break; } diff --git a/Sources/Jazz2/PlayerType.h b/Sources/Jazz2/PlayerType.h index 32dec01f1..ecf7a3e51 100644 --- a/Sources/Jazz2/PlayerType.h +++ b/Sources/Jazz2/PlayerType.h @@ -13,7 +13,38 @@ namespace Jazz2 Spaz, /**< Spaz */ Lori, /**< Lori */ Frog, /**< Frog */ + Bird, /**< Bird */ + BirdYellow, /**< Bird (Yellow) */ Spectate = UINT8_MAX /**< Spectate mode */ }; + + /** @brief Returns next type in base morph cycle (Jazz -> Spaz -> Lori -> Jazz) */ + inline constexpr PlayerType GetNextBaseMorphType(PlayerType currentType) + { + switch (currentType) { + case PlayerType::Spaz: return PlayerType::Lori; + case PlayerType::Lori: return PlayerType::Jazz; + default: return PlayerType::Spaz; + } + } + + /** @brief Returns `true` if the type is one of Bird morph variants */ + inline constexpr bool IsBirdMorphType(PlayerType playerType) + { + return (playerType == PlayerType::Bird || playerType == PlayerType::BirdYellow); + } + + /** @brief Returns next type in cheat morph cycle (Jazz -> Spaz -> Lori -> Frog -> Bird -> BirdYellow -> Jazz) */ + inline constexpr PlayerType GetNextCheatMorphType(PlayerType currentType) + { + switch (currentType) { + case PlayerType::Jazz: return PlayerType::Spaz; + case PlayerType::Spaz: return PlayerType::Lori; + case PlayerType::Lori: return PlayerType::Frog; + case PlayerType::Frog: return PlayerType::Bird; + case PlayerType::Bird: return PlayerType::BirdYellow; + default: return PlayerType::Jazz; + } + } } \ No newline at end of file diff --git a/Sources/Jazz2/UI/HUD.cpp b/Sources/Jazz2/UI/HUD.cpp index 10c996a1d..9d6540f1f 100644 --- a/Sources/Jazz2/UI/HUD.cpp +++ b/Sources/Jazz2/UI/HUD.cpp @@ -60,6 +60,8 @@ namespace Jazz2::UI static constexpr AnimState CharacterSpaz = (AnimState)61; static constexpr AnimState CharacterLori = (AnimState)62; static constexpr AnimState CharacterFrog = (AnimState)63; + static constexpr AnimState CharacterBird = (AnimState)64; + static constexpr AnimState CharacterBirdYellow = (AnimState)65; static constexpr AnimState Heart = (AnimState)70; static constexpr AnimState PickupGemRed = (AnimState)71; static constexpr AnimState PickupGemGreen = (AnimState)72; @@ -559,6 +561,8 @@ namespace Jazz2::UI case PlayerType::Spaz: playerIcon = CharacterSpaz; break; case PlayerType::Lori: playerIcon = CharacterLori; break; case PlayerType::Frog: playerIcon = CharacterFrog; break; + case PlayerType::Bird: playerIcon = CharacterBird; break; + case PlayerType::BirdYellow: playerIcon = CharacterBirdYellow; break; } std::int32_t health = player->GetHealth(); @@ -697,7 +701,7 @@ namespace Jazz2::UI } #endif - if (!player->_weaponAllowed || player->_playerType == PlayerType::Frog) { + if (!player->_weaponAllowed || player->_playerType == PlayerType::Frog || player->_playerType == PlayerType::BirdYellow) { return; } From fab30f5eedc0fdc6cd84598a19aa09bbdd958819 Mon Sep 17 00:00:00 2001 From: marcostocco <26927387+marcostocco@users.noreply.github.com> Date: Thu, 14 May 2026 22:06:18 +0200 Subject: [PATCH 2/2] small fix suggestion by coderabbit --- Content/Metadata/Interactive/PlayerBirdYellow.res | 2 +- Sources/Jazz2/Actors/Player.cpp | 4 ++++ Sources/Jazz2/PlayerType.h | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Content/Metadata/Interactive/PlayerBirdYellow.res b/Content/Metadata/Interactive/PlayerBirdYellow.res index 9779801dc..93e274766 100644 --- a/Content/Metadata/Interactive/PlayerBirdYellow.res +++ b/Content/Metadata/Interactive/PlayerBirdYellow.res @@ -50,7 +50,7 @@ }, "Shoot": { "Path": "BirdyYellow/charge_ver.aura", - "States": [ 16, 144 ], + "States": [ 16, 144 ] }, "Hurt": { "Path": "BirdyYellow/hurt.aura", diff --git a/Sources/Jazz2/Actors/Player.cpp b/Sources/Jazz2/Actors/Player.cpp index bedc6b045..78a394b13 100644 --- a/Sources/Jazz2/Actors/Player.cpp +++ b/Sources/Jazz2/Actors/Player.cpp @@ -139,6 +139,10 @@ namespace Jazz2::Actors _copterSound->stop(); _copterSound = nullptr; } + if (_birdFlySound != nullptr) { + _birdFlySound->stop(); + _birdFlySound = nullptr; + } if (_weaponSound != nullptr) { _weaponSound->stop(); _weaponSound = nullptr; diff --git a/Sources/Jazz2/PlayerType.h b/Sources/Jazz2/PlayerType.h index ecf7a3e51..f15bf57ca 100644 --- a/Sources/Jazz2/PlayerType.h +++ b/Sources/Jazz2/PlayerType.h @@ -41,9 +41,9 @@ namespace Jazz2 switch (currentType) { case PlayerType::Jazz: return PlayerType::Spaz; case PlayerType::Spaz: return PlayerType::Lori; - case PlayerType::Lori: return PlayerType::Frog; + case PlayerType::Lori: return PlayerType::BirdYellow; + case PlayerType::BirdYellow: return PlayerType::Frog; case PlayerType::Frog: return PlayerType::Bird; - case PlayerType::Bird: return PlayerType::BirdYellow; default: return PlayerType::Jazz; } }