Register FAQ Search Today's Posts Mark Forums Read
Go Back   JazzJackrabbit Community Forums » Open Forums » JCS & Scripting

Scripting new characters?

chilistudios

JCF Member

Joined: Jan 1970

Posts: 6

chilistudios has disabled reputation

Nov 10, 2020, 08:04 AM
chilistudios is offline
Reply With Quote
Scripting new characters?

Hi. Is it possible to script extra characters into the game? I mean, if the game's bugs could be fixed in Jazz 2+, It can be possible, right?
Violet CLM Violet CLM's Avatar

JCF Éminence Grise

Joined: Mar 2001

Posts: 10,978

Violet CLM has disabled reputation

Nov 10, 2020, 08:28 AM
Violet CLM is offline
Reply With Quote
It's absolutely possible... the trouble is that each main character has somewhere between 550 and 650 sprites, most of them unique, and that's a lot to ask of an artist. We held a survey a while back about which character people would most like to see added to the game, but no action has been taken on it.

On the other hand, if you have an existing set of sprites you'd like to use, e.g. from some other game, you can use this angelscript to get them into JJ2. This is the basis for Play as Mario!, so you can see that it works.
__________________
chilistudios

JCF Member

Joined: Jan 1970

Posts: 6

chilistudios has disabled reputation

Nov 10, 2020, 10:32 AM
chilistudios is offline
Reply With Quote
Quote:
Originally Posted by Violet CLM View Post
It's absolutely possible... the trouble is that each main character has somewhere between 550 and 650 sprites, most of them unique, and that's a lot to ask of an artist. We held a survey a while back about which character people would most like to see added to the game, but no action has been taken on it.

On the other hand, if you have an existing set of sprites you'd like to use, e.g. from some other game, you can use this angelscript to get them into JJ2. This is the basis for Play as Mario!, so you can see that it works.
Ok. How do I make an extra character?
Stijn Stijn's Avatar

Administrator

Joined: Mar 2001

Posts: 6,964

Stijn is a splendid one to beholdStijn is a splendid one to beholdStijn is a splendid one to beholdStijn is a splendid one to beholdStijn is a splendid one to beholdStijn is a splendid one to beholdStijn is a splendid one to behold

Nov 10, 2020, 11:42 AM
Stijn is offline
Reply With Quote
If you have an existing set of sprites you'd like to use, e.g. from some other game, you can use this angelscript to get them into JJ2. This is the basis for Play as Mario!, so you can see that it works.
chilistudios

JCF Member

Joined: Jan 1970

Posts: 6

chilistudios has disabled reputation

Nov 10, 2020, 02:37 PM
chilistudios is offline
Reply With Quote
Quote:
Originally Posted by Stijn View Post
If you have an existing set of sprites you'd like to use, e.g. from some other game, you can use this angelscript to get them into JJ2. This is the basis for Play as Mario!, so you can see that it works.
No, as in how do I make my own character with unique abilities.
Love & Thunder Love & Thunder's Avatar

JCF Member

Joined: Sep 2011

Posts: 1,101

Love & Thunder has disabled reputation

Nov 10, 2020, 05:32 PM
Love & Thunder is offline
Reply With Quote
Let's start by assuming you have a full set of sprites, nicely set up in a J2A file that will only use palette entries you know are present in the tileset you'll be using in your level. For instance, Batteryman from Battery Check, one of two non-Jazz games to use the Jazz 2 engine. It's freeware, so there should be no issue with simply renaming its J2A to, say, "BatteryCheck.j2a", and loading any sprites from within in Jazz 2 with scripting.

Luckily for you, I'm currently working on porting Battery Check into Jazz 2 using scripting, and it's far along enough that Batteryman himself is basically fully functional (there are a few bugs with idle animations and his low-power stand animation, but other than that, it works fine). Link to an alpha version I packaged up an hour ago is at the bottom of this post.
A debug feature for demonstrating the currently-implemented health features is that you can hold the weapon select button and your health will toggle between being just on the cusp of dropping to medium power, and just on the cusp of dropping to low power. I actually meant to keep this disabled, but I forgot to comment out the relevant section when packaging this alpha version up. Oh well.

