
/**
 * Behavior for bhvKoopa and bhvKoopaRaceEndpoint.
 * bhvKoopa includes normal, unshelled, tiny, and Koopa the Quick.
 * When the race begins, koopa the quick sets his parent to bhvKoopaRaceEndpoint
 * which assists in determining the state of the race. It is positioned at the
 * flag.
 */

/**
 * Hitbox for koopa - this is used for every form except Koopa the Quick, which
 * uses a hardcoded soft hitbox.
 */
static struct ObjectHitbox sKoopaHitbox = {
    /* interactType:      */ INTERACT_KOOPA,
    /* downOffset:        */ 0,
    /* damageOrCoinValue: */ 0,
    /* health:            */ 0,
    /* numLootCoins:      */ -1,
    /* radius:            */ 60,
    /* height:            */ 40,
    /* hurtboxRadius:     */ 40,
    /* hurtboxHeight:     */ 30,
};

/**
 * Attack handlers for unshelled koopa and tiny shelled koopa.
 */
static u8 sKoopaUnshelledAttackHandlers[] = {
    /* ATTACK_PUNCH:                 */ ATTACK_HANDLER_KNOCKBACK,
    /* ATTACK_KICK_OR_TRIP:          */ ATTACK_HANDLER_KNOCKBACK,
    /* ATTACK_FROM_ABOVE:            */ ATTACK_HANDLER_SQUISHED,
    /* ATTACK_GROUND_POUND_OR_TWIRL: */ ATTACK_HANDLER_SQUISHED,
    /* ATTACK_FAST_ATTACK:           */ ATTACK_HANDLER_KNOCKBACK,
    /* ATTACK_FROM_BELOW:            */ ATTACK_HANDLER_KNOCKBACK,
};

/**
 * Attack handlers for regular sized shelled koopa.
 */
static u8 sKoopaShelledAttackHandlers[] = {
    /* ATTACK_PUNCH:                 */ ATTACK_HANDLER_SPECIAL_KOOPA_LOSE_SHELL,
    /* ATTACK_KICK_OR_TRIP:          */ ATTACK_HANDLER_SPECIAL_KOOPA_LOSE_SHELL,
    /* ATTACK_FROM_ABOVE:            */ ATTACK_HANDLER_SPECIAL_KOOPA_LOSE_SHELL,
    /* ATTACK_GROUND_POUND_OR_TWIRL: */ ATTACK_HANDLER_SPECIAL_KOOPA_LOSE_SHELL,
    /* ATTACK_FAST_ATTACK:           */ ATTACK_HANDLER_SPECIAL_KOOPA_LOSE_SHELL,
    /* ATTACK_FROM_BELOW:            */ ATTACK_HANDLER_SPECIAL_KOOPA_LOSE_SHELL,
};

/**
 * Data to control the behavior of each instance of Koopa the Quick.
 */
struct KoopaTheQuickProperties {
    s16 initText;
    s16 winText;
    void const *path;
    Vec3s starPos;
};

/**
 * Properties for the BoB race and the THI race.
 */
static struct KoopaTheQuickProperties sKoopaTheQuickProperties[] = {
    { DIALOG_005, DIALOG_007, bob_seg7_trajectory_koopa, { 3030, 4500, -4600 } },
    { DIALOG_009, DIALOG_031, thi_seg7_trajectory_koopa, { 7100, -1300, -6000 } }
};

/**
 * Initialization function.
 */
void bhv_koopa_init(void) {
    if ((o->oKoopaMovementType = o->oBehParams2ndByte) == KOOPA_BP_TINY) {
        // Tiny koopa in THI
        o->oKoopaMovementType = KOOPA_BP_NORMAL;
        o->oKoopaAgility = 1.6f / 3.0f;
        o->oDrawingDistance = 1500.0f;
        cur_obj_scale(0.8f);
        o->oGravity = -6.4f / 3.0f;
    } else if (o->oKoopaMovementType >= KOOPA_BP_KOOPA_THE_QUICK_BASE) {
        // Koopa the Quick. Race index is 0 for BoB and 1 for THI
        o->oKoopaTheQuickRaceIndex = o->oKoopaMovementType - KOOPA_BP_KOOPA_THE_QUICK_BASE;
        o->oKoopaAgility = 4.0f;
        cur_obj_scale(3.0f);
    } else {
        o->oKoopaAgility = 1.0f;
    }
}

