Name | Author | Game Mode | Rating | |||||
---|---|---|---|---|---|---|---|---|
Holiday Hare '17 | ShadowGPW | Single player | 8.8 | |||||
Holiday Hare '18 | SmokeNC | Single player | 8.9 |
/*************************
Jazz1Enemies v03.asc
version 0.3
API:
Jazz1::Enemy@ Jazz1::MakeEnemy(uint8 eventID, Jazz1::Enemies enemyID, bool useTrueColor = false)
This is the main function and is intended to be called somewhere in the onLevelLoad hook. Its purpose is to convert one of the events in the level, as specified by the eventID argument, to be an enemy from Jazz 1 instead of whatever it originally was. For example:
Jazz1::MakeEnemy(OBJECT::HELMUT, Jazz1::Enemies::Medivo_Helmut);
This function modifies various properties of jjObjectPresets[eventID], in particular jjOBJ::behavior, which is set to a new jjBEHAVIORINTERFACE instance. The value of the "eventID" argument is totally up to you (although using an OBJECT::Object constant is recommended); possible values for the "enemyID" argument are listed shortly below this comment in the "Enemies" enum, starting with "Diamondus_BumblingBee".
The final argument, "useTrueColor", lets you choose which color model to use for drawing this particular enemy:
false (default):
The enemy will be limited to using colors that appear in the standard sprite palette. It will look the same in both 8-bit and 16-bit color, and it will be recolored to fit the atmosphere in tilesets with noticeably altered sprite palettes, e.g. Swamps or Glowee. However, it may not look exactly like the original Jazz 1 sprites, depending on how closely the original colors were or were not similar to those available in Jazz 2's standard sprite palette.
true:
In 8-bit color this option will have no effect, but in 16-bit color the enemy will be drawn in an approximation of palette-independent 24-bit color mode. It should look much closer, if not identical, to the original Jazz 1 sprites; however, it may therefore not match the atmosphere of the tileset, and if you plan to make any calls to jjPAL::apply(), you should use the TrueColor::EnableCaching function (see TrueColor.asc's documentation for details).
That one function call, once per enemy you want to add to your level, is all you need to do to use this library, besides the "#include Jazz1Enemies v03.asc" line (see the "Packaging Instructions" section below for more details). There are also some options available to you, however. Jazz1::MakeEnemy returns a handle to an object of class Jazz1::Enemy, which has a number of methods allowing you to slightly edit how that enemy behaves. Each of those methods returns another handle to the same object, allowing you to chain multiple method calls on the same line as the original Jazz1::MakeEnemy call, e.g.:
Jazz1::MakeEnemy(OBJECT::NORMTURTLE, Jazz1::Enemies::Diamondus_TurtleGoon).SetDirection(Jazz1::Directions::Random).SetSpeed(6);
Many of the object methods are available to certain enemies but not others; for example, SetFireDelay is not generally useful for enemies that do not fire any bullets. If you make such an inappropriate method call, you will see a warning to this effect in your chatlog (if the [General]AngelscriptDebug setting is enabled in your plus.ini) but no behavior change will occur.
Note that each Jazz1::Enemy instance is unique to a Jazz1::MakeEnemy call, even if you use the same value for more than one enemyID argument. To have one event place Tubelectric blasters on the floor and another event place them on the ceiling, for instance, you could write:
Jazz1::MakeEnemy(OBJECT::APPLE, Jazz1::Enemies::Tubelectric_Blaster).SetDirection(Jazz1::Directions::Right);
Jazz1::MakeEnemy(OBJECT::BANANA, Jazz1::Enemies::Tubelectric_Blaster).SetDirection(Jazz1::Directions::Left);
List of Jazz1::Enemy methods (the return type is always Jazz1::Enemy@):
SetSpeed(float speed)
Replaces the enemy's default movement speed with a new one of your choice. Different enemies have different default speeds, so you'll have to play around with this number a bit in order to find what feels best for you.
SetSpeedBasedOnParameter(uint offset, uint length, float modifier = 1.0f)
Instead of its movement speed being a constant, the enemy will base its speed on an event parameter you specified in JCS. (This will require an appropriately edited JCS.ini entry for that eventID). The enemy will get a parameter value based on the "offset" and "length" arguments, familiar from the global "jjParameterGet" function; add 1 to that value (so that the enemy never moves at speed 0); and then multiply that value by the "modifier" argument. For example, if "length" is 2 and "modifier" is 0.5, then any instance of that enemy you place in the level will have its movement speed be either 0.5, 1.0, 1.5, or 2.0, depending on what parameter you set in the level.
SetDirection(int offsetOrConstant)
Four constant values are provided for use with this method, to be found in the Jazz1::Directions enum: Left, Right, FaceJazz, and Random. Mostly this method specifies which direction the enemy should face when it is first spawned in the level, but a handful of enemies will check it constantly, so e.g. a Nippius_SnowGoon enemy would be able to turn around to face the nearest local player at all times if passed Jazz1::Directions::FaceJazz.
If you pass an unsigned integer value instead of a Jazz1::Directions constant, that will make the enemy base its direction on an event parameter instead, specifically a parameter whose length is 1 and whose offset is the value you passed, just like e.g. springs or CTF bases.
SetFireDelay(uint delay)
How many gameticks (70 per second) should pass after the enemy fires a bullet before it fires another one. Each enemy has its own default delay time but you can replace it.
SetUsesJJ2StyleDeathAnimation(bool setTo)
By default, all enemies use more or less their original death animations, e.g. a Tubelectric_Spark enemy explodes into yellow shards. If you pass true to this method, that enemy will instead die by bursting into particles appropriate to a JJ2 enemy dying from whatever means (regular bullet, toaster bullet, physical attack, etc.) was used to kill it.
(This is a PURELY visual change. Either way the player will get points and potentially a pickup for killing the enemy.)
SetWalkingEnemyCliffReaction(Jazz1::CliffReaction)
All enemies that walk back and forth on the floor share their basic code, and this method lets you choose how they should react when they run out of floor to walk on. The available values of the Jazz1::CliffReaction enum are TurnAround (e.g. Diamondus_TurtleGoon), Fall (e.g. Medivo_Helmut), and Careen (not found in Jazz 1, but perhaps a slightly more attractive option than falling straight down).
SetDeathSound(SOUND::Sample sample)
SetBulletFireSound(SOUND::Sample sample)
SetBulletExplosionSound(SOUND::Sample sample)
These methods let you choose which sounds are played when (respectively) the enemy dies, the enemy fires a bullet, or a bullet fired by the enemy explodes.
Additionally, you are welcome to edit various of a jjObjectPreset entry's properties in accordance with their usual meanings, including jjOBJ::energy, jjOBJ::points, jjOBJ::isFreezable, jjOBJ::isTarget, jjOBJ::light, and jjOBJ::lightType; however, the MakeEnemy call will assign to many of them the default values for that enemy, so you should not edit those properties for a given preset until AFTER calling MakeEnemy on it.
When dealing with active objects, rather than presets, the jjKillObject function is fully supported. (As are various others, but that seemed most worthy of note.)
These enemies should work just about as well online (in SP/Coop servers) as any other enemies do, provided the same code is run by all players in the server.
Packaging Instructions:
In the most basic case, your script needs to include only one line:
#include "Jazz1Enemies v03.asc"
This will automatically work in multiplayer servers; this library automatically inserts enough preprocessor instructions for clients to know to download the relevant files.
If you are packaging a .zip archive for a level/mutator using this library, you will need to include all three of these files:
Jazz1Enemies v03.asc
Jazz1Enemies.j2a
TrueColor.asc
If you make one or more calls to Jazz1::MakeEnemy with the "useTrueColor" argument equalling true, however, you will need to handle the .bmp image for each such call manually. It must be included in any .zip archive, and in the event that you want the script to work online, you will need to write a "#pragma require" line specifically for that .bmp.
For example, suppose you have two calls to Jazz1::MakeEnemy, one of them passing "useTrueColor" as false and one passing it as true:
Jazz1::MakeEnemy(OBJECT::BUMBEE, Jazz1::Enemies::Diamondus_BumblingBee, false);
Jazz1::MakeEnemy(OBJECT::NORMTURTLE, Jazz1::Enemies::Diamondus_TurtleGoon, true);
In this case, Diamondus_BumblingBee requires no special treatment (because it is not using True Color), but your script should include both these lines:
#include "Jazz1Enemies v03.asc"
#pragma require "Jazz1Enemies_Diamondus_TurtleGoon.bmp"
And your .zip archive should include these four files:
Jazz1Enemies v03.asc
Jazz1Enemies.j2a
Jazz1Enemies_Diamondus_TurtleGoon.bmp
TrueColor.asc
The pattern for True Color image filenames is perfectly regular: the name of the enemy, with "Jazz1Enemies_" instead of "Jazz1::" as the prefix, followed by the file extension .bmp. So:
Jazz1::Enemies::Letni_BugCeiling -> Jazz1Enemies_Letni_BugCeiling.bmp
Jazz1::Enemies::Marbelara_Schwarzenguard -> Jazz1Enemies_Marbelara_Schwarzenguard.bmp
Jazz1::Enemies::Battleships_Generator -> Jazz1Enemies_Battleships_Generator.bmp
...etc.
*************************/
namespace Jazz1 {
enum Enemies { _Misc_Anims,
//Full list of Enemy IDs for passing as the second argument to Jazz1::MakeEnemy (but remember to precede any name with "Jazz1::Enemies::"):
Diamondus_BumblingBee, Diamondus_TurtleGoon,
Tubelectric_BlasterHorizontal, Tubelectric_BlasterVertical, Tubelectric_Spark, Tubelectric_SparkBarrier, //for BlasterVertical, right=on floor, left=on ceiling
Medivo_GhostRapierHorizontal, Medivo_GhostRapierVertical, Medivo_Helmut,
Letni_Bug, Letni_BugCeiling, Letni_ElecBarrier,
Technoir_MiniMine, Technoir_Misfire, Technoir_TanketyTankTank,
Orbitus_BeholderPurple, Orbitus_BeholderSilver, Orbitus_SilverSnake,
/* todo add more enemy code
Fanolint_FlyFlower, Fanolint_PottedPlant, Fanolint_SuperTankety,
Scraparap_GunnerDrone, Scraparap_LaunchCart, Scraprap_RoboTurtleDrone,
Megairbase_Doofusguard, Megairbase_Missile, Megairbase_SuperSpark, //Megairbase also reuses Tubelectric_Spark, but that does not warrant a separate enum value
Turtemple_JeTurtle, Turtemple_ScorpWeenie,*/
Nippius_SkatePen, Nippius_SkiTurtle, Nippius_SnowGoon,
/*Jungrock_JetSnake, Jungrock_RedBuzzer, Jungrock_YellowBuzzer,
Marbelara_Drageen, Marbelara_Firebomb, Marbelara_Schwarzenguard,
Slugion_Dragoon, Slugion_RedBat, Slugion_Sluggi,
Dreempipes_Minite, Dreempipes_Overgrown, Dreempipes_TerrapinSwimmer,
Pezrox_ClammyLR, Pezrox_ClammyUD, Pezrox_GreenSnake,
Crysilis_GoldenBounceSpike, Crysilis_LooGuard,
Battleships_ArmorDoofi, Battleships_BounceSpike, Battleships_Generator, Battleships_SuperBee,
//todo figure out names for episode ABC enemies*/
Holidaius_BlueDog, Holidaius_Devil, Holidaius_HandHorizontal, Holidaius_HandVertical, Holidaius_SkiTurtle, Holidaius_SnowMonkey, //for HandVertical, right=on floor, left=on ceiling
LAST
};
//Everything below is subject to change and need not therefore be read by users; only the above enum and API description should be taken as promises.
enum Directions { Left = -4, Right = -3, FaceJazz = -2, Random = -1 };
enum CliffReaction { TurnAround, Fall, Careen };
enum _objVar { AnimCounter, DirectionCurrent, FireDelayCounter };
enum _causeOfDeath { Bullet, OrangeShards, BlueShards, GrayShards, PhysicalAttack, FrozenBullet, FrozenPhysicalAttack, AlreadyPerformedAnimation};
const float _levelWidth = jjLayerWidth[4] * 32;
const float _levelHeight = jjLayerHeight[4] * 32;
array<jjANIMSET@> _animSets(Enemies::LAST, null);
bool _trueColorHasProcessedPalette = false;
int _getParameter(jjOBJ@ obj, int offset, int length) {
return jjParameterGet(int(obj.xOrg) / 32, int(obj.yOrg) / 32, offset, length);
}
bool _maskedPixelFloat(float x, float y) {
return jjMaskedPixel(int(x), int(y));
}
jjANIMSET@ _getAnimSet(Enemies enemyID) {
if (_animSets[enemyID] is null)
@_animSets[enemyID] = jjAnimSets[TrueColor::FindCustomAnim()].load(enemyID, "Jazz1Enemies v03.j2a");
return _animSets[enemyID];
}
int _getPresetRelativeAnimID(uint8 eventID, int relativeAnimID) {
const jjOBJ@ preset = jjObjectPresets[eventID];
if (relativeAnimID < 0) //negative numbers mean generic Jazz1 animations
return _getAnimSet(Enemies::_Misc_Anims).firstAnim - 1 - relativeAnimID;
else if (relativeAnimID < 20) //small positive numbers mean animations specific to this Jazz1 enemy
return preset.curAnim + relativeAnimID;
else //large positive numbers are common JJ2 animations
return relativeAnimID;
}
void _explosion(jjOBJ@ obj) {
if (obj.ySpeed != 0) {
obj.xPos += obj.xSpeed;
obj.yPos += obj.ySpeed += 0.125f;
}
if (jjRandom() & 7 < uint(obj.ySpeed == 0 ? 3 : 5)) { //advance frame
jjANIMATION@ anim = jjAnimations[obj.curAnim];
if(++obj.frameID >= int(anim.frameCount)) {
obj.frameID = 0;
if (++obj.counterEnd >= obj.creatorID) { //_killAnimRepetitionCounts
obj.delete();
return;
}
}
obj.curFrame = anim.firstFrame + obj.frameID;
}
jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame);
}
enum _bulletDirection { Left = -1, Either = 0, Right = 1 };
class _bulletPreferences {
//_bullets.insertLast(_bulletPreferences(0, 3, 1, 0, _bulletDirection::Left));
private float xSpeed, ySpeed;
private uint animID, frameID;
_bulletDirection direction;
_bulletPreferences(){}//array purposes
_bulletPreferences(float x, float y, uint a, uint f, _bulletDirection d) { xSpeed = x; ySpeed = y; animID = a; frameID = f; direction = d; }
jjOBJ@ fire(const jjOBJ@ obj, int sound) const {
int x1, x2, y1, y2;
{ //jjOBJ::fireBullet doesn't work with vertically flipped frames, so we'll have to figure out on our own where the bullet object needs to be spawned
const jjANIMFRAME@ frame = jjAnimFrames[obj.curFrame];
if (obj.direction >= 0) {
x1 = frame.hotSpotX;
x2 = frame.gunSpotX;
} else { //horizontally flipped
x2 = frame.hotSpotX;
x1 = frame.gunSpotX;
}
if (obj.direction & 0xC0 == 0) {
y1 = frame.hotSpotY;
y2 = frame.gunSpotY;
} else { //vertically flipped
y2 = frame.hotSpotY;
y1 = frame.gunSpotY;
}
}
jjOBJ@ bullet = jjObjects[jjAddObject(
OBJECT::BULLET,
obj.xPos + x1 - x2,
obj.yPos + y1 - y2,
obj.objectID, CREATOR::OBJECT,
BEHAVIOR::INACTIVE
)];
bullet.xSpeed = xSpeed; bullet.ySpeed = ySpeed;
bullet.curFrame = jjAnimations[jjObjectPresets[obj.eventID].curAnim + animID] + frameID;
bullet.behavior = _bullet;
bullet.special = sound;
return bullet;
}
}
void _bullet(jjOBJ@ obj) {
if (obj.state == STATE::START) {
obj.state = STATE::FLY;
obj.playerHandling = HANDLING::ENEMYBULLET;
obj.animSpeed = 1;
obj.lightType = LIGHT::POINT2;
}
obj.xPos += obj.xSpeed;
obj.yPos += obj.ySpeed;
if (obj.curAnim == 0 || jjColorDepth == 8)
jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame);
else
TrueColor::DrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curAnim);
if (obj.state == STATE::EXPLODE || obj.xPos < 0 || obj.yPos < 0 || obj.xPos >= _levelWidth || obj.yPos >= _levelHeight || _maskedPixelFloat(obj.xPos, obj.yPos)) {
obj.playerHandling = HANDLING::EXPLOSION;
obj.curAnim = _getAnimSet(Enemies::_Misc_Anims).firstAnim + 1;
obj.behavior = _explosion;
obj.creatorID = 1; //repetitions
obj.ySpeed = 0; //don't move
obj.counterEnd = 0;
if (obj.special >= 0)
jjSample(obj.xPos, obj.yPos, SOUND::Sample(obj.special));
}
}
abstract class Enemy : jjBEHAVIORINTERFACE {
private string _behaviorName;
private bool _useTrueColor;
private TrueColor::Bitmap@ _trueColorBitmap;
private array<array<TrueColor::Coordinates>> _trueColorCoordinates = {array<TrueColor::Coordinates>(0)}; //construct the TrueColor animsets as single flat animations, no matter the structures of the animsets from Jazz1Enemies.j2a, because all I really care about is frame offset
private uint _firstFramePaletted, _firstFrameTrueColor;
protected array<int8>@ _animFrames = array<int8>(0), _fireFrames = null;
protected uint _fireAnimID = 0;
Enemy(jjOBJ@ preset, bool tc, string name) {
_behaviorName = name;
_firstFramePaletted = preset.curFrame;
for (uint i = 0; i < jjAnimations[preset.curAnim].frameCount; ++i) //create a default _animFrames array that repeats every frame in the animation exactly once and in linear order
_animFrames.insertLast(i);
preset.behavior = this;
preset.playerHandling = HANDLING::SPECIAL;
preset.bulletHandling = HANDLING::DETECTBULLET;
preset.scriptedCollisions = true;
preset.isTarget = true;
preset.isFreezable = true;
preset.isBlastable = false;
preset.triggersTNT = true;
preset.causesRicochet = false;
preset.deactivates = true;
preset.direction = 1;
preset.state = STATE::START;
preset.freeze = 0;
preset.energy = preset.points = preset.animSpeed = 0; //these should be overridden
preset.killAnim = -1; //first misc anim
preset.var[_objVar::AnimCounter] = 0;
preset.var[_objVar::FireDelayCounter] = 0;
if (_useTrueColor = tc) {
if (!_trueColorHasProcessedPalette) {
TrueColor::ProcessPalette();
_trueColorHasProcessedPalette = true;
}
uint numberOfAnimationsInAnimSet = 0; //this number is not directly stored anywhere, so I have to infer it from when I run out of used animations
{
uint animID = preset.curAnim;
while (jjAnimations[animID++].frameCount != 0)
numberOfAnimationsInAnimSet += 1;
}
uint leftPostionOfAnimFrameInSpriteSheet = 0;
for (uint i = 0; i < numberOfAnimationsInAnimSet; ++i) { //generate an array<TrueColor::Coordinates> based on the animset's animations' frames' properties
const jjANIMATION@ animation = jjAnimations[preset.curAnim + i];
for (uint j = 0; j < animation.frameCount; ++j) {
const jjANIMFRAME@ animFrame = jjAnimFrames[animation + j];
_trueColorCoordinates[0].insertLast(TrueColor::Coordinates(
leftPostionOfAnimFrameInSpriteSheet, 0,
animFrame.width, animFrame.height,
animFrame.hotSpotX, animFrame.hotSpotY/*, //the other properties aren't needed
animFrame.gunSpotX, animFrame.gunSpotY,
animFrame.coldSpotX, animFrame.coldSpotY*/
));
leftPostionOfAnimFrameInSpriteSheet += animFrame.width; //the next sprite on the spritesheet, if any, will be placed immediately to the right of this one
}
}
const ANIM::Set trueColorAnimSet = TrueColor::FindCustomAnim();
TrueColor::AllocateSpriteSheet(
trueColorAnimSet,
@_trueColorBitmap = TrueColor::Bitmap("Jazz1Enemies_" + name),
_trueColorCoordinates
);
_firstFrameTrueColor = jjAnimations[jjAnimSets[trueColorAnimSet]];
}
}
void onBehave(jjOBJ@ obj) {
if (obj.state == STATE::DEACTIVATE)
obj.deactivate();
else if (obj.state == STATE::KILL) {
if (_useJJ2DeathAnimation) {
switch (_died) {
case _causeOfDeath::Bullet:
obj.particlePixelExplosion(0);
break;
case _causeOfDeath::OrangeShards:
jjSample(obj.xPos, obj.yPos, SOUND::COMMON_BURN);
obj.particlePixelExplosion(1);
break;
case _causeOfDeath::BlueShards:
jjSample(obj.xPos, obj.yPos, SOUND::COMMON_BURN);
obj.particlePixelExplosion(32);
break;
case _causeOfDeath::GrayShards:
jjSample(obj.xPos, obj.yPos, SOUND::COMMON_BURN);
obj.particlePixelExplosion(72);
break;
case _causeOfDeath::PhysicalAttack:
obj.particlePixelExplosion(2);
jjSample(obj.xPos, obj.yPos, SOUND::Sample(SOUND::COMMON_SPLAT1 + (jjRandom() & 3)));
break;
case _causeOfDeath::FrozenBullet:
obj.unfreeze(0);
break;
case _causeOfDeath::FrozenPhysicalAttack:
obj.unfreeze(1);
break;
case _causeOfDeath::AlreadyPerformedAnimation:
break;
}
} else { //Jazz1-style
const auto killAnim = _getPresetRelativeAnimID(obj.eventID, int(obj.killAnim));
for (int i = 0; i < 7; ++i) {
jjOBJ@ shard = jjObjects[jjAddObject(OBJECT::EXPLOSION, obj.xPos, obj.yPos, _killAnimRepetitionCounts)];
shard.curAnim = killAnim;
shard.behavior = _explosion;
shard.counterEnd = 0;
if (_killAnimExplosion) {
shard.behave(BEHAVIOR::SHARD); //get some random directions
shard.ySpeed += 0.0001; //non-zero
} else
break; //only one
}
}
if (_deathSound >= 0)
jjSample(obj.xPos, obj.yPos, SOUND::Sample(_deathSound));
obj.delete();
} else if (obj.state == STATE::FREEZE) {
if (obj.freeze-- <= 1) {
obj.freeze = 0;
obj.state = obj.oldState;
}
} else {
if (obj.state == STATE::START) {
if (_supportsSpeed) {
if (_speedParamOffset >= 0)
obj.xSpeed = (_getParameter(obj, _speedParamOffset, _speedParamLength) + 1) * _speed;
else
obj.xSpeed = _speed;
}
if (jjDifficulty >= 3) //turbo
obj.energy += 1;
}
if (_supportsFire) {
if (obj.counterEnd > _fireDelay) { //showing fire animation
obj.var[_objVar::AnimCounter] = obj.var[_objVar::AnimCounter] + 1;
if (obj.var[_objVar::AnimCounter] > obj.animSpeed) {
obj.var[_objVar::AnimCounter] = 0;
if (uint(++obj.frameID) >= _fireFrames.length) {
_fireBullets(obj);
obj.counterEnd = 0;
return;
}
}
obj.curFrame = jjAnimations[_getPresetRelativeAnimID(obj.eventID, _fireAnimID)].firstFrame + _fireFrames[obj.frameID];
return;
} else if (++obj.counterEnd > _fireDelay) { //start firing
if (_fireFrames !is null && _fireFrames.length > 0) {
obj.var[_objVar::AnimCounter] = 0;
obj.frameID = 0;
obj.curFrame = jjAnimations[_getPresetRelativeAnimID(obj.eventID, _fireAnimID)].firstFrame + _fireFrames[obj.frameID];
return;
} else {
_fireBullets(obj);
obj.counterEnd = 0;
//fall through...
}
}
}
myBehave(obj);
jjANIMATION@ anim = jjAnimations[obj.curAnim];
obj.var[_objVar::AnimCounter] = obj.var[_objVar::AnimCounter] + 1;
obj.curFrame = anim.firstFrame + _animFrames[(((obj.var[_objVar::AnimCounter] >> 1) / (obj.animSpeed+1)) % _animFrames.length)];
if (_flipSpriteWhenMovingLeft) //otherwise always face right
obj.direction = obj.var[_objVar::DirectionCurrent];
}
}
protected void myBehave(jjOBJ@ obj) const { obj.state = STATE::IDLE; } //here to be overridden
protected void _drawBodyFrame(const jjOBJ@ obj, float xPos, float yPos, uint frameID, int direction) const {
if (!_useTrueColor || jjColorDepth == 8 || obj.freeze != 0 || obj.justHit != 0)
jjDrawSpriteFromCurFrame(
xPos, yPos, frameID, direction,
(obj.freeze == 0) ? (obj.justHit == 0) ? SPRITE::NORMAL : SPRITE::SINGLECOLOR : SPRITE::FROZEN,
15
);
else
TrueColor::DrawSpriteFromCurFrame(
xPos, yPos,
_firstFrameTrueColor + (frameID - _firstFramePaletted) * TrueColor::NumberOfFramesPerImage,
direction
);
}
void onDraw(jjOBJ@ obj) {
if (obj.isActive)
_drawBodyFrame(obj, obj.xPos, obj.yPos, obj.curFrame, obj.direction);
}
bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) { //mostly copied from plus52Scripting.j2as
if (bullet !is null) {
//recreation of HANDLING::HURTBYBULLET with HANDLING::ENEMY
if (obj.causesRicochet) {
if ((bullet.var[6] & 6) == 0) //not fire-based, not a laser beam
bullet.ricochet();
else if ((bullet.var[6] & 4) == 0) //not a laser beam
bullet.delete();
} else if ((bullet.var[6] & 16) == 0) //not a fireball
bullet.state = STATE::EXPLODE;
if (obj.freeze > 0 && force < 3)
force = 3;
obj.energy -= force;
obj.justHit = 5; //flash white for 5 ticks--jjOBJ::justHit is automatically deincremented by the JJ2 engine, so individual behavior functions don't need to worry about doing that.
if (obj.energy <= 0) { //killed
obj.energy = 0;
if (obj.freeze > 0)
_died = _causeOfDeath::FrozenBullet;
else if ((bullet.var[6] & 2) == 0) //not fire-based
_died = _causeOfDeath::Bullet;
else if ((bullet.var[6] & 4) != 0) //laser beam
_died = _causeOfDeath::GrayShards;
else
_died = ((bullet.var[6] & 8) != 0) ? _causeOfDeath::BlueShards : _causeOfDeath::OrangeShards; //powered-up (blue) or not (orange)
if (player !is null) {
obj.grantPickup(player, (uint(bullet.curAnim) == jjAnimSets[ANIM::AMMO].firstAnim + 17) ? 5 : 10);
givePlayerPointsForObject(player, obj);
}
jjKillObject(obj.objectID);
} else
obj.freeze = 0;
} else { //recreation of HANDLING::ENEMY; player guaranteed to be non-null
if (force != 0) { //attacking via special attack, e.g. buttstomp
obj.energy -= 4; //constant amount of damage for special attacks
if (obj.energy <= 0) { //killed
obj.energy = 0;
if (obj.freeze > 0)
_died = _causeOfDeath::FrozenPhysicalAttack;
else
_died = _causeOfDeath::PhysicalAttack;
givePlayerPointsForObject(player, obj);
jjKillObject(obj.objectID);
} else { //only wounded
obj.justHit = 5;
if (obj.freeze <= 0)
jjSample(obj.xPos, obj.yPos, SOUND::Sample(SOUND::COMMON_SPLAT1 + (jjRandom() & 3)));
}
if (force > 0) { //buttstomp or sugar rush
player.buttstomp = 50; //landing
player.ySpeed = player.ySpeed / -2 - 8;
player.yAcc = 0;
player.extendInvincibility(-70);
} else if (force == -101) { //running into frozen enemy
player.xAcc = 0;
player.xSpeed /= -2;
player.ySpeed = -6;
player.extendInvincibility(-10);
}
} else { //not attacking
if (obj.freeze == 0)
player.hurt();
}
}
return true;
}
private bool givePlayerPointsForObject(jjPLAYER@ player, jjOBJ@ obj) const { //This will probably be made a jjOBJ method as part of the real JJ2+ API eventually, because it shows up all the time in the native code, but that hasn't happened yet, so here you go. Increases the player's jjPLAYER::score to match the object's jjOBJ::points, and creates a string particle with that number which flies up to the top left corner of the screen.
if (player is null)
return false;
if (obj.points != 0 && (jjGameMode == GAME::SP || jjGameMode == GAME::COOP)) {
player.score += obj.points; //todo add some other options for how to translate these scores into JJ2 scores... multiply by 50, or round up to the nearest 50
jjPARTICLE@ particle = jjAddParticle(PARTICLE::STRING);
if (particle !is null) {
particle.xPos = obj.xPos;
particle.yPos = obj.yPos;
particle.xSpeed = (-32768 - int(jjRandom() & 0x3FFF)) / 65536.f;
particle.ySpeed = (-65536 - int(jjRandom() & 0x7FFF)) / 65536.f;
particle.string.text = formatInt(obj.points);
}
obj.points = 0;
return true;
}
return false;
}
private void _fireBullets(const jjOBJ@ obj) const {
for (uint i = 0; i < _bullets.length; ++i)
if (-obj.var[_objVar::DirectionCurrent] != _bullets[i].direction) { //compatible with the object's current direction
jjOBJ@ bullet = _bullets[i].fire(obj, _explosionSound);
bullet.curAnim = (!_useTrueColor ? 0 : (_firstFrameTrueColor + (bullet.curFrame - _firstFramePaletted) * TrueColor::NumberOfFramesPerImage));
}
if (_fireSound >= 0)
jjSample(obj.xPos, obj.yPos, SOUND::Sample(_fireSound));
}
protected bool
_supportsSpeed = false,
_supportsDirection = false,
_supportsFire = false,
_flipSpriteWhenMovingLeft = false;
protected int _adjustObjectDirection(jjOBJ@ obj) {
switch (_direction) {
case Directions::Right:
obj.var[_objVar::DirectionCurrent] = 1;
break;
case Directions::Left:
obj.var[_objVar::DirectionCurrent] = -1;
break;
case Directions::FaceJazz:
obj.var[_objVar::DirectionCurrent] = (obj.xPos > jjLocalPlayers[0].xPos) ? -1 : 1;
break;
case Directions::Random:
obj.var[_objVar::DirectionCurrent] = int(jjRandom() & 1) * 2 - 1;
break;
default:
obj.var[_objVar::DirectionCurrent] = _getParameter(obj, _direction, 1) * 2 - 1;
break;
}
return obj.var[_objVar::DirectionCurrent];
}
protected int _direction = Directions::Right;
Enemy@ SetDirection(int dir) {
if (_supportsDirection)
_direction = dir;
else
jjDebug(_behaviorName + " does not support the SetDirection method.");
return this;
}
protected void _reverseDirection(jjOBJ@ obj) {
obj.var[_objVar::DirectionCurrent] = -obj.var[_objVar::DirectionCurrent];
}
protected float _speed;
private int _speedParamOffset = -1;
private uint _speedParamLength;
Enemy@ SetSpeed(float speed) {
if (_supportsSpeed) {
_speed = abs(speed);
_speedParamOffset = -1;
} else
jjDebug(_behaviorName + " does not support the SetSpeed* methods.");
return this;
}
Enemy@ SetSpeedBasedOnParameter(uint o, uint l, float m = 1.0f) {
if (_supportsSpeed) {
_speed = abs(m);
_speedParamOffset = o;
_speedParamLength = l;
} else
jjDebug(_behaviorName + " does not support the SetSpeed* methods.");
return this;
}
protected uint _fireDelay = 0;
protected array<_bulletPreferences> _bullets(0);
Enemy@ SetFireDelay(uint delay) {
if (_supportsFire)
_fireDelay = delay;
else
jjDebug(_behaviorName + " does not support the SetFireDelayTo method.");
return this;
}
private int _deathSound = -1, _fireSound = -1, _explosionSound = -1;
Enemy@ SetDeathSound(SOUND::Sample sample) {
_deathSound = sample;
return this;
}
Enemy@ SetBulletFireSound(SOUND::Sample sample) {
if (_supportsFire)
_fireSound = sample;
else
jjDebug(_behaviorName + " does not support the SetBulletFireSound method.");
return this;
}
Enemy@ SetBulletExplosionSound(SOUND::Sample sample) {
if (_supportsFire)
_explosionSound = sample;
else
jjDebug(_behaviorName + " does not support the SetBulletExplosionSound method.");
return this;
}
private _causeOfDeath _died = _causeOfDeath::Bullet;
protected bool _killAnimExplosion = false;
protected uint _killAnimRepetitionCounts = 1;
private bool _useJJ2DeathAnimation = false;
Enemy@ SetUsesJJ2StyleDeathAnimation(bool setTo) {
_useJJ2DeathAnimation = setTo;
return this;
}
Enemy@ SetWalkingEnemyCliffReaction(CliffReaction) { //overridden by Walker
jjDebug(_behaviorName + " is not a walking enemy.");
return this;
}
}
Enemy@ MakeEnemy(uint8 eventID, Enemies enemyID, bool useTrueColor = false) {
if (enemyID <= Enemies::_Misc_Anims || enemyID >= Enemies::LAST) {
jjDebug("Invalid Jazz1::Enemies enum value " + enemyID + ".");
return null;
}
jjOBJ@ preset = jjObjectPresets[eventID];
preset.curAnim = _getAnimSet(enemyID).firstAnim;
preset.curFrame = jjAnimations[preset.curAnim].firstFrame;
switch (enemyID) {
case Enemies::Diamondus_TurtleGoon: Diamondus_TurtleGoon (preset, useTrueColor); break;
case Enemies::Diamondus_BumblingBee: Diamondus_BumblingBee (preset, useTrueColor); break;
case Enemies::Tubelectric_BlasterHorizontal: Tubelectric_BlasterHorizontal (preset, useTrueColor); break;
case Enemies::Tubelectric_BlasterVertical: Tubelectric_BlasterVertical (preset, useTrueColor); break;
case Enemies::Tubelectric_Spark: Tubelectric_Spark (preset, useTrueColor); break;
case Enemies::Tubelectric_SparkBarrier: Tubelectric_SparkBarrier (preset, useTrueColor); break;
case Enemies::Medivo_GhostRapierHorizontal: Medivo_GhostRapierHorizontal (preset, useTrueColor); break;
case Enemies::Medivo_GhostRapierVertical: Medivo_GhostRapierVertical (preset, useTrueColor); break;
case Enemies::Medivo_Helmut: Medivo_Helmut (preset, useTrueColor); break;
case Enemies::Letni_Bug: Letni_Bug (preset, useTrueColor); break;
case Enemies::Letni_BugCeiling: Letni_BugCeiling (preset, useTrueColor); break;
case Enemies::Letni_ElecBarrier: Letni_ElecBarrier (preset, useTrueColor); break;
case Enemies::Technoir_MiniMine: Technoir_MiniMine (preset, useTrueColor); break;
case Enemies::Technoir_Misfire: Technoir_Misfire (preset, useTrueColor); break;
case Enemies::Technoir_TanketyTankTank: Technoir_TanketyTankTank (preset, useTrueColor); break;
case Enemies::Orbitus_BeholderPurple: Orbitus_BeholderPurple (preset, useTrueColor); break;
case Enemies::Orbitus_BeholderSilver: Orbitus_BeholderSilver (preset, useTrueColor); break;
case Enemies::Orbitus_SilverSnake: Orbitus_SilverSnake (preset, useTrueColor); break;
case Enemies::Nippius_SkatePen: Nippius_SkatePen (preset, useTrueColor); break;
case Enemies::Nippius_SkiTurtle: Nippius_SkiTurtle (preset, useTrueColor); break;
case Enemies::Nippius_SnowGoon: Nippius_SnowGoon (preset, useTrueColor); break;
case Enemies::Holidaius_BlueDog: Holidaius_BlueDog (preset, useTrueColor); break;
case Enemies::Holidaius_Devil: Holidaius_Devil (preset, useTrueColor); break;
case Enemies::Holidaius_HandHorizontal: Holidaius_HandHorizontal (preset, useTrueColor); break;
case Enemies::Holidaius_HandVertical: Holidaius_HandVertical (preset, useTrueColor); break;
case Enemies::Holidaius_SkiTurtle: Holidaius_SkiTurtle (preset, useTrueColor); break;
case Enemies::Holidaius_SnowMonkey: Holidaius_SnowMonkey (preset, useTrueColor); break;
default: return null;
}
return cast<Enemy>(cast<jjBEHAVIORINTERFACE>(preset.behavior));
}
abstract class Walker : Enemy {
protected CliffReaction _cliffReaction = CliffReaction::TurnAround;
Walker(jjOBJ@ preset, bool tc, string name) {
super(preset, tc, name);
_flipSpriteWhenMovingLeft = _supportsSpeed = _supportsDirection = true;
}
void myBehave(jjOBJ@ obj) const override {
if (obj.state == STATE::START) {
_adjustObjectDirection(obj);
obj.state = STATE::WALK;
obj.putOnGround(true);
obj.special = jjMaskedTopVLine(int(obj.xPos), int(obj.yPos), 64); //properDistanceFromGround
}
const int direction = obj.var[_objVar::DirectionCurrent];
const int spriteWidth = jjAnimFrames[obj.curFrame].width / 2;
const float positionForwards = obj.xPos + direction * spriteWidth;
const float positionBackwards = obj.xPos - direction * spriteWidth;
int distanceFromGroundForwards = jjMaskedTopVLine(int(positionForwards), int(obj.yPos), 64);
int distanceFromGroundBackwards = jjMaskedTopVLine(int(positionBackwards), int(obj.yPos), 64);
if (jjEventGet(int(positionForwards) / 32, int(obj.yPos) / 32) == AREA::STOPENEMY) {
_reverseDirection(obj); //it takes a tick to turn around
} else {
if (distanceFromGroundForwards > obj.special && distanceFromGroundBackwards > obj.special) { //in the air
obj.yPos += obj.ySpeed += 0.125f;
if (obj.ySpeed > 4.f) obj.ySpeed = 4;
distanceFromGroundForwards = jjMaskedTopVLine(int(positionForwards), int(obj.yPos), 64);
distanceFromGroundBackwards = jjMaskedTopVLine(int(positionBackwards), int(obj.yPos), 64);
int currentDistanceFromGround = (distanceFromGroundForwards < distanceFromGroundBackwards) ? distanceFromGroundForwards : distanceFromGroundBackwards;
if (currentDistanceFromGround <= obj.special) { //landed
obj.yPos -= obj.special - currentDistanceFromGround;
distanceFromGroundForwards = obj.special; //don't instantly turn around
obj.ySpeed = 0;
}
if (_cliffReaction != CliffReaction::Careen)
return; //no horizontal movement
}
if (distanceFromGroundForwards < obj.special) {
_reverseDirection(obj);
} else if (distanceFromGroundForwards > obj.special && _cliffReaction == CliffReaction::TurnAround) {
_reverseDirection(obj);
} else {
obj.xPos += direction * obj.xSpeed;
}
}
}
}
const array<array<int8>> _paths = {
{28,23,28,22,27,20,26,18,25,16,25,15,24,15,24,14,23,14,23,12,22,9,22,7,22,6,22,5,22,4,22,3,23,3,23,2,24,2,24,1,24,0,25,0,25,-1,25,-2,26,-2,26,-3,26,-4,27,-4,27,-5,28,-5,28,-6,28,-7,28,-8,29,-8,29,-9,29,-10,30,-10,30,-11,30,-12,29,-13,29,-14,28,-14,28,-15,27,-16,26,-17,25,-17,25,-18,24,-18,23,-19,22,-19,22,-20,21,-20,21,-21,21,-22,21,-23,21,-24,22,-25,23,-25,24,-26,25,-26,26,-26,26,-27,27,-27,28,-27,29,-27,29,-26,30,-26,31,-26,31,-25,32,-25,33,-24,33,-23,34,-23,34,-22,34,-21,34,-20,34,-19,34,-18,33,-18,32,-17,31,-17,30,-16,29,-16,28,-15,27,-14,26,-14,26,-13,25,-13,24,-12,23,-11,22,-11,22,-10,21,-10,21,-8,20,-8,20,-7,20,-6,19,-5,19,-4,19,-3,19,-2,20,-1,21,-1,21,0,22,0,22,2,23,4,24,5,25,5,25,6,26,6,26,7,27,7,28,8,29,8,29,10,30,11,30,12,31,12,31,13,32,13,32,14,33,14,33,15,34,15,35,16,36,17,37,17,37,18,38,18,38,19,38,20,38,21,38,22,38,23,38,24,38,25,37,25,37,26,36,26,35,26,35,27,34,27,33,27,32,27,31,27,30,27,29,27,28,27,28,26,27,26,27,25,27,24,26,24,26,23,27,23}, //bee
{20,37,20,36,20,35,19,35,19,34,19,33,18,33,18,32,18,31,18,30,19,29,19,28,19,27,19,26,19,25,19,23,19,22,19,21,19,20,19,19,19,18,19,17,19,16,19,15,19,14,19,13,19,12,19,11,19,10,20,9,20,8,20,7,20,6,19,5,19,4,19,3,19,2,19,1,18,0,18,-1,18,-2,18,-3,18,-4,17,-5,17,-6,17,-7,17,-8,17,-10,17,-12,17,-13,17,-15,17,-17,17,-18,17,-19,17,-20,17,-21,17,-22,17,-23,17,-24,18,-24,18,-25,19,-26,20,-26,21,-27,22,-27,23,-28,24,-28,25,-28,26,-27,27,-27,27,-26,28,-25,28,-24,28,-23,28,-22,28,-21,28,-20,29,-20,29,-19,29,-18,29,-17,29,-15,29,-14,29,-13,29,-12,29,-11,29,-10,29,-9,30,-9,30,-8,30,-7,30,-6,30,-4,30,-3,30,-1,30,0,30,2,30,3,30,4,30,5,29,5,29,6,29,7,28,7,28,8,28,9,28,10,28,11,28,12,28,13,28,14,27,14,27,15,27,16,27,17,27,18,27,19,27,20,27,21,27,22,27,23,28,23,28,24,28,25,29,25,29,26,29,27,28,28,28,29,28,30,27,30,27,31,26,32,25,33,24,33,24,34,24,35,24,36,23,36,23,37,23,38,22,38,21,38,20,38,20,37,19,37,19,36}, //rapier
{22,4,22,3,22,2,22,1,22,0,22,-1,22,-2,22,-3,22,-4,22,-5,22,-6,22,-7,22,-8,22,-9,22,-10,22,-11,22,-12,22,-13,22,-14,22,-15,22,-16,22,-17,22,-18,22,-19,22,-20,22,-19,22,-18,22,-17,22,-16,22,-15,22,-14,22,-13,22,-12,22,-11,22,-10,22,-9,22,-8,22,-7,22,-6,22,-5,22,-4,22,-3,22,-2,22,-1,22,0,22,1,22,2,22,3,22,4,22,5,22,6,22,7,22,8,22,9,22,10,22,12,22,13,22,14,22,15,22,16,22,17,22,18,22,19,22,20,22,21,22,22,22,23,22,24,22,25,22,26,22,27,22,28,22,29,22,30,22,31,22,30,22,29,22,28,22,27,22,26,22,25,22,23,21,22,21,20,21,19,21,18,21,17,21,16,21,15,21,14,21,13,21,12,21,11,21,10,21,9,21,8,21,7,21,6,21,5,21,4}, //silversnake
{9,10,9,9,9,8,9,7,9,6,9,5,9,4,9,3,9,2,9,1,9,0,9,-1,9,-2,9,-3,9,-4,9,-5,9,-6,9,-7,9,-8,9,-9,9,-10,10,-10,10,-11,11,-12,12,-12,12,-13,13,-14,14,-14,15,-15,16,-15,17,-16,18,-16,19,-16,20,-16,21,-16,22,-16,23,-16,24,-16,25,-16,26,-16,27,-16,28,-16,29,-15,30,-15,30,-14,30,-13,30,-12,30,-11,30,-10,30,-9,30,-8,30,-7,30,-6,30,-5,30,-4,30,-3,30,-2,29,-1,29,0,29,1,29,2,28,3,28,4,28,5,28,6,28,7,28,8,28,9,28,10,27,11,27,12,27,13,26,13,25,14,24,15,23,15,22,15,21,16,20,16,19,16,18,16,17,16,16,16,15,16,14,16,13,16,12,16,11,16,10,15,9,15,8,15,8,14,8,13,8,12,8,11} //devil
};
abstract class PathFollower : Enemy {
PathFollower(jjOBJ@ preset, bool tc, string name, int pathID) {
super(preset, tc, name);
preset.special = pathID;
preset.xAcc = 0; //distance moved
_flipSpriteWhenMovingLeft = _supportsSpeed = true;
}
void myBehave(jjOBJ@ obj) const override {
if (obj.state == STATE::START) {
_adjustObjectDirection(obj);
obj.state = STATE::FLY;
obj.xSpeed /= 4; //adjust for not moving in blocks of 4 pixels at a time anymore
}
const array<int8>@ path = _paths[obj.special];
const int numberOfPointsOnPath = path.length / 2;
obj.xAcc += obj.xSpeed; //move forwards
const int point1 = (int(obj.xAcc) % numberOfPointsOnPath) * 2;
const float nearnessToNextPoint = obj.xAcc % 1.f; //instead of jumping from point to point, move smoothly during the transition gameticks
const float directionMovingAhead = path[(point1+3) % path.length] - path[point1+1];
if (directionMovingAhead > 0)
obj.var[_objVar::DirectionCurrent] = 1;
else if (directionMovingAhead < 0)
obj.var[_objVar::DirectionCurrent] = -1;
obj.xPos = obj.xOrg + (path[point1+1] + nearnessToNextPoint * directionMovingAhead) * 4;
obj.yPos = obj.yOrg + path[point1+0] + nearnessToNextPoint * (path[(point1+2) % path.length] - path[point1+0]);
}
}
abstract class Snake : PathFollower {
protected uint
_segmentStrength = 1, //jjOBJ::energy should be a multiple of this
_segmentSeparation = 1;
Snake(jjOBJ@ preset, bool tc, string name, int pathID) {
super(preset, tc, name, pathID);
}
void onDraw(jjOBJ@ obj) override {
const array<int8>@ path = _paths[obj.special];
const uint numberOfSegments = obj.energy / _segmentStrength + 1;
const uint numberOfPointsOnPath = path.length / 2;
float oldPosition = obj.xAcc - obj.xSpeed * _segmentSeparation * numberOfSegments;
uint i = 0;
while (true) {
const float nearnessToNextPoint = (oldPosition) % 1.f; //instead of jumping from point to point, move smoothly during the transition gameticks
int pointIndex = int(oldPosition);
while (pointIndex < 0) pointIndex += numberOfPointsOnPath; //% doesn't really work with negative numbers
pointIndex %= numberOfPointsOnPath;
const uint pointIndex2 = ((pointIndex + 1) % numberOfPointsOnPath) * 2;
_drawBodyFrame(
obj,
obj.xOrg + (path[pointIndex * 2 + 1] + nearnessToNextPoint * (path[pointIndex2 + 1] - path[pointIndex * 2 + 1])) * 4,
obj.yOrg + (path[pointIndex * 2 + 0] + nearnessToNextPoint * (path[pointIndex2 + 0] - path[pointIndex * 2 + 0])),
jjAnimations[obj.curAnim + 1] + (i == 0 ? 1 : 0), //hardcoded: snake body, tail are the two frames immediately following the head's animation
1
);
if (++i >= numberOfSegments)
break;
oldPosition += obj.xSpeed * _segmentSeparation;
}
Enemy::onDraw(obj);
}
bool onObjectHit(jjOBJ@ obj, jjOBJ@ bullet, jjPLAYER@ player, int force) override {
const auto numberOfSegmentsOld = obj.energy / _segmentStrength;
Enemy::onObjectHit(obj, bullet, player, force);
if (obj.isActive && (obj.energy / _segmentStrength) < numberOfSegmentsOld) { //hurt; drop a segment
jjOBJ@ shard = jjObjects[jjAddObject(OBJECT::EXPLOSION, obj.xPos, obj.yPos, _killAnimRepetitionCounts)];
shard.curAnim = _getPresetRelativeAnimID(obj.eventID, int(obj.killAnim));
shard.behavior = _explosion;
shard.counterEnd = 0;
shard.xSpeed = ((bullet !is null) ? bullet.xSpeed : player.xSpeed) / 2;
shard.ySpeed = ((bullet !is null) ? bullet.ySpeed : player.ySpeed) / 2 + 0.0001; //non-zero
}
return true;
}
}
abstract class StalkerGhost : Enemy {
StalkerGhost(jjOBJ@ preset, bool tc, string name) {
super(preset, tc, name);
_flipSpriteWhenMovingLeft = _supportsSpeed = _supportsDirection = true;
}
void myBehave(jjOBJ@ obj) const override {
if (obj.state == STATE::START) {
_adjustObjectDirection(obj); //get an initial direction in case the player is facing us
obj.state = STATE::FLY;
}
const int nearestPlayerID = obj.findNearestPlayer(320*320);
if (nearestPlayerID >= 0) {
const jjPLAYER@ play = jjPlayers[nearestPlayerID];
const bool objectToRightOfPlayer = obj.xPos > play.xPos;
if ((play.direction >= 0) != objectToRightOfPlayer && abs(obj.xPos - play.xPos) > 20) { //player looking away from a sufficiently distant enemy
obj.var[_objVar::DirectionCurrent] = objectToRightOfPlayer ? -1 : 1;
obj.xPos += obj.xSpeed * obj.var[_objVar::DirectionCurrent];
const float yDist = obj.yPos - play.yPos;
if (abs(yDist) > 20) { //move vertically... but only do so if moving horizontally
if (yDist > 0)
obj.yPos -= obj.xSpeed / 4;
else
obj.yPos += obj.xSpeed / 4;
}
}
}
}
}
abstract class Missile : Enemy {
Missile(jjOBJ@ preset, bool tc, string name) {
super(preset, tc, name);
_flipSpriteWhenMovingLeft = _supportsSpeed = _supportsDirection = true;
}
void myBehave(jjOBJ@ obj) const override {
if (obj.state == STATE::START) {
_adjustObjectDirection(obj); //get an initial direction in case the player is facing us
obj.state = STATE::FLY;
}
obj.xPos += obj.xSpeed * obj.var[_objVar::DirectionCurrent];
}
}
abstract class Floater : Enemy {
Floater(jjOBJ@ preset, bool tc, string name, int pathID) {
super(preset, tc, name);
_flipSpriteWhenMovingLeft = _supportsSpeed = _supportsDirection = true;
}
void myBehave(jjOBJ@ obj) const override {
if (obj.state == STATE::START) {
_adjustObjectDirection(obj);
obj.state = STATE::FLY;
}
const int direction = obj.var[_objVar::DirectionCurrent];
const jjANIMFRAME@ animFrame = jjAnimFrames[obj.curFrame];
const float positionForwards = obj.xPos + direction * animFrame.width / 2;
if (
(jjEventGet(int(positionForwards) / 32, int(obj.yPos) / 32) == AREA::STOPENEMY) ||
(jjMaskedVLine(int(positionForwards), int(obj.yPos + animFrame.hotSpotY), animFrame.height)) //wall
)
_reverseDirection(obj);
else
obj.xPos += direction * obj.xSpeed;
}
}
abstract class WallStickerHorizontal : Enemy {
WallStickerHorizontal(jjOBJ@ preset, bool tc, string name) {
super(preset, tc, name);
_flipSpriteWhenMovingLeft = _supportsDirection = true;
}
void myBehave(jjOBJ@ obj) const override {
if (obj.state == STATE::START) {
if (_adjustObjectDirection(obj) == 1) {
while (obj.xPos > 0 && !_maskedPixelFloat(obj.xPos, obj.yPos))
obj.xPos -= 1;
} else {
obj.direction = SPRITE::FLIPH;
while (obj.xPos < _levelWidth-2 && !_maskedPixelFloat(obj.xPos, obj.yPos))
obj.xPos += 1;
}
obj.state = STATE::WAIT;
}
}
}
abstract class WallStickerVertical : Enemy {
WallStickerVertical(jjOBJ@ preset, bool tc, string name) {
super(preset, tc, name);
_supportsDirection = true;
}
void myBehave(jjOBJ@ obj) const override {
if (obj.state == STATE::START) {
if (_adjustObjectDirection(obj) == 1) { //right = floor
while (obj.yPos < _levelHeight-2 && !_maskedPixelFloat(obj.xPos, obj.yPos))
obj.yPos += 1;
} else { //left = ceiling
obj.direction = SPRITE::FLIPV;
while (obj.yPos > 0 && !_maskedPixelFloat(obj.xPos, obj.yPos))
obj.yPos -= 1;
}
obj.state = STATE::WAIT;
}
}
}
final class Diamondus_TurtleGoon : Walker {
Diamondus_TurtleGoon(jjOBJ@ preset, bool tc) {
super(preset, tc, "Diamondus_TurtleGoon");
_speed = 1.333333;
_direction = Directions::Right;
_killAnimExplosion = true;
preset.killAnim = -1;
preset.points = 100;
preset.energy = 1;
preset.animSpeed = 4;
}
}
final class Diamondus_BumblingBee : PathFollower {
Diamondus_BumblingBee(jjOBJ@ preset, bool tc) {
super(preset, tc, "Diamondus_BumblingBee", 0);
_speed = 2;
_killAnimExplosion = true;
preset.killAnim = -1;
preset.points = 50;
preset.energy = 1;
preset.animSpeed = 3;
}
}
final class Tubelectric_BlasterHorizontal : WallStickerHorizontal {
Tubelectric_BlasterHorizontal(jjOBJ@ preset, bool tc) {
super(preset, tc, "Tubelectric_BlasterHorizontal");
_supportsFire = true;
@_animFrames = array<int8> = {0, 1, 2, 1};
@_fireFrames = array<int8> = {2,2,2,2,2};
_bullets.insertLast(_bulletPreferences(-3,0, 1,0, _bulletDirection::Left));
_bullets.insertLast(_bulletPreferences( 3,0, 1,0, _bulletDirection::Right));
_fireDelay = 75;
preset.playerHandling = HANDLING::SELFCOLLISION;
preset.animSpeed = 4;
}
}
final class Tubelectric_BlasterVertical : WallStickerVertical {
Tubelectric_BlasterVertical(jjOBJ@ preset, bool tc) {
super(preset, tc, "Tubelectric_BlasterVertical");
_supportsFire = true;
@_animFrames = array<int8> = {0, 1, 2, 1};
@_fireFrames = array<int8> = {2,2,2,2,2};
_bullets.insertLast(_bulletPreferences(0, 3, 1,0, _bulletDirection::Left));
_bullets.insertLast(_bulletPreferences(0,-3, 1,0, _bulletDirection::Right));
_fireDelay = 75;
preset.playerHandling = HANDLING::SELFCOLLISION;
preset.animSpeed = 4;
}
}
final class Tubelectric_Spark : StalkerGhost {
Tubelectric_Spark(jjOBJ@ preset, bool tc) {
super(preset, tc, "Tubelectric_Spark");
_speed = 1.33333333;
_direction = Directions::Left;
@_animFrames = array<int8> = {0, 0, 0, 1}; //spend thrice as much time not-flashing as flashing
_killAnimExplosion = true;
_killAnimRepetitionCounts = 6;
preset.killAnim = 1;
preset.points = 20;
preset.energy = 1;
preset.animSpeed = 0;
}
}
final class Tubelectric_SparkBarrier : Enemy {
Tubelectric_SparkBarrier(jjOBJ@ preset, bool tc) {
super(preset, tc, "Tubelectric_SparkBarrier");
@_animFrames = array<int8> = {0, 1, 2, 1};
_killAnimExplosion = true;
_killAnimRepetitionCounts = 7;
preset.killAnim = 1;
preset.points = 20;
preset.energy = 3;
preset.animSpeed = 2;
}
}
final class Medivo_GhostRapierHorizontal : Missile {
Medivo_GhostRapierHorizontal(jjOBJ@ preset, bool tc) {
super(preset, tc, "Medivo_GhostRapierHorizontal");
_speed = 2;
_direction = Directions::FaceJazz;
preset.points = 10;
preset.energy = 1;
preset.animSpeed = 1;
}
}
final class Medivo_GhostRapierVertical : PathFollower {
Medivo_GhostRapierVertical(jjOBJ@ preset, bool tc) {
super(preset, tc, "Medivo_GhostRapierVertical", 1);
_speed = 1.333333;
_flipSpriteWhenMovingLeft = false;
preset.points = 10;
preset.energy = 1;
preset.animSpeed = 1;
}
}
final class Medivo_Helmut : Walker {
Medivo_Helmut(jjOBJ@ preset, bool tc) {
super(preset, tc, "Medivo_Helmut");
_speed = 1.333333;
_cliffReaction = CliffReaction::Fall;
preset.points = 10;
preset.energy = 1;
preset.animSpeed = 3;
_direction = Directions::Left;
}
}
final class Letni_Bug : Walker {
Letni_Bug(jjOBJ@ preset, bool tc) {
super(preset, tc, "Letni_Bug");
_speed = 1.333333;
preset.points = 50;
preset.energy = 1;
preset.animSpeed = 3;
_direction = Directions::Left;
}
}
final class Letni_BugCeiling : Enemy {
Letni_BugCeiling(jjOBJ@ preset, bool tc) {
super(preset, tc, "Letni_BugCeiling");
_speed = 1.333333;
preset.points = 50;
preset.energy = 1;
preset.animSpeed = 3;
_flipSpriteWhenMovingLeft = _supportsSpeed = _supportsDirection = true;
_direction = Directions::Left;
_killAnimExplosion = true; //different from floor variation for some reason
}
void myBehave(jjOBJ@ obj) const override { //like Walker, but on ceiling and without any falling options
if (obj.state == STATE::START) {
{
_adjustObjectDirection(obj);
obj.direction ^= 0xC0; //vertically flipped
while (obj.yPos >= 5 && !_maskedPixelFloat(obj.xPos, obj.yPos - 5))
obj.yPos -= 1;
}
obj.state = STATE::WALK;
}
const int direction = obj.var[_objVar::DirectionCurrent];
const float positionForwards = obj.xPos + direction * jjAnimFrames[obj.curFrame].width / 2;
if (
(jjEventGet(int(positionForwards) / 32, int(obj.yPos) / 32) == AREA::STOPENEMY) ||
(!_maskedPixelFloat(positionForwards, obj.yPos - 5)) || //cliff
(_maskedPixelFloat(positionForwards, obj.yPos - 4)) //wall
)
_reverseDirection(obj);
else
obj.xPos += direction * obj.xSpeed;
}
}
final class Letni_ElecBarrier : Enemy {
Letni_ElecBarrier(jjOBJ@ preset, bool tc) {
super(preset, tc, "Letni_ElecBarrier");
_killAnimExplosion = true;
preset.killAnim = -2;
preset.points = 50;
preset.energy = 3;
preset.animSpeed = 3;
}
}
final class Technoir_MiniMine : Floater {
Technoir_MiniMine(jjOBJ@ preset, bool tc) {
super(preset, tc, "Technoir_MiniMine", 1);
_speed = 0.8f;
_flipSpriteWhenMovingLeft = false;
_killAnimExplosion = true;
_killAnimRepetitionCounts = 10;
preset.killAnim = 1;
preset.points = 0;
preset.energy = 3;
preset.animSpeed = 1;
}
}
final class Technoir_Misfire : Walker { //actually an amalgamation of the Misfires from the two Technoir levels; level 1's movement speed and points, level 2's falling off cliffs
Technoir_Misfire(jjOBJ@ preset, bool tc) {
super(preset, tc, "Technoir_Misfire");
_speed = 1.333333;
_cliffReaction = CliffReaction::Fall;
preset.points = 50;
preset.energy = 1;
preset.animSpeed = 3;
_direction = Directions::Left;
_killAnimExplosion = true;
}
}
final class Technoir_TanketyTankTank : Walker {
Technoir_TanketyTankTank(jjOBJ@ preset, bool tc) {
super(preset, tc, "Technoir_TanketyTankTank");
_speed = 1;
preset.points = 50;
preset.energy = 2;
preset.animSpeed = 3;
_direction = Directions::Left;
_killAnimExplosion = true;
_supportsFire = true;
_fireAnimID = 1;
@_animFrames = array<int8> = {0, 1, 2, 3};
@_fireFrames = array<int8> = {4,4,4,4,4,4};
_bullets.insertLast(_bulletPreferences(-4,0, 1,1, _bulletDirection::Left));
_bullets.insertLast(_bulletPreferences( 4,0, 1,0, _bulletDirection::Right));
_fireDelay = 100;
}
}
final class Orbitus_BeholderPurple : Enemy {
Orbitus_BeholderPurple(jjOBJ@ preset, bool tc) {
super(preset, tc, "Orbitus_BeholderPurple");
preset.points = 30;
preset.energy = 4;
preset.animSpeed = 4;
@_animFrames = array<int8> = {0, 1, 2, 3, 2, 1};
_killAnimExplosion = true;
_killAnimRepetitionCounts = 9;
preset.killAnim = 1;
}
}
final class Orbitus_BeholderSilver : Walker {
Orbitus_BeholderSilver(jjOBJ@ preset, bool tc) {
super(preset, tc, "Orbitus_BeholderSilver");
_speed = 1;
preset.points = 30;
preset.energy = 1;
preset.animSpeed = 5;
@_animFrames = array<int8> = {0, 1, 2, 3, 2, 1};
_direction = Directions::Left;
_flipSpriteWhenMovingLeft = false;
_killAnimExplosion = true;
_killAnimRepetitionCounts = 9;
preset.killAnim = 1;
}
}
final class Orbitus_SilverSnake : Snake {
Orbitus_SilverSnake(jjOBJ@ preset, bool tc) {
super(preset, tc, "Orbitus_SilverSnake", 2);
_speed = 4;
_killAnimExplosion = true;
_killAnimRepetitionCounts = 19;
preset.killAnim = 1;
preset.points = 20;
preset.energy = 10;
preset.animSpeed = 6;
}
}
final class Nippius_SnowGoon : Enemy {
Nippius_SnowGoon(jjOBJ@ preset, bool tc) {
super(preset, tc, "Nippius_SnowGoon");
preset.points = 10;
preset.energy = 1;
preset.animSpeed = 4;
@_animFrames = array<int8> = {0, 1, 2, 1};
_direction = Directions::Left;
_supportsDirection = _flipSpriteWhenMovingLeft = true;
_killAnimExplosion = true;
_killAnimRepetitionCounts = 19;
preset.killAnim = 1;
}
void myBehave(jjOBJ@ obj) const override {
if (obj.state == STATE::START) {
obj.putOnGround(true);
obj.state = STATE::WAIT;
}
_adjustObjectDirection(obj);
}
}
final class Nippius_SkatePen : Walker {
Nippius_SkatePen(jjOBJ@ preset, bool tc) {
super(preset, tc, "Nippius_SkatePen");
_speed = 1.333333;
_cliffReaction = CliffReaction::Fall;
preset.points = 10;
preset.energy = 1;
preset.animSpeed = 4;
_direction = Directions::Left;
}
}
final class Nippius_SkiTurtle : Walker {
Nippius_SkiTurtle(jjOBJ@ preset, bool tc) {
super(preset, tc, "Nippius_SkiTurtle");
_speed = 4; //2 in level 2
preset.points = 10;
preset.energy = 1;
preset.animSpeed = 3;
_direction = Directions::Left;
}
}
final class Holidaius_BlueDog : Walker {
Holidaius_BlueDog(jjOBJ@ preset, bool tc) {
super(preset, tc, "Holidaius_BlueDog");
_speed = 1.333333;
preset.points = 30;
preset.energy = 3;
preset.animSpeed = 4;
_direction = Directions::Left;
_killAnimExplosion = true;
_supportsFire = true;
_fireAnimID = 1;
@_fireFrames = array<int8> = {0,0, 1,1,2,2, 1,1,2,2, 1,1,2,2, 1,1,2,2};
_fireDelay = 150;
}
}
final class Holidaius_Devil : PathFollower {
Holidaius_Devil(jjOBJ@ preset, bool tc) {
super(preset, tc, "Holidaius_Devil", 3);
_speed = 1.333333;
_killAnimExplosion = true;
preset.points = 50;
preset.energy = 3;
preset.animSpeed = 3;
}
}
final class Holidaius_HandHorizontal : WallStickerHorizontal {
Holidaius_HandHorizontal(jjOBJ@ preset, bool tc) {
super(preset, tc, "Holidaius_HandHorizontal");
@_animFrames = array<int8> = {0, 1, 2, 3, 4, 5, 6,6,6,6,6,6,6,6,6,6,6,6,6};
preset.animSpeed = 5;
preset.points = 60;
preset.energy = 1;
_killAnimExplosion = true;
}
}
final class Holidaius_HandVertical : WallStickerVertical {
Holidaius_HandVertical(jjOBJ@ preset, bool tc) {
super(preset, tc, "Holidaius_HandVertical");
@_animFrames = array<int8> = {0, 1, 2, 3, 4, 5, 6,6,6,6,6,6,6,6,6,6,6,6,6};
preset.animSpeed = 5;
preset.points = 60;
preset.energy = 1;
_killAnimExplosion = true;
}
}
final class Holidaius_SkiTurtle : Walker {
Holidaius_SkiTurtle(jjOBJ@ preset, bool tc) {
super(preset, tc, "Holidaius_SkiTurtle");
_speed = 2;
preset.points = 50;
preset.energy = 1;
preset.animSpeed = 3;
_direction = Directions::Left;
_killAnimExplosion = true;
}
}
final class Holidaius_SnowMonkey : Walker {
Holidaius_SnowMonkey(jjOBJ@ preset, bool tc) {
super(preset, tc, "Holidaius_SnowMonkey");
_speed = 4;
preset.points = 30;
preset.energy = 1;
preset.animSpeed = 4;
_direction = Directions::Left;
_killAnimExplosion = true;
_supportsFire = true;
@_fireFrames = array<int8> = {5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5};
_fireDelay = 50;
}
}
}
#pragma require "Jazz1Enemies v03.asc"
#pragma require "Jazz1Enemies v03.j2a"
#include "TrueColor.asc"
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.