Anyway, I don't know how useful this will be, but the best way I can think of to help you is to just explain roughly what I've done so far to make Jazz into Batteryman. Hopefully that should give you an idea of where to begin figuring out how to make your own custom characters.

So, the first thing you have to do is just load those sprites in. Plus gives you loads of room for custom sprites to load in, in the ANIM::CUSTOM sets, but you can't depend on any specific custom sprite set to be available (some other script may be loading in some custom sprites), and there are other potential issues to deal with as well. The function I wrote to handle this is based on Violet's "Replace Jazz's animations" snippet (linked above), but specifically caters to the needs of my Battery Check port (I forget exactly what changes I made, and can't be bothered to check her snippet to figure it out):
Code:
void bcbatterymansprites_setup() {
	uint emptySlot = 0;
	
	// Load in the Batteryman sprites.
	while (jjAnimSets[ANIM::CUSTOM[emptySlot]].firstAnim != 0) {
		++emptySlot;
	}
	jjANIMSET@ batterymanAnm = jjAnimSets[ANIM::CUSTOM[emptySlot]];
	batterymanAnm.load(13, "BatteryCheck.j2a");
	batterymanSet = emptySlot;
	for(uint8 i=0; (i <= 74); i++) {
		for(uint8 j=0; (j < jjAnimations[batterymanAnm.firstAnim+i].frameCount); j++) {
			jjAnimFrames[jjAnimations[batterymanAnm.firstAnim+i].firstFrame+j].hotSpotY -= 25;
		}
	}
}
Don't worry about copypasting this; the end product, such as it is, will be linked at the bottom of this post.

Let's break this down a little:
emptySlot is a number that starts at 0. If the ANIM::CUSTOM animation set slot 0 is taken, then emptySlot becomes 1, then if 1 is taken, it becomes 2... Eventually, an available slot is found, at which point we put Batteryman's sprites into it (I know from my own checks in Jazz Sprite Dynamite that Batteryman's sprites are in the BatteryCheck.J2A set 13, though JazzSD will tell you it's 14, because it wrongly counts starting from 1 instead of 0. So, load(13, "BatteryCheck.j2a") will take Batteryman's animations).

