Name | Author | Game Mode | Rating | |||||
---|---|---|---|---|---|---|---|---|
Astral Witchcraft | minmay | Multiple | 9 |
// by may
// 1.0
// CC0 / public domain
#include "SEweapon.asc"
#include "MLLE-Weapons.asc"
#pragma require "SEweapon.asc"
#pragma require "MLLE-Weapons.asc"
#pragma require "MayZapper.asc"
#pragma require "MayZapper.png"
#pragma require "MayZapper2.png"
#pragma require "MayZapperShoot.wav"
#pragma require "MayZapperShootPU.wav"
#pragma require "MayZapperEnd.wav"
/*
CREDITS:
endSample is from https://freesound.org/people/Glaneur%20de%20sons/sounds/34169/
licensed under CC-BY.
shootSample is made from https://freesound.org/people/Daleonfire/sounds/376694/
and https://freesound.org/people/RICHERlandTV/sounds/661677/ which are both
CC 0 (public domain).
shootSamplePU is made from https://freesound.org/people/Daleonfire/sounds/376694/
and https://freesound.org/people/SomeSine/sounds/404337/ which are both CC 0
(public domain).
Bullet, pickup, powerup, and kill sprites are made from
https://opengameart.org/content/warped-shooting-fx
which are CC 0 (public domain).
*/
namespace MayZapper {
/***** Easily-tweakable things *****/
// Bullet dies after this many ticks.
const uint8 ZAPPER_DURATION = 70;
// Bullet starts with this much speed in the direction it's fired in,
// plus any speed from the player's movement. If it's ever below this
// speed, it will accelerate to it.
const float BASE_SPEED = 12.0f;
// Bullet can bounce up to this many times. Hitting a wall with no
// bounces left will destroy the bullet.
const int NUM_BOUNCES = 1;
const int NUM_BOUNCES_PU = 3;
// If true, enemy Zapper bullets in multiplayer games will be drawn with
// red sprites, so you can tell them apart from your teammates' bullets.
const bool drawEnemyBulletsRed = true;
/***** End of easily-tweakable things *****/
const uint SAMPLE_COUNT = 3;
// catches slopes of up to RADIUS:1 or 1:RADIUS pixels
const int SLOPE_CHECK_RADIUS = 4;
const float PI = 3.141592f;
const int BOUNCES_VAR_INDEX = 5;
const int FLAGS_VAR_INDEX = 8;
enum CUSTOM_FLAGS {
FLAG_NO_SOUND = 1,
}
enum ANIMATION_OFFSETS {
ANIM_ZAPPER_BULLET,
ANIM_ZAPPER_BULLETBOUNCED,
ANIM_ZAPPER_BULLETPU,
ANIM_ZAPPER_BULLETBOUNCEDPU,
ANIM_ZAPPER_BULLETENEMY,
ANIM_ZAPPER_BULLETENEMYBOUNCED,
ANIM_ZAPPER_BULLETENEMYPU,
// okay maybe I should've used more underscores
ANIM_ZAPPER_BULLETENEMYBOUNCEDPU,
ANIM_ZAPPER_PICKUP,
ANIM_ZAPPER_PICKUPPU,
ANIM_ZAPPER_END,
ANIM_ZAPPER_ENDPU,
ANIM_ZAPPER_MONITOR,
ANIM_ZAPPER_CRATE,
ANIM_ZAPPER_HITBOX,
}
void setBounces(jjOBJ@ bullet, int bounces) {
bullet.var[BOUNCES_VAR_INDEX] = bounces;
}
void setHasSound(jjOBJ@ bullet, bool hasSound) {
bullet.var[FLAGS_VAR_INDEX] = hasSound ? 0 : 1;
}
shared class Zapper : se::WeaponInterface {
jjANIMSET@ weaponAnimSet;
bool samplesLoaded = false;
// fallback sounds
SOUND::Sample shootSample = SOUND::AMMO_GUNVELOCITY;
SOUND::Sample shootSamplePU = SOUND::AMMO_GUNVELOCITY;
SOUND::Sample endSample = SOUND::AMMO_FIREGUN2A;
// this weapon is fast so that it's good for chasing people. Its large
// hitbox stops it from phasing through *objects* unless you give it a
// higher speed, but its "hitbox" for colliding with *masks* is one
// pixel. It does enough masked pixel checks to guarantee that walls at
// least this thick always get hit; if your level has thinner walls,
// decrease it. If your level only has thicker walls, you can increase it
// to get better performance.
// DECREASING THIS DOES NOT MAKE COLLISIONS MORE PRECISE FOR WALLS THAT ARE
// ALREADY THICK ENOUGH. DO NOT DECREASE IT UNLESS YOUR LEVEL ACTUALLY HAS
// THIN MASKS. it's in pixels btw
int MIN_SAFE_MASK_THICKNESS = 8;
uint hitboxFrameIndex = 0;
jjANIMSET@ loadAnims(jjANIMSET@ animSet) {
if (animSet !is null) {
@weaponAnimSet = @animSet;
jjPIXELMAP spritesheet("MayZapper.png");
@animSet = animSet.load(spritesheet, 60, 20);
uint firstFrame = jjAnimations[animSet.firstAnim].firstFrame;
for (uint i = firstFrame; i < firstFrame+35; i++) {
jjAnimFrames[i].hotSpotX = -jjAnimFrames[i].width+8;
jjAnimFrames[i].hotSpotY = -jjAnimFrames[i].height/2+1;
}
// setting firstAnim like this looks scary but
// it's fiiiiine!
jjPIXELMAP spritesheet2("MayZapper2.png");
@animSet = animSet.load(spritesheet2, 32, 32);
animSet.firstAnim -= ANIM_ZAPPER_PICKUP;
// use the same hot spot as vanilla powerups
uint monitorFirstFrame = jjAnimations[animSet.firstAnim+ANIM_ZAPPER_MONITOR].firstFrame;
for (uint i = 0; i < 10; i++) {
jjAnimFrames[monitorFirstFrame+i].hotSpotX = -14;
jjAnimFrames[monitorFirstFrame+i].hotSpotY = -14;
}
// use the same hot spot as vanilla crates
// (i don't actually like this spot but whatever, who cares about crates. which is also why i didnt bother making the crate sprite look good)
jjANIMFRAME@ crateFrame = jjAnimFrames[jjAnimations[animSet.firstAnim+ANIM_ZAPPER_CRATE].firstFrame];
crateFrame.hotSpotX = -13;
crateFrame.hotSpotY = -14;
}
hitboxFrameIndex = jjAnimations[animSet.firstAnim+ANIM_ZAPPER_HITBOX].firstFrame;
return @animSet;
}
array<bool>@ loadSamples(const array<SOUND::Sample>& samples) {
if (samples.length() != SAMPLE_COUNT) {
return @array<bool>(SAMPLE_COUNT,false);
}
samplesLoaded = true;
array<bool> rval = {jjSampleLoad(samples[0], "MayZapperShoot.wav"), jjSampleLoad(samples[1], "MayZapperEnd.wav"), jjSampleLoad(samples[2], "MayZapperShootPU.wav")};
if (rval[0]) shootSample = samples[0];
if (rval[1]) endSample = samples[1];
if (rval[2]) shootSamplePU = samples[2];
return @rval;
}
uint getSampleCount() const {
return SAMPLE_COUNT;
}
uint getTraits(bool powerup) const {
return se::WeaponTrait::weapon_default_traits;
}
uint getMaxDamage(bool powerup) const {
return powerup ? 2 : 1;
}
void explode(jjOBJ@ obj) const {
if (obj.var[FLAGS_VAR_INDEX] & FLAG_NO_SOUND == 0) jjSample(obj.xPos, obj.yPos, endSample);
jjObjects[jjAddObject(OBJECT::EXPLOSION, obj.xPos, obj.yPos, obj.objectID, CREATOR::OBJECT)].curAnim = obj.killAnim;
obj.delete();
}
void behave(jjOBJ@ obj) const {
switch (obj.state) {
case STATE::START:
if (obj.creatorType == CREATOR::PLAYER) {
bool powerup = obj.animSpeed == 4;
if (obj.eventID == OBJECT::TNT) {
powerup = jjPlayers[obj.creatorID].powerup[WEAPON::TNT];
if (!powerup) {
obj.animSpeed = 2;
obj.curAnim = weaponAnimSet.firstAnim+ANIM_ZAPPER_BULLET;
obj.killAnim = weaponAnimSet.firstAnim+ANIM_ZAPPER_END;
obj.var[6] = 0x0;
obj.special = weaponAnimSet.firstAnim+ANIM_ZAPPER_BULLET;
}
}
if (jjPlayers[obj.creatorID].isLocal && (obj.var[FLAGS_VAR_INDEX] & FLAG_NO_SOUND == 0)) {
jjSample(obj.xPos, obj.yPos, powerup ? shootSamplePU : shootSample, 63, 39690+jjRandom()%8820);
}
}
obj.counterEnd = jjObjectPresets[obj.eventID].counterEnd;
obj.xSpeed += obj.var[7] / 65536.0f;
obj.state = STATE::FLY;
break;
case STATE::FLY:
{
if (--obj.counterEnd == 0) {
explode(obj);
return;
}
float xSpeed = obj.xSpeed;
float ySpeed = obj.ySpeed;
float xPos = obj.xPos;
float yPos = obj.yPos;
if (jjMaskedPixel(int(xPos), int(yPos))) {
// bullet is in a mask before moving, probably due
// to either the player shooting it inside a wall,
// or an animated tile.
// Try to recover by moving backwards until an unmasked
// pixel is found, up to a limit.
float denom = abs(xSpeed) > abs(ySpeed) ? abs(xSpeed) : abs(ySpeed);
// If the bullet is too slow, don't bother.
if (denom >= 0.125f) {
float xSpeedNorm = xSpeed/denom;
float ySpeedNorm = ySpeed/denom;
uint limit = uint(denom > 16.0f ? 16 : denom);
while (limit-- > 0 && !jjMaskedPixel(int(xPos += xSpeedNorm), int(yPos += ySpeedNorm)));
}
if (jjMaskedPixel(int(xPos), int(yPos))) {
// If it's still in a mask, give up and die.
explode(obj);
return;
}
}
float fractionOfSpeed = 1.0f;
float speedDenom = sqrt(xSpeed*xSpeed+ySpeed*ySpeed);
float speedXNorm = xSpeed/speedDenom;
float speedYNorm = ySpeed/speedDenom;
// bullets below base speed accelerate to base speed
float speedFromBase = BASE_SPEED-speedDenom;
if (speedFromBase > 0.0f) {
float acc = speedFromBase < 0.125f ? speedFromBase : 0.125f;
xSpeed += speedXNorm*acc;
ySpeed += speedYNorm*acc;
}
float maskCheckInc = MIN_SAFE_MASK_THICKNESS/(1.0f+abs(xSpeed)+abs(ySpeed));
for (float hitDist = 1.0f; hitDist > 0.0f; hitDist -= maskCheckInc) {
if (jjMaskedPixel(int(xPos+xSpeed*hitDist), int(yPos+ySpeed*hitDist))) { // bullet will hit a mask
// now find the exact first masked pixel it'll hit.
float searchX = xPos;
float searchY = yPos;
float denom = (abs(xSpeed) > abs(ySpeed) ? abs(xSpeed) : abs(ySpeed))*hitDist;
if (denom >= 1.0f) {
float xSpeedNorm = xSpeed/denom;
float ySpeedNorm = ySpeed/denom;
uint limit = uint(denom > 64.0f ? 64 : denom);
while (limit-- > 0 && !jjMaskedPixel(int(searchX += xSpeedNorm), int(searchY += ySpeedNorm)));
fractionOfSpeed = limit/denom;
} else {
searchX = xPos+xSpeed;
searchY = yPos+ySpeed;
fractionOfSpeed = 1.0f;
}
if (obj.var[BOUNCES_VAR_INDEX] <= 0) {
// No bounces left. Die.
obj.xPos = searchX;
obj.yPos = searchY;
explode(obj);
return;
} else {
// guess a surface normal. This is done by examining
// a plus-shaped area around the hit pixel,
// looking for the closest masked pixel on an adjacent
// line and the closest unmasked pixel on its own line,
// both horizontally and vertically, and assuming the greater
// distance of the two to be a slope, or assuming a straight
// horizontal/vertical wall if the maximum distance was reached.
int horizontalDistance = 0, verticalDistance = 0;
int horizontalSign = 0, verticalSign = 0;
int unmaskedHorizontalDistance = 0;
int xInt = int(searchX);
int yInt = int(searchY);
for (int dx = 1; dx <= SLOPE_CHECK_RADIUS; dx++) {
if (!jjMaskedPixel(xInt+dx,yInt)) {
unmaskedHorizontalDistance = dx;
horizontalSign = 1; // down-right
break;
} else if (!jjMaskedPixel(xInt-dx,yInt)) {
unmaskedHorizontalDistance = dx;
horizontalSign = -1;
break;
}
}
int unmaskedVerticalDistance = 0;
for (int dy = 1; dy <= SLOPE_CHECK_RADIUS; dy++) {
if (!jjMaskedPixel(xInt,yInt+dy)) {
unmaskedVerticalDistance = dy;
verticalSign = 1; // right-down
break;
} else if (!jjMaskedPixel(xInt,yInt-dy)) {
unmaskedVerticalDistance = dy;
verticalSign = -1;
break;
}
}
if (verticalSign != 0) {
for (int dy = 0; dy <= SLOPE_CHECK_RADIUS; dy++) {
if (jjMaskedPixel(xInt+horizontalSign,yInt-dy*verticalSign)) {
verticalDistance = dy+unmaskedVerticalDistance-1;
break;
}
}
}
if (horizontalSign != 0) {
for (int dx = 0; dx <= SLOPE_CHECK_RADIUS; dx++) {
if (jjMaskedPixel(xInt-dx*horizontalSign,yInt+verticalSign)) {
horizontalDistance = dx+unmaskedHorizontalDistance-1;
break;
}
}
}
// Done getting mask info.
float slopeXNorm, slopeYNorm;
if (horizontalSign == 0) {
if (verticalSign == 0) {
// This happens if there are no unmasked pixels nearby,
// due to the bullet being in a tiny hole (the
// bullet starting in a fully masked wall was already
// checked for near the beginning).
explode(obj);
//jjPrint("Zapper bullet trapped");
return;
}
// Horizontal floor or ceiling (or a weird mask
// pattern we might as well treat as one).
slopeXNorm = 0;
slopeYNorm = -verticalSign;
//jjPrint("Horizontal wall detected");
} else if (verticalSign == 0) {
// Vertical wall (or a weird mask pattern we
// might as well treat as one).
slopeXNorm = -horizontalSign;
slopeYNorm = 0;
//jjPrint("Vertical wall detected");
} else if (horizontalDistance >= verticalDistance) {
float slopeDenom = sqrt(horizontalDistance*horizontalDistance+1);
slopeXNorm = horizontalSign/slopeDenom;
slopeYNorm = verticalSign*horizontalDistance/slopeDenom;
//jjPrint("Horizontal slope detected: "+horizontalDistance*horizontalSign+":1");
} else {
float slopeDenom = sqrt(verticalDistance*verticalDistance+1);
slopeYNorm = verticalSign/slopeDenom;
slopeXNorm = horizontalSign*verticalDistance/slopeDenom;
//jjPrint("Vertical slope detected: 1:"+verticalDistance*verticalSign);
}
float dot = slopeXNorm*speedXNorm+slopeYNorm*speedYNorm;
xSpeed = (speedXNorm - 2*dot*slopeXNorm)*speedDenom;
ySpeed = (speedYNorm - 2*dot*slopeYNorm)*speedDenom;
xPos = xInt;
yPos = yInt;
float accDenom = sqrt(obj.xAcc**2+obj.yAcc**2);
if (accDenom > 0.01f) {
obj.xAcc = (obj.xAcc/accDenom - 2*dot*slopeXNorm)*accDenom;
obj.yAcc = (obj.yAcc/accDenom - 2*dot*slopeYNorm)*accDenom;
}
// decrement number of bounces
obj.var[BOUNCES_VAR_INDEX] = obj.var[BOUNCES_VAR_INDEX] - 1;
// switch to bounced sprite
if (obj.var[BOUNCES_VAR_INDEX] == 0) {
obj.curAnim += 1;
}
// bounce effect
if (obj.var[FLAGS_VAR_INDEX] & FLAG_NO_SOUND == 0) jjSample(xPos, yPos, endSample);
jjObjects[jjAddObject(OBJECT::EXPLOSION, xPos, yPos, obj.objectID, CREATOR::OBJECT)].curAnim = obj.killAnim;
}
// don't do further mask checks, we already hit something!
break;
}
}
obj.xPos = xPos + xSpeed * fractionOfSpeed;
obj.yPos = yPos + ySpeed * fractionOfSpeed;
obj.xSpeed = xSpeed + obj.xAcc;
obj.ySpeed = ySpeed + obj.yAcc;
uint16 frameCount = jjAnimations[obj.curAnim].frameCount;
obj.frameID = frameCount - (obj.counterEnd/4 % frameCount) - 1;
// curFrame is set to a special hitbox sprite.
// Using the actual sprite for curFrame has bad collision results because
// there's no way to rotate it. (A large number of pre-rotated hitbox
// sprites would work fine as an alternative, though.)
obj.curFrame = hitboxFrameIndex;
// Instead of obj.direction, zapper bullets face in the direction of
// their movement vector. direction is just set here in case any other
// code wants it.
obj.direction = obj.xSpeed >= 0.0f ? DIRECTION::RIGHT : DIRECTION::LEFT;
int bulletAngle = 1024-int(atan2(obj.ySpeed, obj.xSpeed)/(PI*2)*1024);
for (int i = 0; i < jjLocalPlayerCount; i++) {
int16 anim = obj.curAnim;
// switch to enemy bullet anim if needed
if (drawEnemyBulletsRed && jjPlayers[obj.creatorID].isEnemy(jjLocalPlayers[i])) anim += 4;
int frame = jjAnimations[anim].firstFrame+obj.frameID;
jjDrawRotatedSpriteFromCurFrame(obj.xPos, obj.yPos, frame, bulletAngle, playerID: jjLocalPlayers[i].playerID);
}
break;
}
case STATE::EXPLODE:
explode(obj);
break;
case STATE::KILL:
case STATE::DEACTIVATE:
obj.delete();
break;
}
}
bool setAsWeapon(uint number, se::WeaponHook@ weaponHook) {
if (se::isValidWeapon(number)) {
jjWeapons[number].comesFromBirds = true;
jjWeapons[number].comesFromBirdsPowerup = true;
jjWeapons[number].comesFromGunCrates = true;
jjWeapons[number].defaultSample = false;
jjWeapons[number].gemsLost = 3;
jjWeapons[number].gradualAim = false;
jjWeapons[number].multiplier = 1;
jjWeapons[number].replacedByBubbles = false;
jjWeapons[number].replacedByShield = false;
jjWeapons[number].spread = SPREAD::NORMAL;
jjWeapons[number].style = WEAPON::NORMAL;
uint basic = se::getBasicBulletOfWeapon(number);
uint powered = se::getPoweredBulletOfWeapon(number);
jjOBJ@ preset = jjObjectPresets[basic];
jjOBJ@ presetPower = jjObjectPresets[powered];
preset.animSpeed = 2;
presetPower.animSpeed = 4;
preset.counterEnd = presetPower.counterEnd = ZAPPER_DURATION;
preset.curAnim = weaponAnimSet.firstAnim+ANIM_ZAPPER_BULLET;
presetPower.curAnim = weaponAnimSet.firstAnim+ANIM_ZAPPER_BULLETPU;
preset.curFrame = presetPower.curFrame = hitboxFrameIndex;
preset.direction = presetPower.direction = 0;
// yes, this is the property that determines whether bullets freeze
// enemies/springs/etc. Bullets sure are special aren't they
preset.freeze = presetPower.freeze = 0;
preset.killAnim = weaponAnimSet.firstAnim+ANIM_ZAPPER_END;
presetPower.killAnim = weaponAnimSet.firstAnim+ANIM_ZAPPER_ENDPU;
preset.lightType = presetPower.lightType = LIGHT::POINT;
preset.light = presetPower.light = 8; // alas, doesn't do anything right now
preset.playerHandling = presetPower.playerHandling = HANDLING::PLAYERBULLET;
preset.special = preset.curAnim;
presetPower.special = presetPower.curAnim;
preset.var[3] = presetPower.var[3] = number;
preset.var[BOUNCES_VAR_INDEX] = NUM_BOUNCES;
presetPower.var[BOUNCES_VAR_INDEX] = NUM_BOUNCES_PU;
preset.var[6] = 0x0;
presetPower.var[6] = 0x8;
preset.xAcc = presetPower.xAcc = 0.0f;
preset.xSpeed = presetPower.xSpeed = BASE_SPEED;
preset.yAcc = presetPower.yAcc = 0.0f;
preset.ySpeed = presetPower.ySpeed = 0.0f;
preset.behavior = presetPower.behavior = @jjVOIDFUNCOBJ(behave);
uint ammo3i = se::getAmmoPickupOfWeapon(number);
if (ammo3i != 0) {
jjOBJ@ ammo3 = jjObjectPresets[ammo3i];
ammo3.behavior = @se::AmmoPickup(jjAnimations[weaponAnimSet.firstAnim+ANIM_ZAPPER_PICKUP], jjAnimations[weaponAnimSet.firstAnim+ANIM_ZAPPER_PICKUPPU]);
ammo3.curAnim = weaponAnimSet.firstAnim+ANIM_ZAPPER_PICKUP;
ammo3.frameID = 0;
ammo3.determineCurFrame();
}
uint powerupi = se::getPowerupMonitorOfWeapon(number);
if (powerupi != 0) {
jjOBJ@ powerup = jjObjectPresets[powerupi];
powerup.curAnim = weaponAnimSet.firstAnim+ANIM_ZAPPER_MONITOR;
powerup.frameID = 0;
powerup.determineCurFrame();
}
uint cratei = se::getAmmoCrateOfWeapon(number);
if (cratei != 0) {
jjOBJ@ crate = jjObjectPresets[cratei];
crate.curAnim = weaponAnimSet.firstAnim+ANIM_ZAPPER_CRATE;
crate.frameID = 0;
crate.determineCurFrame();
}
if (weaponHook !is null) {
weaponHook.resetCallbacks(number);
weaponHook.setWeaponSprite(number, false, jjAnimations[weaponAnimSet.firstAnim+ANIM_ZAPPER_PICKUP]);
weaponHook.setWeaponSprite(number, true, jjAnimations[weaponAnimSet.firstAnim+ANIM_ZAPPER_PICKUPPU]);
}
return true;
}
return false;
}
}
class ZapperMLLEWrapper : Zapper, MLLEWeaponApply {
bool Apply(uint number, se::WeaponHook@ weaponHook = null, jjSTREAM@ = null, uint8 ammo15EventID = 0) {
if (weaponAnimSet is null) {
uint8 animSetID = 0;
while (jjAnimSets[ANIM::CUSTOM[animSetID]] != 0)
++animSetID;
loadAnims(jjAnimSets[ANIM::CUSTOM[animSetID]]);
}
if (!samplesLoaded) {
array<SOUND::Sample> samples;
array<SOUND::Sample>@ trySamples = SOUND::GetAllSampleConstantsInRoughOrderOfLikelihoodToBeUsed();
uint index = trySamples.length();
while (samples.length() < getSampleCount()) {
while (index > 0 && jjSampleIsLoaded(trySamples[--index]));
samples.insertLast(trySamples[index]);
}
loadSamples(samples);
}
if (ammo15EventID != 0) {
jjOBJ@ ammo15 = @jjObjectPresets[ammo15EventID];
ammo15.curAnim = weaponAnimSet.firstAnim+ANIM_ZAPPER_CRATE;
ammo15.frameID = 0;
ammo15.determineCurFrame();
}
return setAsWeapon(number, weaponHook);
}
}
}
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.