| Name | Author | Game Mode | Rating | |||||
|---|---|---|---|---|---|---|---|---|
| SUPERBIRD! | Lark | Mutator | N/A | |||||
const uint8 KEY_LEFT = 0x25;
const uint8 KEY_UP = 0x26;
const uint8 KEY_RIGHT = 0x27;
const uint8 KEY_DOWN = 0x28;
const uint8 KEY_ENTER = 0x0D;
const uint8 KEY_ESCAPE = 0x1B;
const uint8 KEY_F = 0x46;
const uint8 KEY_T = 0x54;
const float BIRD_SPEED = 4.0f;
const float BIRD_DASH_SPEED = 8.0f;
const float RIDER_Y_OFFSET = -23.0f;
const float MOUNTED_NAME_Y_OFFSET = -26.0f;
// One continuous target covering the complete visible rider-and-bird sprite.
// These bounds are relative to the native Bird origin at player.xPos/yPos.
const float MOUNTED_HITBOX_HALF_WIDTH = 50.0f;
const float MOUNTED_HITBOX_TOP = -60.0f;
const float MOUNTED_HITBOX_BOTTOM = 36.0f;
const int BIRD_WIPEOUT_COOLDOWN = 350; // five seconds at 70 ticks per second
const int BIRD_TRANSFORM_COOLDOWN = 70; // one second between form changes
const int BIRD_WIPEOUT_RECOVERY = 70; // one second of blinking invulnerability
const float BIRD_WIPEOUT_MAX_X_SPEED = 4.0f;
const int BIRD_FLIGHT_STATE_CLEANUP = 14; // absorb delayed morph state for 0.2 seconds
const uint8 BIRD_FLIGHT_ANIM = 9;
const array<uint8> BIRD_RIDE_WEAPON_ICONS = {18, 25, 29, 34, 49, 57, 59, 61, 68};
array<bool> birdRideMounted(4, false);
array<bool> birdRideReady(4, false);
array<CHAR::Char> birdRideCharacter(4, CHAR::JAZZ);
array<int> birdRideFireCooldown(4, 0);
array<bool> birdRideFireWasPressed(4, false);
array<bool> birdRideSelectWasPressed(4, false);
array<bool> birdRideRunHeld(4, false);
array<bool> birdRideNeedsRespawnReset(4, false);
array<int> birdRideMountCooldown(4, 0);
array<int> birdRideTransformCooldown(4, 0);
array<int> birdRideWipeoutRecovery(4, 0);
array<int> birdRideFlightStateCleanup(4, 0);
array<bool> birdRideMeleeCollisionGuard(4, false);
array<bool> birdRideWipeoutActive(4, false);
array<float> birdRideWipeoutX(4, 0);
array<float> birdRideWipeoutY(4, 0);
array<float> birdRideWipeoutXSpeed(4, 0);
array<float> birdRideWipeoutYSpeed(4, 0);
array<int> birdRideWipeoutAngle(4, 0);
array<uint8> birdRideStoredHealth(4, 0);
array<uint32> birdRideStoredFur(4, 0);
array<int> birdRideColorMappingID(4, -1);
array<string> birdRideCarrotPopupText(4, "");
array<int> birdRideCarrotPopupTimer(4, 0);
array<float> birdRideCarrotPopupX(4, 0);
array<float> birdRideCarrotPopupY(4, 0);
array<bool> birdRideForcedPickupPreset(256, false);
array<bool> birdRideForcedPickupObject(4096, false);
array<bool> birdRideForcedPowerupPreset(256, false);
array<bool> birdRideForcedPowerupObject(4096, false);
array<bool> birdRideBirdBullet(4096, false);
bool birdRideFWasDown = false;
bool birdRideChatOpen = false;
bool birdRideTransformWarningShown = false;
bool birdRideIsWeaponPowerup(int eventID) {
return eventID == OBJECT::BLASTERPOWERUP ||
eventID == OBJECT::BOUNCERPOWERUP ||
eventID == OBJECT::ICEPOWERUP ||
eventID == OBJECT::SEEKERPOWERUP ||
eventID == OBJECT::RFPOWERUP ||
eventID == OBJECT::TOASTERPOWERUP ||
eventID == OBJECT::TNTPOWERUP ||
eventID == OBJECT::GUN8POWERUP ||
eventID == OBJECT::GUN9POWERUP;
}
bool birdRideAnyMounted() {
for (int i = 0; i < jjLocalPlayerCount && i < 4; ++i) {
if (birdRideReady[i] && birdRideMounted[i])
return true;
}
return false;
}
void updateWeaponPowerupBulletHandling() {
const bool ignoreBullets = birdRideAnyMounted();
for (int eventID = 0; eventID < 256; ++eventID) {
if (birdRideIsWeaponPowerup(eventID))
jjObjectPresets[eventID].bulletHandling = ignoreBullets ?
HANDLING::IGNOREBULLET : HANDLING::HURTBYBULLET;
}
for (int i = 1; i < jjObjectCount; ++i) {
jjOBJ@ obj = jjObjects[i];
if (obj.isActive && birdRideIsWeaponPowerup(obj.eventID))
obj.bulletHandling = ignoreBullets ?
HANDLING::IGNOREBULLET : HANDLING::HURTBYBULLET;
}
}
bool birdRideBulletCameFromBird(const jjOBJ@ bullet) {
if (bullet is null)
return false;
if (bullet.objectID > 0 && bullet.objectID < 4096 &&
birdRideBirdBullet[bullet.objectID])
return true;
if (bullet.creatorType != CREATOR::PLAYER)
return false;
const int playerID = bullet.creatorID;
return playerID >= 0 && playerID < 32 &&
jjPlayers[playerID].isActive &&
jjPlayers[playerID].charCurr == CHAR::BIRD;
}
int birdRideRabbitMonitorHitForce(jjPLAYER@ player, jjOBJ@ target, int fallbackForce) {
if (player is null || player.charCurr == CHAR::BIRD)
return fallbackForce;
const CHAR::Char character = player.charCurr;
const bool isRabbit = character == CHAR::JAZZ || character == CHAR::SPAZ ||
(jjIsTSF && character == CHAR::LORI);
if (!isRabbit)
return fallbackForce;
// canHurt is disabled globally so rabbit melee moves cannot damage players.
// Enable it only while asking JJ2 what this monitor collision would normally
// be (-1 for a special move, 1 for a buttstomp), then restore it before the
// collision is actually processed.
const bool previousCanHurt = jjCharacters[character].canHurt;
jjCharacters[character].canHurt = true;
const int nativeForce = player.getObjectHitForce(target);
jjCharacters[character].canHurt = previousCanHurt;
return nativeForce != 0 ? nativeForce : fallbackForce;
}
class BirdRidePowerupCollision : jjBEHAVIORINTERFACE {
private jjBEHAVIOR nativeBehavior;
BirdRidePowerupCollision(const jjBEHAVIOR &in behavior) {
nativeBehavior = behavior;
}
void onBehave(jjOBJ@ obj) {
obj.bulletHandling = birdRideAnyMounted() ?
HANDLING::IGNOREBULLET : HANDLING::HURTBYBULLET;
obj.behave(nativeBehavior);
obj.bulletHandling = birdRideAnyMounted() ?
HANDLING::IGNOREBULLET : HANDLING::HURTBYBULLET;
}
bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) {
// Returning true tells JJ2+ the collision is fully handled. The monitor
// and bullet are therefore left exactly as they were.
if (birdRideBulletCameFromBird(bullet))
return true;
// Rabbit shots retain the monitor's original native collision.
obj.scriptedCollisions = false;
if (bullet !is null)
bullet.objectHit(obj, HANDLING::SPECIAL);
else if (player !is null)
player.objectHit(
obj,
birdRideRabbitMonitorHitForce(player, obj, force),
HANDLING::SPECIAL
);
if (obj.isActive && birdRideIsWeaponPowerup(obj.eventID))
obj.scriptedCollisions = true;
return true;
}
}
void onLevelLoad() {
// Sprite mappings are level-local, so reserve fresh slots for each local
// player whenever a new level is loaded.
for (uint i = 0; i < birdRideColorMappingID.length(); ++i)
birdRideColorMappingID[i] = -1;
// Keep JJ2's normal chat control on T.
jjKeyChat = KEY_T;
// Birds cannot run in unmodified JJ2. Enable their Run state so the
// configured Run control, Caps Lock, and /run can all be detected.
jjCharacters[CHAR::BIRD].canRun = true;
// Rabbit form is for gathering items, not fighting. canHurt leaves each
// rabbit's special move fully usable but removes its damaging hit force.
jjCharacters[CHAR::JAZZ].canHurt = false;
jjCharacters[CHAR::SPAZ].canHurt = false;
if (jjIsTSF)
jjCharacters[CHAR::LORI].canHurt = false;
// Convert every ordinary pickup preset to scripted collision handling.
// onObjectHit below then blocks mounted birds while preserving normal
// collection for Jazz and Spaz after they dismount.
for (int eventID = 0; eventID < 256; ++eventID) {
jjOBJ@ preset = jjObjectPresets[eventID];
if (preset.playerHandling == HANDLING::PICKUP && !preset.scriptedCollisions) {
preset.scriptedCollisions = true;
birdRideForcedPickupPreset[eventID] = true;
}
if (birdRideIsWeaponPowerup(eventID)) {
preset.behavior = BirdRidePowerupCollision(preset.behavior);
preset.scriptedCollisions = true;
// Every game begins mounted, so make monitors physically ignore
// bullets before the first object or collision tick can run.
preset.bulletHandling = HANDLING::IGNOREBULLET;
birdRideForcedPowerupPreset[eventID] = true;
}
}
}
void birdRideAlertWrapped(const string &in text) {
// jjAlert itself does not wrap. Measure the same small font it uses and
// split only at spaces, leaving a comfortable margin on both screen edges.
const int maxWidth = jjResolutionWidth > 160 ? jjResolutionWidth - 96 : 64;
string line = "";
uint cursor = 0;
while (cursor < text.length()) {
while (cursor < text.length() && text[cursor] == 32)
++cursor;
if (cursor >= text.length())
break;
uint wordEnd = cursor;
while (wordEnd < text.length() && text[wordEnd] != 32)
++wordEnd;
const string word = text.substr(cursor, wordEnd - cursor);
const string candidate = line.length() == 0 ? word : line + " " + word;
if (line.length() > 0 &&
jjGetStringWidth(candidate, STRING::SMALL, STRING::NORMAL) > maxWidth) {
jjAlert("|" + line);
line = word;
} else {
line = candidate;
}
cursor = wordEnd + 1;
}
if (line.length() > 0)
jjAlert("|" + line);
}
void onLevelBegin() {
resetBirdRide();
preparePickupBlocking();
// Preserve the user's three intentional paragraph breaks, then wrap each
// paragraph further when the current resolution is too narrow.
birdRideAlertWrapped("Everybody's heard about the bird.");
birdRideAlertWrapped("Use the arrow keys to move your SUPERBIRD, and SPRINT to move faster.");
birdRideAlertWrapped("Press F to jump on and off the bird, but be careful - You are vulnerable when you are off the bird!");
}
void onLevelReload() {
resetBirdRide();
preparePickupBlocking();
}
void resetBirdRide() {
for (uint i = 0; i < 4; ++i) {
birdRideMounted[i] = false;
birdRideReady[i] = false;
birdRideFireCooldown[i] = 0;
birdRideFireWasPressed[i] = false;
birdRideSelectWasPressed[i] = false;
birdRideRunHeld[i] = false;
birdRideNeedsRespawnReset[i] = false;
birdRideMountCooldown[i] = 0;
birdRideTransformCooldown[i] = 0;
birdRideWipeoutRecovery[i] = 0;
birdRideFlightStateCleanup[i] = 0;
birdRideMeleeCollisionGuard[i] = false;
birdRideWipeoutActive[i] = false;
birdRideWipeoutX[i] = 0;
birdRideWipeoutY[i] = 0;
birdRideWipeoutXSpeed[i] = 0;
birdRideWipeoutYSpeed[i] = 0;
birdRideWipeoutAngle[i] = 0;
birdRideStoredHealth[i] = 0;
birdRideStoredFur[i] = 0;
birdRideCarrotPopupText[i] = "";
birdRideCarrotPopupTimer[i] = 0;
birdRideCarrotPopupX[i] = 0;
birdRideCarrotPopupY[i] = 0;
}
for (uint i = 0; i < birdRideForcedPickupObject.length(); ++i)
birdRideForcedPickupObject[i] = false;
for (uint i = 0; i < birdRideForcedPowerupObject.length(); ++i)
birdRideForcedPowerupObject[i] = false;
for (uint i = 0; i < birdRideBirdBullet.length(); ++i)
birdRideBirdBullet[i] = false;
birdRideFWasDown = jjKey[KEY_F];
birdRideChatOpen = false;
birdRideTransformWarningShown = false;
}
CHAR::Char rabbitCharacter(const jjPLAYER@ player) {
if (player.charCurr == CHAR::JAZZ ||
player.charCurr == CHAR::SPAZ ||
(jjIsTSF && player.charCurr == CHAR::LORI))
return player.charCurr;
if (player.charOrig == CHAR::JAZZ ||
player.charOrig == CHAR::SPAZ ||
(jjIsTSF && player.charOrig == CHAR::LORI))
return player.charOrig;
return CHAR::JAZZ;
}
void clearBirdRideFlightState(jjPLAYER@ player) {
// Bird morphs and multiplayer packets can leave these native rabbit-flight
// fields behind. SUPERBIRD movement does not use any of them.
player.fly = FLIGHT::NONE;
player.helicopter = 0;
player.helicopterElapsed = 0;
}
void enforceBirdRideFur(jjPLAYER@ player, uint id) {
// Use the packed value so all four color slots retain their exact order.
// Checking first avoids repeatedly broadcasting an unchanged palette.
if (id < 4 && player.fur != birdRideStoredFur[id])
player.fur = birdRideStoredFur[id];
}
void prepareBirdRideColorMapping(jjPLAYER@ player, uint id) {
if (id >= 4)
return;
if (birdRideColorMappingID[id] < 0)
birdRideColorMappingID[id] = jjSpriteModeFirstFreeMapping();
if (birdRideColorMappingID[id] < 0)
return;
// Build a permanent mapping for the four standard player gradients. This
// makes Bird and rabbit sprites use identical slots regardless of charCurr.
uint8 color1, color2, color3, color4;
enforceBirdRideFur(player, id);
player.furGet(color1, color2, color3, color4);
const array<uint8> colors = {color1, color2, color3, color4};
array<uint8> mapping(256);
for (uint i = 0; i < mapping.length(); ++i)
mapping[i] = uint8(i);
for (uint ramp = 0; ramp < 4; ++ramp) {
for (uint shade = 0; shade < 8; ++shade) {
const uint source = 16 + ramp * 8 + shade;
mapping[source] = uint8(uint(colors[ramp]) + shade);
}
}
jjSpriteModeSetMapping(uint8(birdRideColorMappingID[id]), mapping);
}
void prepareBirdRide(jjPLAYER@ player) {
const uint id = player.localPlayerID;
if (id >= 4 || birdRideReady[id])
return;
birdRideCharacter[id] = rabbitCharacter(player);
// morphTo may rearrange the four PLAYER color ramps for Bird form. Save
// the user's actual palette before the first morph and reapply it afterward.
birdRideStoredFur[id] = player.fur;
prepareBirdRideColorMapping(player, id);
birdRideReady[id] = true;
birdRideMounted[id] = true;
birdRideNeedsRespawnReset[id] = false;
birdRideMountCooldown[id] = 0;
birdRideTransformCooldown[id] = BIRD_TRANSFORM_COOLDOWN;
birdRideWipeoutRecovery[id] = 0;
birdRideFlightStateCleanup[id] = 0;
birdRideMeleeCollisionGuard[id] = false;
birdRideWipeoutActive[id] = false;
player.morphTo(CHAR::BIRD, false);
clearBirdRideFlightState(player);
enforceBirdRideFur(player, id);
player.xSpeed = 0;
player.ySpeed = 0;
player.noFire = false;
// Negative invincibility is invisible. Shots are handled as wipeouts below
// instead of reducing the mounted player's health.
player.invincibility = -2;
}
void dismountBird(jjPLAYER@ player) {
const uint id = player.localPlayerID;
if (id >= 4 || !birdRideReady[id] || !birdRideMounted[id])
return;
// Rabbit form is deliberately fragile. Save the SUPERBIRD's
// current health so it can be restored when the rider mounts again.
birdRideStoredHealth[id] = player.health;
player.morphTo(birdRideCharacter[id], false);
clearBirdRideFlightState(player);
enforceBirdRideFur(player, id);
if (player.health > 0)
player.health = 1;
player.ySpeed = -4.0f;
player.noFire = true;
player.invincibility = 0;
// A voluntary F dismount is not a wipeout. Clear any blink introduced by
// morphing; beginBirdWipeout deliberately reapplies its own timer afterward.
player.blink = 0;
birdRideFireCooldown[id] = 0;
birdRideFireWasPressed[id] = false;
birdRideSelectWasPressed[id] = false;
birdRideMounted[id] = false;
birdRideMeleeCollisionGuard[id] = false;
birdRideTransformCooldown[id] = BIRD_TRANSFORM_COOLDOWN;
birdRideFlightStateCleanup[id] = BIRD_FLIGHT_STATE_CLEANUP;
jjSample(player.xPos, player.yPos, SOUND::COMMON_JUMP);
}
void mountBird(jjPLAYER@ player) {
const uint id = player.localPlayerID;
if (id >= 4 || !birdRideReady[id] || birdRideMounted[id])
return;
if (birdRideMountCooldown[id] > 0) {
const int seconds = (birdRideMountCooldown[id] + 69) / 70;
jjAlert("|SUPERBIRD recovering - " + seconds +
(seconds == 1 ? " second!" : " seconds!"));
return;
}
// Preserve a rabbit morph chosen while dismounted.
birdRideCharacter[id] = rabbitCharacter(player);
player.morphTo(CHAR::BIRD, false);
clearBirdRideFlightState(player);
enforceBirdRideFur(player, id);
if (birdRideStoredHealth[id] > 0)
player.health = birdRideStoredHealth[id];
player.xSpeed = 0;
player.ySpeed = 0;
player.noFire = false;
player.invincibility = -2;
birdRideFireCooldown[id] = 0;
birdRideFireWasPressed[id] = false;
birdRideSelectWasPressed[id] = player.keySelect;
birdRideMounted[id] = true;
birdRideTransformCooldown[id] = BIRD_TRANSFORM_COOLDOWN;
birdRideWipeoutRecovery[id] = 0;
birdRideFlightStateCleanup[id] = 0;
birdRideMeleeCollisionGuard[id] = false;
birdRideWipeoutActive[id] = false;
jjSample(player.xPos, player.yPos, SOUND::COMMON_JUMP);
}
void toggleBirdRide(jjPLAYER@ player) {
const uint id = player.localPlayerID;
if (id >= 4 || !birdRideReady[id])
return;
// Keep the five-second wipeout message authoritative while it is active.
if (!birdRideMounted[id] && birdRideMountCooldown[id] > 0) {
mountBird(player);
return;
}
if (birdRideTransformCooldown[id] > 0) {
if (!birdRideTransformWarningShown) {
jjAlert("|Wait one second before changing form again!");
birdRideTransformWarningShown = true;
}
return;
}
if (birdRideMounted[id])
dismountBird(player);
else
mountBird(player);
}
void beginBirdWipeout(jjPLAYER@ player, uint id, int knockDirection) {
if (id >= 4 || !birdRideReady[id] || !birdRideMounted[id])
return;
// Launch the mount away from the impact before morphing its rider back to
// rabbit form. The separate visual keeps tumbling for the full recovery.
birdRideWipeoutX[id] = player.xPos;
birdRideWipeoutY[id] = player.yPos;
birdRideWipeoutXSpeed[id] = float(knockDirection) * 7.0f;
birdRideWipeoutYSpeed[id] = -5.0f;
birdRideWipeoutAngle[id] = 0;
birdRideWipeoutActive[id] = true;
birdRideMountCooldown[id] = BIRD_WIPEOUT_COOLDOWN;
dismountBird(player);
// A wipeout is a knock-off rather than the normal upward dismount hop.
player.xSpeed = float(knockDirection) * 2.0f;
player.ySpeed = -2.0f;
// Native blink provides visible flashing, damage immunity, and multiplayer
// synchronization. Match it to the separate dizzy-animation timer below.
birdRideWipeoutRecovery[id] = BIRD_WIPEOUT_RECOVERY;
player.blink = BIRD_WIPEOUT_RECOVERY;
jjSample(player.xPos, player.yPos, SOUND::COMMON_SMASH);
}
void updateBirdWipeout(uint id) {
if (id >= 4 || !birdRideWipeoutActive[id])
return;
if (birdRideMountCooldown[id] <= 0) {
birdRideWipeoutActive[id] = false;
return;
}
birdRideWipeoutX[id] += birdRideWipeoutXSpeed[id];
birdRideWipeoutXSpeed[id] *= 0.985f;
birdRideWipeoutYSpeed[id] += 0.25f;
const float nextY = birdRideWipeoutY[id] + birdRideWipeoutYSpeed[id];
if (birdRideWipeoutYSpeed[id] > 0 &&
jjMaskedPixel(int(birdRideWipeoutX[id]), int(nextY + 12.0f))) {
// Settle on the first solid surface instead of disappearing through it.
birdRideWipeoutYSpeed[id] = 0;
birdRideWipeoutXSpeed[id] *= 0.82f;
} else {
birdRideWipeoutY[id] = nextY;
}
birdRideWipeoutAngle[id] =
(birdRideWipeoutAngle[id] + int(birdRideWipeoutXSpeed[id] * 7.0f)) & 1023;
}
void restoreMountedBirdAfterRespawn(jjPLAYER@ player, uint id) {
// JJ2 restores charCurr to the player's original rabbit when a multiplayer
// respawn finishes. Reapply Bird form once, while preserving the health and
// spawn protection assigned by the game.
const int respawnHealth = player.health;
player.morphTo(CHAR::BIRD, false);
clearBirdRideFlightState(player);
enforceBirdRideFur(player, id);
player.health = respawnHealth;
player.xSpeed = 0;
player.ySpeed = 0;
player.noFire = false;
player.invincibility = -2;
birdRideStoredHealth[id] = uint8(respawnHealth);
birdRideFireCooldown[id] = 0;
birdRideFireWasPressed[id] = false;
birdRideSelectWasPressed[id] = player.keySelect;
birdRideRunHeld[id] = false;
birdRideNeedsRespawnReset[id] = false;
birdRideTransformCooldown[id] = BIRD_TRANSFORM_COOLDOWN;
birdRideWipeoutRecovery[id] = 0;
birdRideFlightStateCleanup[id] = 0;
birdRideMeleeCollisionGuard[id] = false;
}
bool mountedBirdHasGroundBelow(const jjPLAYER@ player) {
if (player.platform != 0)
return true;
// coldSpotY identifies the active native Bird frame's ground contact point.
// Sample across its feet so slopes and tile edges count as grounded too.
const jjANIMFRAME@ frame = jjAnimFrames[player.curFrame];
const int footY = int(player.yPos) +
int(frame.hotSpotY) - int(frame.coldSpotY) + 1;
const int centerX = int(player.xPos);
return jjMaskedPixel(centerX - 8, footY) ||
jjMaskedPixel(centerX, footY) ||
jjMaskedPixel(centerX + 8, footY);
}
void steerBird(jjPLAYER@ player) {
const uint id = player.localPlayerID;
float x = 0;
float y = 0;
if (jjKey[KEY_LEFT]) x -= 1;
if (jjKey[KEY_RIGHT]) x += 1;
if (jjKey[KEY_UP]) y -= 1;
if (jjKey[KEY_DOWN]) y += 1;
// Never keep forcing the native Bird collider into the floor. Doing so
// retriggers its impact sound and camera shake every game tick.
if (y > 0 && mountedBirdHasGroundBelow(player))
y = 0;
// Normalize diagonals so they are not faster than straight flight.
if (x != 0 && y != 0) {
x *= 0.70710678f;
y *= 0.70710678f;
}
// Use the state captured by onPlayerInput. That hook runs before the
// native bird movement code can discard Run input.
const bool dashing = id < 4 && birdRideRunHeld[id];
const float speed = dashing ? BIRD_DASH_SPEED : BIRD_SPEED;
player.xSpeed = x * speed;
player.ySpeed = y * speed;
if (x < 0) player.direction = -1;
if (x > 0) player.direction = 1;
// Do not let the player's configured movement keys add a second input
// on top of the arrow-key steering above.
player.keyLeft = false;
player.keyRight = false;
player.keyUp = false;
player.keyDown = false;
}
void cycleBirdWeapon(jjPLAYER@ player) {
uint8 nextWeapon = player.currWeapon;
for (int attempt = 0; attempt < 9; ++attempt) {
nextWeapon = nextWeapon % 9 + 1;
if (nextWeapon == WEAPON::BLASTER || player.ammo[nextWeapon] > 0) {
player.currWeapon = nextWeapon;
return;
}
}
}
bool dismountedRabbitNearPlayer(const jjPLAYER@ player) {
for (int i = 0; i < 32; ++i) {
const jjPLAYER@ other = jjPlayers[i];
if (i == player.playerID || !other.isActive || !other.isInGame || other.health <= 0)
continue;
// Start the collision guard just before the native buttstomp reaches a
// player. This leaves the move, animation, and physics entirely native.
if (abs(other.xPos - player.xPos) <= 40.0f &&
abs(other.yPos - player.yPos) <= 64.0f)
return true;
}
return false;
}
void updateDismountedMeleeCollisionGuard(jjPLAYER@ player, uint id) {
if (id >= 4)
return;
const bool shouldGuard = birdRideReady[id] && !birdRideMounted[id] &&
player.health > 0 && player.buttstomp < 121 &&
dismountedRabbitNearPlayer(player);
if (shouldGuard) {
// A positive blink value prevents player-to-player collision. Two ticks
// are enough to cover contact without disturbing the native move state.
birdRideMeleeCollisionGuard[id] = true;
if (player.blink < 2)
player.blink = 2;
} else {
if (birdRideMeleeCollisionGuard[id] && player.blink <= 2 &&
birdRideWipeoutRecovery[id] <= 0)
player.blink = 0;
birdRideMeleeCollisionGuard[id] = false;
}
}
void onPlayerInput(jjPLAYER@ player) {
const uint id = player.localPlayerID;
if (id < 4 && birdRideReady[id] && birdRideMounted[id]) {
birdRideRunHeld[id] = player.keyRun || player.running;
// Bird morphs do not run JJ2's native weapon-switch routine, so cycle
// through Blaster and every weapon for which the player has ammo.
const bool selectPressed = player.keySelect;
if (!birdRideChatOpen && selectPressed && !birdRideSelectWasPressed[id])
cycleBirdWeapon(player);
birdRideSelectWasPressed[id] = selectPressed;
player.keySelect = false;
} else if (id < 4 && birdRideReady[id] && player.health > 0 &&
player.keyDown && player.platform == 0 &&
dismountedRabbitNearPlayer(player)) {
// Prime the contact guard during native buttstomp startup. Do not alter
// keyDown, buttstomp, specialMove, or movement: JJ2 performs the move.
birdRideMeleeCollisionGuard[id] = true;
if (player.blink < 2)
player.blink = 2;
}
}
void fireFromBird(jjPLAYER@ player) {
const uint id = player.localPlayerID;
// The Bird morph has no native out-of-ammo fallback. Never leave it stuck
// on an empty weapon, including one selected through a number-key binding.
if (int(player.currWeapon) != int(WEAPON::BLASTER) && player.ammo[player.currWeapon] <= 0)
cycleBirdWeapon(player);
const bool firePressed = player.keyFire;
const bool newPress = firePressed && !birdRideFireWasPressed[id];
const int style = int(jjWeapons[player.currWeapon].style);
// POPCORN weapons (the normal toaster style) emit a continuous stream at
// a fixed six-tick interval instead of using the rabbit's normal delay.
const int repeatDelay = style == int(WEAPON::POPCORN) ? 6 :
(player.fastfire > 0 ? player.fastfire : 1);
if (birdRideFireCooldown[id] > 0)
--birdRideFireCooldown[id];
// Normal weapons may be tapped faster than their hold-fire delay, just
// like Jazz and Spaz. CAPPED weapons still enforce that delay, while
// MISSILE weapons fire only once per button press.
const bool tapShot = newPress &&
(style != int(WEAPON::CAPPED) || birdRideFireCooldown[id] <= 0);
const bool heldShot = firePressed && !newPress &&
style != int(WEAPON::MISSILE) && birdRideFireCooldown[id] <= 0;
if (tapShot || heldShot) {
// Bird morphs do not run the rabbit firing routine, so explicitly
// fire the currently selected weapon with normal ammo depletion.
const uint firstNewObject = jjObjectCount;
const int bulletID = player.fireBullet(WEAPON::CURRENT, true, true, DIRECTION::CURRENT);
if (bulletID > 0 && bulletID < 4096)
birdRideBirdBullet[uint(bulletID)] = true;
// Multi-projectile weapons may not return an object ID. Tag every new
// player bullet created by this firing action as a SUPERBIRD shot.
for (uint i = 1; i < jjObjectCount; ++i) {
jjOBJ@ shot = jjObjects[i];
if (shot.isActive && shot.creatorType == CREATOR::PLAYER &&
shot.creatorID == player.playerID &&
shot.playerHandling == HANDLING::PLAYERBULLET &&
(i >= firstNewObject || shot.state == STATE::START))
birdRideBirdBullet[i] = true;
}
birdRideFireCooldown[id] = repeatDelay;
// Match normal rabbit behavior by leaving a weapon as soon as its final
// round is spent, so the next Fire press always has a usable weapon.
if (int(player.currWeapon) != int(WEAPON::BLASTER) && player.ammo[player.currWeapon] <= 0)
cycleBirdWeapon(player);
}
birdRideFireWasPressed[id] = firePressed;
// Leave keyFire intact so held and repeated presses can be tracked on
// subsequent ticks. Bird form has no native firing path to duplicate.
}
string birdRideAmmoText(jjPLAYER@ player) {
const uint8 weapon = player.currWeapon;
if (jjWeapons[weapon].infinite)
return "^"; // JJ2's infinity glyph
// jjPLAYER::ammo already exposes the HUD-ready quantity. Applying the
// weapon multiplier here again incorrectly turns ordinary Toaster ammo
// into zero because Toaster uses a larger internal pickup multiplier.
return "" + player.ammo[weapon];
}
bool birdRideIsMounted(const jjPLAYER@ player) {
if (player is null || !player.isLocal)
return false;
const int id = player.localPlayerID;
return id >= 0 && id < 4 && birdRideReady[id] && birdRideMounted[id];
}
void preparePickupBlocking() {
for (int i = 1; i < jjObjectCount; ++i) {
jjOBJ@ obj = jjObjects[i];
if (!obj.isActive) {
if (i < 4096) {
birdRideForcedPickupObject[i] = false;
birdRideForcedPowerupObject[i] = false;
birdRideBirdBullet[i] = false;
}
continue;
}
if (i < 4096 && obj.playerHandling != HANDLING::PLAYERBULLET)
birdRideBirdBullet[i] = false;
if (obj.playerHandling == HANDLING::PICKUP) {
if (!obj.scriptedCollisions) {
obj.scriptedCollisions = true;
if (i < 4096) birdRideForcedPickupObject[i] = true;
} else if (birdRideForcedPickupPreset[obj.eventID] && i < 4096) {
birdRideForcedPickupObject[i] = true;
}
}
if (birdRideIsWeaponPowerup(obj.eventID)) {
if (!obj.scriptedCollisions) {
obj.scriptedCollisions = true;
if (i < 4096) birdRideForcedPowerupObject[i] = true;
} else if (birdRideForcedPowerupPreset[obj.eventID] && i < 4096) {
birdRideForcedPowerupObject[i] = true;
}
}
}
}
int riderAnimSet(CHAR::Char character) {
if (character == CHAR::SPAZ) return ANIM::SPAZ;
if (jjIsTSF && character == CHAR::LORI) return ANIM::LORI;
return ANIM::JAZZ;
}
uint mountedRiderFrame(CHAR::Char character) {
const jjANIMATION@ anim = jjAnimations[
jjAnimSets[riderAnimSet(character)].firstAnim + RABBIT::STAND
];
return anim.firstFrame + ((jjGameTicks / 7) % anim.frameCount);
}
uint mountedBirdFrame() {
const jjANIMATION@ flight = jjAnimations[
jjAnimSets[ANIM::BIRD].firstAnim + BIRD_FLIGHT_ANIM
];
return flight.firstFrame + ((jjGameTicks / 6) % flight.frameCount);
}
bool mountedBirdsCollide(const jjPLAYER@ first, const jjPLAYER@ second) {
const jjANIMFRAME@ frame = jjAnimFrames[mountedBirdFrame()];
return frame.doesCollide(
int(first.xPos), int(first.yPos), first.direction,
frame, int(second.xPos), int(second.yPos), second.direction, false
);
}
float birdRideSpeedSquared(const jjPLAYER@ player) {
return player.xSpeed * player.xSpeed + player.ySpeed * player.ySpeed;
}
void processMountedBirdCollisions() {
array<bool> shouldWipe(4, false);
array<int> knockDirection(4, 1);
// Decide every local outcome before changing any character. This makes an
// equal-speed split-screen collision wipe out both birds simultaneously.
for (int localID = 0; localID < jjLocalPlayerCount && localID < 4; ++localID) {
jjPLAYER@ player = jjLocalPlayers[localID];
if (!player.isActive || !player.isInGame || player.health <= 0 ||
!birdRideReady[localID] || !birdRideMounted[localID] ||
player.charCurr != CHAR::BIRD)
continue;
const float playerSpeed = birdRideSpeedSquared(player);
for (int otherID = 0; otherID < 32; ++otherID) {
jjPLAYER@ other = jjPlayers[otherID];
if (other.playerID == player.playerID || !other.isActive ||
!other.isInGame || other.health <= 0 ||
other.charCurr != CHAR::BIRD ||
!mountedBirdsCollide(player, other))
continue;
const float otherSpeed = birdRideSpeedSquared(other);
// Merely sharing a spawn point while both birds are stationary is not an
// impact. This prevents an automatic wipeout at level start.
if (playerSpeed < 0.01f && otherSpeed < 0.01f)
continue;
// The slower bird loses. Within a tiny tolerance the speeds count as
// tied, so both clients independently apply a double wipeout.
if (playerSpeed <= otherSpeed + 0.01f) {
shouldWipe[localID] = true;
if (player.xPos < other.xPos)
knockDirection[localID] = -1;
else if (player.xPos > other.xPos)
knockDirection[localID] = 1;
else
knockDirection[localID] = player.direction < 0 ? 1 : -1;
break;
}
}
}
for (int localID = 0; localID < jjLocalPlayerCount && localID < 4; ++localID) {
if (shouldWipe[localID])
beginBirdWipeout(jjLocalPlayers[localID], localID, knockDirection[localID]);
}
}
void drawMountedRider(jjPLAYER@ player, CHAR::Char character) {
// Use a board-free rabbit frame. The bird is drawn afterward so it
// remains in front of the rider's feet.
const uint frame = mountedRiderFrame(character);
SPRITE::Mode spriteMode = SPRITE::PLAYER;
int spriteParam = player.playerID;
if (player.isLocal) {
const int localID = player.localPlayerID;
if (localID >= 0 && localID < 4 && birdRideColorMappingID[localID] >= 0) {
spriteMode = SPRITE::MAPPING;
spriteParam = birdRideColorMappingID[localID];
}
}
jjDrawSpriteFromCurFrame(
player.xPos,
player.yPos + RIDER_Y_OFFSET,
frame,
player.direction,
spriteMode,
spriteParam
);
}
void applyMountedShotWipeouts(jjPLAYER@ player, uint id) {
if (id >= 4 || !birdRideReady[id] || !birdRideMounted[id] ||
player.charCurr != CHAR::BIRD || player.health <= 0)
return;
// JJ2's native collision sees only the small Bird sprite. Treat the entire
// visible rider-and-bird combination as one solid target instead, including
// transparent gaps between limbs, wings, and tail. Enemy shots knock the
// rider off without reducing the SUPERBIRD's stored health.
for (int i = 1; i < jjObjectCount; ++i) {
jjOBJ@ bullet = jjObjects[i];
if (!bullet.isActive || bullet.state == STATE::START)
continue;
const bool enemyShot = bullet.playerHandling == HANDLING::ENEMYBULLET ||
((bullet.playerHandling == HANDLING::PLAYERBULLET ||
bullet.playerHandling == HANDLING::PARTICLE) &&
bullet.creatorType == CREATOR::PLAYER && bullet.creatorID < 32 &&
jjPlayers[bullet.creatorID].isActive &&
jjPlayers[bullet.creatorID].isEnemy(player));
if (!enemyShot)
continue;
const float relativeX = bullet.xPos - player.xPos;
const float relativeY = bullet.yPos - player.yPos;
const float previousRelativeX = relativeX - bullet.xSpeed;
const float previousRelativeY = relativeY - bullet.ySpeed;
const bool insideNow =
abs(relativeX) <= MOUNTED_HITBOX_HALF_WIDTH &&
relativeY >= MOUNTED_HITBOX_TOP &&
relativeY <= MOUNTED_HITBOX_BOTTOM;
const bool insidePrevious =
abs(previousRelativeX) <= MOUNTED_HITBOX_HALF_WIDTH &&
previousRelativeY >= MOUNTED_HITBOX_TOP &&
previousRelativeY <= MOUNTED_HITBOX_BOTTOM;
if (insideNow || insidePrevious) {
int knockDirection;
if (bullet.xSpeed > 0.25f)
knockDirection = 1;
else if (bullet.xSpeed < -0.25f)
knockDirection = -1;
else
knockDirection = bullet.xPos < player.xPos ? 1 : -1;
// Consume the impact just like a normal player hit, but replace damage
// with the same fall-off and five-second cooldown used by bird crashes.
bullet.state = STATE::EXPLODE;
beginBirdWipeout(player, id, knockDirection);
break;
}
}
}
void drawMountedBird(jjPLAYER@ player) {
// Draw the flight loop explicitly. Native Bird logic switches to a standing
// animation on masked ground, which visually separates it from the rider.
const uint frame = mountedBirdFrame();
SPRITE::Mode spriteMode = SPRITE::PLAYER;
int spriteParam = player.playerID;
if (player.isLocal) {
const int localID = player.localPlayerID;
if (localID >= 0 && localID < 4 && birdRideColorMappingID[localID] >= 0) {
spriteMode = SPRITE::MAPPING;
spriteParam = birdRideColorMappingID[localID];
}
}
jjDrawSpriteFromCurFrame(
player.xPos,
player.yPos,
frame,
player.direction,
spriteMode,
spriteParam
);
}
void drawWaitingBird(jjPLAYER@ player, uint localID) {
const uint frame = mountedBirdFrame();
const bool hasMapping = localID < 4 && birdRideColorMappingID[localID] >= 0;
if (localID < 4 && birdRideWipeoutActive[localID]) {
const int direction = birdRideWipeoutXSpeed[localID] < 0 ? -1 : 1;
jjDrawRotatedSpriteFromCurFrame(
birdRideWipeoutX[localID],
birdRideWipeoutY[localID],
frame,
birdRideWipeoutAngle[localID],
float(direction),
1.0f,
hasMapping ? SPRITE::MAPPING : SPRITE::PLAYER,
hasMapping ? birdRideColorMappingID[localID] : player.playerID
);
return;
}
const float side = player.direction < 0 ? 27.0f : -27.0f;
const float bob = jjSin(jjGameTicks * 12) * 2.0f;
const int direction = side < 0 ? 1 : -1;
jjDrawSpriteFromCurFrame(
player.xPos + side,
player.yPos - 16.0f + bob,
frame,
direction,
hasMapping ? SPRITE::MAPPING : SPRITE::PLAYER,
hasMapping ? birdRideColorMappingID[localID] : player.playerID
);
}
void showCarrotPopup(jjPLAYER@ player, const string &in text) {
const uint id = player.localPlayerID;
if (id >= 4)
return;
birdRideCarrotPopupText[id] = text;
birdRideCarrotPopupTimer[id] = 70;
birdRideCarrotPopupX[id] = player.xPos;
birdRideCarrotPopupY[id] = player.yPos - 28.0f;
}
void drawCarrotPopups() {
for (uint id = 0; id < 4; ++id) {
if (birdRideCarrotPopupTimer[id] <= 0)
continue;
const int elapsed = 70 - birdRideCarrotPopupTimer[id];
const float centerOffset = birdRideCarrotPopupText[id] == "FULL NRG" ? 27.0f : 7.0f;
jjDrawString(
birdRideCarrotPopupX[id] - centerOffset,
birdRideCarrotPopupY[id] - elapsed * 0.35f,
birdRideCarrotPopupText[id],
STRING::SMALL,
STRING::NORMAL
);
--birdRideCarrotPopupTimer[id];
}
}
void onMain() {
// Do this before player collision handling every tick, including for
// pickups created dynamically by crates, generators, or other scripts.
preparePickupBlocking();
// Initialize every local player as soon as its player object is ready.
for (int i = 0; i < jjLocalPlayerCount; ++i) {
jjPLAYER@ player = jjLocalPlayers[i];
if (player.isActive && player.isInGame) {
prepareBirdRide(player);
if (birdRideReady[i])
enforceBirdRideFur(player, i);
}
}
for (int i = 0; i < jjLocalPlayerCount && i < 4; ++i) {
if (birdRideMountCooldown[i] > 0)
--birdRideMountCooldown[i];
if (birdRideTransformCooldown[i] > 0)
--birdRideTransformCooldown[i];
if (birdRideWipeoutRecovery[i] > 0)
--birdRideWipeoutRecovery[i];
jjPLAYER@ player = jjLocalPlayers[i];
if (player.isActive && player.isInGame)
updateDismountedMeleeCollisionGuard(player, i);
updateBirdWipeout(i);
}
processMountedBirdCollisions();
// Track the native chat prompt so letters typed into it never trigger
// gameplay actions. Chat opens with the configured key and closes with
// Enter or Escape.
if (!birdRideChatOpen && jjKeyChat != 0 && jjKey[jjKeyChat])
birdRideChatOpen = true;
else if (birdRideChatOpen && (jjKey[KEY_ENTER] || jjKey[KEY_ESCAPE]))
birdRideChatOpen = false;
// F controls the first local player. This avoids one keyboard press
// toggling every player in a split-screen game at the same time.
const bool fIsDown = jjKey[KEY_F];
if (!birdRideChatOpen && fIsDown && !birdRideFWasDown && jjLocalPlayerCount > 0) {
jjPLAYER@ player = jjLocalPlayers[0];
if (player.isActive && player.isInGame && player.health > 0)
toggleBirdRide(player);
}
birdRideFWasDown = fIsDown;
// This engine-level setting is the authoritative guard. IGNOREBULLET
// leaves both the monitor and projectile completely unaffected.
updateWeaponPowerupBulletHandling();
// Any enemy shot touching the visible rider-and-bird area causes the local
// victim to wipe out. Mounted invincibility prevents the native Bird hitbox
// from losing health before this larger collision check runs.
for (int i = 0; i < jjLocalPlayerCount && i < 4; ++i) {
jjPLAYER@ player = jjLocalPlayers[i];
if (player.isActive && player.isInGame)
applyMountedShotWipeouts(player, i);
}
// Native bird form supplies collision and multiplayer position sync.
// Draw the original rabbit on top to turn it into a visible mount.
for (int i = 0; i < 32; ++i) {
jjPLAYER@ player = jjPlayers[i];
if (player.isActive && player.isInGame && player.charCurr == CHAR::BIRD) {
drawMountedRider(player, rabbitCharacter(player));
drawMountedBird(player);
}
}
// The dismounted bird is a local visual companion beside its rider.
for (int i = 0; i < jjLocalPlayerCount; ++i) {
jjPLAYER@ player = jjLocalPlayers[i];
if (player.isActive && player.isInGame && birdRideReady[i] && !birdRideMounted[i])
drawWaitingBird(player, i);
}
drawCarrotPopups();
}
void onPlayer(jjPLAYER@ player) {
const uint id = player.localPlayerID;
if (id < 4 && birdRideReady[id]) {
// Do not steer or fire during the roast/death sequence. Applying mounted
// movement to the temporary respawn rabbit causes slow falling and broken
// jumping after the player comes back.
if (player.health <= 0) {
if (birdRideMounted[id])
birdRideNeedsRespawnReset[id] = true;
player.noFire = true;
birdRideFireCooldown[id] = 0;
birdRideFireWasPressed[id] = false;
birdRideSelectWasPressed[id] = false;
birdRideRunHeld[id] = false;
birdRideMeleeCollisionGuard[id] = false;
return;
}
// A respawn changes charCurr back to charOrig. Restore the mounted Bird
// before any custom movement is applied to the newly alive player.
if (birdRideMounted[id] &&
(birdRideNeedsRespawnReset[id] || player.charCurr != CHAR::BIRD))
restoreMountedBirdAfterRespawn(player, id);
// Native character logic may rewrite fur after morphTo returns. Enforce
// the saved packed palette every tick, before any sprites are queued.
enforceBirdRideFur(player, id);
player.noFire = !birdRideMounted[id];
if (birdRideMounted[id]) {
// Keep the native Bird body immune at every tick. Projectile overlap is
// converted into a wipeout by applyMountedShotWipeouts instead.
player.invincibility = -2;
clearBirdRideFlightState(player);
steerBird(player);
fireFromBird(player);
} else {
// Special moves remain completely native but harmless through canHurt.
// Buttstomps also remain native; the short proximity guard in onMain
// suppresses only their player collision without touching movement state.
jjCharacters[player.charCurr].canHurt = false;
// Clear only for the first few post-morph ticks, catching delayed
// multiplayer state without disabling later rabbit flight pickups.
if (birdRideFlightStateCleanup[id] > 0) {
clearBirdRideFlightState(player);
--birdRideFlightStateCleanup[id];
}
// Wipeouts use JJ2's native blinking state for exactly one second.
// Reassert the remaining duration in case another behavior shortens it.
if (birdRideWipeoutRecovery[id] > 0 &&
player.blink < birdRideWipeoutRecovery[id])
player.blink = birdRideWipeoutRecovery[id];
if (birdRideWipeoutRecovery[id] > 0) {
// Keep normal air/ground physics, but cap horizontal movement while
// the rider is dizzy so recovery movement feels slightly sluggish.
if (player.xSpeed > BIRD_WIPEOUT_MAX_X_SPEED)
player.xSpeed = BIRD_WIPEOUT_MAX_X_SPEED;
else if (player.xSpeed < -BIRD_WIPEOUT_MAX_X_SPEED)
player.xSpeed = -BIRD_WIPEOUT_MAX_X_SPEED;
}
// Carrots may be collected as a rabbit, but rabbit form can never
// hold more than one heart. Leave zero alone so damage is lethal.
if (player.health > 1)
player.health = 1;
player.keyFire = false;
birdRideFireWasPressed[id] = false;
}
}
}
void onRoast(jjPLAYER@ victim, jjPLAYER@ killer) {
if (!victim.isLocal)
return;
const uint id = victim.localPlayerID;
if (id < 4 && birdRideReady[id] && birdRideMounted[id]) {
// Defer morphing until the game has completed its normal respawn. Morphing
// during onRoast can interfere with the death animation and scoring.
birdRideNeedsRespawnReset[id] = true;
birdRideFireCooldown[id] = 0;
birdRideFireWasPressed[id] = false;
birdRideSelectWasPressed[id] = false;
birdRideRunHeld[id] = false;
}
}
bool onDrawAmmo(jjPLAYER@ player, jjCANVAS@ canvas) {
const uint id = player.localPlayerID;
if (id < 4 && birdRideReady[id] && birdRideMounted[id]) {
// Recreate the normal Jazz/Spaz lower-right HUD: animated weapon
// profile icon, then "x" and the adjusted ammo quantity.
int weaponIndex = int(player.currWeapon) - 1;
if (weaponIndex < 0 || weaponIndex >= 9)
weaponIndex = 0;
uint8 iconAnimation = BIRD_RIDE_WEAPON_ICONS[weaponIndex];
if (weaponIndex == 0 && birdRideCharacter[id] == CHAR::SPAZ)
++iconAnimation;
const jjANIMATION@ icon = jjAnimations[jjAnimSets[ANIM::AMMO].firstAnim + iconAnimation];
const uint8 frame = uint8((jjRenderFrame >> 2) % icon.frameCount);
const int y = jjSubscreenHeight - 14;
canvas.drawSprite(
jjSubscreenWidth - 90,
y,
ANIM::AMMO,
iconAnimation,
frame
);
canvas.drawString(
jjSubscreenWidth - 80,
y,
"x" + birdRideAmmoText(player),
STRING::MEDIUM,
STRING::NORMAL
);
return true;
}
return false;
}
void onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) {
if (obj is null || obj.objectID <= 0 || obj.objectID >= 4096)
return;
if (birdRideForcedPowerupObject[obj.objectID] &&
birdRideIsWeaponPowerup(obj.eventID)) {
// SUPERBIRD bullets pass through weapon-powerup monitors without
// damaging them or granting their powerup.
if (birdRideBulletCameFromBird(bullet))
return;
// Preserve normal monitor behavior for bullets fired as a rabbit.
obj.scriptedCollisions = false;
if (bullet !is null)
bullet.objectHit(obj, HANDLING::SPECIAL);
else if (player !is null)
player.objectHit(
obj,
birdRideRabbitMonitorHitForce(player, obj, force),
HANDLING::SPECIAL
);
if (obj.isActive && birdRideIsWeaponPowerup(obj.eventID)) {
obj.scriptedCollisions = true;
birdRideForcedPowerupObject[obj.objectID] = true;
} else {
birdRideForcedPowerupObject[obj.objectID] = false;
}
return;
}
if (player is null)
return;
if (!birdRideForcedPickupObject[obj.objectID] ||
obj.playerHandling != HANDLING::PICKUP)
return;
// A mounted bird consumes the collision, not the item. This deliberately
// leaves every player property and the pickup object completely unchanged.
if (birdRideIsMounted(player))
return;
const uint id = player.localPlayerID;
const bool isCarrot = obj.eventID == OBJECT::CARROT;
const bool isFullEnergy = obj.eventID == OBJECT::FULLENERGY;
const bool isHealthPickup = isCarrot || isFullEnergy;
// Judge carrots by the saved SUPERBIRD health, not the rabbit's one-heart
// limit. A full bird leaves carrots in place, just like a full rabbit.
if (isHealthPickup && id < 4 && int(birdRideStoredHealth[id]) >= jjMaxHealth)
return;
// Jazz/Spaz should collect the same object normally after dismounting.
// Temporarily bypass this scripted interceptor for that one collision.
obj.scriptedCollisions = false;
const bool collected = player.objectHit(obj, 0, HANDLING::PICKUP);
if (isHealthPickup && id < 4 && collected) {
if (isFullEnergy) {
birdRideStoredHealth[id] = uint8(jjMaxHealth);
showCarrotPopup(player, "FULL NRG");
} else {
const int newHealth = int(birdRideStoredHealth[id]) + 1;
birdRideStoredHealth[id] = uint8(newHealth > jjMaxHealth ? jjMaxHealth : newHealth);
showCarrotPopup(player, "+1");
}
// The health belongs to the SUPERBIRD. Rabbit form stays at one heart.
if (player.health > 1)
player.health = 1;
}
if (obj.isActive && obj.playerHandling == HANDLING::PICKUP) {
obj.scriptedCollisions = true;
birdRideForcedPickupObject[obj.objectID] = true;
} else {
birdRideForcedPickupObject[obj.objectID] = false;
}
}
void onPlayerDraw(jjPLAYERDRAW& draw) {
// This is the final hook before the native player sprite is rendered.
// Reassert the exact same palette for the dismounted rabbit as well.
if (draw.player.isLocal) {
const int colorID = draw.player.localPlayerID;
if (colorID >= 0 && colorID < 4 && birdRideReady[colorID]) {
enforceBirdRideFur(draw.player, colorID);
if (birdRideColorMappingID[colorID] >= 0) {
if (draw.spriteMode == SPRITE::PLAYER)
draw.spriteMode = SPRITE::MAPPING;
else if (draw.spriteMode == SPRITE::TRANSLUCENTPLAYER)
draw.spriteMode = SPRITE::TRANSLUCENTMAPPING;
draw.spriteParam = uint8(birdRideColorMappingID[colorID]);
}
}
}
// The two-tick buttstomp collision guard is mechanical, not a wipeout.
// Keep the native move sprite visible instead of flashing it.
if (draw.player.isLocal && draw.player.charCurr != CHAR::BIRD) {
const int guardID = draw.player.localPlayerID;
if (guardID >= 0 && guardID < 4 &&
birdRideMeleeCollisionGuard[guardID] && draw.player.blink <= 2 &&
birdRideWipeoutRecovery[guardID] <= 0)
draw.sprite = true;
}
// For the local rider, require our explicit wipeout timer instead of JJ2's
// generic blink flag: morphing during a normal F dismount may briefly set it.
// Remote riders use the synchronized blink value; voluntary dismounts clear
// that value in dismountBird above.
bool drawWipeoutDizzy = false;
if (draw.player.charCurr != CHAR::BIRD) {
if (draw.player.isLocal) {
const int recoveryID = draw.player.localPlayerID;
drawWipeoutDizzy = recoveryID >= 0 && recoveryID < 4 &&
birdRideWipeoutRecovery[recoveryID] > 0;
} else {
drawWipeoutDizzy = draw.player.blink > 0;
}
}
if (drawWipeoutDizzy) {
const jjANIMATION@ dizzy = jjAnimations[
jjAnimSets[draw.player.setID].firstAnim + RABBIT::STONED
];
if (dizzy.frameCount > 0)
draw.curFrame = dizzy.firstFrame +
((jjGameTicks / 5) % dizzy.frameCount);
}
// Replace the native bird sprite with our explicitly ordered pair:
// rider first, bird second. Other player effects and names remain.
if (draw.player.charCurr == CHAR::BIRD) {
draw.sprite = false;
// The rider sits above the bird's native origin, so move the native
// player name high enough to clear the rider's head.
draw.yOffset += MOUNTED_NAME_Y_OFFSET;
}
}
Jazz2Online © 1999-INFINITY (Site Credits). We have a Privacy Policy. Jazz Jackrabbit, Jazz Jackrabbit 2, Jazz Jackrabbit Advance and all related trademarks and media are ™ and © Epic Games. Lori Jackrabbit is © Dean Dodrill. J2O development powered by Loops of Fury and Chemical Beats.
Eat your lima beans, Johnny.