| Name | Author | Game Mode | Rating | |||||
|---|---|---|---|---|---|---|---|---|
| Sk8er Bunni | Lark | Mutator | N/A | |||||
#pragma name "Sk8er Bunni"
const int TICKS_PER_SECOND = 70;
const int MINIMUM_PICKUP_POINTS = 100;
const int HURT_TIME_PENALTY = 5 * TICKS_PER_SECOND;
const int REVERSE_GRAVITY_DURATION = 10 * TICKS_PER_SECOND;
const int FAST_FEET_SCORE = 1000;
const int GOLD_WEAPON_GEM_SCORE = 5000;
const uint8 GOLD_GEM_PALETTE_START = 40;
const int BURGER_COLLECTED_MARKER = 24681357;
const uint8 PACKET_CHALLENGE_SCORE = 167;
const uint8 PACKET_ROUND_STATE = 168;
const uint8 PACKET_ROUND_TIME_DELTA = 169;
const uint8 PACKET_BANANA_THROW = 170;
const uint8 PACKET_BANANA_WIPEOUT = 171;
const uint8 PACKET_BOMB_THROW = 172;
const uint8 PACKET_BOMB_EXPLODE = 173;
const uint8 PACKET_PLAYER_JOINED = 174;
const uint8 PACKET_BOMB_GEM_DROP = 175;
const uint8 PACKET_BOMB_GEM_COLLECT = 176;
const int SCORE_SYNC_INTERVAL = TICKS_PER_SECOND;
const int ROUND_STATE_SYNC_INTERVAL = TICKS_PER_SECOND / 2;
const int ROUND_DURATION = 60 * TICKS_PER_SECOND;
const int AUTO_RESTART_DURATION = 10 * TICKS_PER_SECOND;
const int BANANA_OBJECT_MARKER = 731905;
const int BANANA_LIFETIME = 12 * TICKS_PER_SECOND;
const int BANANA_ARM_TICKS = 8;
const int MAX_BANANAS_PER_PLAYER = 8;
const int STARTING_BANANA_AMMO = 5;
const int MAX_BANANA_AMMO = 20;
const float BANANA_THROW_X_SPEED = 5.5f;
const float BANANA_THROW_Y_SPEED = 5.25f;
const float BANANA_GRAVITY = 0.22f;
const float BANANA_MAX_FALL_SPEED = 8.0f;
// Event 43 is JJ2's Bomb object. Switch Weapon throws these custom bombs.
const uint8 SKATE_BOMB_EVENT = 43;
const int BOMB_OBJECT_MARKER = 731906;
const int BOMB_AMMO_PICKUP_MARKER = 731907;
const int WEAPON_POWERUP_GOLD_GEM_MARKER = 731908;
const int BOMB_LIFETIME = 5 * TICKS_PER_SECOND;
const int MAX_BOMBS_PER_PLAYER = 4;
const int STARTING_BOMB_AMMO = 5;
const int MAX_BOMB_AMMO = 10;
const int BOMB_EXPLOSION_HISTORY = 16;
const float BOMB_THROW_X_SPEED = 5.0f;
const float BOMB_MAX_NETWORK_X_SPEED = 48.0f;
const float BOMB_THROW_Y_SPEED = 4.5f;
const float BOMB_GRAVITY = 0.28f;
const float BOMB_MAX_FALL_SPEED = 9.0f;
const float BOMB_BLAST_RADIUS = 8.0f * 32.0f;
const float BOMB_KNOCKBACK_X_SPEED = 6.0f;
const float BOMB_KNOCKBACK_Y_SPEED = 3.5f;
const int BOMB_GEM_OBJECT_MARKER = 731908;
const int BOMB_GEM_LIFETIME = 10 * TICKS_PER_SECOND;
const int BOMB_GEM_FLIGHT_TICKS = 35;
const int BOMB_GEM_MIN_COUNT = 5;
const int BOMB_GEM_MAX_COUNT = 10;
const float BOMB_GEM_MIN_DISTANCE = 3.0f * 32.0f;
const float BOMB_GEM_MAX_DISTANCE = 5.0f * 32.0f;
const int BOMB_GEM_MASK_RADIUS = 4;
const float BOMB_GEM_BOUNCE_RETENTION = 0.88f;
const float NORMAL_SPEED = 12.0f;
const float DOWNHILL_SPEED = 18.0f;
const float SLIDE_EVENT_SPEED = 26.0f;
const float DOWNHILL_ACCELERATION = 0.30f;
const float DOWNHILL_START_SPEED = 2.50f;
const float SLIDE_EVENT_ACCELERATION = 0.85f;
const float CONTINUOUS_COAST_MIN_SPEED = 4.0f;
const float AIR_DIRECTION_LOCK_THRESHOLD = 0.50f;
const float WALL_JUMP_X_SPEED = 11.5f;
const float WALL_JUMP_Y_SPEED = -8.5f;
const int GROUND_DUST_INTERVAL = 3;
const float GROUND_DUST_MIN_SPEED = 1.25f;
const int ENEMY_BOUNCE_SCORE = 25;
const float ENEMY_BOUNCE_SPEED = 8.0f;
const int ENEMY_BOUNCE_COOLDOWN = 12;
const int ENEMY_ROCK_TICKS = 24;
const int ONE_SPIN_TICKS = 18;
const int WIPEOUT_DURATION = TICKS_PER_SECOND;
const int WIPEOUT_LYING_TICKS = 20;
const int UPRIGHT_ANGLE_TOLERANCE = 64;
// Wheel appearance and placement.
const float WHEEL_SCALE = 0.28f;
const float WHEEL_X_OFFSET = 11.0f;
const float WHEEL_Y_OFFSET = 30.0f;
const float WHEEL_ROTATION_PER_PIXEL = 46.0f;
const uint8 WHEEL_COLOR_RANGE = 72;
enum TimedRunState {
RUN_COUNTDOWN,
RUN_PLAYING,
RUN_RESULTS
}
TimedRunState runState = RUN_COUNTDOWN;
int countdownTicks = 5 * TICKS_PER_SECOND;
int resultTicks = 0;
int autoRestartTicks = AUTO_RESTART_DURATION;
bool restartRequested = false;
int jazzVoiceCooldown = 0;
int multiplayerScoreSyncTicks = 0;
int roundStateSyncTicks = 0;
uint wheelFrame = 0;
array<bool> scoreInitialized(32, false);
array<int> challengeScore(32, 0);
array<bool> challengeJoined(32, false);
array<bool> playerConnectionKnown(32, false);
array<int> knownPlayerClientID(32, -1);
// This list belongs to the mutator module, so it survives level cycles and
// /r restarts. Player-list slots do not: the same connection may be assigned
// a different slot between rounds.
array<int> instructionJoinedClientIDs;
array<bool> previousInstructionFire(32, false);
array<bool> instructionJoinRequested(32, false);
array<bool> roundParticipant(32, false);
array<bool> playerFinished(32, false);
array<int> playerTimeLeftTicks(32, 0);
array<bool> previousJump(32, false);
array<bool> previousFire(32, false);
array<bool> previousMovementHeld(32, false);
array<bool> previousUppercut(32, false);
array<bool> previousWeaponSwitch(32, false);
array<bool> bombSwitchRequested(32, false);
array<bool> previousButtstomp(32, false);
array<bool> hurtStateInitialized(32, false);
array<uint8> previousHealth(32, 0);
array<int> previousBlink(32, 0);
array<int> reverseGravityTicks(32, 0);
array<int> platformBalanceTicks(32, 0);
array<int> runInertia(32, 0);
array<int> coastTicks(32, 0);
array<int> coastDirection(32, 1);
array<float> coastSpeed(32, 0.0f);
array<float> coastDeceleration(32, 0.0f);
array<bool> continuousCoastActive(32, false);
array<int> wallJumpCooldown(32, 0);
array<int> pendingGroundJumpTicks(32, 0);
array<int> observedSlopeDirection(32, 0);
array<int> observedSlopeTicks(32, 0);
array<float> previousSlopeX(32, 0.0f);
array<float> previousSlopeY(32, 0.0f);
array<bool> slopePositionKnown(32, false);
array<int> springScoreCooldown(32, 0);
array<int> lastScoredSpringObject(32, -1);
array<int> springMovementProtectionTicks(32, 0);
array<float> previousNativeXSpeed(32, 0.0f);
array<float> previousNativeYSpeed(32, 0.0f);
array<bool> nativeSpeedKnown(32, false);
array<int> visualSpinTicks(32, 0);
array<int> visualSpinTotalTicks(32, 0);
array<int> enemyBounceCooldown(32, 0);
array<int> enemyRockTicks(32, 0);
array<int> wipeoutTicks(32, 0);
array<bool> previousGrounded(32, false);
array<bool> groundStateInitialized(32, false);
array<string> wipeoutText(32, "");
array<int> lastSlideEventTile(32, -1);
array<int> phraseTicks(32, 0);
array<string> phraseText(32, "");
array<float> phraseX(32, 0.0f);
array<float> phraseY(32, 0.0f);
array<float> phraseXSpeed(32, 0.0f);
array<int> wheelAngle(32, 0);
array<float> previousWheelX(32, 0.0f);
array<bool> wheelPositionKnown(32, false);
array<int> groundDustCooldown(32, 0);
array<int> bananaSequence(32, 0);
array<int> bananaAmmo(32, STARTING_BANANA_AMMO);
array<int> bombSequence(32, 0);
array<int> bombAmmo(32, STARTING_BOMB_AMMO);
array<int> bombGemDropSequence(32, 0);
array<int> appliedBombExplosionSequence(
32 * BOMB_EXPLOSION_HISTORY,
0
);
// Populated from the native object presets. Only ordinary enemies are marked;
// bosses, boss parts, projectiles, and environmental hazards are excluded.
array<bool> skateboardBounceEnemyEvent(256, false);
array<string> coolPhrases = {
"Cool tricks!",
"Gnarly!",
"Awesome!",
"Dude!",
"Whoa!",
"Sweet!",
"Radical!",
"Totally rad!",
"Nice one!"
};
array<string> firePhrases = {
"Oi oi oi!",
"Up the punx!",
"Pick it up!",
"Ska's not dead!",
"Hey! Ho! Let's go!",
"Gabba Gabba Hey!",
"Unity!",
"Rude boy!",
"Rude girl!"
};
array<string> instructionLines = {
"1. Collect as many points",
"2. Collect BANANAs",
"3. Press FIRE to throw",
"4. Press CHANGE WEAPON",
"5. Press JUMP",
"6. Grab yourself an",
"7. Be careful when",
"8. Grab CLOCKS"
};
array<string> instructionDetailLines = {
"as you can before the timer",
"and BOMBs",
"bananas at your foes.",
"to throw a bomb.",
"while you are in the air",
"ANTI GRAVITY",
"uppercutting and",
"to receive"
};
array<string> instructionLastLines = {
"runs out.",
"as ammo!",
"Total wipe out, dude.",
"Kaboom, bro.",
"to do COOL TRICKS!",
"BURGER!",
"buttstomping!",
"extra time!"
};
array<uint8> instructionColors = {
2, 3, 4, 5, 6, 7, 8, 9
};
array<string> wipeoutPhrases = {
"Watch it!",
"Wipe out!",
"Ouch!",
"Bummer dude!",
"WIPE OUT!",
"Wipe out!!"
};
array<uint8> ammoGemColors = {
OBJECT::REDGEM,
OBJECT::PURPLEGEM,
OBJECT::GREENGEM,
OBJECT::BLUEGEM
};
float absoluteFloat(float value) {
return value < 0.0f ? -value : value;
}
int signOf(float value) {
if (value < 0.0f) return -1;
if (value > 0.0f) return 1;
return 0;
}
float limitSpeed(float value, float maximum) {
if (value > maximum) return maximum;
if (value < -maximum) return -maximum;
return value;
}
int playerSlot(const jjPLAYER@ player) {
int id = int(player.playerID);
if (id < 0 || id >= 32) return 0;
return id;
}
void configureScriptedPickup(uint8 eventID) {
jjObjectPresets[eventID].scriptedCollisions = true;
}
int scoreForPickup(const jjOBJ@ obj) {
int points = int(obj.points);
if (points <= 0)
points = MINIMUM_PICKUP_POINTS;
return points;
}
int scoreForSpring(uint8 eventID) {
if (eventID == OBJECT::REDSPRING ||
eventID == OBJECT::HORREDSPRING)
return 100;
if (eventID == OBJECT::GREENSPRING ||
eventID == OBJECT::HORGREENSPRING)
return 200;
if (eventID == OBJECT::BLUESPRING ||
eventID == OBJECT::HORBLUESPRING)
return 300;
return 0;
}
float speedForSpring(uint8 eventID) {
if (eventID == OBJECT::REDSPRING ||
eventID == OBJECT::HORREDSPRING)
return 16.0f;
if (eventID == OBJECT::GREENSPRING ||
eventID == OBJECT::HORGREENSPRING)
return 24.0f;
if (eventID == OBJECT::BLUESPRING ||
eventID == OBJECT::HORBLUESPRING)
return 32.0f;
return 0.0f;
}
class GuaranteedSpecialPickup : jjBEHAVIORINTERFACE {
void onBehave(jjOBJ@ obj) {
obj.behave(BEHAVIOR::PICKUP);
}
bool onObjectHit(
jjOBJ@ obj,
jjOBJ@ bullet,
jjPLAYER@ player,
int force
) {
if (player is null)
return false;
int id = playerSlot(player);
if (
runState != RUN_PLAYING ||
!roundParticipant[id] ||
playerFinished[id]
)
return false;
if (obj.eventID == OBJECT::EXTRATIME) {
if (runState == RUN_PLAYING)
adjustRoundTime(player, 5 * TICKS_PER_SECOND);
}
else if (obj.eventID != OBJECT::FASTFEET) {
return false;
}
awardFloatingScore(
player,
obj.eventID == OBJECT::FASTFEET ?
FAST_FEET_SCORE :
scoreForPickup(obj)
);
jjSample(obj.xPos, obj.yPos, SOUND::COMMON_PICKUP1, 63);
obj.behavior = BEHAVIOR::EXPLOSION2;
obj.scriptedCollisions = false;
obj.frameID = 0;
return true;
}
}
class FastfireToFastFeet : jjBEHAVIORINTERFACE {
void onBehave(jjOBJ@ obj) override {
transformObjectToPreset(
obj,
OBJECT::FASTFEET
);
}
}
void gravityBurgerBehavior(jjOBJ@ obj) {
obj.playerHandling = HANDLING::PARTICLE;
obj.scriptedCollisions = false;
obj.behave(BEHAVIOR::PICKUP);
obj.playerHandling = HANDLING::PARTICLE;
}
void transformObjectToPreset(
jjOBJ@ obj,
uint8 eventID
) {
jjOBJ@ preset = jjObjectPresets[eventID];
obj.eventID = eventID;
obj.behavior = preset.behavior;
obj.curAnim = preset.curAnim;
obj.killAnim = preset.killAnim;
obj.frameID = preset.frameID;
obj.animSpeed = preset.animSpeed;
obj.special = preset.special;
obj.state = preset.state;
obj.energy = preset.energy;
obj.points = preset.points;
obj.playerHandling = preset.playerHandling;
obj.bulletHandling = preset.bulletHandling;
obj.scriptedCollisions = preset.scriptedCollisions;
obj.isTarget = preset.isTarget;
obj.isFreezable = preset.isFreezable;
obj.isBlastable = preset.isBlastable;
obj.deactivates = preset.deactivates;
obj.doesHurt = preset.doesHurt;
obj.counterEnd = preset.counterEnd;
obj.xSpeed = preset.xSpeed;
obj.ySpeed = preset.ySpeed;
obj.xAcc = preset.xAcc;
obj.yAcc = preset.yAcc;
for (uint8 i = 0; i < 10; ++i)
obj.var[i] = preset.var[i];
obj.determineCurFrame();
obj.behave(preset.behavior);
}
uint8 chooseAmmoGemColor(const jjOBJ@ obj) {
uint choice;
if (jjGameConnection == GAME::LOCAL) {
choice = jjRandom() & 3;
}
else {
// Online players derive the same color from stable object information so
// gem values and challenge scores cannot disagree between computers.
uint seed =
uint(int(obj.xOrg)) * 73856093 +
uint(int(obj.yOrg)) * 19349663 +
uint(obj.eventID) * 83492791 +
uint(obj.objectID) * 1640531513;
choice = seed & 3;
}
return ammoGemColors[choice];
}
class RandomAmmoGem : jjBEHAVIORINTERFACE {
void onBehave(jjOBJ@ obj) override {
transformObjectToPreset(
obj,
chooseAmmoGemColor(obj)
);
}
}
bool isWeaponPowerupEvent(uint8 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;
}
void initializeWeaponPowerupGoldGem(jjOBJ@ obj) {
jjOBJ@ preset = jjObjectPresets[OBJECT::REDGEM];
// Copy an ordinary gem's properties while preserving the placed weapon
// powerup's event ID, which keeps its JCS/MLLE generator association intact.
obj.curAnim = preset.curAnim;
obj.killAnim = preset.killAnim;
obj.frameID = preset.frameID;
obj.animSpeed = preset.animSpeed;
obj.special = preset.special;
obj.state = preset.state;
obj.energy = preset.energy;
obj.points = preset.points;
obj.playerHandling = preset.playerHandling;
obj.bulletHandling = preset.bulletHandling;
obj.scriptedCollisions = preset.scriptedCollisions;
obj.isTarget = preset.isTarget;
obj.isFreezable = preset.isFreezable;
obj.isBlastable = preset.isBlastable;
obj.deactivates = preset.deactivates;
obj.doesHurt = preset.doesHurt;
obj.counterEnd = preset.counterEnd;
obj.xSpeed = preset.xSpeed;
obj.ySpeed = preset.ySpeed;
obj.xAcc = preset.xAcc;
obj.yAcc = preset.yAcc;
for (uint8 i = 0; i < 10; ++i)
obj.var[i] = preset.var[i];
obj.playerHandling = HANDLING::PICKUP;
obj.scriptedCollisions = true;
obj.age = WEAPON_POWERUP_GOLD_GEM_MARKER;
obj.determineCurFrame();
}
class WeaponPowerupToGoldGem : jjBEHAVIORINTERFACE {
void onBehave(jjOBJ@ obj) override {
if (obj.age != WEAPON_POWERUP_GOLD_GEM_MARKER)
initializeWeaponPowerupGoldGem(obj);
obj.behave(BEHAVIOR::PICKUP, false);
obj.playerHandling = HANDLING::PICKUP;
obj.scriptedCollisions = true;
obj.behavior = this;
}
void onDraw(jjOBJ@ obj) {
if (
obj is null ||
!obj.isActive ||
obj.state == STATE::KILL ||
obj.state == STATE::DONE ||
obj.state == STATE::EXPLODE
)
return;
jjDrawSpriteFromCurFrame(
obj.xPos,
obj.yPos,
obj.curFrame,
obj.direction,
SPRITE::SINGLEHUE,
GOLD_GEM_PALETTE_START
);
}
bool onObjectHit(
jjOBJ@ obj,
jjOBJ@ bullet,
jjPLAYER@ player,
int force
) {
if (bullet !is null)
return true;
if (player is null)
return false;
int id = playerSlot(player);
if (
runState != RUN_PLAYING ||
!roundParticipant[id] ||
playerFinished[id]
)
return true;
awardMulticolorGoldGemScore(player);
jjSample(
obj.xPos,
obj.yPos,
SOUND::COMMON_PICKUP1,
63
);
obj.behavior = BEHAVIOR::EXPLOSION2;
obj.scriptedCollisions = false;
obj.frameID = 0;
return true;
}
bool onGetActive(jjOBJ@ obj) {
return obj !is null && obj.isActive;
}
void onSetActive(jjOBJ@ obj, bool setTo) {
if (obj !is null && !setTo)
obj.delete();
}
}
bool chooseBombAmmoPickup(const jjOBJ@ obj) {
if (jjGameConnection == GAME::LOCAL)
return (jjRandom() & 1) != 0;
// Use stable object data online so every computer chooses the same pickup.
uint seed =
uint(int(obj.xOrg)) * 73856093 +
uint(int(obj.yOrg)) * 19349663 +
uint(obj.eventID) * 83492791 +
uint(obj.objectID) * 1640531513;
return (seed & 1) != 0;
}
void initializeMixedAmmoPickup(
jjOBJ@ obj,
jjBEHAVIOR pickupBehavior,
bool makeBomb
) {
jjOBJ@ pickupPreset =
jjObjectPresets[OBJECT::BANANA];
jjOBJ@ visualPreset =
jjObjectPresets[
makeBomb ?
SKATE_BOMB_EVENT :
OBJECT::BANANA
];
uint16 originalPoints = obj.points;
obj.eventID =
makeBomb ? SKATE_BOMB_EVENT : OBJECT::BANANA;
obj.behavior = pickupBehavior;
obj.curAnim = visualPreset.curAnim;
obj.killAnim = visualPreset.killAnim;
obj.frameID = visualPreset.frameID;
obj.animSpeed = pickupPreset.animSpeed;
obj.special = pickupPreset.special;
obj.state = pickupPreset.state;
obj.energy = pickupPreset.energy;
obj.points =
originalPoints > 0 ?
originalPoints : pickupPreset.points;
obj.playerHandling = HANDLING::PICKUP;
obj.bulletHandling = pickupPreset.bulletHandling;
obj.scriptedCollisions = true;
obj.isTarget = false;
obj.isFreezable = false;
obj.isBlastable = false;
obj.deactivates = pickupPreset.deactivates;
obj.doesHurt = 0;
obj.counterEnd = pickupPreset.counterEnd;
obj.xSpeed = pickupPreset.xSpeed;
obj.ySpeed = pickupPreset.ySpeed;
obj.xAcc = pickupPreset.xAcc;
obj.yAcc = pickupPreset.yAcc;
for (uint8 i = 0; i < 10; ++i)
obj.var[i] = pickupPreset.var[i];
obj.var[5] =
makeBomb ? BOMB_AMMO_PICKUP_MARKER : 0;
obj.determineCurFrame();
obj.behave(pickupBehavior);
}
class MixedBananaBombPickup : jjBEHAVIORINTERFACE {
jjBEHAVIOR pickupBehavior;
MixedBananaBombPickup(jjBEHAVIOR behavior) {
pickupBehavior = behavior;
}
void onBehave(jjOBJ@ obj) override {
initializeMixedAmmoPickup(
obj,
pickupBehavior,
chooseBombAmmoPickup(obj)
);
}
}
class BouncerAmmoToBanana : jjBEHAVIORINTERFACE {
void onBehave(jjOBJ@ obj) override {
transformObjectToPreset(
obj,
OBJECT::BANANA
);
}
}
class AmmoCrateToGemCrate : jjBEHAVIORINTERFACE {
void onBehave(jjOBJ@ obj) override {
transformObjectToPreset(
obj,
OBJECT::GEMCRATE
);
}
}
class CarrotToExtraTime : jjBEHAVIORINTERFACE {
void onBehave(jjOBJ@ obj) override {
transformObjectToPreset(
obj,
OBJECT::EXTRATIME
);
}
}
class FullEnergyToBurger : jjBEHAVIORINTERFACE {
void onBehave(jjOBJ@ obj) override {
transformObjectToPreset(
obj,
OBJECT::BURGER
);
}
}
void configureCarrotReplacements() {
jjObjectPresets[OBJECT::CARROT].behavior =
CarrotToExtraTime();
jjObjectPresets[OBJECT::FULLENERGY].behavior =
FullEnergyToBurger();
}
void configureAmmoGemReplacements() {
for (
int eventID = OBJECT::ICEAMMO3;
eventID <= OBJECT::GUN9AMMO3;
++eventID
) {
if (eventID == OBJECT::BOUNCERAMMO3)
continue;
jjObjectPresets[eventID].behavior =
RandomAmmoGem();
}
// Bouncer +3 ammo is now a collectible banana-food event. The existing
// banana pickup handler adds it to the player's banana-ammo counter.
jjObjectPresets[OBJECT::BOUNCERAMMO3].behavior =
BouncerAmmoToBanana();
jjObjectPresets[OBJECT::GUNCRATE].behavior =
AmmoCrateToGemCrate();
for (
int eventID = OBJECT::ICEAMMO15;
eventID <= OBJECT::TOASTERAMMO15;
++eventID
) {
jjObjectPresets[eventID].behavior =
AmmoCrateToGemCrate();
}
}
void configureWeaponPowerupReplacements() {
jjObjectPresets[OBJECT::BLASTERPOWERUP].behavior =
WeaponPowerupToGoldGem();
jjObjectPresets[OBJECT::BOUNCERPOWERUP].behavior =
WeaponPowerupToGoldGem();
jjObjectPresets[OBJECT::ICEPOWERUP].behavior =
WeaponPowerupToGoldGem();
jjObjectPresets[OBJECT::SEEKERPOWERUP].behavior =
WeaponPowerupToGoldGem();
jjObjectPresets[OBJECT::RFPOWERUP].behavior =
WeaponPowerupToGoldGem();
jjObjectPresets[OBJECT::TOASTERPOWERUP].behavior =
WeaponPowerupToGoldGem();
jjObjectPresets[OBJECT::TNTPOWERUP].behavior =
WeaponPowerupToGoldGem();
jjObjectPresets[OBJECT::GUN8POWERUP].behavior =
WeaponPowerupToGoldGem();
jjObjectPresets[OBJECT::GUN9POWERUP].behavior =
WeaponPowerupToGoldGem();
}
void configureMixedBananaBombPickups() {
jjBEHAVIOR nativeBananaBehavior =
jjObjectPresets[OBJECT::BANANA].behavior;
jjObjectPresets[OBJECT::BANANA].behavior =
MixedBananaBombPickup(nativeBananaBehavior);
// Seeker +3 no longer becomes a gem. It uses the same 50/50 banana/bomb
// ammo selection as placed Banana events.
jjObjectPresets[OBJECT::SEEKERAMMO3].behavior =
MixedBananaBombPickup(nativeBananaBehavior);
}
bool isExcludedBossEvent(uint8 eventID) {
return
eventID == OBJECT::TUFBOSS ||
eventID == OBJECT::BILSY ||
eventID == OBJECT::XMASBILSY ||
eventID == OBJECT::QUEEN ||
eventID == OBJECT::UTERUS ||
eventID == OBJECT::UTERUSSPIKEBALL ||
eventID == OBJECT::CRAB ||
eventID == OBJECT::BUBBA ||
eventID == OBJECT::DEVILDEVAN ||
eventID == OBJECT::DEVANROBOT ||
eventID == OBJECT::ROBOT ||
eventID == OBJECT::BOLLY ||
eventID == OBJECT::TWEEDLEBOSS;
}
class HarmlessSkateEnemy : jjBEHAVIORINTERFACE {
jjBEHAVIOR originalBehavior;
HarmlessSkateEnemy(jjBEHAVIOR behavior) {
originalBehavior = behavior;
}
void onBehave(jjOBJ@ obj) override {
// Some native enemy behaviors restore HANDLING::ENEMY every tick, so set
// the harmless handling both before and after running the native code.
obj.playerHandling = HANDLING::PARTICLE;
obj.scriptedCollisions = false;
obj.behave(originalBehavior);
obj.playerHandling = HANDLING::PARTICLE;
obj.scriptedCollisions = false;
}
}
void configureHarmlessSkateEnemies() {
if (
jjGameMode != GAME::SP &&
jjGameMode != GAME::COOP
)
return;
for (int eventID = 1; eventID < 256; ++eventID) {
jjOBJ@ preset = jjObjectPresets[eventID];
if (
preset.playerHandling != HANDLING::ENEMY ||
isExcludedBossEvent(uint8(eventID))
)
continue;
skateboardBounceEnemyEvent[eventID] = true;
preset.behavior = HarmlessSkateEnemy(preset.behavior);
preset.playerHandling = HANDLING::PARTICLE;
preset.scriptedCollisions = false;
}
}
void onLevelLoad() {
jjCharacters[CHAR::JAZZ].airJump = AIR::NONE;
jjCharacters[CHAR::JAZZ].doubleJumpCountMax = 0;
jjCharacters[CHAR::JAZZ].helicopterDurationMax = 0;
configureHarmlessSkateEnemies();
for (int eventID = 1; eventID < 256; ++eventID) {
if (jjObjectPresets[eventID].playerHandling == HANDLING::PICKUP)
jjObjectPresets[eventID].scriptedCollisions = true;
}
jjObjectPresets[OBJECT::FASTFEET].scriptedCollisions = true;
jjObjectPresets[OBJECT::FASTFEET].behavior =
GuaranteedSpecialPickup();
jjObjectPresets[OBJECT::FASTFIRE].behavior =
FastfireToFastFeet();
jjObjectPresets[OBJECT::BURGER].playerHandling =
HANDLING::PARTICLE;
jjObjectPresets[OBJECT::BURGER].scriptedCollisions = false;
jjObjectPresets[OBJECT::BURGER].behavior =
gravityBurgerBehavior;
jjObjectPresets[OBJECT::EXTRATIME].scriptedCollisions = true;
jjObjectPresets[OBJECT::EXTRATIME].behavior =
GuaranteedSpecialPickup();
configureCarrotReplacements();
// Keep all springs completely native.
jjObjectPresets[OBJECT::REDSPRING].scriptedCollisions = false;
jjObjectPresets[OBJECT::GREENSPRING].scriptedCollisions = false;
jjObjectPresets[OBJECT::BLUESPRING].scriptedCollisions = false;
jjObjectPresets[OBJECT::HORREDSPRING].scriptedCollisions = false;
jjObjectPresets[OBJECT::HORGREENSPRING].scriptedCollisions = false;
jjObjectPresets[OBJECT::HORBLUESPRING].scriptedCollisions = false;
configureAmmoGemReplacements();
configureMixedBananaBombPickups();
configureWeaponPowerupReplacements();
if (jjAnimSets[ANIM::PICKUPS].firstAnim == 0)
jjAnimSets[ANIM::PICKUPS].load();
jjObjectPresets[OBJECT::SILVERCOIN].frameID = 0;
jjObjectPresets[OBJECT::SILVERCOIN].determineCurFrame();
wheelFrame = jjObjectPresets[OBJECT::SILVERCOIN].curFrame;
}
void resetSkateboardRound() {
runState = RUN_COUNTDOWN;
countdownTicks = 5 * TICKS_PER_SECOND;
resultTicks = 0;
autoRestartTicks = AUTO_RESTART_DURATION;
restartRequested = false;
jazzVoiceCooldown = 0;
multiplayerScoreSyncTicks = 0;
roundStateSyncTicks = 0;
for (int i = 0; i < 32; ++i) {
scoreInitialized[i] = false;
challengeScore[i] = 0;
roundParticipant[i] = false;
playerFinished[i] = false;
playerTimeLeftTicks[i] = 0;
previousJump[i] = false;
previousFire[i] = false;
previousMovementHeld[i] = false;
previousUppercut[i] = false;
previousWeaponSwitch[i] = false;
bombSwitchRequested[i] = false;
previousButtstomp[i] = false;
hurtStateInitialized[i] = false;
previousHealth[i] = 0;
previousBlink[i] = 0;
reverseGravityTicks[i] = 0;
platformBalanceTicks[i] = 0;
runInertia[i] = 0;
coastTicks[i] = 0;
coastDirection[i] = 1;
coastSpeed[i] = 0.0f;
coastDeceleration[i] = 0.0f;
continuousCoastActive[i] = false;
wallJumpCooldown[i] = 0;
pendingGroundJumpTicks[i] = 0;
observedSlopeDirection[i] = 0;
observedSlopeTicks[i] = 0;
slopePositionKnown[i] = false;
springScoreCooldown[i] = 0;
lastScoredSpringObject[i] = -1;
springMovementProtectionTicks[i] = 0;
nativeSpeedKnown[i] = false;
visualSpinTicks[i] = 0;
visualSpinTotalTicks[i] = 0;
enemyBounceCooldown[i] = 0;
enemyRockTicks[i] = 0;
wipeoutTicks[i] = 0;
previousGrounded[i] = false;
groundStateInitialized[i] = false;
wipeoutText[i] = "";
lastSlideEventTile[i] = -1;
phraseTicks[i] = 0;
phraseText[i] = "";
wheelAngle[i] = 0;
wheelPositionKnown[i] = false;
groundDustCooldown[i] = 0;
bananaSequence[i] = 0;
bananaAmmo[i] = STARTING_BANANA_AMMO;
bombSequence[i] = 0;
bombAmmo[i] = STARTING_BOMB_AMMO;
bombGemDropSequence[i] = 0;
for (
int history = 0;
history < BOMB_EXPLOSION_HISTORY;
++history
)
appliedBombExplosionSequence[
i * BOMB_EXPLOSION_HISTORY + history
] = 0;
}
// A /r restart does not reload the level, so remove old projectiles and
// bomb-dropped gems before the next countdown begins.
for (int i = 1; i < jjObjectCount; ++i) {
jjOBJ@ obj = jjObjects[i];
if (
obj.isActive &&
(
(
obj.eventID == OBJECT::BANANA &&
obj.var[5] == BANANA_OBJECT_MARKER
) ||
(
obj.eventID == SKATE_BOMB_EVENT &&
obj.var[5] == BOMB_OBJECT_MARKER
) ||
(
obj.var[5] == BOMB_GEM_OBJECT_MARKER
)
)
)
obj.delete();
}
}
void onLevelBegin() {
resetSkateboardRound();
}
void onLevelReload() {
resetSkateboardRound();
}
// A multiplayer /r starts a new timed challenge without reloading the level.
void onGameStart(bool firstTime) {
if (!firstTime)
resetSkateboardRound();
}
bool isRoundAuthority() {
return
jjGameConnection == GAME::LOCAL ||
jjIsServer;
}
bool isEligibleRoundPlayer(const jjPLAYER@ player) {
return
player !is null &&
player.isActive &&
player.isInGame &&
!player.isSpectating;
}
bool instructionConnectionHasJoined(int clientID) {
for (
uint i = 0;
i < instructionJoinedClientIDs.length();
++i
) {
if (instructionJoinedClientIDs[i] == clientID)
return true;
}
return false;
}
void rememberInstructionConnection(int clientID) {
if (
clientID < 0 ||
instructionConnectionHasJoined(clientID)
)
return;
instructionJoinedClientIDs.insertLast(clientID);
}
void markInstructionConnectionJoined(jjPLAYER@ player) {
if (
player is null ||
!player.isActive
)
return;
int clientID = int(player.clientID);
rememberInstructionConnection(clientID);
// A connection may own multiple splitscreen players. Once that connection
// dismisses the one-time instructions, let all of its players join.
for (int i = 0; i < 32; ++i) {
jjPLAYER@ connectedPlayer = jjPlayers[i];
if (
connectedPlayer.isActive &&
int(connectedPlayer.clientID) == clientID
) {
challengeJoined[i] = true;
instructionJoinRequested[i] = false;
}
}
}
void registerPlayerConnection(jjPLAYER@ player) {
if (
player is null ||
!player.isActive
)
return;
int id = playerSlot(player);
int clientID = int(player.clientID);
if (
playerConnectionKnown[id] &&
knownPlayerClientID[id] == clientID
)
return;
playerConnectionKnown[id] = true;
knownPlayerClientID[id] = clientID;
challengeJoined[id] =
instructionConnectionHasJoined(clientID);
previousInstructionFire[id] = false;
instructionJoinRequested[id] = false;
}
void updatePlayerConnections() {
for (int i = 0; i < 32; ++i) {
jjPLAYER@ player = jjPlayers[i];
if (player.isActive)
registerPlayerConnection(player);
}
}
bool hasEligibleJoinedPlayer() {
for (int i = 0; i < 32; ++i) {
if (
isEligibleRoundPlayer(jjPlayers[i]) &&
challengeJoined[i]
)
return true;
}
return false;
}
int currentRoundTimeForLateJoiner() {
int remainingTime = 0;
for (int i = 0; i < 32; ++i) {
if (
roundParticipant[i] &&
!playerFinished[i] &&
playerTimeLeftTicks[i] > remainingTime
)
remainingTime = playerTimeLeftTicks[i];
}
return remainingTime;
}
void initializeLateRoundParticipant(
jjPLAYER@ player,
int remainingTime
) {
if (
player is null ||
!isEligibleRoundPlayer(player) ||
remainingTime <= 0
)
return;
int id = playerSlot(player);
if (roundParticipant[id])
return;
roundParticipant[id] = true;
playerFinished[id] = false;
playerTimeLeftTicks[id] = remainingTime;
scoreInitialized[id] = false;
challengeScore[id] = 0;
bananaAmmo[id] = STARTING_BANANA_AMMO;
bombAmmo[id] = STARTING_BOMB_AMMO;
reverseGravityTicks[id] = 0;
platformBalanceTicks[id] = 0;
wipeoutTicks[id] = 0;
wipeoutText[id] = "";
runInertia[id] = 0;
coastTicks[id] = 0;
coastSpeed[id] = 0.0f;
continuousCoastActive[id] = false;
}
void joinConnectionToCurrentRound(int clientID) {
if (
!isRoundAuthority() ||
runState != RUN_PLAYING
)
return;
int remainingTime = currentRoundTimeForLateJoiner();
if (remainingTime <= 0)
return;
for (int i = 0; i < 32; ++i) {
jjPLAYER@ player = jjPlayers[i];
if (
player.isActive &&
int(player.clientID) == clientID &&
challengeJoined[i]
)
initializeLateRoundParticipant(
player,
remainingTime
);
}
}
void joinWaitingPlayersToCurrentRound() {
if (
!isRoundAuthority() ||
runState != RUN_PLAYING
)
return;
int remainingTime = currentRoundTimeForLateJoiner();
if (remainingTime <= 0)
return;
bool joinedAnyPlayer = false;
for (int i = 0; i < 32; ++i) {
jjPLAYER@ player = jjPlayers[i];
if (
!challengeJoined[i] ||
roundParticipant[i] ||
!isEligibleRoundPlayer(player)
)
continue;
initializeLateRoundParticipant(
player,
remainingTime
);
if (roundParticipant[i])
joinedAnyPlayer = true;
}
if (
joinedAnyPlayer &&
jjIsServer
)
sendRoundState();
}
void requestChallengeJoin(jjPLAYER@ player) {
if (
player is null ||
!player.isLocal
)
return;
registerPlayerConnection(player);
int id = playerSlot(player);
markInstructionConnectionJoined(player);
if (
jjGameConnection == GAME::LOCAL ||
jjIsServer
) {
joinConnectionToCurrentRound(
int(player.clientID)
);
if (jjIsServer)
sendRoundState();
return;
}
jjSTREAM packet;
packet.push(PACKET_PLAYER_JOINED);
packet.push(uint8(id));
jjSendPacket(packet);
}
void captureRoundParticipants() {
for (int i = 0; i < 32; ++i) {
roundParticipant[i] =
isEligibleRoundPlayer(jjPlayers[i]) &&
challengeJoined[i];
playerFinished[i] = false;
playerTimeLeftTicks[i] =
roundParticipant[i] ?
ROUND_DURATION : 0;
}
}
bool allRoundParticipantsFinished() {
for (int i = 0; i < 32; ++i) {
if (
roundParticipant[i] &&
!playerFinished[i]
)
return false;
}
return true;
}
void beginRoundResults() {
runState = RUN_RESULTS;
resultTicks = 0;
autoRestartTicks = AUTO_RESTART_DURATION;
if (jjIsServer)
sendRoundState();
}
void requestRoundRestart() {
if (restartRequested)
return;
restartRequested = true;
if (jjGameMode == GAME::BATTLE)
jjChat("/r");
else
jjNxt(
jjLevelFileName,
true,
true
);
}
void updateRoundPlayerTimers() {
bool authority = isRoundAuthority();
for (int i = 0; i < 32; ++i) {
if (
!roundParticipant[i] ||
playerFinished[i]
)
continue;
if (
authority &&
!isEligibleRoundPlayer(jjPlayers[i])
) {
playerTimeLeftTicks[i] = 0;
playerFinished[i] = true;
continue;
}
if (playerTimeLeftTicks[i] > 0)
--playerTimeLeftTicks[i];
if (playerTimeLeftTicks[i] <= 0) {
playerTimeLeftTicks[i] = 0;
playerFinished[i] = true;
}
}
if (
authority &&
allRoundParticipantsFinished()
)
beginRoundResults();
}
void onMain() {
updatePlayerConnections();
joinWaitingPlayersToCurrentRound();
if (jazzVoiceCooldown > 0)
--jazzVoiceCooldown;
if (runState == RUN_COUNTDOWN) {
bool authority = isRoundAuthority();
if (
countdownTicks > 0 &&
(
(
authority &&
hasEligibleJoinedPlayer()
) ||
(
!authority &&
countdownTicks <
5 * TICKS_PER_SECOND
)
)
)
--countdownTicks;
if (countdownTicks <= 0) {
countdownTicks = 0;
if (authority) {
captureRoundParticipants();
runState = RUN_PLAYING;
if (jjIsServer)
sendRoundState();
}
}
}
else if (runState == RUN_PLAYING) {
updateRoundPlayerTimers();
}
else {
++resultTicks;
if (autoRestartTicks > 0)
--autoRestartTicks;
if (
autoRestartTicks <= 0 &&
isRoundAuthority()
)
requestRoundRestart();
}
for (int i = 0; i < 32; ++i) {
if (phraseTicks[i] > 0) {
--phraseTicks[i];
phraseX[i] += phraseXSpeed[i];
phraseY[i] -= 0.55f;
}
// Local wipeouts count down in onPlayer. Network-reported wipeouts for
// remote players need their own visual countdown on each observer.
if (
wipeoutTicks[i] > 0 &&
jjPlayers[i].isActive &&
!jjPlayers[i].isLocal
) {
--wipeoutTicks[i];
if (wipeoutTicks[i] <= 0) {
wipeoutTicks[i] = 0;
wipeoutText[i] = "";
}
}
}
if (
jjGameMode == GAME::BATTLE &&
jjGameConnection != GAME::LOCAL
) {
++multiplayerScoreSyncTicks;
if (multiplayerScoreSyncTicks >= SCORE_SYNC_INTERVAL) {
multiplayerScoreSyncTicks = 0;
sendAllLocalChallengeScores();
}
if (jjIsServer) {
++roundStateSyncTicks;
if (roundStateSyncTicks >= ROUND_STATE_SYNC_INTERVAL) {
roundStateSyncTicks = 0;
sendRoundState();
}
}
}
}
bool hasGroundSurfaceAt(
int x,
int y,
int gravityDirection
) {
bool previousMasked = jjMaskedPixel(
x,
y + gravityDirection * 7
);
for (int distance = 8; distance <= 31; ++distance) {
bool masked = jjMaskedPixel(
x,
y + gravityDirection * distance
);
if (!previousMasked && masked)
return true;
previousMasked = masked;
}
return false;
}
bool isOnGround(const jjPLAYER@ player) {
if (player.platform > 0)
return true;
int x = int(player.xPos);
int y = int(player.yPos);
int gravityDirection = player.antiGrav ? -1 : 1;
if (!player.antiGrav && player.ySpeed < -0.5f)
return false;
if (player.antiGrav && player.ySpeed > 0.5f)
return false;
array<int> xOffsets = {
-14, -9, -4, 0, 4, 9, 14
};
for (uint i = 0; i < xOffsets.length(); ++i) {
if (
hasGroundSurfaceAt(
x + xOffsets[i],
y,
gravityDirection
)
)
return true;
}
return false;
}
// Returns -1 when the slope goes down toward the left.
// Returns +1 when it goes down toward the right.
int downhillDirection(const jjPLAYER@ player) {
if (!isOnGround(player))
return 0;
if (player.antiGrav)
return 0;
int x = int(player.xPos);
int y = int(player.yPos) - 32;
const int scanLength = 112;
array<int> offsets = {
-16, -12, -8, -4, 0, 4, 8, 12, 16
};
int firstOffset = 0;
int firstSurface = 0;
int lastOffset = 0;
int lastSurface = 0;
bool foundSurface = false;
for (uint i = 0; i < offsets.length(); ++i) {
int surface = jjMaskedTopVLine(
x + offsets[i],
y,
scanLength
);
if (surface > scanLength)
continue;
if (!foundSurface) {
firstOffset = offsets[i];
firstSurface = surface;
foundSurface = true;
}
lastOffset = offsets[i];
lastSurface = surface;
}
if (!foundSurface || firstOffset == lastOffset)
return 0;
int difference = lastSurface - firstSurface;
if (difference >= 2)
return 1;
if (difference <= -2)
return -1;
return 0;
}
void updateObservedSlope(jjPLAYER@ player) {
int id = playerSlot(player);
if (!slopePositionKnown[id]) {
previousSlopeX[id] = player.xPos;
previousSlopeY[id] = player.yPos;
slopePositionKnown[id] = true;
return;
}
float deltaX = player.xPos - previousSlopeX[id];
float deltaY = player.yPos - previousSlopeY[id];
previousSlopeX[id] = player.xPos;
previousSlopeY[id] = player.yPos;
if (
absoluteFloat(deltaX) > 24.0f ||
absoluteFloat(deltaY) > 24.0f
) {
observedSlopeTicks[id] = 0;
return;
}
if (observedSlopeTicks[id] > 0)
--observedSlopeTicks[id];
int travelDirection = signOf(deltaX);
bool surfaceMovement = isOnGround(player);
if (
!surfaceMovement ||
travelDirection == 0 ||
absoluteFloat(deltaX) < 0.10f
)
return;
if (deltaY < -0.10f) {
observedSlopeDirection[id] = -travelDirection;
observedSlopeTicks[id] = 14;
}
else if (deltaY > 0.10f) {
observedSlopeDirection[id] = travelDirection;
observedSlopeTicks[id] = 14;
}
}
int effectiveDownhillDirection(const jjPLAYER@ player) {
int id = playerSlot(player);
if (
observedSlopeTicks[id] > 0 &&
observedSlopeDirection[id] != 0
)
return observedSlopeDirection[id];
return downhillDirection(player);
}
int inputDirection(const jjPLAYER@ player) {
if (player.keyLeft && !player.keyRight)
return -1;
if (player.keyRight && !player.keyLeft)
return 1;
return 0;
}
bool isTouchingWall(
const jjPLAYER@ player,
int direction
) {
int x = int(player.xPos) + direction * 13;
int y = int(player.yPos);
int solidPixels = 0;
if (jjMaskedPixel(x, y - 10))
++solidPixels;
if (jjMaskedPixel(x, y))
++solidPixels;
if (jjMaskedPixel(x, y + 10))
++solidPixels;
return solidPixels >= 2;
}
int eventTileAt(int xPixel, int yPixel) {
int tileX = xPixel / 32;
int tileY = yPixel / 32;
if (tileX < 0 || tileY < 0)
return -1;
if (
tileX >= int(jjLayerWidth[4]) ||
tileY >= int(jjLayerHeight[4])
)
return -1;
if (
jjEventGet(uint16(tileX), uint16(tileY)) !=
AREA::SLIDE
)
return -1;
return tileX + tileY * 65536;
}
int touchedSlideEventTile(const jjPLAYER@ player) {
int x = int(player.xPos);
int y = int(player.yPos);
int tile = eventTileAt(x, y + 16);
if (tile >= 0)
return tile;
tile = eventTileAt(x - 10, y + 16);
if (tile >= 0)
return tile;
tile = eventTileAt(x + 10, y + 16);
if (tile >= 0)
return tile;
return eventTileAt(x, y);
}
void makeSparks(
const jjPLAYER@ player,
int amount = 1
) {
for (int i = 0; i < amount; ++i) {
jjPARTICLE@ spark =
jjAddParticle(PARTICLE::SPARK);
if (spark is null)
continue;
spark.xPos =
player.xPos -
player.direction *
(6.0f + float(jjRandom() & 7));
spark.yPos = player.yPos + 17.0f;
spark.xSpeed =
-player.xSpeed * 0.10f +
float(int(jjRandom() & 31) - 15) / 20.0f;
spark.ySpeed =
-0.5f -
float(jjRandom() & 15) / 18.0f;
}
}
void updateGroundDust(jjPLAYER@ player) {
int id = playerSlot(player);
if (groundDustCooldown[id] > 0)
--groundDustCooldown[id];
bool onNormalGround =
runState == RUN_PLAYING &&
wipeoutTicks[id] <= 0 &&
!player.antiGrav &&
player.platform <= 0 &&
isOnGround(player) &&
touchedSlideEventTile(player) < 0 &&
absoluteFloat(player.xSpeed) >=
GROUND_DUST_MIN_SPEED;
if (!onNormalGround) {
groundDustCooldown[id] = 0;
return;
}
if (groundDustCooldown[id] > 0)
return;
groundDustCooldown[id] = GROUND_DUST_INTERVAL;
jjPARTICLE@ dust = jjAddParticle(PARTICLE::SMOKE);
if (dust is null)
return;
int movementDirection = signOf(player.xSpeed);
if (movementDirection == 0)
movementDirection = player.direction < 0 ? -1 : 1;
dust.xPos =
player.xPos -
float(movementDirection) *
(9.0f + float(jjRandom() & 7));
dust.yPos =
player.yPos +
27.0f +
float(int(jjRandom() & 3) - 1);
dust.xSpeed =
-player.xSpeed * 0.06f +
float(int(jjRandom() & 15) - 7) /
30.0f;
dust.ySpeed =
-0.15f -
float(jjRandom() & 7) /
30.0f;
dust.smoke.countdown = 26;
}
void sendChallengeScore(const jjPLAYER@ player) {
if (
jjGameMode != GAME::BATTLE ||
jjGameConnection == GAME::LOCAL
)
return;
int id = playerSlot(player);
jjSTREAM packet;
packet.push(PACKET_CHALLENGE_SCORE);
packet.push(uint8(id));
packet.push(challengeScore[id]);
jjSendPacket(packet);
}
void sendAllLocalChallengeScores() {
if (
jjGameMode != GAME::BATTLE ||
jjGameConnection == GAME::LOCAL
)
return;
for (int i = 0; i < jjLocalPlayerCount; ++i) {
const jjPLAYER@ localPlayer = jjLocalPlayers[i];
if (
localPlayer !is null &&
localPlayer.isActive
)
sendChallengeScore(localPlayer);
}
}
int findSkateBanana(
int ownerID,
int sequence
) {
for (int i = 1; i < jjObjectCount; ++i) {
const jjOBJ@ obj = jjObjects[i];
if (
obj.isActive &&
obj.eventID == OBJECT::BANANA &&
obj.creatorType == CREATOR::PLAYER &&
int(obj.creatorID) == ownerID &&
obj.var[5] == BANANA_OBJECT_MARKER &&
obj.var[6] == sequence
)
return i;
}
return 0;
}
int countSkateBananas(int ownerID) {
int count = 0;
for (int i = 1; i < jjObjectCount; ++i) {
const jjOBJ@ obj = jjObjects[i];
if (
obj.isActive &&
obj.eventID == OBJECT::BANANA &&
obj.creatorType == CREATOR::PLAYER &&
int(obj.creatorID) == ownerID &&
obj.var[5] == BANANA_OBJECT_MARKER
)
++count;
}
return count;
}
int findOldestSkateBanana(int ownerID) {
int oldestObjectID = 0;
int oldestAge = -1;
for (int i = 1; i < jjObjectCount; ++i) {
const jjOBJ@ obj = jjObjects[i];
if (
obj.isActive &&
obj.eventID == OBJECT::BANANA &&
obj.creatorType == CREATOR::PLAYER &&
int(obj.creatorID) == ownerID &&
obj.var[5] == BANANA_OBJECT_MARKER &&
obj.counter > oldestAge
) {
oldestAge = obj.counter;
oldestObjectID = i;
}
}
return oldestObjectID;
}
void removeSkateBanana(
int ownerID,
int sequence
) {
int objectID = findSkateBanana(
ownerID,
sequence
);
if (objectID > 0)
jjObjects[objectID].delete();
}
bool canBananaTripPlayer(
int ownerID,
const jjPLAYER@ victim
) {
if (
ownerID < 0 ||
ownerID >= 32 ||
victim is null
)
return false;
int victimID = playerSlot(victim);
const jjPLAYER@ owner = jjPlayers[ownerID];
return
runState == RUN_PLAYING &&
roundParticipant[ownerID] &&
!playerFinished[ownerID] &&
roundParticipant[victimID] &&
!playerFinished[victimID] &&
owner.isActive &&
victim.isActive &&
victim.isInGame &&
owner.isEnemy(victim);
}
bool bananaTouchesPlayer(
const jjOBJ@ banana,
const jjPLAYER@ victim
) {
if (!isOnGround(victim))
return false;
int bananaGravity =
banana.var[7] < 0 ? -1 : 1;
int playerGravity =
victim.antiGrav ? -1 : 1;
if (bananaGravity != playerGravity)
return false;
float xDistance =
victim.xPos - banana.xPos;
if (xDistance < 0.0f)
xDistance = -xDistance;
float footY =
victim.yPos +
float(playerGravity) * 24.0f;
float yDistance = footY - banana.yPos;
if (yDistance < 0.0f)
yDistance = -yDistance;
return
xDistance <= 15.0f &&
yDistance <= 13.0f;
}
void relayBananaWipeout(
int ownerID,
int victimID,
int sequence
) {
if (jjGameConnection == GAME::LOCAL)
return;
jjSTREAM packet;
packet.push(PACKET_BANANA_WIPEOUT);
packet.push(uint8(ownerID));
packet.push(uint8(victimID));
packet.push(sequence);
jjSendPacket(packet);
}
void applyBananaWipeout(
int ownerID,
int victimID,
int sequence
) {
removeSkateBanana(ownerID, sequence);
if (
victimID < 0 ||
victimID >= 32
)
return;
jjPLAYER@ victim = jjPlayers[victimID];
if (
canBananaTripPlayer(ownerID, victim) &&
wipeoutTicks[victimID] <= 0
)
startWipeout(victim);
}
bool tryBananaWipeout(jjOBJ@ banana) {
int ownerID = int(banana.creatorID);
for (int i = 0; i < jjLocalPlayerCount; ++i) {
jjPLAYER@ victim = jjLocalPlayers[i];
if (
!canBananaTripPlayer(ownerID, victim) ||
wipeoutTicks[playerSlot(victim)] > 0 ||
!bananaTouchesPlayer(banana, victim)
)
continue;
int victimID = playerSlot(victim);
int sequence = banana.var[6];
applyBananaWipeout(
ownerID,
victimID,
sequence
);
relayBananaWipeout(
ownerID,
victimID,
sequence
);
return true;
}
return false;
}
bool bananaHasGroundAt(
float x,
float y,
int gravityDirection
) {
int groundY =
int(y) + gravityDirection * 7;
return
jjMaskedPixel(int(x) - 3, groundY) ||
jjMaskedPixel(int(x), groundY) ||
jjMaskedPixel(int(x) + 3, groundY);
}
void skateboardBananaBehavior(jjOBJ@ banana) {
if (banana.state == STATE::START) {
banana.state = STATE::FLY;
banana.playerHandling = HANDLING::PARTICLE;
banana.bulletHandling = HANDLING::IGNOREBULLET;
banana.scriptedCollisions = false;
banana.isTarget = false;
banana.deactivates = false;
banana.counter = 0;
banana.determineCurFrame();
}
int ownerID = int(banana.creatorID);
if (
runState != RUN_PLAYING ||
ownerID < 0 ||
ownerID >= 32 ||
!roundParticipant[ownerID] ||
playerFinished[ownerID] ||
!jjPlayers[ownerID].isActive
) {
banana.delete();
return;
}
++banana.counter;
if (banana.counter >= BANANA_LIFETIME) {
banana.delete();
return;
}
int gravityDirection =
banana.var[7] < 0 ? -1 : 1;
if (banana.state == STATE::FLY) {
banana.var[9] =
(
banana.var[9] +
int(banana.xSpeed * 30.0f)
) &
1023;
float nextX =
banana.xPos + banana.xSpeed;
float nextY =
banana.yPos + banana.ySpeed;
int horizontalDirection =
banana.xSpeed < 0.0f ? -1 : 1;
bool hitWall =
banana.xSpeed != 0.0f &&
jjMaskedPixel(
int(nextX) +
horizontalDirection * 6,
int(nextY)
);
if (hitWall) {
banana.xSpeed *= -0.35f;
nextX = banana.xPos;
}
bool movingTowardGround =
banana.ySpeed *
float(gravityDirection) >=
0.0f;
if (
movingTowardGround &&
bananaHasGroundAt(
nextX,
nextY,
gravityDirection
)
) {
banana.xPos = nextX;
banana.yPos = nextY;
for (int i = 0; i < 12; ++i) {
if (!jjMaskedPixel(
int(banana.xPos),
int(banana.yPos)
))
break;
banana.yPos -=
float(gravityDirection);
}
banana.xSpeed = 0.0f;
banana.ySpeed = 0.0f;
banana.state = STATE::WAIT;
banana.var[9] =
gravityDirection > 0 ? 0 : 512;
}
else {
banana.xPos = nextX;
banana.yPos = nextY;
banana.ySpeed +=
float(gravityDirection) *
BANANA_GRAVITY;
if (
banana.ySpeed *
float(gravityDirection) >
BANANA_MAX_FALL_SPEED
)
banana.ySpeed =
float(gravityDirection) *
BANANA_MAX_FALL_SPEED;
}
}
else if (
banana.counter >= BANANA_ARM_TICKS &&
tryBananaWipeout(banana)
)
return;
banana.determineCurFrame();
jjDrawRotatedSpriteFromCurFrame(
banana.xPos,
banana.yPos,
banana.curFrame,
banana.var[9]
);
}
void initializeSkateBanana(
jjOBJ@ banana,
int ownerID,
int sequence,
float x,
float y,
float xSpeed,
float ySpeed,
int gravityDirection
) {
banana.xOrg = x;
banana.yOrg = y;
banana.xPos = x;
banana.yPos = y;
banana.xSpeed = xSpeed;
banana.ySpeed = ySpeed;
banana.creatorID = uint16(ownerID);
banana.creatorType = CREATOR::PLAYER;
banana.direction = xSpeed < 0.0f ? -1 : 1;
banana.var[5] = BANANA_OBJECT_MARKER;
banana.var[6] = sequence;
banana.var[7] = gravityDirection < 0 ? -1 : 1;
banana.var[9] = 0;
banana.counter = 0;
banana.playerHandling = HANDLING::PARTICLE;
banana.bulletHandling = HANDLING::IGNOREBULLET;
banana.scriptedCollisions = false;
banana.isTarget = false;
banana.deactivates = false;
banana.state = STATE::START;
banana.behavior = skateboardBananaBehavior;
// Run the first frame immediately. This prevents a newly created banana
// from spending one tick with its former pickup/inactive behavior.
skateboardBananaBehavior(banana);
jjSample(
x,
y,
SOUND::MONKEY_THROW,
48
);
}
bool spawnSkateBanana(
int ownerID,
int sequence,
float x,
float y,
float xSpeed,
float ySpeed,
int gravityDirection
) {
if (
ownerID < 0 ||
ownerID >= 32 ||
sequence <= 0
)
return false;
if (findSkateBanana(ownerID, sequence) > 0)
return true;
int objectID = 0;
if (
countSkateBananas(ownerID) >=
MAX_BANANAS_PER_PLAYER
)
objectID = findOldestSkateBanana(ownerID);
if (objectID <= 0) {
objectID = jjAddObject(
OBJECT::BANANA,
x,
y,
uint16(ownerID),
CREATOR::PLAYER,
BEHAVIOR::INACTIVE
);
}
// If JJ2 has no free object slot, reuse this player's oldest peel. This
// guarantees a new throw once that player already has a peel in the level.
if (objectID <= 0)
objectID = findOldestSkateBanana(ownerID);
if (objectID <= 0)
return false;
initializeSkateBanana(
jjObjects[objectID],
ownerID,
sequence,
x,
y,
xSpeed,
ySpeed,
gravityDirection
);
return true;
}
bool throwSkateBanana(jjPLAYER@ player) {
if (
player is null ||
!player.isLocal
)
return false;
int ownerID = playerSlot(player);
if (bananaAmmo[ownerID] <= 0)
return false;
int direction =
player.direction < 0 ? -1 : 1;
int gravityDirection =
player.antiGrav ? -1 : 1;
++bananaSequence[ownerID];
if (bananaSequence[ownerID] <= 0)
bananaSequence[ownerID] = 1;
int sequence = bananaSequence[ownerID];
float x =
player.xPos + float(direction) * 18.0f;
float y =
player.yPos -
float(gravityDirection) * 8.0f;
float xSpeed =
float(direction) * BANANA_THROW_X_SPEED +
player.xSpeed * 0.30f;
float ySpeed =
-float(gravityDirection) *
BANANA_THROW_Y_SPEED;
bool spawned = spawnSkateBanana(
ownerID,
sequence,
x,
y,
xSpeed,
ySpeed,
gravityDirection
);
if (!spawned)
return false;
--bananaAmmo[ownerID];
if (jjGameConnection == GAME::LOCAL)
return true;
jjSTREAM packet;
packet.push(PACKET_BANANA_THROW);
packet.push(uint8(ownerID));
packet.push(sequence);
packet.push(x);
packet.push(y);
packet.push(xSpeed);
packet.push(ySpeed);
packet.push(int8(gravityDirection));
jjSendPacket(packet);
return true;
}
int findSkateBomb(
int ownerID,
int sequence
) {
for (int i = 1; i < jjObjectCount; ++i) {
const jjOBJ@ obj = jjObjects[i];
if (
obj.isActive &&
obj.eventID == SKATE_BOMB_EVENT &&
obj.creatorType == CREATOR::PLAYER &&
int(obj.creatorID) == ownerID &&
obj.var[5] == BOMB_OBJECT_MARKER &&
obj.var[6] == sequence
)
return i;
}
return 0;
}
int countSkateBombs(int ownerID) {
int count = 0;
for (int i = 1; i < jjObjectCount; ++i) {
const jjOBJ@ obj = jjObjects[i];
if (
obj.isActive &&
obj.eventID == SKATE_BOMB_EVENT &&
obj.creatorType == CREATOR::PLAYER &&
int(obj.creatorID) == ownerID &&
obj.var[5] == BOMB_OBJECT_MARKER
)
++count;
}
return count;
}
int findOldestSkateBomb(int ownerID) {
int oldestObjectID = 0;
int oldestAge = -1;
for (int i = 1; i < jjObjectCount; ++i) {
const jjOBJ@ obj = jjObjects[i];
if (
obj.isActive &&
obj.eventID == SKATE_BOMB_EVENT &&
obj.creatorType == CREATOR::PLAYER &&
int(obj.creatorID) == ownerID &&
obj.var[5] == BOMB_OBJECT_MARKER &&
obj.counter > oldestAge
) {
oldestAge = obj.counter;
oldestObjectID = i;
}
}
return oldestObjectID;
}
void removeSkateBomb(
int ownerID,
int sequence
) {
int objectID = findSkateBomb(ownerID, sequence);
if (objectID > 0)
jjObjects[objectID].delete();
}
bool canBombKnockPlayer(
int ownerID,
const jjPLAYER@ victim
) {
if (
ownerID < 0 ||
ownerID >= 32 ||
victim is null
)
return false;
int victimID = playerSlot(victim);
const jjPLAYER@ owner = jjPlayers[ownerID];
return
runState == RUN_PLAYING &&
roundParticipant[ownerID] &&
!playerFinished[ownerID] &&
roundParticipant[victimID] &&
!playerFinished[victimID] &&
owner.isActive &&
victim.isActive &&
victim.isInGame &&
owner.isEnemy(victim);
}
bool skateBombTouchesEnemy(const jjOBJ@ bomb) {
int ownerID = int(bomb.creatorID);
for (int i = 0; i < 32; ++i) {
const jjPLAYER@ victim = jjPlayers[i];
if (!canBombKnockPlayer(ownerID, victim))
continue;
if (
absoluteFloat(victim.xPos - bomb.xPos) <= 18.0f &&
absoluteFloat(victim.yPos - bomb.yPos) <= 26.0f
)
return true;
}
return false;
}
bool isSkateBombBreakableContainer(const jjOBJ@ target) {
if (
target is null ||
!target.isActive ||
target.state == STATE::KILL ||
target.state == STATE::DONE ||
target.state == STATE::EXPLODE
)
return false;
uint8 eventID = target.eventID;
return
target.behavior == BEHAVIOR::MONITOR ||
eventID == OBJECT::GUNCRATE ||
eventID == OBJECT::CARROTCRATE ||
eventID == OBJECT::ONEUPCRATE ||
eventID == OBJECT::BOMBCRATE ||
eventID == OBJECT::SPRINGCRATE ||
eventID == OBJECT::GEMCRATE ||
eventID == OBJECT::GEMBARREL ||
eventID == OBJECT::CARROTBARREL ||
eventID == OBJECT::ONEUPBARREL ||
eventID == OBJECT::GUNBARREL ||
eventID == OBJECT::TRIGGERCRATE ||
eventID == OBJECT::BIRDCAGE ||
eventID == OBJECT::DESTRUCTSCENERY ||
(
eventID >= OBJECT::ICEAMMO15 &&
eventID <= OBJECT::TOASTERAMMO15
[preview ends here]
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.