/**
 * Play the appropriate footstep sound on the two provided animation frames.
 */
static void koopa_play_footstep_sound(s8 animFrame1, s8 animFrame2) {
    s32 sound;
    if (o->header.gfx.scale[0] > 1.5f) {
        sound = SOUND_OBJ_KOOPA_THE_QUICK_WALK;
    } else {
        sound = SOUND_OBJ_KOOPA_WALK;
    }

    cur_obj_play_sound_at_anim_range(animFrame1, animFrame2, sound);
}

/**
 * If mario is close to koopa, and koopa is facing toward mario, then begin
 * running away.
 */
static s32 koopa_check_run_from_mario(void) {
    if (o->oKoopaDistanceToMario < 300.0f
        && abs_angle_diff(o->oKoopaAngleToMario, o->oMoveAngleYaw) < 0x3000) {
        o->oAction = KOOPA_SHELLED_ACT_RUN_FROM_MARIO;
        return TRUE;
    }

    return FALSE;
}

/**
 * Stay still for a while, then change target yaw by 45 degrees and begin
 * walking.
 */
static void koopa_shelled_act_stopped(void) {
    o->oForwardVel = 0.0f;
    if (cur_obj_init_anim_and_check_if_end(7)) {
        o->oAction = KOOPA_SHELLED_ACT_WALK;
        o->oKoopaTargetYaw = o->oMoveAngleYaw + 0x2000 * (s16) random_sign();
    }
}

/**
 * Begin walking, then increment sub-action.
 */
static void koopa_walk_start(void) {
    obj_forward_vel_approach(3.0f * o->oKoopaAgility, 0.3f * o->oKoopaAgility);

    if (cur_obj_init_anim_and_check_if_end(11)) {
        o->oSubAction += 1;
        o->oKoopaCountdown = random_linear_offset(30, 100);
    }
}

/**
 * Walk until oKoopaCountdown hits zero, then increment sub-action.
 */
static void koopa_walk(void) {
    cur_obj_init_animation_with_sound(9);
    koopa_play_footstep_sound(2, 17);

    if (o->oKoopaCountdown != 0) {
        o->oKoopaCountdown -= 1;
    } else if (cur_obj_check_if_near_animation_end()) {
        o->oSubAction += 1;
    }
}

/**
 * Stop walking, then enter action KOOPA_SHELLED_ACT_STOPPED.
 */
static void koopa_walk_stop(void) {
    obj_forward_vel_approach(0.0f, 1.0f * o->oKoopaAgility);
    if (cur_obj_init_anim_and_check_if_end(10)) {
        o->oAction = KOOPA_SHELLED_ACT_STOPPED;
    }
}

/**
 * Walk for a while, then come to a stop. During this time, turn toward the
 * target yaw.
 */
static void koopa_shelled_act_walk(void) {
    if (o->oKoopaTurningAwayFromWall) {
        o->oKoopaTurningAwayFromWall = obj_resolve_collisions_and_turn(o->oKoopaTargetYaw, 0x200);
    } else {
        // If far from home, then begin turning toward home
        if (o->oDistanceToMario >= 25000.0f) {
            o->oKoopaTargetYaw = o->oAngleToMario;
        }

        o->oKoopaTurningAwayFromWall = obj_bounce_off_walls_edges_objects(&o->oKoopaTargetYaw);
        cur_obj_rotate_yaw_toward(o->oKoopaTargetYaw, 0x200);
    }

    switch (o->oSubAction) {
        case KOOPA_SHELLED_SUB_ACT_START_WALK:
            koopa_walk_start();
            break;
        case KOOPA_SHELLED_SUB_ACT_WALK:
            koopa_walk();
            break;
        case KOOPA_SHELLED_SUB_ACT_STOP_WALK:
            koopa_walk_stop();
            break;
    }

    koopa_check_run_from_mario();
}

/**
 * Run while turning away from mario. Come to a stop once mario is far enough
 * away.
 */