There's also a for loop at the end, which itself contains another for loop. This wouldn't need to be there if these sprites were custom-made (or even just put into a custom J2A instead of the one already created for Battery Check. I don't imagine Play As Mario had to deal with this).
The problem I encountered when I wrote these two loops is that Batteryman's sprites' hotspots are set in an awkward position, 25 pixels too high, as I found out.



You'll also notice that Batteryman is entirely black. We'll get to that in a moment, but once again, that's a problem stemming from using an existing J2A.

Anyway, hotspots are basically the sprite's anchor point. Move that, and it will move where it's drawn on the screen. The hotspot is set on a frame-by-frame basis in each animation, so we have to loop through each of Batteryman's animations (the outer loop), and each frame in each of those animations (the inner loop), with each frame's hotspot lowered by 25 pixels, which moves the sprite up by 25 pixels.
As for Batteryman being black in this image, because he's using a lot of weird palette entries, and the game is only expecting a player (or anything else using the SPRITE::PLAYER sprite mode) to use certain colour ranges, you need to set the player's sprite mode to be SPRITE::NORMAL, which I do in onPlayer (it's probably bad practice to run this every frame, and I don't in the latest version, but the version I threw together and uploaded at the bottom of this post runs it every frame. Oh well. It shouldn't be a huge performance impact unless your computer is really, really terrible).


Okay, we're in business!

... But not quite. I've told you how to load these sprites in, and make sure they display properly (at least, if you're not pulling properly-set-up player sprites), but I've not told you how to actually make the player use them.

For my Jazz 2 port of Battery Check, I made the function that replaces all the player sprites with Batteryman's able to function with any of the three characters in the game. I also set up an enum to make it easier to understand what's going on with this code.

The enum definition looks like this:
Code:
enum BATTERYMAN {LEDGEWIGGLE, USEBATTERY_FULLPOWER, FLYINGBATTERY, LANDINGBATTERY, USEDBATTERY, USESUPBATTERY, FULLBATTERY, HEAL, DANCE, DIE_CRUSH1, DIE_CRUSH2, DIE_CRUSH3, DIE_POWERDOWN, CORPSE_POWERDOWN, DIE_DROWN, CORPSE_DROWN, DIVE, LOOKINGDOWN, DIVEUP, IDLE1, IDLE2, ELECTROCUTE1, ELECTROCUTE2, ATTACKED1, ATTACKED2, FALL_INTO, SWIMMING_START, IDLE3, IDLE4, IDLE5, LAUGH, IDLE6, USEBATTERY_LOWPOWER, STAND_LOWPOWER, STAND_MIDPOWER, STAND_HUNCH1, STAND_HUNCH2, WALK_START_FULLPOWER, HUNCH_STOP, JUMP_START, PUSH, JUMPING1, JUMPING2, JUMPING3, FALLLAND, FALL, CLIMBUP, UNUSEDPIPE1, UNUSEDPIPE2, UNUSEDPIPE3, IDLE7, WALK_START_LOWPOWER, WALKING_LOWPOWER, SKID2, SKID1, SKID3, RUN2, EMPTY, PIPE_EXIT_UP, PIPE_ENTER_UP, RUN1, SPARK, APPEAR, SUPERBATTERY, RUN3, SWIMMING_MOVE, SWIMMING_STILL, LOOKUP_START, LOOKINGUP, LOOKUP_STOP, DEFLATTEN, JUMP_MID2, JUMP_MID3, JUMP_MID4, LAND2}
So, rather than reference Batteryman's ledge wiggle animation as 0, I can reference it as BATTERYMAN::LEDGEWIGGLE (since LEDGEWIGGLE is the first entry in this enum, and computers count from 0, "BATTERYMAN::LEDGEWIGGLE" is read by the computer as 0). I mostly attempted to follow the lead of the Jazz 2 RABBIT enums, but I only halfheartedly did so; Batteryman's sprites are set up a little differently from the Jazz characters.

Anyway, a snippet of the function that replaces a character's sprites in my Battery Check port looks like this:
Code:
void batteryman_replaceAnims(uint characterAnim) {
	jjAnimations[jjAnimSets[characterAnim].firstAnim+RABBIT::STAND] = jjAnimations[jjAnimSets[ANIM::CUSTOM[batterymanSet]].firstAnim+BATTERYMAN::STAND_HUNCH1];
	jjAnimations[jjAnimSets[characterAnim].firstAnim+RABBIT::DIVE] = jjAnimations[jjAnimSets[ANIM::CUSTOM[batterymanSet]].firstAnim+BATTERYMAN::DIVE];
	jjAnimations[jjAnimSets[characterAnim].firstAnim+RABBIT::DIVE].frameCount++;
	jjAnimFrames[jjAnimations[jjAnimSets[characterAnim].firstAnim+RABBIT::DIVE].firstFrame+3] = jjAnimFrames[jjAnimations[jjAnimSets[ANIM::CUSTOM[batterymanSet]].firstAnim+BATTERYMAN::LOOKINGDOWN].firstFrame];
	jjAnimations[jjAnimSets[characterAnim].firstAnim+RABBIT::DIVEUP] = jjAnimations[jjAnimSets[ANIM::CUSTOM[batterymanSet]].firstAnim+BATTERYMAN::DIVEUP];
	jjAnimations[jjAnimSets[characterAnim].firstAnim+RABBIT::FALL].frameCount = 1;
	jjAnimFrames[jjAnimations[jjAnimSets[characterAnim].firstAnim+RABBIT::FALL].firstFrame] = jjAnimFrames[jjAnimations[jjAnimSets[ANIM::CUSTOM[batterymanSet]].firstAnim+BATTERYMAN::FALL].firstFrame];
	jjAnimations[jjAnimSets[characterAnim].firstAnim+RABBIT::FALLLAND] = jjAnimations[jjAnimSets[ANIM::CUSTOM[batterymanSet]].firstAnim+BATTERYMAN::FALLLAND];
}
The form of this all is basically like this...

A line roughly equivalent to jjAnimations[jjAnimSets[characterAnim].firstAnim+RABBIT::STAND], which represents any one character animation, is set to instead be jjAnimations[jjAnimSets[ANIM::CUSTOM[batterymanSet]].firstAnim+BATTERYMAN::STAND_HUNCH1]

A few of these in this function are actually a little different and involve loops, basically because I had to join two separate Batteryman animations into one Jazz animation... It's unlikely you'll need to do this, but if you do, you can inspect my code for inspiration. Or just ask for help; I certainly did.

Anyway, let's assume your sprite replacement has gone without issue at this point. Your sprites are all loaded in and displaying correctly.

But, we have a problem: Your character is the wrong size!

Batteryman appears correctly, but he clips into ceilings! He's a lot taller than Jazz, and quite a bit wider too. So, we have to create a custom collision routine. I set up a fairly simple one that's called in onPlayer:
Code:
void batteryman_collide(jjPLAYER@ player) {
	if (!player.noclipMode) {
		if (player.ySpeed < 0 && jjEventGet(int(player.xPos/32),int(player.yPos/32)) != 1 && jjEventGet(int(player.xPos/32),int(player.yPos/32)-1) != 1 && jjEventGet(int(player.xPos/32),int(player.yPos/32)-2) != 1) {
			for(int i = 0; (i < 24); i++) {
				if (jjMaskedVLine(int(player.xPos-12+i), int(player.yPos-50), 28)) {
					player.ySpeed = 0;
					break;
				}
			}
		}
		
		if (player.xSpeed != 0) {
			int dir = (player.xSpeed > 0) ? 1 : -1; // -1 if moving left, 1 if moving right
			
			for(int i = 0; (i < 32); i++) {
				if (jjMaskedHLine(int(player.xPos) + 6*dir, 12, int(player.yPos-42+i))) {
					if (player.xSpeed != 0) {
						player.xPos -= player.xSpeed;
					}
					player.xSpeed = 0;
					break;
				}
			}
		}
	}
}
How this works is that if the player is not in noclip mode (not a strictly necessary check, but useful to have if you plan on using noclip while testing your level), then it runs two checks: If the player is moving up and they're not moving through one-way floors, then we check if they're bumping into the ceiling; if they are, then we stop them moving.
A similar process is repeated for sideways movement, but without the one-way floor check, and we move them back away from the direction they were moving slightly before we stop them from moving (prevents some issues).

Now for the spicy stuff: Mechanical differences!

Sorry it's taken so long to get here. Let's not dillydally...

Batteryman is not Jazz. He plays a little differently. For starters, Batteryman is a pacifist; he carries no weapon. So, in onPlayer, I have player.noFire = true;
Custom weapons are a whole other thing, so I won't go into those, but you can make custom weapons too, if you want.

Batteryman can't stomp, so also in onPlayer, I have
Code:
if (player.ySpeed != 0) {
	player.keyDown = false;
}
Batteryman also can't helicopter (or double jump), and he can't uppercut (or kick), so in onLevelLoad, I put
Code:
jjCharacters[CHAR::JAZZ].airJump = AIR::NONE;
jjCharacters[CHAR::JAZZ].groundJump = GROUND::CROUCH;
If you want your character to helicopter like Jazz and Lori, then set airJump to AIR::HELICOPTER, and if you want them to double-jump like Spaz, set it to AIR::DOUBLEJUMP.
Similarly, you can set groundJump to GROUND::JAZZ, GROUND::SPAZ, GROUND::LORI (even in 1.23), GROUND::CROUCH, or GROUND::JUMP, which respectively gives a Jazz uppercut, a Spaz kick, a Lori kick, leaves the player crouching, or makes the player jump (as if they pressed jump while not crouching).
Also note that I applied these, in my snippet, only to CHAR::JAZZ. There's no reason you can't apply this to any other character, even the frog or either of the two birds.

Presumably, unlike Batteryman, your custom character may not be a pacifist, but you might want to create different special moves that don't have any resemblance to what the existing characters use. For instance, here's a bit of sample code I just threw together that should, in theory, give Jazz several double jumps and the ability to flip gravity when he does an uppercut.
Code:
void onLevelLoad() {
	jjCharacters[CHAR::JAZZ].airJump = AIR::DOUBLEJUMP;
}

void onPlayer(jjPLAYER@ player) {
	if (player.charCurr == CHAR::JAZZ) {
		newSpecialMoves(player);
	}
}

void newSpecialMoves(jjPLAYER@ player) {
	// If the player is not pressing down or jump,
	if (!(player.keyDown || player.keyJump)) {

		// Then they are not performing their special move. This means they will be able to use their special move again.
		player.specialMove = 0;

	// Otherwise, if they were not previously performing their special move,
	} else if (player.specialMove == 0) {
		
		// Flip the player's gravity, like in VVVVVV.
		player.antiGrav = !player.antiGrav;
		
		// Therefore, they're performing their special move.
		player.specialMove = 1;
	}
	
	// Progressively reduce the strength of the player's double-jump.
	if (player.doubleJumpCount > 0 && player.doubleJumpCount < 8) {
		player.jumpStrength = -8/doubleJumpCount;
	} else if (player.doubleJumpCount == 0) {
		player.jumpStrength = -10;
	}
}
I don't have the time or patience to actually test this code, so you'll have to figure out if I've made any mistakes and correct any broken functionality yourself (for instance, I don't actually know if this, in its current form, would actually allow for more than one double-jump, and I'm pretty sure I'd need to set Jazz to not uppercut when he does his special move), but this should give you a rough idea of the kind of thing you can put together for new special moves and the like.

