Name | Author | Game Mode | Rating | |||||
---|---|---|---|---|---|---|---|---|
Astral Witchcraft | minmay | Multiple | N/A |
// by may
// 0.1
//
// reduced version of MayLib for release with Astral Witchcraft. The following
// features are removed from this version:
// - all packet-related code
// - all player/server configuration
// - all in-game UI
const double MAX_UINT_DOUBLE = 4294967295.0; // useful for jjRandom
const double MAX_INT_DOUBLE = 2147483648.0;
const uint64 MAX_UINT64 = 0xFFFFFFFFFFFFFFFF;
const double MAX_UINT64_DOUBLE = 18446744073709551615.0; // useful for jjRNG
const float PI = 3.141592f;
// JJ2-specific constants
const int MAX_PARTICLES = 1024;
const int MAX_PLAYERS = 32;
const int MAX_WEAPON = 9; // maximum value for weapon slot (0-9), not the number of weapons
const int BUTTSTOMP_STARTUP_FRAMES = 40;
const int TILE_SIZE = 32;
const int GENERATOR_PARAM_BIT_COUNT = 17;
const int MCE_EVENT_ID = 255;
// first palette index of sprite colors other than white
const int SPRITE_GRADIENTS_START = 16;
// length of the gradients in the sprite colors, except the yellow one which we
// simply pretend nothing is wrong with
const int SPRITE_GRADIENT_LENGTH = 8;
const int SPRITE_COLORS_END = 96;
// info about some particles
const float FIRE_GRAVITY = 0.015625f;
const float ICETRAIL_GRAVITY = FIRE_GRAVITY*2;
const float PIXEL_GRAVITY = FIRE_GRAVITY*4;
const float STAR_GRAVITY = FIRE_GRAVITY;
const float SPARK_GRAVITY = FIRE_GRAVITY;
const float TILE_GRAVITY = FIRE_GRAVITY*4;
// Random number from 0 to n-1 inclusive.
uint randomUpTo(uint n) {
return jjRandom() % n;
}
int min(int a, int b) {
return a > b ? b : a;
}
int max(int a, int b) {
return a > b ? a : b;
}
float min(float a, float b) {
return a > b ? b : a;
}
float max(float a, float b) {
return a > b ? a : b;
}
int getMaxAmmo(int weapon) {
int m = jjWeapons[weapon].maximum;
return m == -1 ? ((jjGameMode == GAME::SP || jjGameMode == GAME::COOP) ? 99 : 50) : m;
}
TEAM::Color indexToTeam(int i) {
if (i == 0) return TEAM::BLUE;
if (i == 1) return TEAM::RED;
if (i == 2) return TEAM::GREEN;
return TEAM::YELLOW;
}
// XXX: This is not very future-proof but I think it's the best that can be done
// right now? All the team-based game modes use CTF as their base mode but maybe
// they won't always so this checks them explicitly. Still useless if new modes
// get added though.
bool gameModeHasTeams() {
return jjGameMode == GAME::CTF || jjGameCustom == GAME::DCTF ||
jjGameCustom == GAME::DOM || jjGameCustom == GAME::FR ||
jjGameCustom == GAME::JB || jjGameCustom == GAME::TB || jjGameCustom == GAME::TLRS;
}
// functions to rotate a segment of an array by one step, in-place.
// Multiple steps can be done efficiently too, with a different algorithm (that
// is less efficient for single steps), but I never want to rotate something
// multiple steps anyway, so I didn't bother with that functionality.
void rotateArraySegmentLeft_uint8(array<uint8> &arr, int start, int segmentLength) {
int end = start+segmentLength-1; // inclusive end
int tmp = arr[start];
for (int i = start; i < end; i++) {
arr[i] = arr[i+1];
}
arr[end] = tmp;
}
void rotateArraySegmentRight_uint8(array<uint8> &arr, int start, int segmentLength) {
int end = start+segmentLength-1; // inclusive end
int tmp = arr[end];
for (int i = end; i > start; i--) {
arr[i] = arr[i-1];
}
arr[start] = tmp;
}
void pointDistanceFromRectangle(int px, int py, int rx, int ry, int rw, int rh, int &out distX, int &out distY) {
if (px < rx) {
distX = px-rx;
} else if (px >= rx+rw) {
distX = px-(rx+rw)+1;
} else {
distX = 0;
}
if (px < ry) {
distY = py-ry;
} else if (py >= ry+rh) {
distY = py-(ry+rh)+1;
} else {
distY = 0;
}
}
void distanceFromCameraRect(jjPLAYER@ player, int x, int y, int &out distX, int &out distY) {
// we don't care about subpixels here
int cameraX = int(player.cameraX);
int cameraY = int(player.cameraY);
pointDistanceFromRectangle(x, y, cameraX, cameraY, jjSubscreenWidth-jjBorderWidth*2, jjSubscreenHeight-jjBorderHeight*2, distX, distY);
}
// Returns true if the passed point is within maxDistance pixels of any local
// player's camera view. With a maxDistance of 0, this tells you whether a pixel
// at that point can be seen by any local player.
//
// Using this to decide whether to call drawing functions is generally not
// productive, unless the function is drawResizedSprite/drawRotatedSprite or a
// friend of those.
bool pointIsCloseToCamera(int pointX, int pointY, int maxDistance = 0) {
int minDist = maxDistance+1;
for (int i = 0; i < jjLocalPlayerCount; i++) {
int distX;
int distY;
distanceFromCameraRect(jjLocalPlayers[i], pointX, pointY, distX, distY);
if (distX < minDist) minDist = distX;
if (distY < minDist) minDist = distY;
}
return minDist <= maxDistance;
}
namespace MayLib {
// Right now the public interface doesn't do anything, it just exists so
// you can detect the presence of MayLib
shared interface MayLibInterface : jjPUBLICINTERFACE {}
class MayLibPublicInterface : MayLibInterface {
string getVersion() const {
return "0.0.1";
}
}
MayLibPublicInterface interf;
MayLibPublicInterface@ onGetPublicInterface() {
return interf;
}
// The first animation in our custom anim set is a bundle of non-animated
// images.
const uint FRAME_OFFSET_GENERATOR_CLOCK_HAND = 0;
const uint FRAME_OFFSET_MINIMAP = 1;
const uint FRAME_OFFSET_MINIMAP_LARGE = 2;
uint customAnimSet;
uint firstCustomAnim;
uint firstCustomAnimFrame;
MayLibLevelConfig@ config;
MayMinimap@ minimap;
// for nicePowerups option.
// 9 bits per player so doesn't quite fit into an int. could fit it in 8
// if at least one weapon was always infinite, but scripts can give all
// 9 weapons finite ammo.
uint64 localPlayerNicePowerupsStateLastFrame = 0;
// Angelscript bug: if a function is named the same thing as a funcdef
// type, script loading will sometimes AV/segfault, apparently looking
// for some member of a null struct pointer.
// To prevent the user from accidentally triggering this bug, the
// funcdef type has a ridiculous name.
funcdef void oNdRaWmInImAp(MayMinimap@ minimap, jjCANVAS@ canvas, int x, int y, float scale);
class MayMinimapConfig {
// Whether the minimap should be generated and displayed at all.
bool enable = true;
// Number of tiles to exclude from the minimap on each edge.
// Use this for levels that have large non-playable areas at the
// edges, such as the bottom of maysolar.j2l.
uint cropLeft = 0;
uint cropRight = 0;
uint cropTop = 0;
uint cropBottom = 0;
oNdRaWmInImAp@ drawHook;
}
class MayLibLevelConfig {
MayMinimapConfig minimapConfig;
// Any generator object with an "MCE Event" directly under it will be
// moved [EventID]-16 pixels to the right and [Delay Secs]-16 pixels down.
// Additionally, place an "MCE Event" either directly to the left of that
// generator, or an unbroken chain of generators leading to one, to move the
// generators to tile [EventID],[Delay Secs] of that other MCE Event.
//
// Eh, forget trying to describe it, here's an example:
// BGGGGGGG
// AAAA AA
//
// G = generators
// A = MCE Events with EventID 24 and Delay Secs 24
// B = MCE Event with EventID 20 and Delay Secs 50
//
// All the generators except the fifth one from the left will be
// repositioned to pixel position 648 (20*32+24-16), 1608 (50*32+24-16).
// That one generator will stay where it is, because it doesn't have an
// MCE Event directly under it.
//
// For levels with greater width or height than 256, you can put yet
// another MCE Event to the left of the left MCE Event and that MCE Event
// will add [EventID]*256 and [Delay Secs]*256 to the tile index used.
//
//
//
// Note that if the generator is moved to a new tile, that tile's
// parameters will need to be changed to the generator's. Thus you can
// only put multiple generators on one tile if they all have the same
// parameters, and you will probably screw things up if you move a
// generator to a tile that already has a different event on it. (If you
// break either of these rules, the script will jjPrint a warning but
// still move the generator anyway, regardless of the consequences.)
//
//
//
// As for the *purpose* of all this? Well, it was coded (but not released)
// a bit prior to MLLE getting its own support for arbitrary event positions,
// so it made it relatively easy to put regenerating pickups in funny places
// in multiplayer, while being religiously paranoid about keeping things in
// sync between players (no objects are created or destroyed).
//
// I quickly discovered an unexpected secondary use of it, which is perhaps
// the primary use now: manipulating UIDs! Because the generator still gets
// created in its original position, you can deal with any generator UID
// collision by moving the events but offsetting them to their desired
// position.
bool generatorOffsets = false;
// display remaining time on generators, analog clock style. Also removes the explosion
// created by generators right before their spawned object is, as it becomes redundant.
// this is quite lazy and doesn't do things like draw the correct fastfire sprite for Spaz
//
// there is at least one mutator already floating around that does a similar thing...
bool showGeneratorTimers = false;
// Sets players' fastfire to 6 while using weapons with infinite
// ammo (usually just blaster) and 35 (the default) otherwise,
// and sets non-POPCORN infinite weapons to CAPPED.
// Do not put fastfire pickups in your level if you use this,
// since they won't do anything!
bool fastInfiniteWeapons = false;
// Powerups are not lost when running out of ammo.
// Specifically, if a player had both a powerup and ammo for a
// weapon on one frame, and has no ammo or powerup for that
// weapon on the next frame, and didn't die, the script will
// give them the powerup back, assuming that they lost it by
// using the last of their ammo.
// A script that takes away someone's powerup will generally
// still work with this option, unless you take away their ammo
// at the same time, in which case this option will have a false
// positive and give them the powerup back.
bool nicePowerups = false;
}
enum MinimapStrictness {
SHOW_ONLY_SELF,
SHOW_ONLY_ALLIES,
SHOW_ALL_PLAYERS
}
class MayMinimap {
MinimapStrictness mapStrictness = SHOW_ONLY_ALLIES;
uint width = 0;
uint height = 0;
uint8 emptyColor = 39; // very dark blue
uint8 wallColor = 36; // less dark blue
uint8 pitColor = 24; // red
// colors for rabbit locations, these cycle through 8 palette
// indices, with these variables being the start indices
uint8 selfColor = 16; // green
uint8 allyColor = 64; // beige
uint8 enemyColor = 24; // red
MayMinimapConfig @minimapConfig;
uint animFrame;
uint yOffset = 28;
uint scale = 1;
MayMinimap(MayMinimapConfig @newConfig, uint startAnimFrameToReplace) {
@minimapConfig = newConfig;
animFrame = startAnimFrameToReplace;
jjLAYER@ layer = jjLayers[4];
int newWidth = layer.width - minimapConfig.cropLeft - minimapConfig.cropRight;
int newHeight = layer.height - minimapConfig.cropTop - minimapConfig.cropBottom;
if (newWidth <= 0 || newHeight <= 0) {
width = 0;
height = 0;
return;
}
width = newWidth;
height = newHeight;
jjPIXELMAP image(width, height);
jjPIXELMAP bigImage(width*2, height*2);
bool hasPits = levelHasPits();
for (int y = minimapConfig.cropTop; y < layer.height-minimapConfig.cropBottom; y++) {
for (int x = minimapConfig.cropLeft; x < layer.width-minimapConfig.cropRight; x++) {
bool quadrant1 = layer.maskedPixel(x*32+8,y*32+8);
bool quadrant2 = layer.maskedPixel(x*32+24,y*32+8);
bool quadrant3 = layer.maskedPixel(x*32+8,y*32+24);
bool quadrant4 = layer.maskedPixel(x*32+24,y*32+24);
uint px = x-minimapConfig.cropLeft;
uint py = y-minimapConfig.cropTop;
uint8 curEmptyColor = emptyColor;
if (hasPits && py == height-1) {
emptyColor = pitColor;
}
bigImage[px*2, py*2] = quadrant1 ? wallColor : emptyColor;
bigImage[px*2+1, py*2] = quadrant2 ? wallColor : emptyColor;
bigImage[px*2, py*2+1] = quadrant3 ? wallColor : emptyColor;
bigImage[px*2+1, py*2+1] = quadrant4 ? wallColor : emptyColor;
// XXX: this is overly pessimistic, e.g. a 32-pixel wide tunnel
// centered on the edge between tiles will get rendered as a solid wall.
image[x-minimapConfig.cropLeft, y-minimapConfig.cropTop] = (quadrant1 || quadrant2 || quadrant3 || quadrant4) ? wallColor : emptyColor;
}
}
image.save(jjAnimFrames[startAnimFrameToReplace]);
bigImage.save(jjAnimFrames[startAnimFrameToReplace+1]);
jjAnimFrames[startAnimFrameToReplace].hotSpotX = 0;
jjAnimFrames[startAnimFrameToReplace].hotSpotY = 0;
jjAnimFrames[startAnimFrameToReplace+1].hotSpotX = 0;
jjAnimFrames[startAnimFrameToReplace+1].hotSpotY = 0;
}
int drawX(float xPos) {
return jjResolutionWidth-width*scale+(int(xPos*scale) >> 5);
}
int drawY(float yPos) {
return (int(yPos*scale) >> 5) + yOffset;
}
void draw(jjCANVAS@ canvas) {
if (scale > 0) {
canvas.drawSpriteFromCurFrame(jjResolutionWidth-width*scale, yOffset, animFrame+scale-1, 0, SPRITE::TRANSLUCENT);
// If splitscreeners are on different teams, and we're meant to show allies but not
// enemies, allies won't be drawn, since it would mean one or more splitscreeners
// could see the position of their enemies!
bool drawAllies = gameModeHasTeams() && mapStrictness >= SHOW_ONLY_ALLIES;
if (drawAllies && mapStrictness < SHOW_ALL_PLAYERS) {
int splitscreenerTeam = jjLocalPlayers[0].team;
for (int i = 1; i < jjLocalPlayerCount; i++) {
if (jjLocalPlayers[i].team != splitscreenerTeam) drawAllies = false;
}
}
for (uint i = 0; i < MAX_PLAYERS; i++) {
if (jjPlayers[i].isInGame) {
uint8 color;
if (jjPlayers[i].isLocal) {
color = selfColor;
} else if (drawAllies && jjPlayers[i].team == jjLocalPlayers[0].team) {
color = allyColor;
} else if (mapStrictness >= SHOW_ALL_PLAYERS) {
color = enemyColor;
} else { // don't draw this player
continue;
}
int x = drawX(jjPlayers[i].xPos);
int y = drawY(jjPlayers[i].yPos);
canvas.drawPixel(x, y, color+((jjGameTicks >> 2) & 3));
// Flag holders and Eva's Ring holders twinkle and show health
if (jjPlayers[i].flag != 0 || jjPlayers[i] is jjTokenOwner) {
if ((jjGameTicks >> 3) & 1 == 1) {
canvas.drawPixel(x-1, y, color);
canvas.drawPixel(x+1, y, color);
canvas.drawPixel(x, y-1, color);
canvas.drawPixel(x, y+1, color);
}
int health = jjPlayers[i].health;
for (int h = 0; h < health; h++) {
canvas.drawPixel(x-health+h*2+1, y-3, 49);
}
}
}
}
/**** begin crap added at the last minute for Astral Witchcraft ****/
for (int i = 1; i < jjObjectCount; i++) {
jjOBJ @obj = jjObjects[i];
if (obj.isActive) {
// draw CTF bases as Xes of the team's color
if (obj.eventID == OBJECT::CTFBASE) {
int x = drawX(obj.xOrg);
int y = drawY(obj.yOrg);
uint8 color;
switch (obj.var[1]) {
case 0:
color = 34; // blue base
break;
case 1:
color = 25; // red base
break;
case 2:
color = 18; // green base
break;
case 3:
default:
color = 40; // yellow base
break;
}
canvas.drawPixel(x-1, y-1, color);
canvas.drawPixel(x+1, y-1, color);
canvas.drawPixel(x, y, color);
canvas.drawPixel(x-1, y+1, color);
canvas.drawPixel(x+1, y+1, color);
} else if (obj.eventID == OBJECT::GENERATOR) {
int eventIDToGenerate = obj.var[3];
int x = drawX(obj.xPos);
int y = drawY(obj.yPos);
switch (eventIDToGenerate) {
// draw carrot generators as diagonal orange lines
case OBJECT::CARROT:
case OBJECT::FULLENERGY:
canvas.drawPixel(x, y, 42);
canvas.drawPixel(x-1, y+1, 42);
break;
default:
break;
}
}
}
}
if (minimapConfig.drawHook !is null) minimapConfig.drawHook(this, canvas, jjResolutionWidth-width*scale, yOffset, scale);
/**** end of crap added at the last minute for Astral Witchcraft ****/
}
}
void processToggleCommand() {
scale = (scale+1)%3;
}
}
// My "new" levels use a consistent configuration.
MayLibLevelConfig@ defaultMayLevelConfig() {
MayLibLevelConfig rval();
rval.generatorOffsets = true;
rval.showGeneratorTimers = true;
rval.fastInfiniteWeapons = true;
rval.nicePowerups = true;
return @rval;
}
// Kill the explosion objects created by generators
class MayLibExplosion : jjBEHAVIORINTERFACE {
void onBehave(jjOBJ@ obj) {
if (obj.lightType == LIGHT::POINT && obj.curAnim == int(jjAnimSets[ANIM::PICKUPS].firstAnim + 86)) {
obj.delete();
} else {
obj.behave(BEHAVIOR::EXPLOSION);
}
}
}
void warnMultiplayerOnly() {
if (jjObjectMax == 2048) {
jjAlert("|This level can only be played in an online server.", false, STRING::MEDIUM);
jjLayers[4].hasTiles = false;
jjLayerOrderSet({jjLayers[4]});
}
}
bool _configCheck() {
if (config is null) {
jjPrint("WARNING: MayLib function called without first calling callOnLevelLoad(), ignoring");
return false;
}
return true;
}
bool levelHasPits() {
// HACK: jjEventGet() does not work for the bottom row of events (always returns 0).
// Use an undocumented feature of jjParameterGet() instead: if called with an offset of
// -12 and length of 32 or -32, it returns the full event bitfield for that tile, the
// lower 8 bits of which are the event ID.
// (Calling with an offset of -12 and length of 8 won't work, that gives you 0 for the
// bottom row like jjEventGet() does.)
return (jjParameterGet(jjLayers[4].width-1,jjLayers[4].height-1,-12,32) & 255) == 255;
}
void _repositionGenerator(jjOBJ@ generator, float x, float y, bool ignoreWarning) {
int xTile = int(x)/32;
int yTile = int(y)/32;
int startXTile = int(generator.xPos)/32;
int startYTile = int(generator.yPos)/32;
// if the generator is moving to a different tile, the params
// need to be copied to that tile
if (xTile != startXTile || yTile != startYTile) {
// if it looks like copying the params might screw
// something up, warn about it!
int eventAtDestination = jjEventGet(xTile, yTile);
int params = jjParameterGet(startXTile, startYTile, 0, GENERATOR_PARAM_BIT_COUNT);
int paramsAtDestination = jjParameterGet(xTile, yTile, 0, GENERATOR_PARAM_BIT_COUNT);
if (!ignoreWarning && ((eventAtDestination != 0 || paramsAtDestination != 0) && paramsAtDestination != params)) {
jjPrint("WARNING: MayLib: Destination tile ("+xTile+","+yTile+") for generator has conflicting parameters. Either there is an event on this tile other than the generator, a non-identical generator was already moved to this tile, or the j2l is dirty.");
}
jjParameterSet(xTile, yTile, 0, GENERATOR_PARAM_BIT_COUNT, params);
}
generator.xPos = x;
generator.yPos = y;
}
void _applyGeneratorOffsets() {
for (int i = 1; i < jjObjectCount; i++) {
jjOBJ@ obj = jjObjects[i];
if (obj.isActive && obj.eventID == OBJECT::GENERATOR) {
int xTile = int(obj.xPos)/32;
int yTile = int(obj.yPos)/32;
if (yTile < jjLayers[4].height-1) {
int eventBelow = jjEventGet(xTile, yTile+1);
if (eventBelow == MCE_EVENT_ID) {
int xOffset = jjParameterGet(xTile, yTile+1, 0, 8); // "EventID" parameter of standard MCE Event
int yOffset = jjParameterGet(xTile, yTile+1, 8, 8); // "Delay Secs" parameter of standard MCE Event
int leftTile = xTile-1;
while (leftTile >= 0) {
int eventLeft = jjEventGet(leftTile, yTile);
if (eventLeft == MCE_EVENT_ID) {
int destTileX = jjParameterGet(leftTile, yTile, 0, 8); // "EventID" parameter of standard MCE Event
int destTileY = jjParameterGet(leftTile, yTile, 8, 8); // "Delay Secs" parameter of standard MCE Event
if (leftTile > 0 && jjEventGet(leftTile-1,yTile) == MCE_EVENT_ID) {
destTileX += 256*jjParameterGet(leftTile-1, yTile, 0, 8); // "EventID" parameter of standard MCE Event
destTileY += 256*jjParameterGet(leftTile-1, yTile, 8, 8); // "Delay Secs" parameter of standard MCE Event
}
xOffset += destTileX*32 - int(obj.xPos);
yOffset += destTileY*32 - int(obj.yPos);
break;
} else if (eventLeft != OBJECT::GENERATOR) {
// No generator chain, no MCE Event specifying tile.
break;
}
leftTile--;
}
int destTileX = int(obj.xPos+xOffset)/32;
int destTileY = int(obj.yPos+yOffset)/32;
// don't warn if the generator is being moved on top of its own MCE Event
_repositionGenerator(obj, obj.xPos+xOffset, obj.yPos+yOffset, destTileX == xTile && destTileY == yTile+1);
}
}
}
}
}
int nextCustomAnimSet() {
for (int i = 0; i < 256; i++) {
if (jjAnimSets[ANIM::CUSTOM[i]].firstAnim == 0) {
return ANIM::CUSTOM[i];
}
}
return -1;
}
void callOnLevelLoad(MayLibLevelConfig@ configuration) {
if (!(config is null)) {
jjPrint("WARNING: MayLib already initialized, ignoring callOnLevelLoad call.");
return;
}
@config = configuration;
// Anims are always allocated even for disabled features
customAnimSet = nextCustomAnimSet();
array<uint> animAllocs = {3};
jjAnimSets[customAnimSet].allocate(animAllocs);
firstCustomAnim = jjAnimSets[customAnimSet].firstAnim;
firstCustomAnimFrame = jjAnimations[firstCustomAnim].firstFrame;
if (config.showGeneratorTimers) {
jjObjectPresets[OBJECT::EXPLOSION].behavior = MayLibExplosion();
}
jjPIXELMAP generatorClockHand(2,16);
for (int y = 0; y < 16; y++) {
generatorClockHand[0,y] = 40+y/2;
generatorClockHand[1,y] = 40+y/2;
}
generatorClockHand.save(jjAnimFrames[firstCustomAnimFrame+FRAME_OFFSET_GENERATOR_CLOCK_HAND]);
jjAnimFrames[firstCustomAnimFrame+FRAME_OFFSET_GENERATOR_CLOCK_HAND].hotSpotX = -1;
if (config.minimapConfig.enable) {
@minimap = @MayMinimap(config.minimapConfig, firstCustomAnimFrame+FRAME_OFFSET_MINIMAP);
}
if (config.fastInfiniteWeapons) {
for (int i = 0; i < 9; i++) {
if (jjWeapons[i].infinite && jjWeapons[i].style != WEAPON::POPCORN) {
jjWeapons[i].style == WEAPON::CAPPED;
}
}
}
}
bool onLevelBeginCalled;
void callOnLevelBegin() {
if (!_configCheck()) return;
if (onLevelBeginCalled) {
jjPrint("WARNING: MayLib callOnLevelBegin already called, ignoring.");
return;
}
onLevelBeginCalled = true;
if (config.generatorOffsets) {
_applyGeneratorOffsets();
}
}
void callOnMain() {
if (!_configCheck()) return;
if (config.showGeneratorTimers) {
for (int i = 1; i < jjObjectCount; i++) {
jjOBJ@ obj = jjObjects[i];
// generator var[2] is delay, var[1] counts down to 0 from that delay
// var[0] is the object ID the generator last generated, var[3] is its event ID
// however, clients do not count down var[1], instead the server tells them when to
// spawn the object. So for clients the indicator is just an estimate, it can't be
// perfectly accurate unless latency is perfectly constant (although the estimate
// could certainly be improved by adding some sendPacket()s).
// no display for generators with delay 1
// the bank robbery levels have their own vfx for coin regeneration
if (obj.isActive && obj.eventID == OBJECT::GENERATOR && obj.var[2] > 80 && obj.var[3] >= 0 && obj.var[3] < 256) {
bool generating = false;
if (jjIsServer) {
generating = obj.var[1] != obj.var[2];
} else {
// var[0] could get set to a bad value by a script, don't crash in that case
generating = obj.var[0] > 0 && obj.var[0] < jjObjectMax &&
// objects created by a generator get a creator value of CREATOR::LEVEL+[generator object id]
(!jjObjects[obj.var[0]].isActive || jjObjects[obj.var[0]].creator != CREATOR::LEVEL + i);
}
if (generating) {
if (!jjIsServer) {
obj.var[10] = min(obj.var[10]-1, obj.var[2]);
}
float time = 1.0f - float(jjIsServer ? obj.var[1] : obj.var[10]) / float(obj.var[2]);
float x = obj.xPos;
float y = obj.yPos;
jjOBJ@ preset = jjObjectPresets[obj.var[3]];
const jjANIMATION@ animation = jjAnimations[preset.curAnim];
uint curFrame = (animation.frameCount == 0) ? 0 :
(animation.firstFrame + ((obj.objectID*3 + jjRenderFrame >> 2) % animation.frameCount));
if (time < 0.0f) {
// We're a client and the object *should* be back by now, but it isn't. Could
// just be lag, or could be a broken generator, we don't really know.
jjDrawSpriteFromCurFrame(x, y, curFrame, 0, SPRITE::TRANSLUCENTSINGLEHUE, 24);
} else {
jjDrawRotatedSprite(x+1.0, y, customAnimSet, 0, 0, (int(time*1023.0)+512)%1024);
jjDrawSpriteFromCurFrame(x, y, curFrame, 0, SPRITE::TRANSLUCENT);
}
} else {
obj.var[10] = obj.var[2];
}
}
}
}
}
void callOnPlayer(jjPLAYER@ p) {
if (!_configCheck()) return;
if (config.fastInfiniteWeapons) {
p.fastfire = jjWeapons[p.currWeapon].infinite ? 6 : 35;
}
if (config.nicePowerups) {
uint64 playerOffset = uint64(1) << (p.localPlayerID*9);
for (int gun = 1; gun < 10; gun++) {
uint64 gunBit = playerOffset << (gun-1);
bool hasAmmo = jjWeapons[gun].infinite || p.ammo[gun] > 0;
bool hasPowerup = p.powerup[gun];
bool hadPowerupAndAmmoLastFrame = (localPlayerNicePowerupsStateLastFrame & gunBit != 0);
if (p.health <= 0 || (hasAmmo && !hasPowerup)) {
// death or script took powerup away, probably. leave it gone.
localPlayerNicePowerupsStateLastFrame &= ~gunBit;
} else {
if (hadPowerupAndAmmoLastFrame && !hasAmmo && !hasPowerup) {
// running out of ammo took powerup away, probably. bring it back.
p.powerup[gun] = true;
}
if (hasAmmo && hasPowerup) {
localPlayerNicePowerupsStateLastFrame |= gunBit;
}
}
}
}
}
bool callOnDrawGameModeHUD(jjPLAYER@ player, jjCANVAS@ canvas) {
// player can be null if spectating
if (player is null || player.localPlayerID == 0) {
if (minimap !is null) {
minimap.draw(canvas);
}
}
return false;
}
}
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.