static void koopa_shelled_act_run_from_mario(void) {
    cur_obj_init_animation_with_sound(1);
    koopa_play_footstep_sound(0, 11);

    // If far from home, run toward it
    if (o->oDistanceToMario >= 25000.0f) {
        o->oAngleToMario += 0x8000;
        o->oDistanceToMario = 0.0f;
    }

    if (o->oTimer > 30 && o->oDistanceToMario > 800.0f) {
        if (obj_forward_vel_approach(0.0f, 1.0f)) {
            o->oAction = KOOPA_SHELLED_ACT_STOPPED;
        }
    } else {
        cur_obj_rotate_yaw_toward(o->oAngleToMario + 0x8000, 0x400);
        obj_forward_vel_approach(17.0f, 1.0f);
    }
}

/**
 * If on the ground, decelerate. Generate dust if moving fast enough.
 */
static void koopa_dive_update_speed(f32 decel) {
    if (o->oMoveFlags & OBJ_MOVE_MASK_ON_GROUND) {
        obj_forward_vel_approach(0.0f, decel);
        if (o->oForwardVel > 5.0f) {
            if (!(o->oTimer % 4)) {
                spawn_object_with_scale(o, MODEL_SMOKE, bhvWhitePuffSmoke2, 1.0f);
            }
        }
    }
}

/**
 * Slide on the ground and then come to a stop.
 */
static void koopa_shelled_act_lying(void) {
    if (o->oForwardVel != 0.0f) {
        if (o->oMoveFlags & OBJ_MOVE_HIT_WALL) {
            o->oMoveAngleYaw = cur_obj_reflect_move_angle_off_wall();
        }

        cur_obj_init_anim_extend(5);
        koopa_dive_update_speed(0.3f);
    } else if (o->oKoopaCountdown != 0) {
        o->oKoopaCountdown -= 1;
        cur_obj_extend_animation_if_at_end();
    } else if (cur_obj_init_anim_and_check_if_end(6)) {
        o->oAction = KOOPA_SHELLED_ACT_STOPPED;
    }
}

/**
 * Attack handler for regular-sized shelled koopa.
 * Lose shell and enter lying action.
 */
void shelled_koopa_attack_handler(s32 attackType) {
    if (o->header.gfx.scale[0] > 0.8f) {
        cur_obj_play_sound_2(SOUND_OBJ_KOOPA_DAMAGE);

        o->oKoopaMovementType = KOOPA_BP_UNSHELLED;
        o->oAction = KOOPA_UNSHELLED_ACT_LYING;
        o->oForwardVel = 20.0f;

        // If attacked from the side, get knocked away from mario
        if (attackType != ATTACK_FROM_ABOVE && attackType != ATTACK_GROUND_POUND_OR_TWIRL) {
            o->oMoveAngleYaw = obj_angle_to_object(gMarioObject, o);
        }

        cur_obj_set_model(MODEL_KOOPA_WITHOUT_SHELL);
        spawn_object(o, MODEL_KOOPA_SHELL, bhvKoopaShell);

        //! Because bob-ombs/corkboxes come after koopa in processing order,
        //  they can interact with the koopa on the same frame that this
        //  happens. This causes the koopa to die immediately.
        cur_obj_become_intangible();
    } else {
        // Die if tiny koopa
        obj_die_if_health_non_positive();
    }
}

/**
 * Update function for both regular and tiny shelled koopa.
 */
static void koopa_shelled_update(void) {
    cur_obj_update_floor_and_walls();
    obj_update_blinking(&o->oKoopaBlinkTimer, 20, 50, 4);

    switch (o->oAction) {
        case KOOPA_SHELLED_ACT_STOPPED:
            koopa_shelled_act_stopped();
            koopa_check_run_from_mario();
            break;

        case KOOPA_SHELLED_ACT_WALK:
            koopa_shelled_act_walk();
            break;

        case KOOPA_SHELLED_ACT_RUN_FROM_MARIO:
            koopa_shelled_act_run_from_mario();
            break;

        case KOOPA_SHELLED_ACT_LYING:
            koopa_shelled_act_lying();
            break;
    }

    if (o->header.gfx.scale[0] > 0.8f) {
        obj_handle_attacks(&sKoopaHitbox, o->oAction, sKoopaShelledAttackHandlers);
    } else {
        // If tiny koopa, die after attacking mario.
        obj_handle_attacks(&sKoopaHitbox, KOOPA_SHELLED_ACT_DIE, sKoopaUnshelledAttackHandlers);
        if (o->oAction == KOOPA_SHELLED_ACT_DIE) {
            obj_die_if_health_non_positive();
        }
    }

    cur_obj_move_standard(-78);
}

