diff --git a/data/file_list.yml b/data/file_list.yml index 86ad680e..8ccaf306 100644 --- a/data/file_list.yml +++ b/data/file_list.yml @@ -25800,580 +25800,580 @@ Enemy/Bubble.o: - offset: 0x0df7ec size: 516 label: _ZN6BubbleC2EPKc - status: NotDecompiled + status: Matching - offset: 0x0df9f0 size: 528 label: _ZN6BubbleC1EPKc - status: NotDecompiled + status: Matching - offset: 0x0dfc00 size: 1640 label: _ZN6Bubble4initERKN2al13ActorInitInfoE - status: NotDecompiled + status: Matching - offset: 0x0e0268 size: 508 label: _ZN6Bubble18recalcClippingInfoEv - status: NotDecompiled + status: Matching - offset: 0x0e0464 size: 108 label: _ZN6Bubble5startEv - status: NotDecompiled + status: Matching - offset: 0x0e04d0 size: 16 label: _ZN6Bubble7killAllEv - status: NotDecompiled + status: Matching - offset: 0x0e04e0 size: 20 label: _ZNK6Bubble16isIgnoreTriangleERKN2al8TriangleE - status: NotDecompiled + status: Matching - offset: 0x0e04f4 size: 72 label: _ZN6Bubble18initAfterPlacementEv - status: NotDecompiled + status: Matching - offset: 0x0e053c size: 396 label: _ZN6Bubble14checkEffectPosEv - status: NotDecompiled + status: Matching - offset: 0x0e06c8 size: 56 label: _ZNK6Bubble27isValidCollisionOrWaveCheckEv - status: NotDecompiled + status: Matching - offset: 0x0e0700 size: 160 label: _ZN6Bubble6appearEv - status: NotDecompiled + status: Matching - offset: 0x0e07a0 size: 1816 label: _ZN6Bubble7controlEv - status: NotDecompiled + status: Matching - offset: 0x0e0eb8 size: 296 label: _ZNK6Bubble6isHackEv - status: NotDecompiled + status: Matching - offset: 0x0e0fe0 size: 180 label: _ZN6Bubble6reviveEv - status: NotDecompiled + status: Matching - offset: 0x0e1094 size: 564 label: _ZN6Bubble14updateLavaWaveEv - status: NotDecompiled + status: Matching - offset: 0x0e12c8 size: 172 label: _ZN6Bubble20updateScrollAnimRateEv - status: NotDecompiled + status: Matching - offset: 0x0e1374 size: 60 label: _ZNK6Bubble10isOnGroundEv - status: NotDecompiled + status: Matching - offset: 0x0e13b0 size: 208 label: _ZN6Bubble8calcAnimEv - status: NotDecompiled + status: Matching - offset: 0x0e1480 size: 364 label: _ZN6Bubble14updateColliderEv - status: NotDecompiled + status: Matching - offset: 0x0e15ec size: 1048 label: _ZN6Bubble12attackSensorEPN2al9HitSensorES2_ - status: NotDecompiled + status: Matching - offset: 0x0e1a04 size: 2592 label: _ZN6Bubble10receiveMsgEPKN2al9SensorMsgEPNS0_9HitSensorES5_ - status: NotDecompiled + status: Matching - offset: 0x0e2424 size: 216 label: _ZN6Bubble9setupHackEv - status: NotDecompiled + status: Matching - offset: 0x0e24fc size: 224 label: _ZNK6Bubble38isCurrentNerveEnableLockOnAndStartHackEv - status: NotDecompiled + status: Matching - offset: 0x0e25dc size: 48 label: _ZN6Bubble16offGroupClippingEv - status: NotDecompiled + status: Matching - offset: 0x0e260c size: 104 label: _ZN6Bubble14startHackLocalEPN2al9HitSensorES2_ - status: NotDecompiled + status: Matching - offset: 0x0e2674 size: 100 label: _ZNK6Bubble19isInvalidHackEscapeEv - status: NotDecompiled + status: Matching - offset: 0x0e26d8 size: 56 label: _ZN6Bubble14prepareEndHackEv - status: NotDecompiled + status: Matching - offset: 0x0e2710 size: 152 label: _ZN6Bubble15endHackByCancelEv - status: NotDecompiled + status: Matching - offset: 0x0e27a8 size: 72 label: _ZN6Bubble12forceEndHackEv - status: NotDecompiled + status: Matching - offset: 0x0e27f0 size: 4 label: _ZN6Bubble10exeStandByEv - status: NotDecompiled + status: Matching - offset: 0x0e27f4 size: 68 label: _ZN6Bubble8exeDelayEv - status: NotDecompiled + status: Matching - offset: 0x0e2838 size: 244 label: _ZN6Bubble11exeRailMoveEv - status: NotDecompiled + status: Matching - offset: 0x0e292c size: 60 label: _ZN6Bubble15updatePosOnWaveEv - status: NotDecompiled + status: Matching - offset: 0x0e2968 size: 472 label: _ZN6Bubble8exeReadyEv - status: NotDecompiled + status: Matching - offset: 0x0e2b40 size: 324 label: _ZN6Bubble30updateVelocityIfValidCollisionEv - status: NotDecompiled + status: Matching - offset: 0x0e2c84 size: 28 label: _ZNK6Bubble14isOnDamageFireEv - status: NotDecompiled + status: Matching - offset: 0x0e2ca0 size: 600 label: _ZN6Bubble5exeUpEv - status: NotDecompiled + status: Matching - offset: 0x0e2ef8 size: 244 label: _ZN6Bubble21tryStartHitReactionUpEv - status: NotDecompiled + status: Matching - offset: 0x0e2fec size: 88 label: _ZN6Bubble22updateShadowMaskLengthEv - status: NotDecompiled + status: Matching - offset: 0x0e3044 size: 76 label: _ZN6Bubble7exeTurnEv - status: NotDecompiled + status: Matching - offset: 0x0e3090 size: 584 label: _ZN6Bubble7exeDownEv - status: NotDecompiled + status: Matching - offset: 0x0e32d8 size: 244 label: _ZN6Bubble23tryStartHitReactionDownEv - status: NotDecompiled + status: Matching - offset: 0x0e33cc size: 332 label: _ZN6Bubble9shiftSinkEv - status: NotDecompiled + status: Matching - offset: 0x0e3518 size: 272 label: _ZN6Bubble7exeSinkEv - status: NotDecompiled + status: Matching - offset: 0x0e3628 size: 368 label: _ZN6Bubble6headUpEv - status: NotDecompiled + status: Matching - offset: 0x0e3798 size: 436 label: _ZN6Bubble7exeWaitEv - status: NotDecompiled + status: Matching - offset: 0x0e394c size: 84 label: _ZN6Bubble16shiftReadyOrMoveEv - status: NotDecompiled + status: Matching - offset: 0x0e39a0 size: 52 label: _ZN6Bubble6exeDieEv - status: NotDecompiled + status: Matching - offset: 0x0e39d4 size: 4 label: _ZN6Bubble11exeWaitHackEv - status: NotDecompiled + status: Matching - offset: 0x0e39d8 size: 300 label: _ZN6Bubble16exeWaitHackStartEv - status: NotDecompiled + status: Matching - offset: 0x0e3b04 size: 8 label: _ZN6Bubble16endWaitHackStartEv - status: NotDecompiled + status: Matching - offset: 0x0e3b0c size: 332 label: _ZN6Bubble15exeWaitHackFallEv - status: NotDecompiled + status: Matching - offset: 0x0e3c58 size: 160 label: _ZN6Bubble25trySendMsgStartInSaucePanEv - status: NotDecompiled + status: Matching - offset: 0x0e3cf8 size: 100 label: _ZN6Bubble11exeHackFallEv - status: NotDecompiled + status: Matching - offset: 0x0e3d5c size: 176 label: _ZN6Bubble26tryHitReactionThroughFenceEv - status: NotDecompiled + status: Matching - offset: 0x0e3e0c size: 660 label: _ZN6Bubble12tryShiftLandEv - status: NotDecompiled + status: Matching - offset: 0x0e40a0 size: 772 label: _ZN6Bubble11exeHackMoveEv - status: NotDecompiled + status: Matching - offset: 0x0e43a4 size: 24 label: _ZNK6Bubble16isHoldHackActionEv - status: NotDecompiled + status: Matching - offset: 0x0e43bc size: 600 label: _ZN6Bubble16tryBoundMoveWallEv - status: NotDecompiled + status: Matching - offset: 0x0e4614 size: 1348 label: _ZN6Bubble18updateHackOnGroundEv - status: NotDecompiled + status: Matching - offset: 0x0e4b58 size: 1652 label: _ZN6Bubble19constrainLavaDomainEv - status: NotDecompiled + status: Matching - offset: 0x0e51cc size: 64 label: _ZN6Bubble12tryShiftFallEv - status: NotDecompiled + status: Matching - offset: 0x0e520c size: 24 label: _ZNK6Bubble18isTriggerHackSwingEv - status: NotDecompiled + status: Matching - offset: 0x0e5224 size: 536 label: _ZN6Bubble13faceToMoveVecEv - status: NotDecompiled + status: Matching - offset: 0x0e543c size: 24 label: _ZNK6Bubble17isTriggerHackJumpEv - status: NotDecompiled + status: Matching - offset: 0x0e5454 size: 8 label: _ZN6Bubble11endHackMoveEv - status: NotDecompiled + status: Matching - offset: 0x0e545c size: 2720 label: _ZN6Bubble11exeHackJumpEv - status: NotDecompiled + status: NonMatchingMajor - offset: 0x0e5efc size: 408 label: _ZN6Bubble26revertTargetQuatInHackJumpEPN4sead4QuatIfEES3_ - status: NotDecompiled + status: Matching - offset: 0x0e6094 size: 36 label: _ZNK6Bubble17calcHackerMoveVecEPN4sead7Vector3IfEERKS2_ - status: NotDecompiled + status: Matching - offset: 0x0e60b8 size: 916 label: _ZN6Bubble25makeDisplayQuatInHackJumpERKN4sead4QuatIfEES4_S4_b - status: NotDecompiled + status: Matching - offset: 0x0e644c size: 116 label: _ZNK6Bubble21isDropAttackCollisionEv - status: NotDecompiled + status: Matching - offset: 0x0e64c0 size: 104 label: _ZNK6Bubble21isRiseAttackCollisionEv - status: NotDecompiled + status: Matching - offset: 0x0e6528 size: 24 label: _ZNK6Bubble14isHoldHackJumpEv - status: NotDecompiled + status: Matching - offset: 0x0e6540 size: 360 label: _ZN6Bubble22tryShiftContinuousJumpEv - status: NotDecompiled + status: Matching - offset: 0x0e66a8 size: 8 label: _ZN6Bubble11endHackJumpEv - status: NotDecompiled + status: Matching - offset: 0x0e66b0 size: 484 label: _ZN6Bubble11exeHackLandEv - status: NotDecompiled + status: Matching - offset: 0x0e6894 size: 40 label: _ZNK6Bubble17calcHackerMoveDirEPN4sead7Vector3IfEERKS2_ - status: NotDecompiled + status: Matching - offset: 0x0e68bc size: 676 label: _ZN6Bubble17exeHackInLauncherEv - status: NotDecompiled + status: Matching - offset: 0x0e6b60 size: 76 label: _ZN6Bubble17endHackInLauncherEv - status: NotDecompiled + status: Matching - offset: 0x0e6bac size: 1060 label: _ZN6Bubble15exeHackResetPosEv - status: NotDecompiled + status: Matching - offset: 0x0e6fd0 size: 4 label: _ZN6Bubble11exeHackDemoEv - status: NotDecompiled + status: Matching - offset: 0x0e6fd4 size: 192 label: _ZN6Bubble9exeReviveEv - status: NotDecompiled + status: Matching - offset: 0x0e7094 size: 68 label: _ZNK6Bubble15isWaitingLaunchEv - status: NotDecompiled + status: Matching - offset: 0x0e70d8 size: 116 label: _ZN6Bubble6launchERKN4sead7Vector3IfEEfPN2al12CameraTicketE - status: NotDecompiled + status: Matching - offset: 0x0e714c size: 272 label: _ZN6Bubble12launchCancelERKN4sead7Vector3IfEE - status: NotDecompiled + status: Matching - offset: 0x0e725c size: 8 label: _ZN6Bubble11showHackCapEv - status: NotDecompiled + status: Matching - offset: 0x0e7264 size: 8 label: _ZN6Bubble11hideHackCapEv - status: NotDecompiled + status: Matching - offset: 0x0e726c size: 88 label: _ZNK6Bubble13isTriggerJumpEv - status: NotDecompiled + status: Matching - offset: 0x0e72c4 size: 32 label: _ZNK6Bubble15isTriggerActionEv - status: NotDecompiled + status: Matching - offset: 0x0e72e4 size: 24 label: _ZNK6Bubble19isTriggerHackActionEv - status: NotDecompiled + status: Matching - offset: 0x0e72fc size: 16 label: _ZNK6Bubble15isTriggerCancelEv - status: NotDecompiled + status: Matching - offset: 0x0e730c size: 112 label: _ZN6Bubble13startJumpAnimEf - status: NotDecompiled + status: Matching - offset: 0x0e737c size: 144 label: _ZN6Bubble14resetAndAppearERKN4sead7Vector3IfEERKNS0_4QuatIfEEf - status: NotDecompiled + status: Matching - offset: 0x0e740c size: 572 label: _ZN6Bubble13endHackCommonEv - status: NotDecompiled + status: Matching - offset: 0x0e7648 size: 216 label: _ZNK6Bubble13calcLaunchPosEPN4sead7Vector3IfEERKS2_ff - status: NotDecompiled + status: Matching - offset: 0x0e7720 size: 52 label: _ZN6Bubble15onGroupClippingEv - status: NotDecompiled + status: Matching - offset: 0x0e7754 size: 60 label: _ZNK6Bubble20isOnGroundNoVelocityEv - status: NotDecompiled + status: Matching - offset: 0x0e7790 size: 352 label: _ZN6Bubble24updateCollisionPartsMoveEv - status: NotDecompiled + status: Matching - offset: 0x0e78f0 size: 1412 label: _ZN6Bubble10accelStickEv - status: NotDecompiled + status: NonMatchingMajor - offset: 0x0e7e74 size: 48 label: _ZN6Bubble22addHackActorAccelStickEPN4sead7Vector3IfEEfRKS2_ - status: NotDecompiled + status: Matching - offset: 0x0e7ea4 size: 128 label: _ZNK6Bubble19isGroundOverTheWaveEbRKN4sead7Vector3IfEE - status: NotDecompiled + status: Matching - offset: 0x0e7f24 size: 136 label: _ZNK6Bubble23isEnableSnapWaveSurfaceEv - status: NotDecompiled + status: Matching - offset: 0x0e7fac size: 4 label: _ZNK12_GLOBAL__N_116BubbleNrvStandBy7executeEPN2al11NerveKeeperE - status: NotDecompiled + status: Matching guess: true - offset: 0x0e7fb0 size: 8 label: _ZNK12_GLOBAL__N_123BubbleNrvHackInLauncher7executeEPN2al11NerveKeeperE - status: NotDecompiled + status: Matching guess: true - offset: 0x0e7fb8 size: 76 label: _ZNK12_GLOBAL__N_123BubbleNrvHackInLauncher12executeOnEndEPN2al11NerveKeeperE - status: NotDecompiled + status: Matching guess: true - offset: 0x0e8004 size: 8 label: _ZNK12_GLOBAL__N_115BubbleNrvRevive7executeEPN2al11NerveKeeperE - status: NotDecompiled + status: Matching guess: true - offset: 0x0e800c size: 8 label: _ZNK12_GLOBAL__N_121BubbleNrvWaitHackFall7executeEPN2al11NerveKeeperE - status: NotDecompiled + status: Matching guess: true - offset: 0x0e8014 size: 72 label: _ZNK12_GLOBAL__N_114BubbleNrvDelay7executeEPN2al11NerveKeeperE - status: NotDecompiled + status: Matching guess: true - offset: 0x0e805c size: 8 label: _ZNK12_GLOBAL__N_111BubbleNrvUp7executeEPN2al11NerveKeeperE - status: NotDecompiled + status: Matching guess: true - offset: 0x0e8064 size: 80 label: _ZNK12_GLOBAL__N_113BubbleNrvTurn7executeEPN2al11NerveKeeperE - status: NotDecompiled + status: Matching guess: true - offset: 0x0e80b4 size: 8 label: _ZNK12_GLOBAL__N_113BubbleNrvDown7executeEPN2al11NerveKeeperE - status: NotDecompiled + status: Matching guess: true - offset: 0x0e80bc size: 56 label: _ZNK12_GLOBAL__N_112BubbleNrvDie7executeEPN2al11NerveKeeperE - status: NotDecompiled + status: Matching guess: true - offset: 0x0e80f4 size: 8 label: _ZNK12_GLOBAL__N_117BubbleNrvHackJump7executeEPN2al11NerveKeeperE - status: NotDecompiled + status: Matching guess: true - offset: 0x0e80fc size: 12 label: _ZNK12_GLOBAL__N_117BubbleNrvHackJump12executeOnEndEPN2al11NerveKeeperE - status: NotDecompiled + status: Matching guess: true - offset: 0x0e8108 size: 8 label: _ZNK12_GLOBAL__N_121BubbleNrvHackJumpHigh7executeEPN2al11NerveKeeperE - status: NotDecompiled + status: Matching guess: true - offset: 0x0e8110 size: 12 label: _ZNK12_GLOBAL__N_121BubbleNrvHackJumpHigh12executeOnEndEPN2al11NerveKeeperE - status: NotDecompiled + status: Matching guess: true - offset: 0x0e811c size: 8 label: _ZNK12_GLOBAL__N_123BubbleNrvHackCancelJump7executeEPN2al11NerveKeeperE - status: NotDecompiled + status: Matching guess: true - offset: 0x0e8124 size: 12 label: _ZNK12_GLOBAL__N_123BubbleNrvHackCancelJump12executeOnEndEPN2al11NerveKeeperE - status: NotDecompiled + status: Matching guess: true - offset: 0x0e8130 size: 104 label: _ZNK12_GLOBAL__N_117BubbleNrvHackFall7executeEPN2al11NerveKeeperE - status: NotDecompiled + status: Matching guess: true - offset: 0x0e8198 size: 8 label: _ZNK12_GLOBAL__N_121BubbleNrvHackResetPos7executeEPN2al11NerveKeeperE - status: NotDecompiled + status: Matching guess: true - offset: 0x0e81a0 size: 4 label: _ZNK12_GLOBAL__N_117BubbleNrvHackDemo7executeEPN2al11NerveKeeperE - status: NotDecompiled + status: Matching guess: true - offset: 0x0e81a4 size: 8 label: _ZNK12_GLOBAL__N_117BubbleNrvHackMove7executeEPN2al11NerveKeeperE - status: NotDecompiled + status: Matching guess: true - offset: 0x0e81ac size: 12 label: _ZNK12_GLOBAL__N_117BubbleNrvHackMove12executeOnEndEPN2al11NerveKeeperE - status: NotDecompiled + status: Matching guess: true - offset: 0x0e81b8 size: 8 label: _ZNK12_GLOBAL__N_117BubbleNrvWaitHack7executeEPN2al11NerveKeeperE - status: NotDecompiled + status: Matching guess: true - offset: 0x0e81c0 size: 8 label: _ZNK12_GLOBAL__N_122BubbleNrvWaitHackStart7executeEPN2al11NerveKeeperE - status: NotDecompiled + status: Matching guess: true - offset: 0x0e81c8 size: 12 label: _ZNK12_GLOBAL__N_122BubbleNrvWaitHackStart12executeOnEndEPN2al11NerveKeeperE - status: NotDecompiled + status: Matching guess: true - offset: 0x0e81d4 size: 8 label: _ZNK12_GLOBAL__N_114BubbleNrvReady7executeEPN2al11NerveKeeperE - status: NotDecompiled + status: Matching guess: true - offset: 0x0e81dc size: 8 label: _ZNK12_GLOBAL__N_113BubbleNrvWait7executeEPN2al11NerveKeeperE - status: NotDecompiled + status: Matching guess: true - offset: 0x0e81e4 size: 8 label: _ZNK12_GLOBAL__N_113BubbleNrvSink7executeEPN2al11NerveKeeperE - status: NotDecompiled + status: Matching guess: true - offset: 0x0e81ec size: 8 label: _ZNK12_GLOBAL__N_117BubbleNrvHackLand7executeEPN2al11NerveKeeperE - status: NotDecompiled + status: Matching guess: true - offset: 0x0e81f4 size: 8 label: _ZNK12_GLOBAL__N_117BubbleNrvRailMove7executeEPN2al11NerveKeeperE - status: NotDecompiled + status: Matching guess: true - offset: 0x0e81fc size: 28 label: _ZNK2al10FunctorV0MIP6BubbleMS1_FvvEEclEv - status: NotDecompiled + status: Matching lazy: true - offset: 0x0e8218 size: 76 label: _ZNK2al10FunctorV0MIP6BubbleMS1_FvvEE5cloneEv - status: NotDecompiled + status: Matching lazy: true - offset: 0x0e8264 size: 4 label: _ZN2al10FunctorV0MIP6BubbleMS1_FvvEED0Ev - status: NotDecompiled + status: Matching lazy: true - offset: 0x0e8268 size: 28 label: _ZNK2al23TriangleFilterDelegatorI6BubbleE17isInvalidTriangleERKNS_8TriangleE - status: NotDecompiled + status: Matching lazy: true - offset: 0x0e8284 size: 176 - label: '' - status: NotDecompiled + label: _GLOBAL__sub_I_Bubble.cpp + status: Matching Enemy/Bubble2D.o: '.text': - offset: 0x0e8334 @@ -133715,7 +133715,7 @@ Scene/ProjectActorFactory.o: - offset: 0x4b68b0 size: 52 label: _ZN2al19createActorFunctionI6BubbleEEPNS_9LiveActorEPKc - status: NotDecompiled + status: Matching lazy: true - offset: 0x4b68e4 size: 52 diff --git a/src/Enemy/Bubble.cpp b/src/Enemy/Bubble.cpp new file mode 100644 index 00000000..0b49dfba --- /dev/null +++ b/src/Enemy/Bubble.cpp @@ -0,0 +1,2402 @@ +#include "Enemy/Bubble.h" + +#include "Library/Bgm/BgmLineFunction.h" +#include "Library/Camera/CameraUtil.h" +#include "Library/Collision/Collider.h" +#include "Library/Collision/CollisionParts.h" +#include "Library/Collision/CollisionPartsKeeperUtil.h" +#include "Library/Collision/KCollisionServer.h" +#include "Library/Collision/PartsInterpolator.h" +#include "Library/Effect/EffectSystemInfo.h" +#include "Library/Joint/JointSpringControllerHolder.h" +#include "Library/LiveActor/ActorActionFunction.h" +#include "Library/LiveActor/ActorAnimFunction.h" +#include "Library/LiveActor/ActorAreaFunction.h" +#include "Library/LiveActor/ActorClippingFunction.h" +#include "Library/LiveActor/ActorCollisionFunction.h" +#include "Library/LiveActor/ActorFlagFunction.h" +#include "Library/LiveActor/ActorInitUtil.h" +#include "Library/LiveActor/ActorModelFunction.h" +#include "Library/LiveActor/ActorMovementFunction.h" +#include "Library/LiveActor/ActorPoseUtil.h" +#include "Library/LiveActor/ActorSensorUtil.h" +#include "Library/Math/MathUtil.h" +#include "Library/Movement/AnimScaleController.h" +#include "Library/Nature/NatureUtil.h" +#include "Library/Nerve/NerveSetupUtil.h" +#include "Library/Nerve/NerveUtil.h" +#include "Library/Placement/PlacementFunction.h" +#include "Library/Placement/PlacementInfo.h" +#include "Library/Play/Camera/ActorMatrixCameraTarget.h" +#include "Library/Rail/RailUtil.h" +#include "Library/Shadow/ActorShadowUtil.h" +#include "Library/Stage/StageSwitchUtil.h" +#include "Library/Thread/FunctorV0M.h" + +#include "Enemy/BubbleStateInLauncher.h" +#include "Enemy/DisregardReceiver.h" +#include "Player/IUsePlayerHack.h" +#include "Player/PlayerHackStartShaderCtrl.h" +#include "Util/CameraUtil.h" +#include "Util/CollisionUtil.h" +#include "Util/Hack.h" +#include "Util/InputInterruptTutorialUtil.h" +#include "Util/PlayerUtil.h" +#include "Util/SensorMsgFunction.h" +#include "Util/ShadowUtil.h" + +namespace { +NERVE_IMPL(Bubble, StandBy) +NERVE_IMPL(Bubble, Delay) +NERVE_IMPL(Bubble, RailMove) +NERVE_IMPL(Bubble, Ready) +NERVE_IMPL(Bubble, Up) +NERVE_IMPL(Bubble, Turn) +NERVE_IMPL(Bubble, Down) +NERVE_IMPL(Bubble, Sink) +NERVE_IMPL(Bubble, Wait) +NERVE_IMPL(Bubble, Die) +NERVE_IMPL(Bubble, WaitHack) +NERVE_END_IMPL(Bubble, WaitHackStart) +NERVE_IMPL(Bubble, WaitHackFall) +NERVE_IMPL(Bubble, HackFall) +NERVE_END_IMPL(Bubble, HackMove) +NERVE_END_IMPL(Bubble, HackJump) +// NOTE: this jump is actually not as high as `HackJump` - it's just triggered by motion. +NERVE_END_IMPL_(Bubble, HackJumpHigh, HackJump) +NERVE_END_IMPL_(Bubble, HackCancelJump, HackJump) +NERVE_IMPL(Bubble, HackLand) +NERVE_END_IMPL(Bubble, HackInLauncher) +NERVE_IMPL(Bubble, HackResetPos) +NERVE_IMPL(Bubble, HackDemo) +NERVE_IMPL(Bubble, Revive) + +NERVES_MAKE_STRUCT(Bubble, StandBy, HackInLauncher, Revive, WaitHackFall, Delay, Up, Turn, Down, + Die, HackJump, HackJumpHigh, HackCancelJump, HackFall, HackResetPos, HackDemo, + HackMove, WaitHack, WaitHackStart, Ready, Wait, Sink, HackLand, RailMove) +} // namespace + +static al::AnimScaleParam gAnimScaleParam(0.15f, 0.87f, 0.6f, 1.3f, -0.4f, 0.3f, 0.9f, 20, 0.25f, + 0.9f, 5.2f, 0.05f); +static PlayerHackStartShaderParam gPlayerHackStartShaderParam(true, 200.0f, 10, 20); + +Bubble::Bubble(const char* name) + : al::LiveActor(name), mAnimScaleController(new al::AnimScaleController(&gAnimScaleParam)) {} + +void Bubble::init(const al::ActorInitInfo& initInfo) { + using BubbleFunctor = al::FunctorV0M; + + al::initActorWithArchiveName(this, initInfo, "Bubble", nullptr); + + sead::Vector3f moveNext; + if (al::tryGetLinksTrans(&moveNext, initInfo, "MoveNext")) { + mJumpHeight = sead::Mathf::abs(moveNext.y - al::getTrans(this).y); + if (moveNext.y < al::getTrans(this).y) + al::getTransPtr(this)->set(moveNext); + } else { + mJumpHeight = 800.0f; + } + + if (al::isExistRail(this)) { + al::setSyncRailToStart(this); + mRailTotalLength = al::getRailTotalLength(this); + + if (!al::tryGetArg(&mRailMoveFrame, initInfo, "RailMoveFrame") || mRailMoveFrame < 1) { + f32 railMoveSpeed = 10.0f; + al::tryGetArg(&railMoveSpeed, initInfo, "RailMoveSpeed"); + if (railMoveSpeed <= 0.0f) + railMoveSpeed = 10.0f; + mRailMoveFrame = sead::Mathf::ceil(mRailTotalLength / railMoveSpeed); + } + } + + mStartActionPosition = al::getTrans(this); + mStartingRotation.set(al::getQuat(this)); + mRevivePosition.set(al::getTrans(this)); + mReviveStartActionPosition.set(mStartActionPosition); + mTurnFrameDelay = mJumpHeight / 13.0f; + recalcClippingInfo(); + + al::tryGetArg(&mWaitFrameNum, initInfo, "WaitFrameNum"); + if (mWaitFrameNum < 0) + mWaitFrameNum = 0; + + al::tryGetArg(&mDelayFrameNum, initInfo, "DelayFrameNum"); + if (mDelayFrameNum < 0) + mDelayFrameNum = 0; + + al::tryGetArg(&mIsWaveCheckOn, initInfo, "IsWaveCheckOn"); + mReviveDelayTime = mTurnFrameDelay * 2 + mWaitFrameNum + 80; + + if (al::isExistRail(this)) { + f32 travelTime = mRailMoveFrame + mReviveDelayTime + 1; + mReviveDelayTime = travelTime * 2; + } + + f32 shootDegree; + al::tryGetArg(&shootDegree, initInfo, "ShootDegree"); + al::initNerve(this, &NrvBubble.StandBy, 1); + mBubbleStateInLauncher = new BubbleStateInLauncher(this); + al::initNerveState(this, mBubbleStateInLauncher, &NrvBubble.HackInLauncher, "バブルランチャー"); + al::hideModel(this); + + if (!al::listenStageSwitchOnStart(this, BubbleFunctor(this, &Bubble::start))) + start(); + + al::listenStageSwitchOnKill(this, BubbleFunctor(this, &Bubble::killAll)); + mCapTargetInfo = rs::createCapTargetInfo(this, nullptr); + + bool stageStartHack = false; + al::tryGetArg(&stageStartHack, initInfo, "StageStartHack"); + if (!stageStartHack) { + al::PlacementInfo placementInfo; + if (al::tryGetLinksInfo(&placementInfo, initInfo, "MoveNext")) + al::tryGetArg(&stageStartHack, placementInfo, "StageStartHack"); + } + + if (stageStartHack) + rs::requestStageStartHack(this, al::getHitSensor(this, "Cap"), mCapTargetInfo, nullptr); + + mJointSpringControllerHolder = + al::JointSpringControllerHolder::tryCreateAndInitJointControllerKeeper(this, "Dynamics"); + mJointSpringControllerHolder->offControlAll(); + al::setEffectNamedMtxPtr(this, "LavaSurface", &mLavaSurfaceMtx); + makeActorAlive(); + al::offCollide(this); + mActorMatrixCameraTarget = al::createActorMatrixCameraTarget(this, &mCameraMtx); + + al::hideSilhouetteModelIfShow(this); + rs::initHackShadow(this); + + auto* triangleFilter = new al::TriangleFilterDelegator(this, &Bubble::isIgnoreTriangle); + al::setColliderFilterTriangle(this, triangleFilter); + al::setColliderReactMovePower(this, false); + + al::invalidateColliderRobustCheck(this); + mCollisionPartsFilter = new al::CollisionPartsFilterSpecialPurpose("Bubble"); + + al::setColliderFilterCollisionParts(this, mCollisionPartsFilter); + mClippingProbeActor = new al::LiveActor("グループ同期"); + + al::initActorWithArchiveName(mClippingProbeActor, initInfo, "Bubble", "Probe"); + + al::setClippingInfo(mClippingProbeActor, al::getClippingRadius(this), &mClippingPos); + mClippingProbeActor->makeActorAlive(); + + if (al::isExistRail(this) && al::isExistLinkChild(initInfo, "TurnTarget", 0)) { + sead::Vector3f trans; + al::tryGetLinksQT(&mRailTargetRotation, &trans, initInfo, "TurnTarget"); + } else { + mRailTargetRotation.set(mStartingRotation); + } + + mShadowMaskOffset.set(al::getShadowMaskOffset(this, "body")); + mShadowMaskDropLength = al::getShadowMaskDropLength(this, "body"); + mDisregardReceiver = new DisregardReceiver(this, nullptr); + mPlayerHackStartShaderCtrl = new PlayerHackStartShaderCtrl(this, &gPlayerHackStartShaderParam); + + al::startAction(this, "Wait"); + al::startMtsAnim(this, "Scroll"); +} + +void Bubble::initAfterPlacement() { + checkEffectPos(); + if (!isValidCollisionOrWaveCheck()) + al::tryUpdateEffectMaterialCode(this, mMaterialCode); +} + +void Bubble::attackSensor(al::HitSensor* self, al::HitSensor* other) { + if (al::isNerve(this, &NrvBubble.Die) || al::isNerve(this, &NrvBubble.Revive)) + return; + + if (al::isSensorEnemyBody(self)) { + if (isHack() && mPlayerHack) + rs::sendMsgHackerNoReaction(mPlayerHack, other, self); + return; + } + + if (!al::isSensorEnemyAttack(self)) { + if (al::isSensorName(self, "Cap") || al::isSensorName(self, "CapAbove") || + al::isSensorName(self, "CapBelow")) { + return; + } + + if (al::isSensorName(self, "PechoAttack")) + rs::sendMsgBubbleAttackToPecho(other, self); + return; + } + + if (!al::isNerve(this, &NrvBubble.HackJump) && !al::isNerve(this, &NrvBubble.HackJumpHigh) && + !al::isNerve(this, &NrvBubble.HackCancelJump) && !al::isNerve(this, &NrvBubble.HackFall) && + rs::sendMsgBubbleReflectH(other, self)) { + sead::Vector3f distance = al::getSensorPos(self) - al::getSensorPos(other); + distance.y = 0.0f; + al::tryNormalizeOrZero(&distance); + f32 travel = al::getVelocity(this).dot(distance); + if (travel < 0.0f) + al::addVelocity(this, -distance * travel); + al::addVelocity(this, distance * 15.0f); + mAnimScaleController->startHitReaction(); + return; + } + + if (isHack() || al::isNerve(this, &NrvBubble.WaitHackFall)) { + if (rs::sendMsgBubbleAttack(other, self)) + return; + + if (al::sendMsgEnemyAttackFire(other, self, "LavaRed")) + return; + + if (al::isNerve(this, &NrvBubble.HackFall) || + al::isNerve(this, &NrvBubble.HackCancelJump) || + al::isNerve(this, &NrvBubble.HackJump) || al::isNerve(this, &NrvBubble.HackJumpHigh)) { + if (al::getVelocity(this).y > 0.0f) { + if (!al::isExistCollisionParts(al::getSensorHost(other)) && + al::sendMsgEnemyUpperPunch(other, self)) { + al::setVelocityZeroV(this); + } + } else if (rs::sendMsgBubbleReflectV(other, self)) { + mJumpForce.set(0.0f, 15.0f, 0.0f); + al::setVelocityZeroV(this); + mAnimScaleController->startHitReaction(); + al::setNerve(this, &NrvBubble.HackJump); + return; + } + } + } + + if (al::isNerve(this, &NrvBubble.StandBy) || al::isNerve(this, &NrvBubble.Wait) || + al::isNerve(this, &NrvBubble.Delay) || al::isNerve(this, &NrvBubble.Die) || + al::isNerve(this, &NrvBubble.RailMove) || al::isNerve(this, &NrvBubble.Ready)) { + return; + } + + if (!al::isNerve(this, &NrvBubble.WaitHackFall) && + (rs::sendMsgBubbleAttack(other, self) || + al::sendMsgEnemyAttackFire(other, self, "LavaRed") || + al::sendMsgEnemyAttack(other, self))) { + return; + } + + if (!isHack() && al::sendMsgPush(other, self)) + return; + + rs::sendMsgPushToPlayer(other, self); +} + +bool Bubble::receiveMsg(const al::SensorMsg* message, al::HitSensor* other, al::HitSensor* self) { + if (rs::tryReceiveMsgInitCapTargetAndSetCapTargetInfo(message, mCapTargetInfo)) + return true; + + if (rs::isMsgEnableMapCheckPointWarp(message)) + return isOnGround(); + + if (mDisregardReceiver->receiveMsg(message, other, self)) + return true; + + if (rs::isMsgEndInSaucePan(message)) { + mIsPlayerCaptured = false; + mIsInSaucePan = false; + return true; + } + + if (isHack()) { + if (!al::isNerve(this, &NrvBubble.HackResetPos) && + rs::tryGetBossMagmaResetPos(message, &mResetTargetPos)) { + al::setNerve(this, &NrvBubble.HackResetPos); + return true; + } + + if (rs::isMsgBossMagmaCatchPlayer(message)) { + mIsPlayerCaptured = true; + mIsInBossSequence = true; + return true; + } + + if (rs::isMsgBossMagmaReleasePlayer(message)) { + mIsPlayerCaptured = false; + return true; + } + + if (rs::isMsgBossMagmaDeadDemoStart(message)) { + al::hideModelIfShow(this); + al::hideSilhouetteModelIfShow(this); + hideHackCap(); + al::tryKillEmitterAndParticleAll(this); + al::setNerve(this, &NrvBubble.HackDemo); + return true; + } + + if (rs::isMsgBossMagmaDeadDemoEnd(message)) { + sead::Vector3f endTargetPos; + rs::tryGetBossMagmaDeadDemoEndTargetPos(message, &endTargetPos); + sead::Vector3f firePos; + sead::Vector3f fireNormal; + if (al::calcFindFireSurface(&firePos, &fireNormal, this, mResetTargetPos, + sead::Vector3f::ey, 800.0f)) { + sead::Vector3f resetPos = mResetTargetPos; + resetPos.y = firePos.y; + al::resetPosition(this, resetPos); + al::faceToTarget(this, endTargetPos); + mJumpForce.set(0.0f, 0.0f, 0.0f); + al::setVelocityZero(this); + mIsOnLavaSurface = true; + al::tryUpdateEffectMaterialCode(this, al::getFireMaterialCode(this)); + mIsInBossSequence = false; + mFireNormal.set(fireNormal); + al::setNerve(this, &NrvBubble.HackMove); + } else { + mJumpForce.set(0.0f, 0.0f, 0.0f); + al::setVelocityZero(this); + al::setNerve(this, &NrvBubble.HackJump); + } + al::showModel(this); + al::showSilhouetteModel(this); + showHackCap(); + + return true; + } + + if (rs::isMsgBossMagmaQueryToBubble(message)) + return !mIsOnLavaSurface; + } + + if (al::isSensorEnemyAttack(self)) + return false; + + if (rs::isMsgHackDirectStageInit(&mPlayerHack, message)) { + if (!al::isSensorName(self, "Cap") && !al::isSensorName(self, "CapAbove") && + !al::isSensorName(self, "CapBelow")) { + return false; + } + + al::resetPosition(this, + al::getTrans(this) + al::getColliderRadius(this) * sead::Vector3f::ey); + setupHack(); + rs::setupHackShadow(this); + al::setNerve(this, &NrvBubble.HackFall); + return true; + } + + if (rs::isMsgCapEnableLockOn(message)) { + if (!isCurrentNerveEnableLockOnAndStartHack()) + return false; + + if (!al::isSensorName(self, "Cap") && !al::isSensorName(self, "CapAbove") && + !al::isSensorName(self, "CapBelow")) { + return false; + } + + al::setNerve(this, &NrvBubble.WaitHack); + return true; + } + + if (rs::isMsgCapCancelLockOn(message)) { + if (!isCurrentNerveEnableLockOnAndStartHack()) + return false; + + if (!al::isSensorName(self, "Cap") && !al::isSensorName(self, "CapAbove") && + !al::isSensorName(self, "CapBelow")) + return false; + + al::invalidateClipping(this); + offGroupClipping(); + mJumpForce.set(0.0f, 0.0f, 0.0f); + al::onCollide(this); + mWaitReviveDelay = 5; + al::setVelocityZero(this); + al::setNerve(this, &NrvBubble.WaitHackFall); + return true; + } + + if (rs::isMsgStartHack(message)) { + if (!isCurrentNerveEnableLockOnAndStartHack()) + return false; + + if (!al::isSensorName(self, "Cap") && !al::isSensorName(self, "CapAbove") && + !al::isSensorName(self, "CapBelow")) { + return false; + } + + startHackLocal(self, other); + return true; + } + + if (rs::isMsgHackInvalidEscapeNoReaction(message)) { + if (mCancelLaunchCooldown != 0) + return true; + + if (al::isNerve(this, &NrvBubble.HackInLauncher)) + return mBubbleStateInLauncher->isWaiting(); + + return false; + } + + if (rs::isMsgHackInvalidEscape(message)) { + if (!isHack()) + return false; + + if (!al::isInAreaObj(this, "HackInvalidEscapeArea") && + !al::isNerve(this, &NrvBubble.HackResetPos)) + return false; + + return true; + } + + if (rs::isMsgCancelHack(message) || rs::isMsgCancelHackByDokan(message)) { + prepareEndHack(); + endHackByCancel(); + return true; + } + + if (rs::isMsgHackMarioCheckpointFlagWarp(message)) { + prepareEndHack(); + endHackByCancel(); + al::startVisAnim(this, "CapOff"); + al::startMtpAnim(this, "CapOff"); + return true; + } + + if (rs::isMsgHackMarioDemo(message)) { + al::tryKillEmitterAndParticleAll(this); + forceEndHack(); + return true; + } + + if (rs::isMsgHackMarioDead(message)) { + forceEndHack(); + return true; + } + + if (rs::isMsgHackSyncDamageVisibility(message)) { + if (al::isNerve(this, &NrvBubble.HackInLauncher) || al::isNerve(this, &NrvBubble.HackDemo)) + return false; + + rs::syncDamageVisibility(this, mPlayerHack); + return true; + } + + sead::Vector3f breathForce; + if (rs::tryGetBossMagmaBreathForce(message, &breathForce)) { + if (!al::isSensorName(self, "Cap")) + return false; + + sead::Vector3f force = breathForce; + if (al::tryNormalizeOrZero(&force)) { + f32 forceLength = force.dot(al::getVelocity(this)); + if (breathForce.length() > forceLength) + al::addVelocity(this, breathForce * 0.05f); + } + return true; + } + + if (rs::isMsgBubbleLauncherStart(message)) { + if (!al::isSensorName(self, "Body")) + return false; + + if (al::isNerve(this, &NrvBubble.HackJump) || al::isNerve(this, &NrvBubble.HackJumpHigh) || + al::isNerve(this, &NrvBubble.HackCancelJump)) { + al::setNerve(this, &NrvBubble.HackInLauncher); + return true; + } + + if (al::isNerve(this, &NrvBubble.HackInLauncher) && mPlayerHack) { + al::setNerve(this, &NrvBubble.HackInLauncher); + return true; + } + return false; + } + + if (isHack()) { + if (!al::isNerve(this, &NrvBubble.HackInLauncher) && + !al::isNerve(this, &NrvBubble.WaitHackStart) && + !al::isNerve(this, &NrvBubble.HackResetPos)) { + if (rs::tryReceiveMsgPushToPlayerAndAddVelocityH(this, message, other, self, 3.0f)) + return true; + + if (rs::isMsgBubbleAttack(message)) { + al::pushAndAddVelocityH(this, other, self, 3.0f); + return true; + } + } + + if (rs::isMsgHackerDamageAndCancel(message)) { + if (rs::isMsgFireDamageAll(message)) + return false; + + if (rs::isMsgEnemyAttackFireCollision(message)) + return false; + + if (al::isMsgEnemyAttack(message)) { + if (rs::sendMsgBubbleAttackToPecho(other, self)) + return false; + + if (rs::sendMsgBubbleAttack(other, self)) + return false; + } + return rs::requestDamage(mPlayerHack); + } + return false; + } + + if (!al::isNoCollide(this) && + al::tryReceiveMsgPushAndAddVelocityH(this, message, other, self, 3.0f)) + return true; + + if (rs::isMsgKillByShineGet(message) || rs::isMsgKillByHomeDemo(message) || + rs::isMsgKillByMoonRockDemo(message)) { + al::tryKillEmitterAndParticleAll(this); + revive(); + return true; + } + + if (rs::isMsgHammerBrosHammerHackAttack(message) || + rs::isMsgHammerBrosHammerEnemyAttack(message)) { + if (!al::isNerve(this, &NrvBubble.Up) && !al::isNerve(this, &NrvBubble.Turn) && + !al::isNerve(this, &NrvBubble.Down)) + return false; + + rs::requestHitReactionToAttacker(message, self, other); + return true; + } + return false; +} + +inline s32 getSurfaceProbeIndex(Bubble* bubble) { + s32 index = 0; + if (al::isExistRail(bubble) && !al::isRailGoingToEnd(bubble)) + index = 1; + return index; +} + +void Bubble::control() { + mDisregardReceiver->set44(al::getSensorPos(this, "Body") + + al::getSensorRadius(this, "Body") * sead::Vector3f::ey); + + if (isValidCollisionOrWaveCheck()) { + if (mIsOnLavaSurface) { + mLavaSurfaceMtx.makeQT(al::getQuat(this), {0.0f, 0.0f, 0.0f}); + mLavaSurfaceMtx.setTranslation(al::getTrans(this)); + } else if (al::isOnGround(this, 0)) { + mLavaSurfaceMtx.makeQT(al::getQuat(this), {0.0f, 0.0f, 0.0f}); + + sead::Vector3f trans = al::getTrans(this); + trans.y = al::getCollidedGroundPos(this).y; + mLavaSurfaceMtx.setTranslation(trans); + } + } else { + mLavaSurfaceMtx.makeQT(al::getQuat(this), {0.0f, 0.0f, 0.0f}); + getSurfaceProbeIndex(this); + al::getTrans(this); + mLavaSurfaceMtx.setTranslation(al::getTrans(this)); + } + + if (!isHack() && !al::isNerve(this, &NrvBubble.Revive) && + (al::isInWater(this) || al::isInDeathArea(this))) { + al::setVelocityZero(this); + al::startHitReaction(this, "死亡"); + revive(); + } + + if (mCancelLaunchCooldown != 0) + mCancelLaunchCooldown--; + + if (isHack() && !al::isActiveCameraTarget(mActorMatrixCameraTarget)) + al::setCameraTarget(this, mActorMatrixCameraTarget); + + if (isHack() && !isWaitingLaunch() && !al::isHideModel(this)) + al::showSilhouetteModelIfHide(this); + else + al::hideSilhouetteModelIfShow(this); + + if (isHack() || al::isNerve(this, &NrvBubble.WaitHackFall)) + updateLavaWave(); + else if (!isValidCollisionOrWaveCheck()) + mIsOnLavaSurface = false; + + if (isHack()) { + if (!al::isVisAnimPlaying(this, "CapOn")) + al::startVisAnim(this, "CapOn"); + if (!al::isMtpAnimPlaying(this, "CapOn")) + al::startMtpAnim(this, "CapOn"); + } else { + if (!al::isVisAnimPlaying(this, "CapOff")) + al::startVisAnim(this, "CapOff"); + if (!al::isMtpAnimPlaying(this, "CapOff")) + al::startMtpAnim(this, "CapOff"); + } + + updateScrollAnimRate(); + mAnimScaleController->update(); + al::setScale(this, mAnimScaleController->getScale()); + + if (isHack() && isOnGround()) + mIsHackOnGround = true; + else + mIsHackOnGround = false; + + if (!al::isNerve(this, &NrvBubble.StandBy) && !al::isNerve(this, &NrvBubble.Delay) && + (!isHack() || !rs::isActiveHackStartDemo(mPlayerHack)) && + !al::isClipped(mClippingProbeActor) && !mIsClipped) { + mReviveDelayCount = al::modi(mReviveDelayCount + 1 + mReviveDelayTime, mReviveDelayTime); + } + + mIsClipped = al::isClipped(mClippingProbeActor); + if (!isHack()) + al::scaleVelocityHV(this, 0.9f, 1.0f); + mCameraMtx.makeQT(mGroundRotation, {0.0f, 0.0f, 0.0f}); + + mCameraMtx.setTranslation(al::getTrans(this) + (0.0f * sead::Vector3f::ey)); + if (al::isNerve(this, &NrvBubble.Up) || al::isNerve(this, &NrvBubble.Turn) || + al::isNerve(this, &NrvBubble.Down)) { + mDisregardReceiver->setEDC(false, false, false); + } else { + mDisregardReceiver->setEDC(false, false, true); + } + + if (mHackTurnFrame < mHackTurnDelay) + mHackTurnFrame++; +} + +void Bubble::updateCollider() { + if (al::isNoCollide(this)) { + al::getTransPtr(this)->add(al::getVelocity(this)); + getCollider()->onInvalidate(); + } else { + if (isHack()) + rs::solveCollisionInHacking(this, al::getVelocity(this) + mColliderGroundOffset); + else + al::getTransPtr(this)->add( + getCollider()->collide(al::getVelocity(this) + mColliderGroundOffset)); + } + + mColliderGroundOffset.set(0.0f, 0.0f, 0.0f); +} + +void Bubble::appear() { + al::LiveActor::appear(); + al::setNerve(this, &NrvBubble.StandBy); + al::startVisAnim(this, "CapOff"); + + if (!al::isValidSwitchStart(this)) + start(); +} + +void Bubble::calcAnim() { + al::LiveActor::calcAnim(); + + sead::Matrix34f mtx; + mtx.setInverse(*getBaseMtx()); + sead::Vector3f offset = mShadowMaskOffset; + offset.rotate(mtx); + al::setShadowMaskOffset(this, offset, "body"); +} + +void Bubble::recalcClippingInfo() { + sead::Vector3f max = mStartActionPosition; + sead::Vector3f min = mStartActionPosition; + al::updateBoundingBox(mJumpHeight * sead::Vector3f::ey + mStartActionPosition, &max, &min); + + if (al::isExistRail(this)) { + s32 pointNum = al::getRailPointNum(this); + for (s32 i = 0; i < pointNum; i++) { + sead::Vector3f pos; + al::calcRailPointPos(&pos, this, i); + al::updateBoundingBox(pos, &max, &min); + } + + f32 prevRailCoord = al::calcRailCoordByPoint(this, 0); + for (s32 i = 1; i < pointNum; i++) { + f32 currentRailCoord = al::calcRailCoordByPoint(this, i); + sead::Vector3f pos; + al::calcRailPosAtCoord(&pos, this, (prevRailCoord + currentRailCoord) * 0.5f); + al::updateBoundingBox(pos, &max, &min); + prevRailCoord = currentRailCoord; + } + } + + mClippingPos = (max + min) * 0.5f; + al::setClippingInfo(this, (max - min).length() * 0.5f + 200.0f, &mClippingPos); +} + +void Bubble::start() { + if (!al::isNerve(this, &NrvBubble.StandBy)) + return; + + al::setTrans(this, mStartActionPosition); + al::hideModelIfShow(this); + mReviveDelayCount = 0; + + if (mDelayFrameNum == 0) + al::setNerve(this, &NrvBubble.Ready); + else + al::setNerve(this, &NrvBubble.Delay); +} + +void Bubble::killAll() { + al::setNerve(this, &NrvBubble.Die); +} + +bool Bubble::isIgnoreTriangle(const al::Triangle& triangle) const { + return al::isFloorCode(triangle, "Fence"); +} + +void Bubble::checkEffectPos() { + if (!mIsCheckEffectPosRequired || al::isNerve(this, &NrvBubble.StandBy) || mIsWaveCheckOn) + return; + + al::Triangle triangle; + if (!alCollisionUtil::getFirstPolyOnArrow( + this, mSurfaceProbePos, &triangle, al::getTrans(this) + 100.0f * sead::Vector3f::ey, + -200.0f * sead::Vector3f::ey, mCollisionPartsFilter, nullptr)) { + return; + } + + mMaterialCode = al::getMaterialCodeName(triangle); + if (!al::isExistRail(this)) { + mIsCheckEffectPosRequired = false; + return; + } + + sead::Vector3f railPos; + al::calcRailPosAtCoord(&railPos, this, al::getRailTotalLength(this)); + + // BUG: missing `mCollisionPartsFilter` as parameter + if (alCollisionUtil::getFirstPolyOnArrow(this, &mSurfaceProbePos[1], nullptr, + railPos + 100.0f * sead::Vector3f::ey, + -200.0f * sead::Vector3f::ey, nullptr, nullptr)) { + mIsCheckEffectPosRequired = false; + } +} + +bool Bubble::isValidCollisionOrWaveCheck() const { + return !al::isNoCollide(this) || mIsWaveCheckOn; +} + +bool Bubble::isHack() const { + return (al::isNerve(this, &NrvBubble.WaitHackStart) && + rs::isHackStartDemoEnterMario(mPlayerHack)) || + al::isNerve(this, &NrvBubble.HackFall) || al::isNerve(this, &NrvBubble.HackMove) || + al::isNerve(this, &NrvBubble.HackJump) || al::isNerve(this, &NrvBubble.HackJumpHigh) || + al::isNerve(this, &NrvBubble.HackCancelJump) || al::isNerve(this, &NrvBubble.HackLand) || + al::isNerve(this, &NrvBubble.HackResetPos) || al::isNerve(this, &NrvBubble.HackDemo) || + (al::isNerve(this, &NrvBubble.HackInLauncher) && mPlayerHack); +} + +void Bubble::revive() { + if (al::isNerve(this, &NrvBubble.Revive)) + return; + + if (al::isExistRail(this)) { + al::setRailPosToStart(this); + if (!al::isRailGoingToEnd(this)) + al::reverseRail(this); + } + + al::offCollide(this); + al::validateClipping(this); + onGroupClipping(); + al::hideModelIfShow(this); + al::setVelocityZero(this); + al::setNerve(this, &NrvBubble.Revive); +} + +// Hack to make updateLavaWave match +inline void x1(sead::Vector3f* p, const sead::Vector3f& v) { + __asm(""); + p->set(v); +} + +void Bubble::updateLavaWave() { + sead::Vector3f negColliderBottomOffset = + (al::getColliderRadius(this) - al::getColliderOffsetY(this)) * sead::Vector3f::ey; + + sead::Vector3f colliderPos = al::getTrans(this) - negColliderBottomOffset; + sead::Vector3f firePos = colliderPos - 200.0f * sead::Vector3f::ey; + + bool isInFire = al::isInFirePos(this, firePos); + + sead::Vector3f fireNormal; + al::calcFindFireSurface(&firePos, &fireNormal, this, colliderPos, sead::Vector3f::ey, 200.0f); + firePos.x = colliderPos.x; + firePos.z = colliderPos.z; + + if (al::isOnGround(this, 0)) { + if (!isInFire || firePos.y < al::getTrans(this).y - al::getColliderRadius(this) + + al::getColliderOffsetY(this)) { + mIsOnLavaSurface = false; + return; + } + } + + // BUG: 'firePos + negColliderBottomOffset' should be 'firePos - negColliderBottomOffset + if (isEnableSnapWaveSurface()) { + if (isInFire) { + x1(al::getTransPtr(this), firePos + negColliderBottomOffset); + mFireNormal.set(fireNormal); + al::tryUpdateEffectMaterialCode(this, al::getFireMaterialCode(this)); + return; + } + } else if (isInFire && firePos.y >= colliderPos.y) { + x1(al::getTransPtr(this), firePos + negColliderBottomOffset); + + mIsOnLavaSurface = true; + mFireNormal.set(fireNormal); + al::tryUpdateEffectMaterialCode(this, al::getFireMaterialCode(this)); + return; + } + + mIsOnLavaSurface = false; +} + +void Bubble::updateScrollAnimRate() { + if (al::isActionPlaying(this, mCurrentActionName)) + return; + + mCurrentActionName = al::getActionName(this); + + if (isHack() && + (al::isActionPlaying(this, "BubbleCannonJump") || + al::isActionPlaying(this, "HackHighJump") || al::isActionPlaying(this, "HackWaitSpeedy") || + al::isActionPlaying(this, "MoveSpeedy"))) { + al::setMtsAnimFrameRate(this, 2.0f); + } else + al::setMtsAnimFrameRate(this, 1.0f); +} + +bool Bubble::isOnGround() const { + return al::isOnGround(this, 0) || mIsOnLavaSurface; +} + +void Bubble::setupHack() { + if (!isValidCollisionOrWaveCheck()) { + f32 probePos = mSurfaceProbePos[getSurfaceProbeIndex(this)].y + al::getColliderRadius(this); + if (al::getTrans(this).y < probePos) + al::getTransPtr(this)->y = probePos; + } + + al::invalidateClipping(this); + offGroupClipping(); + mJumpForce = {0.0f, 0.0f, 0.0f}; + al::onCollide(this); + mWaitReviveDelay = 5; + al::setVelocityZero(this); + mIsPlayerCaptured = false; + mJointSpringControllerHolder->onControlAll(); +} + +bool Bubble::isCurrentNerveEnableLockOnAndStartHack() const { + if (al::isHideModel(this)) + return false; + + if (al::isNerve(this, &NrvBubble.Up)) { + if (al::getTrans(this).y - mStartActionPosition.y > al::getColliderRadius(this)) + return true; + } + + if (al::isNerve(this, &NrvBubble.Turn)) + return true; + + if (al::isNerve(this, &NrvBubble.Down) && + al::getTrans(this).y - mStartActionPosition.y > al::getColliderRadius(this)) + return true; + + return al::isNerve(this, &NrvBubble.WaitHack) || al::isNerve(this, &NrvBubble.WaitHackFall); +} + +void Bubble::offGroupClipping() { + if (mIsOnGroupClipping) { + al::offGroupClipping(this); + mIsOnGroupClipping = false; + } +} + +void Bubble::startHackLocal(al::HitSensor* self, al::HitSensor* other) { + setupHack(); + mPlayerHack = rs::startHack(self, other, nullptr); + rs::startHackStartDemo(mPlayerHack, this); + al::setCameraTarget(this, mActorMatrixCameraTarget); + al::setNerve(this, &NrvBubble.WaitHackStart); +} + +bool Bubble::isInvalidHackEscape() const { + if (isHack()) { + if (al::isInAreaObj(this, "HackInvalidEscapeArea")) + return true; + + if (al::isNerve(this, &NrvBubble.HackResetPos)) + return true; + } + + return false; +} + +void Bubble::prepareEndHack() { + al::startHitReaction(this, "憑依解除(通常)"); + rs::tryEndHackStartDemo(mPlayerHack, this); + al::showModelIfHide(this); +} + +void Bubble::endHackByCancel() { + al::endBgmSituation(this, "HackBubble", false); + + if (mLaunchCameraTicket && al::isActiveCamera(mLaunchCameraTicket)) { + al::endCamera(this, mLaunchCameraTicket, -1, false); + mLaunchCameraTicket = nullptr; + } + + endHackCommon(); + if (al::isNerve(this, &NrvBubble.HackInLauncher) && mBubbleStateInLauncher->isFlying()) + return; + + al::setNerve(this, &NrvBubble.WaitHackFall); +} + +void Bubble::forceEndHack() { + if (mPlayerHack) { + al::startHitReaction(this, "憑依解除(着地強制)"); + endHackCommon(); + } + + al::setVelocityZero(this); + revive(); +} + +void Bubble::updatePosOnWave() { + if (isValidCollisionOrWaveCheck()) + updateLavaWave(); +} + +void Bubble::updateVelocityIfValidCollision() { + if (!isValidCollisionOrWaveCheck()) + return; + + updateLavaWave(); + sead::Vector3f velocity; + if (al::isOnGroundNoVelocity(this, 0)) + velocity.set(al::getOnGroundNormal(this, 0)); + else + velocity.set(sead::Vector3f::ey); + + sead::Vector3f velocityH; + sead::Vector3f velocityV; + al::separateVelocityDirHV(&velocityH, &velocityV, this, velocity); + velocityH *= 0.92f; + if (al::isOnGroundNoVelocity(this, 0)) + velocityV.set(0.0f, 0.0f, 0.0f); + + velocityV -= 1.1f * velocity; + al::setVelocity(this, velocityH + velocityV); +} + +bool Bubble::isOnDamageFire() const { + return mIsOnLavaSurface || al::isCollidedGroundFloorCode(this, "DamageFire"); +} + +void Bubble::tryStartHitReactionUp() { + if (mIsOnHitReactionEffect || mIsCheckEffectPosRequired) + return; + + s32 index = getSurfaceProbeIndex(this); + + if (mSurfaceProbePos[index].y < al::getTrans(this).y) { + al::startHitReactionHitEffect(this, "発射", + mSurfaceProbePos[index] + 0.0f * sead::Vector3f::ey); + mIsOnHitReactionEffect = true; + } +} + +void Bubble::updateShadowMaskLength() { + f32 newLength = sead::Mathf::clamp(al::getTrans(this).y - mStartActionPosition.y + 300.0f, + 300.0f, mShadowMaskDropLength); + al::setShadowMaskDropLength(this, newLength); +} + +void Bubble::tryStartHitReactionDown() { + if (mIsOnHitReactionEffect || mIsCheckEffectPosRequired) + return; + + s32 index = getSurfaceProbeIndex(this); + + if (mSurfaceProbePos[index].y > al::getTrans(this).y) { + al::startHitReactionHitEffect(this, "着地", + mSurfaceProbePos[index] + 0.0f * sead::Vector3f::ey); + mIsOnHitReactionEffect = true; + } +} + +void Bubble::shiftSink() { + sead::Vector3f hitEffect; + if (mIsCheckEffectPosRequired || mIsOnHitReactionEffect) { + if (isValidCollisionOrWaveCheck() && !trySendMsgStartInSaucePan()) { + hitEffect = + al::getTrans(this) - (al::getColliderRadius(this) + 0.0f) * sead::Vector3f::ey; + al::startHitReactionHitEffect(this, "着地", hitEffect); + } + } else { + s32 index = getSurfaceProbeIndex(this); + if (!trySendMsgStartInSaucePan()) { + hitEffect = mSurfaceProbePos[index] + 0.0f * sead::Vector3f::ey; + al::startHitReactionHitEffect(this, "着地", hitEffect); + } + } + al::setNerve(this, &NrvBubble.Sink); +} + +void Bubble::headUp() { + sead::Vector3f upDir; + al::calcUpDir(&upDir, this); + if (upDir.y > 0.99f) + return; + + sead::Quatf quat; + al::makeQuatRotationRate(&quat, upDir, sead::Vector3f::ey, 1.0f); + quat = quat * al::getQuat(this); + quat.normalize(); + al::setQuat(this, quat); +} + +void Bubble::shiftReadyOrMove() { + if (al::isExistRail(this) && al::isNoCollide(this)) + al::setNerve(this, &NrvBubble.RailMove); + else + al::setNerve(this, &NrvBubble.Ready); +} + +bool Bubble::trySendMsgStartInSaucePan() { + if (mIsInSaucePan || !al::isCollidedFloorCode(this, "DamageFire")) + return false; + + al::HitSensor* hitSensor = al::tryGetCollidedGroundSensor(this); + if (!hitSensor) + return false; + + if (rs::sendMsgStartInSaucePan(hitSensor, al::getHitSensor(this, "Body"), false)) { + al::startHitReaction(this, "入鍋"); + al::tryAddRippleLarge(this); + mIsInSaucePan = true; + mIsPlayerCaptured = true; + return true; + } + + return false; +} + +void Bubble::tryHitReactionThroughFence() { + sead::Vector3f velocity = al::getVelocity(this); + if (al::isNearZero(velocity)) + return; + + sead::Vector3f pos; + al::Triangle triangle; + if (alCollisionUtil::getFirstPolyOnArrow(this, &pos, &triangle, al::getTrans(this), velocity, + nullptr, nullptr) && + isIgnoreTriangle(triangle)) { + al::startHitReaction(this, "金網通過"); + mAnimScaleController->startHitReaction(); + } +} + +bool Bubble::tryShiftLand() { + if (!al::isOnGround(this, 0) && !(mIsOnLavaSurface && al::getVelocity(this).y < 0.0f)) + return false; + + if (!isOnDamageFire()) { + forceEndHack(); + return true; + } + + if (al::isOnGround(this, 0)) + mFireNormal.set(al::getOnGroundNormal(this, 0)); + sead::Quatf quat; + al::makeQuatRotationRate(&quat, sead::Vector3f::ey, mFireNormal, 1.0f); + al::getVelocityPtr(this)->rotate(quat); + sead::Vector3f* velPtr = al::getVelocityPtr(this); + al::verticalizeVec(velPtr, mFireNormal, *velPtr); + + if (mPlayerHack) { + mAirTime = 0; + mLandVelocity.set(0.0f, 0.0f, 0.0f); + updateCollisionPartsMove(); + al::getVelocityPtr(this)->add(mLandVelocity); + if (al::isCollidedGround(this)) { + rs::sendMsgBubbleGroundTouchTrigger(al::getCollidedGroundSensor(this), + al::getHitSensor(this, "Attack")); + } + al::setNerve(this, &NrvBubble.HackLand); + return true; + } + + al::setNerve(this, &NrvBubble.Sink); + return true; +} + +bool Bubble::isHoldHackAction() const { + return !mIsPlayerCaptured && rs::isHoldHackAction(mPlayerHack); +} + +bool Bubble::tryBoundMoveWall() { + if (!al::isCollidedWall(this) || !al::isCollidedGround(this)) + return false; + + const al::CollisionParts* collisionParts = al::getCollidedWallCollisionParts(this); + if (collisionParts->get_15c() != 0 && !collisionParts->isMoving()) + return false; + + sead::Vector3f wallNormal = al::getCollidedWallNormal(this); + const sead::Vector3f& groundNormal = al::getCollidedGroundNormal(this); + if (groundNormal.dot(wallNormal) < 0.1736482f) // cos(80°) + return false; + + al::verticalizeVec(&wallNormal, groundNormal, wallNormal); + if (!al::tryNormalizeOrZero(&wallNormal)) + return false; + + sead::Vector3f prevTrans = collisionParts->getPrevBaseMtx().getTranslation(); + sead::Vector3f baseTrans = collisionParts->getBaseMtx().getTranslation(); + sead::Vector3f moveH = baseTrans - prevTrans; + al::verticalizeVec(&moveH, groundNormal, moveH); + + f32 speedH = moveH.length(); + if (al::isNearZero(speedH)) + return false; + + sead::Vector3f moveDirH = moveH * (1.0f / speedH); + + if (moveDirH.dot(wallNormal) < 0.7071068f) // cos(45°) + return false; + + sead::Vector3f velH; + sead::Vector3f velV; + al::separateVectorParallelVertical(&velH, &velV, moveDirH, al::getVelocity(this)); + if (velH.dot(moveDirH) >= speedH * 5.0f) + return false; + + al::getVelocityPtr(this)->setAdd(5.0f * speedH * moveDirH, velV); + mAnimScaleController->startHitReaction(); + return true; +} + +void Bubble::updateHackOnGround() { + static sead::Vector3f magic = {0.0f, 0.0f, 0.0f}; + + *al::getVelocityPtr(this) -= mLandVelocity; + if (!al::isOnGroundNoVelocity(this, 0)) { + if (mIsOnLavaSurface) { + mAirTime = 0; + sead::Vector3f* velPtr = al::getVelocityPtr(this); + al::verticalizeVec(velPtr, sead::Vector3f::ey, *velPtr); + } else { + sead::Vector3f groundPos; + sead::Vector3f upDir; + al::calcUpDir(&upDir, this); + + sead::Vector3f colliderPos = al::getTrans(this) + al::getColliderOffsetY(this) * upDir; + + if (alCollisionUtil::getFirstPolyOnArrow(this, &groundPos, nullptr, colliderPos, + (-50.0f - al::getColliderRadius(this)) * + sead::Vector3f::ey, + mCollisionPartsFilter, nullptr)) { + mColliderGroundOffset = + groundPos - (colliderPos - al::getColliderRadius(this) * sead::Vector3f::ey); + } + mAirTime++; + } + } else if (mIsOnLavaSurface) { + mAirTime = 0; + sead::Vector3f* velPtr = al::getVelocityPtr(this); + al::verticalizeVec(velPtr, sead::Vector3f::ey, *velPtr); + } else { + mAirTime = 0; + mFireNormal.set(al::getOnGroundNormal(this, 0)); + sead::Vector3f* velPtr = al::getVelocityPtr(this); + al::verticalizeVec(velPtr, mFireNormal, *velPtr); + } + updateCollisionPartsMove(); + al::setQuat(this, mGroundRotation); + accelStick(); + mGroundRotation.set(al::getQuat(this)); + sead::Vector3f updir2; + al::calcUpDir(&updir2, this); + + sead::Quatf quat; + al::makeQuatRotationRate(&quat, updir2, mUpDir, 1.0f); + al::setQuat(this, quat * al::getQuat(this)); + + sead::Quatf quat2; + al::makeQuatRotationRate(&quat2, mUpDir, mFireNormal, 0.1f); + al::setQuat(this, quat2 * al::getQuat(this)); + + al::calcUpDir(&mUpDir, this); + + sead::Quatf quat3; + al::makeQuatZDegree(&quat3, ((f32)mHackTurnFrame / (f32)mHackTurnDelay) * mHackTurnAngle); + quat3.normalize(); + al::setQuat(this, al::getQuat(this) * quat3); + + al::getVelocityPtr(this)->add(mLandVelocity); +} + +// TODO: might be moved into `sead` +inline f32 normalize2(sead::Vector3f* v, f32 scalar) { + const f32 len = v->length(); + if (len > 0) { + const f32 inv_len = scalar / len; + v->x *= inv_len; + v->y *= inv_len; + v->z *= inv_len; + } + + return len; +} + +bool Bubble::constrainLavaDomain() { + if (!isOnGroundNoVelocity()) + return true; + + f32 colliderRadius = al::getColliderRadius(this); + sead::Vector3f front; + al::calcFrontDir(&front, this); + front.y = 0.0; + al::tryNormalizeOrDirZ(&front); + front *= colliderRadius; + + sead::Vector3f side; + side.setCross(sead::Vector3f::ey, front); + normalize2(&side, colliderRadius); + + sead::Vector3f basePos = al::getTrans(this) + colliderRadius * sead::Vector3f::ey; + sead::Vector3f checkPos[4]; + checkPos[0] = basePos + (-side + front); + checkPos[1] = basePos + (side + front); + checkPos[2] = basePos + (-side - front); + checkPos[3] = basePos + (side - front); + sead::Vector3f checkDir = sead::Vector3f::ey * (colliderRadius * -3.0f); + + bool isFire[4] = {false, false, false, false}; + bool isPoly[4] = {false, false, false, false}; + f32 height[4] = {0.0f, 0.0f, 0.0f, 0.0f}; + + for (s32 i = 0; i < 4; i++) { + sead::Vector3f hitPos; + al::Triangle triangle; + if (alCollisionUtil::getFirstPolyOnArrow(this, &hitPos, &triangle, checkPos[i], checkDir, + mCollisionPartsFilter, nullptr)) { + height[i] = hitPos.y; + isFire[i] = al::isFloorCode(triangle, "DamageFire"); + isPoly[i] = true; + } + } + + sead::Vector3f firePos; + sead::Vector3f fireNormal; + for (s32 i = 0; i < 4; i++) { + if (!al::calcFindFireSurface(&firePos, &fireNormal, this, checkPos[i], sead::Vector3f::ey, + 200.0f)) + continue; + if (isPoly[i] && height[i] > firePos.y) + continue; + + isFire[i] = true; + isPoly[i] = true; + } + + if ((!al::isOnGroundNoVelocity(this, 0) || + !al::isCollidedGroundFloorCode(this, "DamageFire")) && + (!isPoly[0] || !isFire[0]) && (!isPoly[1] || !isFire[1]) && (!isPoly[2] || !isFire[2]) && + (!isPoly[3] || !isFire[3])) { + forceEndHack(); + return false; + } + + u32 noFirePosCount = 0; + sead::Vector3f noFirePosSum = {0.0f, 0.0f, 0.0f}; + for (s32 i = 0; i < 4; i++) { + if (isPoly[i] && !isFire[i]) { + noFirePosSum += checkPos[i]; + noFirePosCount++; + } + } + + if (noFirePosCount != 0) { + sead::Vector3f pushDirection = -(1.0f / noFirePosCount) * noFirePosSum + al::getTrans(this); + pushDirection.y = 0.0f; + + al::tryNormalizeOrZero(&pushDirection); + if (pushDirection.dot(al::getVelocity(this)) > 0.0f) { + sead::Vector3f* velPtr = al::getVelocityPtr(this); + al::verticalizeVec(velPtr, pushDirection, *velPtr); + } + + al::addVelocity(this, pushDirection * 3.0f); + } + + return true; +} + +bool Bubble::tryShiftFall() { + if (mAirTime > 9) { + mJumpForce.set(0.0f, 0.0f, 0.0f); + al::setNerve(this, &NrvBubble.HackJump); + return true; + } + + return false; +} + +bool Bubble::isTriggerHackSwing() const { + return !mIsPlayerCaptured && rs::isTriggerHackSwing(mPlayerHack); +} + +void Bubble::faceToMoveVec() { + sead::Vector3f moveVec; + calcHackerMoveVec(&moveVec, sead::Vector3f::ey); + + if (al::isNearZero(moveVec)) + return; + + al::normalize(&moveVec); + al::faceToDirection(this, moveVec); + sead::Vector3f velocity = al::getVelocity(this); + velocity.y = 0.0f; + if (!al::tryNormalizeOrZero(&velocity)) + return; + + sead::Quatf quat; + al::makeQuatRotationRate(&quat, velocity, moveVec, 1.0f); + quat.normalize(); + + sead::Vector3f newVelocity = al::getVelocity(this); + newVelocity.rotate(quat); + al::setVelocity(this, newVelocity); +} + +bool Bubble::isTriggerHackJump() const { + return !mIsPlayerCaptured && rs::isTriggerHackPreInputJump(mPlayerHack); +} + +void Bubble::revertTargetQuatInHackJump(sead::Quatf* prevQuat, sead::Quatf* deltaQuat) { + prevQuat->set(al::getQuat(this)); + sead::Quatf invRotation; + invRotation.setInverse(mGroundRotation); + deltaQuat->set(invRotation * al::getQuat(this)); + deltaQuat->normalize(); + al::setQuat(this, mGroundRotation); +} + +void Bubble::calcHackerMoveVec(sead::Vector3f* moveVec, const sead::Vector3f& inputDir) const { + if (mIsPlayerCaptured) { + moveVec->set(0.0f, 0.0f, 0.0f); + return; + } + rs::calcHackerMoveVec(moveVec, mPlayerHack, inputDir); +} + +void Bubble::makeDisplayQuatInHackJump(const sead::Quatf& prevQuat, const sead::Quatf& deltaQuat, + const sead::Quatf& quatZY, bool flipDirection) { + sead::Vector3f moveDir = al::getTrans(this) - mPreviousTrans; + mPreviousTrans.set(al::getTrans(this)); + + sead::Quatf newQuat = mGroundRotation; + if (al::tryNormalizeOrZero(&moveDir)) { + f32 angle = al::calcAngleDegree(sead::Vector3f::ey, moveDir); + if (flipDirection) + angle = !(al::getVelocity(this).y >= 0.0f) ? 180.0f : 0.0f; + sead::Vector3f dir = sead::Vector3f::ey; + dir.rotate(prevQuat); + dir.negate(); + sead::Quatf rotationQuat; + al::makeQuatRotateDegree( + &rotationQuat, sead::Vector3f::ex, + sead::Mathf::max(al::calcAngleDegree(sead::Vector3f::ey, dir), angle)); + newQuat = newQuat * rotationQuat * quatZY; + } else { + newQuat *= deltaQuat; + } + + sead::Quatf slerpQuat; + al::slerpQuat(&slerpQuat, prevQuat, newQuat, 0.1f); + al::setQuat(this, slerpQuat); +} + +bool Bubble::isDropAttackCollision() const { + if (al::getVelocity(this).y > 0.0f) + return false; + + if (!al::isOnGround(this, 0)) + return false; + + const sead::Vector3f& groundPos = al::getCollidedGroundPos(this); + const sead::Vector3f& trans = al::getTrans(this); + return !(groundPos.y - trans.y >= 0.0f); +} + +bool Bubble::isRiseAttackCollision() const { + if (al::getVelocity(this).y < 0.0f) + return false; + + if (!al::isCollidedCeiling(this)) + return false; + + const sead::Vector3f& ceilingPos = al::getCollidedCeilingPos(this); + const sead::Vector3f& trans = al::getTrans(this); + return !(ceilingPos.y - trans.y <= 0.0f); +} + +bool Bubble::isHoldHackJump() const { + return !mIsPlayerCaptured && rs::isHoldHackJump(mPlayerHack); +} + +bool Bubble::tryShiftContinuousJump() { + if (!al::isNerve(this, &NrvBubble.HackJumpHigh)) + return false; + + if (mContinuousJumpDelay < 15) + mContinuousJumpDelay++; + + if (isTriggerHackSwing()) + mContinuousJumpDelay = 0; + + if (!isOnGround()) + return false; + + if (al::getVelocity(this).y >= 0.0f) + return false; + + if (!isOnDamageFire()) + return false; + + if (mContinuousJumpDelay >= 15) + return false; + + mJumpForce.set({0.0f, 25.0f, 0.0f}); + al::getVelocityPtr(this)->y = 0.0f; + al::startHitReaction(this, "水切りジャンプ"); + al::setNerve(this, &NrvBubble.HackJumpHigh); + + if (al::isOnGround(this, 0)) + mFireNormal.set(al::getOnGroundNormal(this, 0)); + + mAirTime = 0; + mLandVelocity.set(0.0f, 0.0f, 0.0f); + + if (al::isCollidedGround(this)) { + rs::sendMsgBubbleGroundTouchTrigger(al::getCollidedGroundSensor(this), + al::getHitSensor(this, "Attack")); + } + + al::tryAddRippleMiddle(this); + return true; +} + +bool Bubble::calcHackerMoveDir(sead::Vector3f* moveDir, const sead::Vector3f& inputDir) const { + if (mIsPlayerCaptured) { + moveDir->set(0.0f, 0.0f, 0.0f); + return false; + } + return rs::calcHackerMoveDir(moveDir, mPlayerHack, inputDir); +} + +bool Bubble::isWaitingLaunch() const { + return al::isNerve(this, &NrvBubble.HackInLauncher) && mBubbleStateInLauncher->isWaiting(); +} + +void Bubble::launch(const sead::Vector3f& targetPos, f32 launchDegree, + al::CameraTicket* cameraTicket) { + if (!al::isNerve(this, &NrvBubble.HackInLauncher)) + return; + + mBubbleStateInLauncher->shoot(targetPos, launchDegree); + if (cameraTicket) + mLaunchCameraTicket = cameraTicket; + rs::hideTutorial(this); + mIsShowTutorial = true; +} + +void Bubble::launchCancel(const sead::Vector3f& targetPos) { + if (!al::isNerve(this, &NrvBubble.HackInLauncher)) + return; + + mBubbleStateInLauncher->kill(); + al::showModelIfHide(this); + al::showSilhouetteModelIfHide(this); + showHackCap(); + + sead::Vector3f forceH = al::getTrans(this) - targetPos; + forceH.y = 0.0f; + al::tryNormalizeOrZero(&forceH); + mJumpForce.set(25.0f * sead::Vector3f::ey + 7.0f * forceH); + + al::faceToDirection(this, forceH); + mCancelLaunchCooldown = 5; + al::setNerve(this, &NrvBubble.HackCancelJump); +} + +void Bubble::showHackCap() { + rs::showHackCap(mPlayerHack); +} + +void Bubble::hideHackCap() { + rs::hideHackCap(mPlayerHack); +} + +bool Bubble::isTriggerJump() const { + return mPlayerHack && (isTriggerHackJump() || isTriggerHackSwing()); +} + +bool Bubble::isTriggerAction() const { + return mPlayerHack && isTriggerHackAction(); +} + +bool Bubble::isTriggerHackAction() const { + return !mIsPlayerCaptured && rs::isTriggerHackAction(mPlayerHack); +} + +bool Bubble::isTriggerCancel() const { + return mPlayerHack && HackFunction::isTriggerCancelBubbleLauncher(mPlayerHack); +} + +void Bubble::startJumpAnim(f32 force) { + if (force > 0.0f) { + al::startAction(this, "Up"); + return; + } + + if (al::isActionPlaying(this, "Down") || al::isActionPlaying(this, "Turn")) + return; + + al::startAction(this, "Turn"); +} + +void Bubble::resetAndAppear(const sead::Vector3f& trans, const sead::Quatf& quat, f32 unused) { + mRevivePosition.set(trans); + mStartingRotation.set(quat); + mStartActionPosition.set(trans); + mReviveStartActionPosition.set(mStartActionPosition); + al::resetPosition(this, trans); + al::setQuat(this, quat); + recalcClippingInfo(); + appear(); +} + +void Bubble::endHackCommon() { + al::resetCameraTarget(this, mActorMatrixCameraTarget); + sead::Vector3f colliderPos; + al::calcColliderPos(&colliderPos, this); + f32 colliderRadius = al::getColliderRadius(this); + sead::Vector3f targetPos = colliderPos; + targetPos.y += colliderRadius; + sead::Vector3f move = targetPos - colliderPos; + + alCollisionUtil::SphereMoveHitInfo sphereMoveHitInfo; + + if (alCollisionUtil::checkStrikeSphereMove(this, &sphereMoveHitInfo, 8, colliderPos, + colliderRadius, move, nullptr, nullptr) != 0) { + targetPos = sphereMoveHitInfo._0 * move + colliderPos; + } + + sead::Vector3f upDir; + sead::Quatf rotationRate; + al::calcUpDir(&upDir, this); + al::makeQuatRotationRate(&rotationRate, upDir, sead::Vector3f::ey, 1.0f); + + rotationRate *= al::getQuat(this); + rs::endHackFromTargetPos(&mPlayerHack, targetPos, rotationRate, sead::Vector3f::zero); + al::showModelIfHide(this); + rs::endHackShadow(this); + mJointSpringControllerHolder->offControlAll(); + mAnimScaleController->startHitReaction(); + al::endBgmSituation(this, "HackBubble", false); +} + +void Bubble::calcLaunchPos(sead::Vector3f* pos, const sead::Vector3f& targetPos, f32 launchDegree, + f32 frame) const { + s32 index = sead::Mathf::floor(frame); + f32 t = frame - index; + + sead::Vector3f startPos; + sead::Vector3f endPos; + mBubbleStateInLauncher->calcLaunchPos(&startPos, targetPos, launchDegree, index); + mBubbleStateInLauncher->calcLaunchPos(&endPos, targetPos, launchDegree, index + 1); + + // Lerp + pos->set((1.0f - t) * startPos + t * endPos); +} + +void Bubble::onGroupClipping() { + if (!mIsOnGroupClipping) { + al::onGroupClipping(this); + mIsOnGroupClipping = true; + } +} + +bool Bubble::isOnGroundNoVelocity() const { + return al::isOnGroundNoVelocity(this, 0) || mIsOnLavaSurface; +} + +void Bubble::updateCollisionPartsMove() { + const al::CollisionParts* collisionParts = al::tryGetCollidedGroundCollisionParts(this); + if (!collisionParts) { + if (mIsOnLavaSurface || mAirTime > 9) + mLandVelocity.set(0.0f, 0.0f, 0.0f); + return; + } + sead::Vector3f trans = al::getTrans(this); + trans *= collisionParts->getPrevBaseInvMtx(); + trans *= collisionParts->getBaseMtx(); + mLandVelocity.set(trans - al::getTrans(this)); +} + +// NON_MATCHING: Bad branch order https://decomp.me/scratch/ACufd +void Bubble::accelStick() { + sead::Vector3f upDir; + if (al::isOnGroundNoVelocity(this, 0)) + upDir.set(al::getOnGroundNormal(this, 0)); + else + upDir.set(sead::Vector3f::ey); + + sead::Vector3f stickForceNormalized = mStickForce; + if (al::tryNormalizeOrZero(&stickForceNormalized)) { + sead::Vector3f cross; + cross.setCross(upDir, stickForceNormalized); + if (al::tryNormalizeOrZero(&cross)) { + f32 dot = cross.dot(al::getVelocity(this)); + *al::getVelocityPtr(this) -= dot * cross; + *al::getVelocityPtr(this) += (dot * 0.9f) * cross; + } + f32 force = sead::Mathf::clampMax( + mStickForce.length() - (stickForceNormalized.dot(al::getVelocity(this))), 0.5f); + if (force > 0.0f) + al::getVelocityPtr(this)->add(force * stickForceNormalized); + } + + sead::Vector3f velocityDir = upDir * upDir.dot(al::getVelocity(this)); + al::setVelocity(this, + (al::getVelocity(this) - velocityDir) * 0.92f + (velocityDir - upDir * 1.1f)); + mStickForce.set({0.0f, 0.0f, 0.0f}); + + if (mIsPlayerCaptured) { + mHackTurnAngle *= 0.9f; + return; + } + + // this screams for addHackActorAccelStick(&stickAccel, scale, upDir) + sead::Vector3f stickAccel; + f32 scale = rs::isHoldHackAction(mPlayerHack) ? 1.6f : 1.4f; + if (mIsPlayerCaptured) { + stickAccel.set(0.0f, 0.0f, 0.0f); + mHackTurnAngle *= 0.9f; + return; + } + + if (rs::addHackActorAccelStick(this, mPlayerHack, &stickAccel, scale, upDir)) { + al::turnToDirection(this, stickAccel, 10.0f); + + sead::Vector3f rotationAxis; + rotationAxis.setRotated(mGroundRotation, sead::Vector3f::ez); + stickAccel.y = 0.0f; + + if (!al::tryNormalizeOrZero(&stickAccel)) + stickAccel.set(rotationAxis); + + f32 dot = stickAccel.dot(rotationAxis); + f32 angle = atan2f(rotationAxis.cross(stickAccel).length(), dot); + angle = (sead::Mathf::clamp(angle, 0.0f, sead::Mathf::piHalf()) * 72.0f) / + sead::Mathf::piHalf(); + + if (rotationAxis.z * stickAccel.x - rotationAxis.x * stickAccel.z > 0.0f) + angle = -angle; + + bool condition = !(mHackTurnAngle * angle < 0.0f) && + sead::Mathf::abs(mHackTurnAngle) > sead::Mathf::abs(angle); + + if (condition) { + mHackTurnAngle *= 0.4f; + mHackTurnAngle += angle * 0.6f; + } else { + mHackTurnAngle *= 0.95f; + mHackTurnAngle += angle * 0.05f; + } + } +} + +bool Bubble::addHackActorAccelStick(sead::Vector3f* stickAccel, f32 scale, + const sead::Vector3f& dir) { + if (mIsPlayerCaptured) { + stickAccel->set(0.0f, 0.0f, 0.0f); + return false; + } + return rs::addHackActorAccelStick(this, mPlayerHack, stickAccel, scale, dir); +} + +bool Bubble::isGroundOverTheWave(bool checkHeight, const sead::Vector3f& wavePos) const { + if (al::isOnGround(this, 0)) { + if (!checkHeight) + return true; + + if (wavePos.y < + al::getTrans(this).y - al::getColliderRadius(this) + al::getColliderOffsetY(this)) { + return true; + } + } + + return false; +} + +bool Bubble::isEnableSnapWaveSurface() const { + return mIsOnLavaSurface && !al::isNerve(this, &NrvBubble.HackJump) && + !al::isNerve(this, &NrvBubble.HackJumpHigh) && + !al::isNerve(this, &NrvBubble.HackCancelJump) && !al::isNerve(this, &NrvBubble.Down); +} + +void Bubble::exeStandBy() {} + +void Bubble::exeDelay() { + if (al::isGreaterEqualStep(this, mDelayFrameNum)) + al::setNerve(this, &NrvBubble.Ready); +} + +void Bubble::exeRailMove() { + if (al::isFirstStep(this)) + al::startAction(this, "Move"); + + al::tryAddRippleSmall(this); + f32 nerveRate = al::calcNerveRate(this, mRailMoveFrame); + if (!al::isRailGoingToEnd(this)) + nerveRate = 1.0 - nerveRate; + al::setSyncRailToCoord(this, mRailTotalLength * nerveRate); + + updatePosOnWave(); + + if (al::isGreaterEqualStep(this, mRailMoveFrame)) { + if (al::isRailGoingToEnd(this)) + al::setQuat(this, mRailTargetRotation); + else + al::setQuat(this, mStartingRotation); + al::reverseRail(this); + al::setNerve(this, &NrvBubble.Ready); + } +} + +void Bubble::exeReady() { + if (al::isFirstStep(this)) { + if (isValidCollisionOrWaveCheck()) { + al::startHitReactionHitEffect( + this, "予兆", + al::getTrans(this) - (al::getColliderRadius(this) + 0.0f) * sead::Vector3f::ey); + } else if (!mIsCheckEffectPosRequired) { + al::startHitReactionHitEffect(this, "予兆", + mSurfaceProbePos[getSurfaceProbeIndex(this)] + + 0.0f * sead::Vector3f::ey); + } + Bubble::initAfterPlacement(); + } + updateVelocityIfValidCollision(); + if (isValidCollisionOrWaveCheck() && !isOnDamageFire()) + forceEndHack(); + else if (al::isGreaterEqualStep(this, 60)) + al::setNerve(this, &NrvBubble.Up); +} + +void Bubble::exeUp() { + if (al::isFirstStep(this)) { + al::showModel(this); + al::startAction(this, "Up"); + if (isValidCollisionOrWaveCheck()) { + mStartActionPosition.set(al::getTrans(this)); + al::startHitReactionHitEffect( + this, "発射", + al::getTrans(this) - (al::getColliderRadius(this) + 0.0f) * sead::Vector3f::ey); + mIsOnHitReactionEffect = true; + } else { + mIsOnHitReactionEffect = false; + } + al::tryAddRippleMiddle(this); + mTurnFrameCount = mTurnFrameDelay; + } + tryStartHitReactionUp(); + sead::Vector3f nextPos = al::getTrans(this); + nextPos.y = mStartActionPosition.y + + al::calcNerveSquareOutValue(this, mTurnFrameCount, 0.0f, mJumpHeight); + + if (isValidCollisionOrWaveCheck()) + al::setVelocity(this, nextPos - al::getTrans(this)); + else + al::setTrans(this, nextPos); + updateShadowMaskLength(); + + if (!al::isActionPlaying(this, "Turn") && al::isCollidedCeiling(this)) + mTurnFrameCount = al::getNerveStep(this) + 8; + + if (al::isStep(this, mTurnFrameCount - 8)) + al::tryStartActionIfNotPlaying(this, "Turn"); + + if (al::isGreaterEqualStep(this, mTurnFrameCount)) + al::setNerve(this, &NrvBubble.Turn); +} + +void Bubble::exeTurn() { + al::isFirstStep(this); + if (al::isGreaterEqualStep(this, 4)) + al::setNerve(this, &NrvBubble.Down); +} + +void Bubble::exeDown() { + if (al::isFirstStep(this)) { + al::startAction(this, "Down"); + if (isValidCollisionOrWaveCheck()) { + mStartActionPosition.set(al::getTrans(this)); + mStartActionPosition.y -= mJumpHeight; + mIsOnHitReactionEffect = true; + } else { + mIsOnHitReactionEffect = false; + } + } + + tryStartHitReactionDown(); + sead::Vector3f nextPos = al::getTrans(this); + + f32 step = al::getNerveStep(this); + f32 rate = sead::Mathf::square(step / mTurnFrameCount); + if (!isValidCollisionOrWaveCheck()) + rate = sead::Mathf::clamp(rate, 0.0f, 1.0f); + + nextPos.y = mStartActionPosition.y + mJumpHeight * (1.0f - rate); + + if (isValidCollisionOrWaveCheck()) + al::setVelocity(this, nextPos - al::getTrans(this)); + else + al::setTrans(this, nextPos); + + updatePosOnWave(); + + updateShadowMaskLength(); + + if (isValidCollisionOrWaveCheck()) { + if (isOnGround()) { + if (!isOnDamageFire()) { + al::startHitReaction(this, "地面での消滅"); + revive(); + } else { + al::tryAddRippleMiddle(this); + shiftSink(); + } + } + } else { + if (al::isGreaterEqualStep(this, mTurnFrameCount)) + shiftSink(); + } +} + +void Bubble::exeSink() { + if (al::isFirstStep(this)) { + al::tryAddRippleMiddle(this); + if (al::isCollidedGround(this)) { + rs::sendMsgBubbleGroundTouchTrigger(al::getCollidedGroundSensor(this), + al::getHitSensor(this, "Attack")); + } + } + + f32 nerveRate = al::calcNerveRate(this, 10); + if (isValidCollisionOrWaveCheck()) { + updateLavaWave(); + al::setVelocityY(this, -30.0f); + al::setColliderOffsetY(this, nerveRate * 150.0f); + } else { + al::getTransPtr(this)->y = mStartActionPosition.y + nerveRate * -150.0f; + } + + if (al::isGreaterEqualStep(this, 10)) { + headUp(); + al::setNerve(this, &NrvBubble.Wait); + } +} + +void Bubble::exeWait() { + if (al::isFirstStep(this)) { + al::startAction(this, "Wait"); + al::hideModel(this); + al::setVelocityZero(this); + if (isValidCollisionOrWaveCheck()) { + al::getTransPtr(this)->y += al::getColliderOffsetY(this); + al::setColliderOffsetY(this, 0.0f); + } else { + al::getTransPtr(this)->y = mStartActionPosition.y; + } + } + + updateVelocityIfValidCollision(); + + if (isValidCollisionOrWaveCheck() && !isOnDamageFire()) { + forceEndHack(); + return; + } + + if (al::isGreaterEqualStep(this, mWaitFrameNum) && !mIsInSaucePan) { + if (!al::isNoCollide(this) && mWaitReviveDelay < 1) { + revive(); + return; + } + + mWaitReviveDelay--; + + if (!al::isValidSwitchStart(this) || al::isOnSwitchStart(this)) + shiftReadyOrMove(); + } +} + +void Bubble::exeDie() { + al::startHitReaction(this, "死亡"); + kill(); +} + +void Bubble::exeWaitHack() { + setVelocityZero(this); +} + +void Bubble::exeWaitHackStart() { + al::setVelocityZero(this); + if (rs::isHackStartDemoEnterMario(mPlayerHack)) { + al::startHitReaction(this, "憑依開始"); + if (!al::isActionPlaying(this, "HackStartDown") && + !al::isActionPlaying(this, "HackStart")) { + if (al::isActionPlaying(this, "Down") || al::isActionPlaying(this, "Turn")) + al::startAction(this, "HackStartDown"); + else + al::startAction(this, "HackStart"); + rs::setupHackShadow(this); + mPlayerHackStartShaderCtrl->start(); + mJointSpringControllerHolder->offControlAll(); + } + } + + if (al::isActionPlaying(this, "HackStartDown") || al::isActionPlaying(this, "HackStart")) { + mPlayerHackStartShaderCtrl->update(); + if (al::isActionEnd(this)) { + mPlayerHackStartShaderCtrl->end(); + rs::endHackStartDemo(mPlayerHack, this); + al::setNerve(this, &NrvBubble.HackFall); + return; + } + } +} + +void Bubble::endWaitHackStart() { + mJointSpringControllerHolder->onControlAll(); +} + +void Bubble::exeWaitHackFall() { + if (al::isFirstStep(this)) + al::setVelocityZero(this); + + al::addVelocityToGravity(this, 1.1f); + al::scaleVelocityHV(this, 1.0f, 0.995f); + + if (isOnGround()) { + if (isOnDamageFire()) { + mStartActionPosition.set(al::getTrans(this)); + + if (!trySendMsgStartInSaucePan()) { + al::startHitReactionHitEffect( + this, "着地", + al::getTrans(this) - (al::getColliderRadius(this) + 0.0f) * sead::Vector3f::ey); + } + al::setNerve(this, &NrvBubble.Sink); + } else { + al::startHitReaction(this, "地面での消滅"); + revive(); + } + + al::setVelocityZero(this); + } +} + +void Bubble::exeHackFall() { + if (al::isFirstStep(this)) + mGroundRotation.set(al::getQuat(this)); + + al::getVelocityPtr(this)->y -= 1.1f; + tryHitReactionThroughFence(); + tryShiftLand(); +} + +void Bubble::exeHackMove() { + if (al::isFirstStep(this)) { + mAirTime = 0; + mGroundRotation.set(al::getQuat(this)); + al::calcUpDir(&mUpDir, this); + } + + if (al::getNerveStep(this) % 20 == 0 && al::isOnGround(this, 0)) { + const char* materialCodeName = al::getCollidedFloorMaterialCodeName(this); + if (materialCodeName && al::isEqualString("LavaRed", materialCodeName)) + al::tryAddRippleRandomBlur(this, al::getTrans(this), 0.45f, 110.0f, 0.0f); + else + al::tryAddRippleRandomBlur(this, al::getTrans(this), 0.45f, 110.0f, 100.0f); + } + + if (mIsPlayerCaptured) { + al::tryStartActionIfNotPlaying(this, "HackWait"); + } else if (!rs::isOnHackMoveStick(mPlayerHack)) { + if (isHoldHackAction()) + al::tryStartActionIfNotPlaying(this, "HackWaitSpeedy"); + else + al::tryStartActionIfNotPlaying(this, "HackWait"); + } else if (!mIsPlayerCaptured && rs::isHoldHackAction(mPlayerHack)) { // isHoldHackAction() + al::tryStartActionIfNotPlaying(this, "MoveSpeedy"); + if (!mIsInBossSequence) + rs::requestDownToDefaultCameraAngleBySpeed(this, 6.0f, 0); + } else { + al::tryStartActionIfNotPlaying(this, "Move"); + } + + if (al::isCollidedWall(this)) { + rs::sendMsgBubbleWallTouch(al::getCollidedWallSensor(this), + al::getHitSensor(this, "Attack")); + if (!tryBoundMoveWall()) { + f32 force = al::getCollidedWallNormal(this).dot(al::getVelocity(this)); + if (force < 0.0f) + *al::getVelocityPtr(this) -= force * al::getCollidedWallNormal(this); + } + } + + updateHackOnGround(); + if (!constrainLavaDomain() || tryShiftFall()) + return; + + if (isTriggerHackSwing()) { + faceToMoveVec(); + mJumpForce.set({0.0f, 25.0f, 0.0f}); + al::setNerve(this, &NrvBubble.HackJumpHigh); + return; + } + + if (isTriggerHackJump()) { + faceToMoveVec(); + mJumpForce.set({0.0f, 45.0f, 0.0f}); + al::setNerve(this, &NrvBubble.HackJump); + } +} + +void Bubble::endHackMove() { + al::setQuat(this, mGroundRotation); +} + +inline sead::Quatf getQuatZY() { + sead::Quatf quatZ; + sead::Quatf quatY; + al::makeQuatRotateDegree(&quatZ, sead::Vector3f::ez, 180.0f); + al::makeQuatRotateDegree(&quatY, sead::Vector3f::ey, 180.0f); + return quatZ * quatY; +} + +// NON_MATCHING: Wrong math operation https://decomp.me/scratch/P9h4T +void Bubble::exeHackJump() { + bool isJumpSwing = al::isNerve(this, &NrvBubble.HackJumpHigh); + f32 gravity = !isJumpSwing ? 1.1f : 1.2f; + f32 frontAccel = !isJumpSwing ? 0.5f : 2.5f; + + sead::Quatf prevQuat; + sead::Quatf deltaQuat; + sead::Quatf quatZY = getQuatZY(); + + if (al::isFirstStep(this)) { + mContinuousJumpDelay = 15; + al::getVelocityPtr(this)->add(mJumpForce); + al::limitVelocityH(this, 20.0f); + if (al::isNerve(this, &NrvBubble.HackJumpHigh)) { + al::startHitReaction(this, "憑依振りジャンプ開始"); + al::startAction(this, "HackHighJump"); + } else { + bool isHackCancelJump = al::isNerve(this, &NrvBubble.HackCancelJump); + al::startHitReaction(this, "憑依ジャンプ開始"); + if (isHackCancelJump) + al::startAction(this, "HackCancelJump"); + else + al::startAction(this, "HackJump"); + } + al::endBgmSituation(this, "HackBubble", false); + mUpperPunchFreeze = 0; + mPreviousTrans.set(al::getTrans(this)); + mGroundRotation.set(al::getQuat(this)); + + sead::Vector3f upDir; + al::calcUpDir(&upDir, this); + if (upDir.y < 0.99f) { + if (upDir.y < 0.0f) { + al::makeQuatRotationRate(&prevQuat, upDir, -sead::Vector3f::ey, 1.0f); + mGroundRotation = prevQuat * mGroundRotation; + sead::Quatf rotateAxis; + rotateAxis.setAxisRadian(sead::Vector3f::ey, -sead::Mathf::pi()); + mGroundRotation = mGroundRotation * rotateAxis; + } else { + al::makeQuatRotationRate(&prevQuat, upDir, sead::Vector3f::ey, 1.0f); + mGroundRotation = prevQuat * mGroundRotation; + } + } + prevQuat = al::getQuat(this) * quatZY; + al::setQuat(this, prevQuat); + } + // Note: This overrides any data that prevQuat or deltaQuat has + revertTargetQuatInHackJump(&prevQuat, &deltaQuat); + al::getVelocityPtr(this)->y -= gravity; + sead::Vector3f frontDir; + al::calcFrontDir(&frontDir, this); + sead::Vector3f hackerMoveVec = {0.0f, 0.0f, 0.0f}; + calcHackerMoveVec(&hackerMoveVec, sead::Vector3f::ey); + + bool flipDirection = false; + sead::Vector3f hackerMoveDir = {0.0f, 0.0f, 0.0f}; + if (al::tryNormalizeOrZero(&hackerMoveDir, hackerMoveVec)) { + // if angle between 140 and 220 degree => 180 +/- 40 + if (frontDir.dot(hackerMoveDir) < sead::Mathf::cos(sead::Mathf::deg2rad(220.0f))) { + sead::Vector3f vel = al::getVelocity(this); + vel.y = 0.0f; + if (vel.dot(hackerMoveDir) > 0.0f || al::isNearZero(vel, 1.0f)) { + al::addVelocity(this, -vel * 0.08f + hackerMoveDir * 0.57f); + flipDirection = true; + } else { + al::scaleVelocityHV(this, 0.85f, 1.0f); + flipDirection = true; + } + } else { + al::turnToDirection(this, hackerMoveDir, 5.0f); + + al::calcFrontDir(&frontDir, this); + + sead::Vector3f velH; + al::verticalizeVec(&velH, sead::Vector3f::ey, al::getVelocity(this)); + al::addVelocity(this, -velH); + f32 frontHSpeed = frontDir.dot(velH); + sead::Vector3f velHtoFront = (velH - frontDir * frontHSpeed) * 0.98f; + + f32 newFrontSpeed = + frontHSpeed * 0.92f + + frontAccel * sead::Mathf::clamp(frontDir.dot(hackerMoveDir), 0.0f, 1.0f); + + al::addVelocity(this, velHtoFront + + frontDir * sead::Mathf::clampMax(newFrontSpeed, frontHSpeed)); + flipDirection = false; + } + } + + mGroundRotation.set(al::getQuat(this)); + makeDisplayQuatInHackJump(prevQuat, deltaQuat, quatZY, flipDirection); + + if (isDropAttackCollision()) { + if (!al::sendMsgEnemyAttackFire(al::getCollidedGroundSensor(this), + al::getHitSensor(this, "Attack"), "LavaRed")) { + if (!rs::sendMsgBubbleAttack(al::getCollidedGroundSensor(this), + al::getHitSensor(this, "Attack"))) { + if (rs::sendMsgBubbleReflectV(al::getCollidedGroundSensor(this), + al::getHitSensor(this, "Attack"))) { + mJumpForce = {0.0f, 15.0f, 0.0f}; + al::setVelocityZeroV(this); + al::setNerve(this, &NrvBubble.HackJump); + return; + } + } + } + } + + if (isRiseAttackCollision()) { + if (al::sendMsgEnemyAttackFire(al::getCollidedCeilingSensor(this), + al::getHitSensor(this, "Attack"), "LavaRed")) + al::setVelocityZeroV(this); + else if (isHoldHackJump() && al::sendMsgEnemyUpperPunch(al::getCollidedCeilingSensor(this), + al::getHitSensor(this, "Attack"))) + mUpperPunchFreeze = 20; + else + al::setVelocityZeroV(this); + + if (mUpperPunchFreeze != 0) { + mUpperPunchFreeze--; + al::getVelocityPtr(this)->y += gravity; + } + } else { + mUpperPunchFreeze = 0; + } + + if (al::isCollidedWall(this)) { + rs::sendMsgBubbleWallTouch(al::getCollidedWallSensor(this), + al::getHitSensor(this, "Attack")); + } + + tryHitReactionThroughFence(); + tryShiftContinuousJump() || tryShiftLand(); +} + +void Bubble::endHackJump() { + al::setQuat(this, mGroundRotation); +} + +void Bubble::exeHackLand() { + if (al::isFirstStep(this)) { + if (al::isActionPlaying(this, "HackStart")) + al::startAction(this, "LandFrontUp"); + else if (al::isActionPlaying(this, "HackStartDown")) + al::startAction(this, "LandFrontDown"); + else + al::startAction(this, "Land"); + + if (!trySendMsgStartInSaucePan()) + al::startHitReaction(this, "憑依着地"); + + sead::Vector3f moveDir; + if (mIsPlayerCaptured) { + moveDir.set(0.0f, 0.0f, 0.0f); + sead::Vector3f* velPtr = al::getVelocityPtr(this); + al::parallelizeVec(velPtr, mFireNormal, *velPtr); + } else if (!rs::calcHackerMoveDir(&moveDir, mPlayerHack, sead::Vector3f::ey)) { + sead::Vector3f* velPtr = al::getVelocityPtr(this); + al::parallelizeVec(velPtr, mFireNormal, *velPtr); + } + + al::tryAddRippleMiddle(this); + mAirTime = 0; + al::startBgmSituation(this, "HackBubble", false, true); + mUpDir = mFireNormal; + mHackTurnFrame = 0; + mHackTurnDelay = 60; + } + + updateHackOnGround(); + if (!constrainLavaDomain()) + return; + + if (tryShiftFall()) + return; + + if (al::isGreaterEqualStep(this, 1) && isTriggerHackSwing()) { + mJumpForce.set({0.0f, 25.0f, 0.0f}); + al::setNerve(this, &NrvBubble.HackJumpHigh); + return; + } + + if (isTriggerHackJump()) { + mJumpForce.set({0.0f, 45.0f, 0.0f}); + al::setNerve(this, &NrvBubble.HackJump); + return; + } + + if (al::isActionEnd(this)) + al::setNerve(this, &NrvBubble.HackMove); +} + +void Bubble::exeHackInLauncher() { + tryHitReactionThroughFence(); + bool isHack2 = isHack(); + if (!isHack2 && mIsShowTutorial) { + rs::showTutorial(this); + mIsShowTutorial = false; + } + + if (tryShiftLand()) { + mBubbleStateInLauncher->kill(); + sead::Vector3f initialDirection = sead::Vector3f::ey; + initialDirection.rotate(al::getQuat(this)); + + sead::Quatf quat; + al::makeQuatRotationRate(&quat, initialDirection, sead::Vector3f::ey, 1.0f); + mGroundRotation = quat * al::getQuat(this); + + if (isOnDamageFire()) + al::startHitReaction(this, "バブルキャノン着地"); + else if (!isHack2) + al::startHitReaction(this, "地面での消滅"); + return; + } + + if (al::updateNerveState(this)) { + if (mPlayerHack) { + mJumpForce.set(0.0f, 0.0f, 0.0f); + al::startHitReaction(this, "バブルキャノン着地"); + al::setNerve(this, &NrvBubble.HackJump); + } else { + al::setNerve(this, &NrvBubble.WaitHackFall); + } + } +} + +void Bubble::endHackInLauncher() { + if (mLaunchCameraTicket) { + al::endCamera(this, mLaunchCameraTicket, -1, false); + mLaunchCameraTicket = nullptr; + } + + if (mIsShowTutorial) { + rs::showTutorial(this); + mIsShowTutorial = false; + } +} + +void Bubble::exeHackResetPos() { + sead::Quatf prevQuat; + sead::Quatf deltaQuat; + sead::Quatf quatZY = getQuatZY(); + + if (al::isFirstStep(this)) { + sead::Vector3f deltaPos = mResetTargetPos - al::getTrans(this); + sead::Vector3f newVelocity = deltaPos; + newVelocity.y = 0.0f; + normalize2(&newVelocity, newVelocity.length() / 180.0f); + newVelocity.y = deltaPos.y / 180.0f + 99.0f; + + al::setVelocity(this, newVelocity); + al::startAction(this, "HackJump"); + sead::Vector3f direction = al::getTrans(this) - mResetTargetPos; + al::tryNormalizeOrZero(&direction); + al::faceToDirection(this, direction); + mPreviousTrans.set(al::getTrans(this)); + mGroundRotation.set(al::getQuat(this)); + + al::setQuat(this, al::getQuat(this) * quatZY); + } + + al::addVelocityToGravity(this, 1.1f); + + revertTargetQuatInHackJump(&prevQuat, &deltaQuat); + makeDisplayQuatInHackJump(prevQuat, deltaQuat, quatZY, true); + + if (al::isGreaterEqualStep(this, 180)) { + sead::Vector3f firePos; + sead::Vector3f fireNormal; + if (!al::calcFindFireSurface(&firePos, &fireNormal, this, al::getTrans(this), + sead::Vector3f::ey, 800.0f)) { + mJumpForce.set(0.0f, 0.0f, 0.0f); + al::setVelocityZero(this); + al::setNerve(this, &NrvBubble.HackJump); + } else { + sead::Vector3f trans = al::getTrans(this); + trans.y = firePos.y; + al::resetPosition(this, trans); + mJumpForce.set(0.0f, 0.0f, 0.0f); + al::setVelocityZero(this); + mIsOnLavaSurface = true; + al::tryUpdateEffectMaterialCode(this, al::getFireMaterialCode(this)); + mFireNormal.set(fireNormal); + al::setNerve(this, &NrvBubble.HackLand); + } + } +} + +void Bubble::exeHackDemo() {} + +void Bubble::exeRevive() { + if (al::isFirstStep(this)) { + rs::startReset(this); + al::stopAction(this); + al::resetPosition(this, mRevivePosition); + al::setQuat(this, mStartingRotation); + mGroundRotation.set(mStartingRotation); + mStartActionPosition = mReviveStartActionPosition; + mAnimScaleController->resetScale(); + mIsInSaucePan = false; + } + + if (mReviveDelayCount == mReviveDelayTime - 1) { + rs::endReset(this); + al::setNerve(this, &NrvBubble.Ready); + return; + } +} diff --git a/src/Enemy/Bubble.h b/src/Enemy/Bubble.h new file mode 100644 index 00000000..026e0033 --- /dev/null +++ b/src/Enemy/Bubble.h @@ -0,0 +1,202 @@ +#pragma once + +#include +#include +#include +#include + +#include "Library/LiveActor/LiveActor.h" + +namespace al { +struct ActorInitInfo; +class ActorMatrixCameraTarget; +class AnimScaleController; +class CameraTicket; +class CollisionPartsFilterSpecialPurpose; +class HitSensor; +class JointSpringControllerHolder; +class SensorMsg; +class Triangle; +} // namespace al + +class CapTargetInfo; +class IUsePlayerHack; +class DisregardReceiver; +class PlayerHackStartShaderCtrl; +class BubbleStateInLauncher; + +class Bubble : public al::LiveActor { +public: + Bubble(const char* name); + + void init(const al::ActorInitInfo& info) override; + void initAfterPlacement() override; + void attackSensor(al::HitSensor* self, al::HitSensor* other) override; + bool receiveMsg(const al::SensorMsg* message, al::HitSensor* other, + al::HitSensor* self) override; + void control() override; + void updateCollider() override; + void appear() override; + void calcAnim() override; + + void recalcClippingInfo(); + void start(); + void killAll(); + bool isIgnoreTriangle(const al::Triangle& triangle) const; + void checkEffectPos(); + bool isValidCollisionOrWaveCheck() const; + bool isHack() const; + void revive(); + void updateLavaWave(); + void updateScrollAnimRate(); + bool isOnGround() const; + void setupHack(); + bool isCurrentNerveEnableLockOnAndStartHack() const; + void offGroupClipping(); + void startHackLocal(al::HitSensor* self, al::HitSensor* other); + bool isInvalidHackEscape() const; + void prepareEndHack(); + void endHackByCancel(); + void forceEndHack(); + void updatePosOnWave(); + void updateVelocityIfValidCollision(); + bool isOnDamageFire() const; + void tryStartHitReactionUp(); + void updateShadowMaskLength(); + void tryStartHitReactionDown(); + void shiftSink(); + void headUp(); + void shiftReadyOrMove(); + bool trySendMsgStartInSaucePan(); + void tryHitReactionThroughFence(); + bool tryShiftLand(); + bool isHoldHackAction() const; + bool tryBoundMoveWall(); + void updateHackOnGround(); + bool constrainLavaDomain(); + bool tryShiftFall(); + bool isTriggerHackSwing() const; + void faceToMoveVec(); + bool isTriggerHackJump() const; + void revertTargetQuatInHackJump(sead::Quatf* quatA, sead::Quatf* quatB); + void calcHackerMoveVec(sead::Vector3f* moveVec, const sead::Vector3f& inputDir) const; + void makeDisplayQuatInHackJump(const sead::Quatf& quatA, const sead::Quatf& quatB, + const sead::Quatf& quatC, bool isValue); + bool isDropAttackCollision() const; + bool isRiseAttackCollision() const; + bool isHoldHackJump() const; + bool tryShiftContinuousJump(); + bool calcHackerMoveDir(sead::Vector3f* moveDir, const sead::Vector3f& inputDir) const; + bool isWaitingLaunch() const; + void launch(const sead::Vector3f& dir, f32 force, al::CameraTicket* cameraTicket); + void launchCancel(const sead::Vector3f& dir); + void showHackCap(); + void hideHackCap(); + bool isTriggerJump() const; + bool isTriggerAction() const; + bool isTriggerHackAction() const; + bool isTriggerCancel() const; + void startJumpAnim(f32 force); + void resetAndAppear(const sead::Vector3f& trans, const sead::Quatf& quat, f32 unused); + void endHackCommon(); + void calcLaunchPos(sead::Vector3f* pos, const sead::Vector3f& vecA, f32 valA, f32 valB) const; + void onGroupClipping(); + bool isOnGroundNoVelocity() const; + void updateCollisionPartsMove(); + void accelStick(); + bool addHackActorAccelStick(sead::Vector3f* stickAccel, f32 scale, const sead::Vector3f& dir); + bool isGroundOverTheWave(bool checkHeight, const sead::Vector3f& wavePos) const; + bool isEnableSnapWaveSurface() const; + + void exeStandBy(); + void exeDelay(); + void exeRailMove(); + void exeReady(); + void exeUp(); + void exeTurn(); + void exeDown(); + void exeSink(); + void exeWait(); + void exeDie(); + void exeWaitHack(); + void exeWaitHackStart(); + void endWaitHackStart(); + void exeWaitHackFall(); + void exeHackFall(); + void exeHackMove(); + void endHackMove(); + void exeHackJump(); + void endHackJump(); + void exeHackLand(); + void exeHackInLauncher(); + void endHackInLauncher(); + void exeHackResetPos(); + void exeHackDemo(); + void exeRevive(); + +private: + sead::Vector3f mStartActionPosition = {0.0f, 0.0f, 0.0f}; + f32 mJumpHeight = 0.0f; + s32 mTurnFrameDelay = 0; + u32 mTurnFrameCount = 0; + f32 mRailTotalLength = 0.0f; + s32 mRailMoveFrame = 0; + s32 mWaitFrameNum = 0; + s32 mDelayFrameNum = 0; + sead::Vector3f mClippingPos = {0.0f, 0.0f, 0.0f}; + u32 mReviveDelayCount = 0; + u32 mReviveDelayTime = 0; + sead::Vector3f mJumpForce = {0.0f, 0.0f, 0.0f}; + IUsePlayerHack* mPlayerHack = nullptr; + CapTargetInfo* mCapTargetInfo = nullptr; + u32 mAirTime = 0; + sead::Quatf mStartingRotation = sead::Quatf::unit; + sead::Vector3f mRevivePosition = {0.0f, 0.0f, 0.0f}; + sead::Vector3f mReviveStartActionPosition = {0.0f, 0.0f, 0.0f}; + bool mIsOnLavaSurface = false; + sead::Vector3f mLandVelocity = {0.0f, 0.0f, 0.0f}; + BubbleStateInLauncher* mBubbleStateInLauncher = nullptr; + al::CameraTicket* mLaunchCameraTicket = nullptr; + al::ActorMatrixCameraTarget* mActorMatrixCameraTarget = nullptr; + sead::Matrix34f mCameraMtx = sead::Matrix34f::ident; + sead::Vector3f mStickForce = {0.0f, 0.0f, 0.0f}; + s32 mUpperPunchFreeze = 0; + sead::Quatf mGroundRotation = sead::Quatf::unit; + sead::Vector3f mUpDir = sead::Vector3f::ey; + f32 mHackTurnAngle = 0.0f; + u32 mHackTurnFrame = 1; + u32 mHackTurnDelay = 1; + sead::Vector3f mFireNormal = sead::Vector3f::ey; + al::LiveActor* mClippingProbeActor = nullptr; + bool mIsClipped = false; + s32 mWaitReviveDelay = 0; + sead::Vector3f mPreviousTrans = {0.0f, 0.0f, 0.0f}; + sead::Matrix34f _24c = sead::Matrix34f::ident; + bool mIsWaveCheckOn = false; + sead::Quatf mRailTargetRotation = sead::Quatf::unit; + sead::Vector3f mShadowMaskOffset = {0.0f, 0.0f, 0.0f}; + f32 mShadowMaskDropLength = 0; + s32 mCancelLaunchCooldown = 0; + sead::Vector3f mColliderGroundOffset = {0.0f, 0.0f, 0.0f}; + u32 mContinuousJumpDelay = 15; + al::CollisionPartsFilterSpecialPurpose* mCollisionPartsFilter = nullptr; + bool mIsCheckEffectPosRequired = true; + sead::Vector3f mSurfaceProbePos[2]; + bool mIsOnHitReactionEffect = true; + sead::Matrix34f mLavaSurfaceMtx = sead::Matrix34f::ident; + const char* mMaterialCode = ""; + sead::Vector3f mResetTargetPos = {0.0f, 0.0f, 0.0f}; + bool mIsPlayerCaptured = false; + bool mIsInBossSequence = false; + bool mIsHackOnGround = false; + DisregardReceiver* mDisregardReceiver = nullptr; + PlayerHackStartShaderCtrl* mPlayerHackStartShaderCtrl = nullptr; + al::JointSpringControllerHolder* mJointSpringControllerHolder = nullptr; + const char* mCurrentActionName = ""; + al::AnimScaleController* mAnimScaleController = nullptr; + bool mIsInSaucePan = false; + bool mIsShowTutorial = false; + bool mIsOnGroupClipping = true; +}; + +static_assert(sizeof(Bubble) == 0x358); diff --git a/src/Scene/ProjectActorFactory.cpp b/src/Scene/ProjectActorFactory.cpp index c457542f..9461e758 100644 --- a/src/Scene/ProjectActorFactory.cpp +++ b/src/Scene/ProjectActorFactory.cpp @@ -30,6 +30,7 @@ #include "Boss/BarrierField.h" #include "Boss/BossForest/BossForestWander.h" #include "Boss/Mofumofu/MofumofuScrap.h" +#include "Enemy/Bubble.h" #include "Enemy/Gamane.h" #include "Enemy/KaronWing.h" #include "Enemy/Kuribo2D.h" @@ -137,7 +138,7 @@ const al::NameToCreator sProjectActorFactoryEntries[] {"BossRaidRivet", nullptr}, {"BreakablePole", nullptr}, {"Breeda", nullptr}, - {"Bubble", nullptr}, + {"Bubble", al::createActorFunction}, {"Bubble2D", nullptr}, {"BubbleLauncher", nullptr}, {"Bull", nullptr},