Understanding sprite arrays

JJ2's thousands of sprites and animations are accessible through AngelScript as a three-tiered system of classes and matching global arrays. If you're familiar with the contents of anims.j2a from Jazz Sprite Dynamite, that should make it easier to understand how this works; most simply, the three tiers of classes/arrays (jjANIMSET, jjANIMATION, jjANIMFRAME) correspond to the three scrollbars (Set ID, Animation, Frame #) in JSD, although JSD uses 1-indexed numbers for everything and JJ2+ does not. To take a concrete example, the sprite frame used by the Cake pickup is Set ID=68, Animation=13, and Frame #=1 in JSD. (Or Set ID=72 in 1.24.) In AngelScript terms, this is jjAnimFrames[jjAnimations[jjAnimSets[ANIM::PICKUPS].firstAnim + 12].firstFrame].

An image may help to explain the three arrays. The lowest level, jjAnimFrames, is a long series of individual animation frames (sprites) with some basic properties for setting the hotspots, default transparency, and the like. (To edit the actual sprite image, you'll need the jjPIXELMAP class described below.) Individual frames have no idea where they fit in any given animation; that is handled by the jjAnimations array, each entry in which defines a single animation by its starting frame (firstFrame) and number of frames (frameCount). Finally the jjAnimSets array points you to the first animations of each anim set, and additionally carries a few methods for creating new anim sets either from scratch or from specific .j2a files.

A few examples should make clearer the three arrays' (and classes') uses and relationships. As seen above, the twenty animations and many corresponding frames of ANIM::BIRD are always loaded immediately before the ninety-five animations and many frames of ANIM::PICKUPS. An animation is essentially nothing more than a first frame and a frame count. So if you wanted Extra Life pickups to use the dead bird animation for some reason, there are (at least) three ways to do that:

    #1
jjObjectPresets[OBJECT::EXTRALIFE].curAnim = jjAnimSets[ANIM::BIRD].firstAnim + 19;
	#2
jjANIMATION@ extraLifeAnimation = jjAnimations[jjAnimSets[ANIM::PICKUPS].firstAnim];
extraLifeAnimation.frameCount = 1;
extraLifeAnimation.firstFrame -= 1;
	#3
jjAnimations[jjAnimSets[ANIM::PICKUPS].firstAnim] = jjAnimations[jjAnimSets[ANIM::BIRD].firstAnim + 19];

Crucially, none of those options would have any effect on what actual birds looked like after being roasted; any frame may be used in any number of animations. Moreover, the boundary lines between animations are completely arbitrary. Consider the following code:

jjAnimations[jjAnimSets[ANIM::PICKUPS].firstAnim].firstFrame -= 5;

This would cause Extra Lives (and any other objects using the same animation) to display an animation ten frames long: four frames of a bird sitting idly in place, one frame of a roasted bird, and five frames of green "1UP" text. The reason for this is that jjObjectPresets[OBJECT::EXTRALIFE].curAnim is, on level load, exactly equal to jjAnimSets[ANIM::PICKUPS].firstAnim. (Likewise, jjObjectPresets[OBJECT::APPLE].curAnim is equal to jjAnimSets[ANIM::PICKUPS].firstAnim+1, and so on.) Anytime you see a property or argument called curFrame, that means it is an index for jjAnimFrames[]. Likewise, anything called curAnim is an index for jjAnimations[], and anything called setID is an index for jjAnimSets[].

At the top level is the jjAnimSets array, which loads new series of animations and frames from various .j2a files and then points you to where they can be found in the lower two arrays. At the beginning of a level, all three animation arrays are nearly empty (jjAnimFrames and jjAnimations each contain an empty entry at index [0] for various internal purposes). Then JJ2+ runs the following line of code internally:

jjAnimSets[ANIM::FONT].load(ANIM::PLUS_FONT, "plus.j2a");

This opens up the plus.j2a file and discovers that ANIM::PLUS_FONT is exactly four animations long. It loads those animations (basically their firstFrame and frameCount properties) into jjAnimations[1–4]. Between them, those four animations include 224+224+224+2=674 frames, so those (images, hotspots, etc.) are loaded into jjAnimFrames[1–674]. And lastly, jjAnimSets[ANIM::FONT].firstAnim (and jjAnimations[jjAnimSets[ANIM::FONT].firstAnim].firstFrame) are set to 1. Next:

jjAnimSets[ANIM::JAZZ].load(); //in full: .load(ANIM::JAZZ, "anims.j2a");

ANIM::JAZZ has a massive 104 different animations (or 78 in TSF). But jjAnimations[1–4] are already in use, so JJ2+ loads the first ANIM::JAZZ animation into jjAnimations[5], and carries on the rest from there. Likewise the hundreds of individual frames are loaded into jjAnimFrames starting at jjAnimFrames[675], where the last jjANIMSET::load call left off. And jjAnimations[jjAnimSets[ANIM::JAZZ].firstAnim].firstFrame is set to 675, and the firstFrame properties of the remaining animations from ANIM::JAZZ are also adjusted accordingly.

So the process continues, loading first all the animations that every level uses, then a few level-specific ones (mostly enemies), then a few more that JJ2+ thinks might be useful. After that, you are free to mess up the existing order of frames and animations in any way you like, or simply to load or otherwise create all-new ones. Any call to jjANIMSET::load or jjANIMSET::allocate appends new entries to the ends of jjAnimFrames and jjAnimations, adjusting the animations' firstFrame properties to match their new positions, and then sets jjANIMSET::firstAnim to match. If an anim set has not yet been loaded from any .j2a file (or allocated), its firstAnim will equal 0.