/**
 * Attempt to run toward the shell if it exists while still running away from
 * mario. If the shell doesn't exist, run around randomly.
 * When close enough to the shell and with good angle and speed, enter the dive
 * action.
 */
static void koopa_unshelled_act_run(void) {
    f32 distToShell = 99999.0f;
    struct Object *shell;

    cur_obj_init_animation_with_sound(3);
    koopa_play_footstep_sound(0, 6);

    if (o->oKoopaTurningAwayFromWall) {
        o->oKoopaTurningAwayFromWall = obj_resolve_collisions_and_turn(o->oKoopaTargetYaw, 0x600);
    } else {
        // If far from home, then turn toward home
        if (o->oDistanceToMario >= 25000.0f) {
            o->oKoopaTargetYaw = o->oAngleToMario;
        }

        // If shell exists, then turn toward shell
        shell = cur_obj_find_nearest_object_with_behavior(bhvKoopaShell, &distToShell);
        if (shell != NULL) {
            //! This overrides turning toward home
            o->oKoopaTargetYaw = obj_angle_to_object(o, shell);
        } else if (!(o->oKoopaTurningAwayFromWall =
                         obj_bounce_off_walls_edges_objects(&o->oKoopaTargetYaw))) {
            // Otherwise run around randomly
            if (o->oKoopaUnshelledTimeUntilTurn != 0) {
                o->oKoopaUnshelledTimeUntilTurn -= 1;
            } else {
                o->oKoopaTargetYaw = obj_random_fixed_turn(0x2000);
            }
        }

        // If mario is far away, or our running away from mario coincides with
        // running toward the shell
        if (o->oDistanceToMario > 800.0f
            || (shell != NULL
                && abs_angle_diff(o->oKoopaTargetYaw, o->oAngleToMario + 0x8000) < 0x2000)) {
            // then turn toward the shell
            cur_obj_rotate_yaw_toward(o->oKoopaTargetYaw, 0x600);
        } else {
            // otherwise continue running from mario
            cur_obj_rotate_yaw_toward(o->oAngleToMario + 0x8000, 0x600);
        }
    }

    // If we think we have a shot, dive for the shell
    if (obj_forward_vel_approach(20.0f, 1.0f) && distToShell < 600.0f
        && abs_angle_diff(o->oKoopaTargetYaw, o->oMoveAngleYaw) < 0xC00) {
        o->oMoveAngleYaw = o->oKoopaTargetYaw;
        o->oAction = KOOPA_UNSHELLED_ACT_DIVE;
        o->oForwardVel *= 1.2f;
        o->oVelY = distToShell / 20.0f;
        o->oKoopaCountdown = 20;
    }
}

/**
 * Dive and slide along the ground. If close enough to the shell, pick it up,
 * and otherwise enter the running action.
 */
static void koopa_unshelled_act_dive(void) {
    struct Object *shell;
    f32 distToShell;

    if (o->oTimer > 10) {
        cur_obj_become_tangible();
    }

    if (o->oTimer > 10) {
        shell = cur_obj_find_nearest_object_with_behavior(bhvKoopaShell, &distToShell);

        // If we got the shell and mario didn't, put on the shell
        //! The shell comes after koopa in processing order, and the shell is
        //  responsible for positioning itself under mario.
        //  If mario has more than 200 speed and is riding the shell, then
        //  from the perspective of the koopa, the shell is always lagging 200
        //  units behind mario.
        //  Using this, we can get the koopa to pick up and despawn its shell
        //  while mario is riding it.
        if (shell != NULL && dist_between_objects(shell, gMarioObject) > 200.0f
            && distToShell < 50.0f) {
            o->oKoopaMovementType = KOOPA_BP_NORMAL;
            o->oAction = KOOPA_SHELLED_ACT_LYING;
            o->oForwardVel *= 0.5f;

            cur_obj_set_model(MODEL_KOOPA_WITH_SHELL);
            obj_mark_for_deletion(shell);
            goto end;
        }
    }

    if (o->oForwardVel != 0.0f) {
        if (o->oAction == KOOPA_UNSHELLED_ACT_LYING) {
            o->oAnimState = 1;
            cur_obj_init_anim_extend(2);
        } else {
            cur_obj_init_anim_extend(5);
        }
        koopa_dive_update_speed(0.5f);
    } else if (o->oKoopaCountdown != 0) {
        o->oKoopaCountdown -= 1;
        cur_obj_extend_animation_if_at_end();
    } else if (cur_obj_init_anim_and_check_if_end(6)) {
        o->oAction = KOOPA_UNSHELLED_ACT_RUN;
    }

end:;
}