Batteryman doesn't have his own special moves, of course. He can, however, supply batteries to battery holders. The code that checks this is in the battery holder class, meaning this code runs for all battery holder objects on screen. A simplified version looks like this:
Code:
int nearest = obj.findNearestPlayer(4096);
// Here, we're determining if the player is using the battery station.
if (nearest >= 0) {
	jjPLAYER@ player = jjPlayers[nearest];
	if (player.keyUp == true && (obj.frameID == 17 || obj.frameID == 0) /*If a player presses up while the station is idle*/ && ((batteries[nearest] > 0 && obj.var[6] == 0) || (supBatteries[nearest] > 0 && obj.var[6] == 1)) /*And you have at least 1 of the correct type of battery*/ ) {
		obj.state = STATE::HIT;
		obj.counterEnd = 0;
		obj.frameID = 1;
		if (obj.var[6] == 1) {
			supBatteries[nearest]--;
		} else {
			batteries[nearest]--;
			empties[nearest]++;
		}
	}
}
Note that var[6] is whether or not this battery holder is a super battery holder. I also omitted some animation-related code that doesn't matter for our purposes.
findNearestPlayer is a function built into Jazz 2's AngelScript API. The ever-helopful AngelScript readme explains its usage thusly:

So, nearest will be a negative number if no player is within the range I set. (4096. The square root of 4096 is 64, so the maximum distance is 64 pixels in any direction)
If a player is found within the range I set, then we check if that player is pressing up (the battery station use key. I set the fire key to be a proxy for this in the level's onPlayer, because the fire key is how you use battery stations in Battery Check), and if the battery station is not already being used by Batteryman, and if the player has enough of the correct type of battery...
If all this checks out, then the battery station sets its state to STATE::HIT, and subtracts the relevant battery from the player's total, and if it's a normal battery, gives them an empty battery back.

Elsewhere in the battery holder's code (in fact, right above this part), there's a part that checks the battery holder's state and performs various actions based on this; most notably, if its state is STATE::HIT, it turns on a trigger, and it counts down a counter until it reaches zero, at which point it returns to its resting state and turns off the trigger.

So, with this in mind, if you're willing to deal with custom objects, and have your custom character rely on those objects to do anything interesting, you can set up some other interesting mechanics. Batteryman can also interact with recharging stations that replenish his ever-depleting health, and there are battery pickups that he has to get to use the battery stations...

Speaking of Batteryman's health, his speed varies depending on his health level.
In the function that determines his health level, I run this:
Code:
if (hp_current > (hp_max*70)/2) { // Player is above half health; their speed gets a +2 buff.
	player.keyRun = true; // The game won't let you set the player's speed higher than their walking speed if they're not running. If they are running, they actually run a bit faster than Batteryman, so we need to limit the speed.
	
	// The speed limits. I set these to 5.7 and -5.7, but the game constantly accelerates you a little, so this actually works out to roughly 6 and -6 speed in reality.
	if (player.keyRight && player.xSpeed > 5.7) {
		player.xSpeed = 5.7;
	} else if (player.keyLeft && player.xSpeed < -5.7) {
		player.xSpeed = -5.7;
	}
} else if (hp_current < (hp_max*70)/8) { // Player is below an eighth health; their speed gets a -2 debuff.
	if (player.xSpeed > 2) {
		player.xSpeed = 2;
	} else if (player.xSpeed < -2) {
		player.xSpeed = -2;
	}
}
I've added a couple of extra comments to explain this better than in the asc file in the download below, and with that, I don't think any further explanation is needed.

However, here's one last snippet I can think to include here:
Code:
// Whether or not the player is dead.
if (hp_current == 0) {
	player.kill();
	hp_current = hp_max*70;
} else {
	hp_current = hp_current - 1; // If the player isn't dead, tick their health down.
}
This is basically the heart of the Batteryman health code. After this is the healing arch check, then after that is the player's speed change.

I also set up some custom interface code which is worth looking at. Most of it is fairly simple. You can use custom sprite sets as fonts for things like lives counters, or you can use the ingame fonts. But, at this point, I think I've explored basically everything I can think of exploring to help you get started on building a custom character.

Here's the alpha version of Batteryman In Jazz 2, if you want to dig through my code for yourself, or just run the level to play around as Batteryman as a demonstration of what can be done.
PlusOBC-1.j2as is the core script, but a lot of it is just running the functions from the other two files; BatteryCheck_objects.asc contains all the game objects, and BatteryCheck_UI_batteryman.asc contains all the UI, Batteryman sprites, and health stuff.
I actually write/wrote this starting in just one j2as, and then split out any parts that make sense to split out into separate asc files, so some of the splitting will probably seem pretty arbitrary and weird. In particular, the BatteryCheck_UI_batteryman split in general is a work in progress; I haven't put all the pieces in there for it to work on its own yet.
And, in general, there's probably a lot of bad practices and hacky nonsense, and it's a bit messy, and there are a load of bugs...
But, this Battery Check alpha thing should give you an idea of some of the stuff you can do with a custom character.
__________________
chilistudios

JCF Member

Joined: Jan 1970

Posts: 6

chilistudios has disabled reputation

Nov 12, 2020, 10:31 AM
chilistudios is offline
Reply With Quote
how do i patch a character mod in?
Stijn Stijn's Avatar

Administrator

Joined: Mar 2001

Posts: 6,964

Stijn is a splendid one to beholdStijn is a splendid one to beholdStijn is a splendid one to beholdStijn is a splendid one to beholdStijn is a splendid one to beholdStijn is a splendid one to beholdStijn is a splendid one to behold

Nov 12, 2020, 04:02 PM
Stijn is offline
Reply With Quote
Love & Thunder's post is a great starting point. Please read the posts above and ask specific questions if you have them. Do not ask the same question all the time.
Violet CLM Violet CLM's Avatar

JCF Éminence Grise

Joined: Mar 2001

Posts: 10,978

Violet CLM has disabled reputation

Nov 13, 2020, 08:53 AM
Violet CLM is offline
Reply With Quote
Backing up a little:

For the most part, editing things in Jazz 2 is done by writing code. For users, such as you, the code you write must be in the language called AngelScript.

There is not a graphical tool you can use to import a 3D model or something which automatically creates a character. Some things can be done in AngelScript pretty simply, because they don't have a lot of steps or because code libraries have been developed to make them easier. Writing a custom character is not so easy because it has a few different pieces.

The first and most important step is getting your sprites into the game. Use some variation on this angelscript here. You will also need a .j2a file with the animations in it. Use these Python scripts.

Once you've gotten your sprites in the game, and only once you've done that, it makes sense to start worrying about unique abilities. But the sprites are the first step.
__________________
Reply

Thread Tools

Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

BB code is On
Smilies are On
[IMG] code is On
HTML code is On

Forum Jump

All times are GMT -8. The time now is 08:18 AM.