/**
 * Unused action function.
 */
static void koopa_unshelled_act_unused3(void) {
    cur_obj_init_anim_extend(0);
}

/**
 * Update function for koopa after losing his shell.
 */
static void koopa_unshelled_update(void) {
    cur_obj_update_floor_and_walls();
    obj_update_blinking(&o->oKoopaBlinkTimer, 10, 15, 3);

    switch (o->oAction) {
        case KOOPA_UNSHELLED_ACT_RUN:
            koopa_unshelled_act_run();
            break;
        case KOOPA_UNSHELLED_ACT_DIVE:
        case KOOPA_UNSHELLED_ACT_LYING:
            koopa_unshelled_act_dive();
            break;
        case KOOPA_UNSHELLED_ACT_UNUSED3:
            koopa_unshelled_act_unused3();
            break;
    }

    obj_handle_attacks(&sKoopaHitbox, o->oAction, sKoopaUnshelledAttackHandlers);
    cur_obj_move_standard(-78);
}

/**
 * Wait 50 frames, then play the race starting sound, disable time stop, and
 * optionally begin the timer.
 */
s32 obj_begin_race(s32 noTimer) {
    if (o->oTimer == 50) {
        cur_obj_play_sound_2(SOUND_GENERAL_RACE_GUN_SHOT);

        if (!noTimer) {
            play_music(SEQ_PLAYER_LEVEL, SEQUENCE_ARGS(4, SEQ_LEVEL_SLIDE), 0);

            level_control_timer(TIMER_CONTROL_SHOW);
            level_control_timer(TIMER_CONTROL_START);

            o->parentObj->oKoopaRaceEndpointRaceBegun = TRUE;
        }

        // Unfreeze mario and disable time stop to begin the race
        set_mario_npc_dialog(0);
        disable_time_stop_including_mario();
    } else if (o->oTimer > 50) {
        return TRUE;
    }

    return FALSE;
}

/**
 * Wait for mario to approach, and then enter the show init text action.
 */
static void koopa_the_quick_act_wait_before_race(void) {
    koopa_shelled_act_stopped();

    if (o->oKoopaTheQuickInitTextboxCooldown != 0) {
        o->oKoopaTheQuickInitTextboxCooldown -= 1;
    } else if (cur_obj_can_mario_activate_textbox_2(400.0f, 400.0f)) {
        //! The next action doesn't execute until next frame, giving mario one
        //  frame where he can jump, and thus no longer be ready to speak.
        //  (On J, he has two frames and doing this enables time stop - see
        //  cur_obj_update_dialog_with_cutscene for that glitch)
        o->oAction = KOOPA_THE_QUICK_ACT_SHOW_INIT_TEXT;
        o->oForwardVel = 0.0f;
        cur_obj_init_animation_with_sound(7);
    }
}

/**
 * Display the dialog asking mario if he wants to race. Begin the race or
 * return to the waiting action.
 */
static void koopa_the_quick_act_show_init_text(void) {
    s32 response = obj_update_race_proposition_dialog(
        sKoopaTheQuickProperties[o->oKoopaTheQuickRaceIndex].initText);
    UNUSED s32 unused;

    if (response == 1) {
        gMarioShotFromCannon = FALSE;
        o->oAction = KOOPA_THE_QUICK_ACT_RACE;
        o->oForwardVel = 0.0f;

        o->parentObj = cur_obj_nearest_object_with_behavior(bhvKoopaRaceEndpoint);
        o->oPathedStartWaypoint = o->oPathedPrevWaypoint =
            segmented_to_virtual(sKoopaTheQuickProperties[o->oKoopaTheQuickRaceIndex].path);

        o->oKoopaTurningAwayFromWall = FALSE;
        o->oFlags |= OBJ_FLAG_ACTIVE_FROM_AFAR;
        ;
    } else if (response == 2) {
        o->oAction = KOOPA_THE_QUICK_ACT_WAIT_BEFORE_RACE;
        o->oKoopaTheQuickInitTextboxCooldown = 60;
    }
}

/**
 * If there is a bowling ball nearby, either slow down, or return +/-1 to
 * indicate that the ball is likely to collide.
 */
static s32 koopa_the_quick_detect_bowling_ball(void) {
    struct Object *ball;
    f32 distToBall;
    s16 angleToBall;
    f32 ballSpeedInKoopaRunDir;

    ball = cur_obj_find_nearest_object_with_behavior(bhvBowlingBall, &distToBall);
    if (ball != NULL) {
        angleToBall = obj_turn_toward_object(o, ball, O_MOVE_ANGLE_YAW_INDEX, 0);
        ballSpeedInKoopaRunDir = ball->oForwardVel * coss(ball->oMoveAngleYaw - o->oMoveAngleYaw);

        if (abs_angle_diff(o->oMoveAngleYaw, angleToBall) < 0x4000) {
            // The ball is in front of ktq

            if (distToBall < 400.0f) {
                if (ballSpeedInKoopaRunDir < o->oForwardVel * 0.7f) {
                    // The ball is moving slowly or toward him
                    return 1;
                } else {
                    // The ball is moving relatively quickly, so we'll just
                    // slow down a bit
                    //! This can go negative and is unbounded. If placed next to
                    //  oob, ktq can get PU speed.
                    o->oForwardVel -= 2.0f;
                }
            }
        } else if (distToBall < 300.0f && ballSpeedInKoopaRunDir > o->oForwardVel) {
            // The ball is coming at ktq from behind, and it's moving faster
            // than him
            return -1;
        }
    }

    return 0;
}

/**
 * Play the running animation at a speed proportional to ktq's forward velocity.
 * If he is moving backward, then the animation will play backward.
 */
static void koopa_the_quick_animate_footsteps(void) {
    //! With high negative speed (using the bowling ball deceleration), we can
    //  index out of the animation's bounds
    cur_obj_init_animation_with_accel_and_sound(9, o->oForwardVel * 0.09f);
    koopa_play_footstep_sound(2, 17);
}

/**
 * Begin the race, then follow the race path. Avoid bowling balls by slowing
 * down or jumping. After finishing the race, enter the decelerate action.
 */
static void koopa_the_quick_act_race(void) {
    f32 downhillSteepness;
    s32 bowlingBallStatus;

    if (obj_begin_race(FALSE)) {
        // Hitbox is slightly larger while racing
        cur_obj_push_mario_away_from_cylinder(180.0f, 300.0f);

        if (cur_obj_follow_path(0) == PATH_REACHED_END) {
            o->oAction = KOOPA_THE_QUICK_ACT_DECELERATE;
        } else {
            downhillSteepness = 1.0f + sins((s16)(f32) o->oPathedTargetPitch);
            cur_obj_rotate_yaw_toward(o->oPathedTargetYaw, (s32)(o->oKoopaAgility * 150.0f));

            switch (o->oSubAction) {
                case KOOPA_THE_QUICK_SUB_ACT_START_RUN:
                    koopa_walk_start();
                    break;

                case KOOPA_THE_QUICK_SUB_ACT_RUN:
                    koopa_the_quick_animate_footsteps();

                    if (o->parentObj->oKoopaRaceEndpointRaceStatus != 0 && o->oDistanceToMario > 1500.0f
                        && (o->oPathedPrevWaypointFlags & WAYPOINT_MASK_00FF) < 28) {
                        // Move faster if mario has already finished the race or
                        // cheated by shooting from cannon
                        o->oKoopaAgility = 8.0f;
                    } else if (o->oKoopaTheQuickRaceIndex != KOOPA_THE_QUICK_BOB_INDEX) {
                        o->oKoopaAgility = 6.0f;
                    } else {
                        o->oKoopaAgility = 4.0f;
                    }

                    obj_forward_vel_approach(o->oKoopaAgility * 6.0f * downhillSteepness,
                                             o->oKoopaAgility * 0.1f);

                    // Move upward if we hit a wall, to climb it
                    if (o->oMoveFlags & OBJ_MOVE_HIT_WALL) {
                        o->oVelY = 20.0f;
                    }

                    // If we're about to collide with a bowling ball or we hit an
                    // edge, jump
                    bowlingBallStatus = koopa_the_quick_detect_bowling_ball();
                    if (bowlingBallStatus != 0 || (o->oMoveFlags & OBJ_MOVE_HIT_EDGE)) {
                        // If the ball is coming at us from behind, then set speed
                        // to zero to let it move under and past us
                        if (bowlingBallStatus < 0) {
                            o->oForwardVel = 0.0f;
                        }

                        if (bowlingBallStatus != 0
                            || (o->oPathedPrevWaypointFlags & WAYPOINT_MASK_00FF) >= 8) {
                            o->oVelY = 80.0f;
                        } else {
                            o->oVelY = 40.0f;
                        }

                        o->oGravity = -6.0f;
                        o->oSubAction = 2;
                        o->oMoveFlags = 0;

                        cur_obj_init_animation_with_sound(12);
                    }
                    break;

                case KOOPA_THE_QUICK_SUB_ACT_JUMP:
                    // We could perform a goomba double jump if we could deactivate
                    // ktq
                    if (o->oMoveFlags & OBJ_MOVE_MASK_ON_GROUND) {
                        if (cur_obj_init_anim_and_check_if_end(13)) {
                            o->oSubAction -= 1;
                        }

                        koopa_the_quick_detect_bowling_ball();
                    }
            }
        }
    }
}

/**
 * Decelerate and then enter the stop action.
 */
static void koopa_the_quick_act_decelerate(void) {
    obj_forward_vel_approach(3.0f, 1.0f);
    cur_obj_init_animation_with_accel_and_sound(9, 0.99f);

    if (cur_obj_check_if_near_animation_end()) {
        o->oAction = KOOPA_THE_QUICK_ACT_STOP;
        o->oForwardVel = 3.0f;
    }
}

/**
 * Stop and then enter the after race action.
 */
static void koopa_the_quick_act_stop(void) {
    koopa_walk_stop();

    // koopa_walk_stop() was written for shelled koopa, so it enters the
    // KOOPA_SHELLED_ACT_STOPPED at the end
    if (o->oAction == KOOPA_SHELLED_ACT_STOPPED) {
        o->oAction = KOOPA_THE_QUICK_ACT_AFTER_RACE;
    }
}

/**
 * Wait for mario to approach, then show text indicating the status of the race.
 * If mario got to the finish line first and didn't use the cannon, then spawn
 * the star.
 */
static void koopa_the_quick_act_after_race(void) {
    cur_obj_init_animation_with_sound(7);

    if (o->parentObj->oKoopaRaceEndpointUnk100 == 0) {
        if (cur_obj_can_mario_activate_textbox_2(400.0f, 400.0f)) {
            stop_background_music(SEQUENCE_ARGS(4, SEQ_LEVEL_SLIDE));

            // Determine which text to display

            if (o->parentObj->oKoopaRaceEndpointRaceStatus != 0) {
                if (o->parentObj->oKoopaRaceEndpointRaceStatus < 0) {
                    // Mario cheated
                    o->parentObj->oKoopaRaceEndpointRaceStatus = 0;
                    o->parentObj->oKoopaRaceEndpointUnk100 = DIALOG_006;
                } else {
                    // Mario won
                    o->parentObj->oKoopaRaceEndpointUnk100 =
                        sKoopaTheQuickProperties[o->oKoopaTheQuickRaceIndex].winText;
                }
            } else {
                // KtQ won
                o->parentObj->oKoopaRaceEndpointUnk100 = DIALOG_041;
            }

            o->oFlags &= ~OBJ_FLAG_ACTIVE_FROM_AFAR;
        }
    } else if (o->parentObj->oKoopaRaceEndpointUnk100 > 0) {
        s32 dialogResponse = cur_obj_update_dialog_with_cutscene(2, 1, CUTSCENE_DIALOG, o->parentObj->oKoopaRaceEndpointUnk100);
        if (dialogResponse != 0) {
            o->parentObj->oKoopaRaceEndpointUnk100 = -1;
            o->oTimer = 0;
        }
    } else if (o->parentObj->oKoopaRaceEndpointRaceStatus != 0) {
        spawn_default_star(sKoopaTheQuickProperties[o->oKoopaTheQuickRaceIndex].starPos[0],
                   sKoopaTheQuickProperties[o->oKoopaTheQuickRaceIndex].starPos[1],
                   sKoopaTheQuickProperties[o->oKoopaTheQuickRaceIndex].starPos[2]);

        o->parentObj->oKoopaRaceEndpointRaceStatus = 0;
    }
}

/**
 * Update function for koopa the quick.
 */
static void koopa_the_quick_update(void) {
    cur_obj_update_floor_and_walls();
    obj_update_blinking(&o->oKoopaBlinkTimer, 10, 15, 3);

    switch (o->oAction) {
        case KOOPA_THE_QUICK_ACT_WAIT_BEFORE_RACE:
        case KOOPA_THE_QUICK_ACT_UNUSED1:
            koopa_the_quick_act_wait_before_race();
            break;
        case KOOPA_THE_QUICK_ACT_SHOW_INIT_TEXT:
            koopa_the_quick_act_show_init_text();
            break;
        case KOOPA_THE_QUICK_ACT_RACE:
            koopa_the_quick_act_race();
            break;
        case KOOPA_THE_QUICK_ACT_DECELERATE:
            koopa_the_quick_act_decelerate();
            break;
        case KOOPA_THE_QUICK_ACT_STOP:
            koopa_the_quick_act_stop();
            break;
        case KOOPA_THE_QUICK_ACT_AFTER_RACE:
            koopa_the_quick_act_after_race();
            break;
    }

    if (o->parentObj != o) {
        if (dist_between_objects(o, o->parentObj) < 400.0f) {
            o->parentObj->oKoopaRaceEndpointKoopaFinished = TRUE;
        }
    }

    cur_obj_push_mario_away_from_cylinder(140.0f, 300.0f);
    cur_obj_move_standard(-78);
}

/**
 * Update function.
 */
void bhv_koopa_update(void) {
    // PARTIAL_UPDATE
    o->oDeathSound = SOUND_OBJ_KOOPA_FLYGUY_DEATH;

    if (o->oKoopaMovementType >= KOOPA_BP_KOOPA_THE_QUICK_BASE) {
        koopa_the_quick_update();
    } else if (obj_update_standard_actions(o->oKoopaAgility * 1.5f)) {
        o->oKoopaDistanceToMario = o->oDistanceToMario;
        o->oKoopaAngleToMario = o->oAngleToMario;
        treat_far_home_as_mario(1000.0f);

        switch (o->oKoopaMovementType) {
            case KOOPA_BP_UNSHELLED:
                koopa_unshelled_update();
                break;
            case KOOPA_BP_NORMAL:
                koopa_shelled_update();
                break;
            case KOOPA_BP_KOOPA_THE_QUICK_BOB:
            case KOOPA_BP_KOOPA_THE_QUICK_THI:
                koopa_the_quick_update();
                break;
        }
    } else {
        o->oAnimState = 1;
    }

    obj_face_yaw_approach(o->oMoveAngleYaw, 0x600);
}

/**
 * Update function for bhvKoopaRaceEndpoint.
 */
void bhv_koopa_race_endpoint_update(void) {
    if (o->oKoopaRaceEndpointRaceBegun && !o->oKoopaRaceEndpointRaceEnded) {
        if (o->oKoopaRaceEndpointKoopaFinished || o->oDistanceToMario < 400.0f) {
            o->oKoopaRaceEndpointRaceEnded = TRUE;
            level_control_timer(TIMER_CONTROL_STOP);

            if (!o->oKoopaRaceEndpointKoopaFinished) {
                play_race_fanfare();
                if (gMarioShotFromCannon) {
                    o->oKoopaRaceEndpointRaceStatus = -1;
                } else {
                    o->oKoopaRaceEndpointRaceStatus = 1;
                }
            }
        }
    }
}
