Downloads containing gfv.mut

Downloads
Name Author Game Mode Rating
JJ2+ Only: gfv.mut Violet CLM Mutator N/A Download file

File preview

#pragma name "Ground Force"
uint32 LevelProperties;
jjSTREAM LevelGFData;
bool DefiningLevelProperties = false;


enum LevelPropertyEnum {
	DefiniteWaitingAreaBoundaries, DefiniteFailureAreaBoundariesDistinctFromWaitingArea, DefiniteArenaBoundaries, OneOrMoreStartWarpIDs, TriggerIDsToTurnOnAtLevelStart, TriggerIDsToTurnOnAtGameStart, SetWaterLevelAtGameStart, GiveAmmoAtGameStart, ShowTextStringAtGameStart, SetLightAtGameStart, GiveRandomizedPowerupsAtGameStart, MorphPlayerOnGameStart, TriggerIDsForSuddenDeath,
	_LAST
};
bool LevelHasProperty(LevelPropertyEnum prop) {
	return (LevelProperties & (1 << prop)) != 0;
}
void LevelSetProperty(LevelPropertyEnum prop, bool setTo = true) {
	if (setTo)
		LevelProperties |= 1 << prop;
	else
		LevelProperties &= ~(1 << prop);
}

void onLevelBegin() { //can't use commands while cycling
	if (jjIsServer || jjGameConnection == GAME::LOCAL) {
		if (LevelProperties != 0) {
			if (jjGameMode != GAME::CTF) { //non-team-based
				if (jjGameCustom != GAME::LRS && jjGameCustom != GAME::XLRS)
					jjChat("/lrs");
			} else {
				if (jjGameCustom != GAME::TLRS)
					jjChat("/tlrs");
			}
		}
	} else
		jjSendPacket(jjSTREAM());
}

void onReceive(jjSTREAM&in packet, int fromClientID) {
	if (jjIsServer)
		jjSendPacket(LevelGFData, fromClientID);
	else
		ProcessPropertyStream(packet);
}

array<uint8> StartWarpIDs(), LevelTriggerIDs(), GameTriggerIDs(), SuddenDeathTriggerIDs();
array<uint16> WaitingAreaBoundaries(4,0), FailureAreaBoundaries(4,0), ArenaBoundaries(4,0); //left, top, right, bottom
array<uint16> StartAmmo();
uint16 WaterLevel = 0;
int8 StartTextString = -1;
uint8 RandomPowerups = 0, StartLighting = 0;
uint8 MorphPlayerType = 0;
uint SuddenDeathCounter = 0;

void onLevelLoad() {
	SpinningString.align = STRING::CENTER;
	
	if (jjIsServer || jjGameConnection == GAME::LOCAL) {
		const string levelRoot = jjLevelFileName.substr(0, jjLevelFileName.length-4);
		jjSTREAM input(levelRoot + ".gf");
		if (input.isEmpty()) input = jjSTREAM(levelRoot + ".gf.asdat");
		if (!input.isEmpty()) ProcessPropertyStream(input);
		else DefinitionSequence::Start();
	}
	
	jjObjectPresets[OBJECT::WITCH].behavior = BEHAVIOR::INACTIVE;
}
void ProcessPropertyStream(jjSTREAM& input) {
	LevelGFData = input;
	input.pop(LevelProperties);
	
	if (LevelProperties & ~((1 << LevelPropertyEnum::_LAST) - 1) != 0) { //one or more bits set higher than the highest bit this file knows about
		jjAlert("|.gf compatibility warning!", false, STRING::MEDIUM);
		jjAlert("|WARNING: This .gf file was saved with a later version of gfv.mut!");
		jjAlert("|The level will still run but not all features may run correctly.");
	}
	
	jjCharacters[CHAR::JAZZ].airJump = jjCharacters[CHAR::SPAZ].airJump;
	jjCharacters[CHAR::JAZZ].doubleJumpCountMax = jjCharacters[CHAR::SPAZ].doubleJumpCountMax;
	jjCharacters[CHAR::JAZZ].doubleJumpXSpeed = jjCharacters[CHAR::SPAZ].doubleJumpXSpeed;
	jjCharacters[CHAR::JAZZ].doubleJumpYSpeed = jjCharacters[CHAR::SPAZ].doubleJumpYSpeed;
	if (jjIsTSF) {
		jjCharacters[CHAR::LORI].airJump = jjCharacters[CHAR::SPAZ].airJump;
		jjCharacters[CHAR::LORI].doubleJumpCountMax = jjCharacters[CHAR::SPAZ].doubleJumpCountMax;
		jjCharacters[CHAR::LORI].doubleJumpXSpeed = jjCharacters[CHAR::SPAZ].doubleJumpXSpeed;
		jjCharacters[CHAR::LORI].doubleJumpYSpeed = jjCharacters[CHAR::SPAZ].doubleJumpYSpeed;
	}
	
	const array<array<uint16>@> AllBoundaries = {WaitingAreaBoundaries, FailureAreaBoundaries, ArenaBoundaries};
	for (uint i = 0; i < AllBoundaries.length; ++i)
		if (LevelHasProperty(LevelPropertyEnum(i))) {
			for (int j = 0; j < 4; ++j)
				input.pop(AllBoundaries[i][j]);
		}
	if (!LevelHasProperty(DefiniteFailureAreaBoundariesDistinctFromWaitingArea))
		FailureAreaBoundaries = WaitingAreaBoundaries;
		
	const array<array<uint8>@> AllIDs = {StartWarpIDs, LevelTriggerIDs, GameTriggerIDs};
	for (uint i = 0; i < AllIDs.length; ++i)
		if (i == 0 || LevelHasProperty(LevelPropertyEnum(i + OneOrMoreStartWarpIDs))) {
			uint8 count;
			input.pop(count);
			for (uint j = 0; j < count; ++j) {
				uint8 value;
				input.pop(value);
				AllIDs[i].insertLast(value);
			}
		}
	for (uint i = 0; i < LevelTriggerIDs.length; ++i)
		jjTriggers[LevelTriggerIDs[i]] = true;
		
	if (LevelHasProperty(SetWaterLevelAtGameStart))
		input.pop(WaterLevel);
		
	if (LevelHasProperty(GiveAmmoAtGameStart)) {
		StartAmmo.resize(9);
		for (uint i = 0; i < 9; ++i) {
			input.pop(StartAmmo[i]);
		}
	}
	
	if (LevelHasProperty(ShowTextStringAtGameStart))
		input.pop(StartTextString);
		
	if (LevelHasProperty(SetLightAtGameStart))
		input.pop(StartLighting);
		
	if (LevelHasProperty(GiveRandomizedPowerupsAtGameStart))
		input.pop(RandomPowerups);
		
	if (LevelHasProperty(MorphPlayerOnGameStart))
		input.pop(MorphPlayerType);
		
	if (LevelHasProperty(TriggerIDsForSuddenDeath)) {
		uint8 count;
		input.pop(count);
		SuddenDeathTriggerIDs.resize(count);
		for (uint i = 0; i < count; ++i)
			input.pop(SuddenDeathTriggerIDs[i]);
	}
}

bool GetGameActive() {
	if (jjGameTicks < 70)
		return false;
	if (jjGameState == GAME::STARTED || jjGameState == GAME::OVERTIME)
		return true;
	if (jjGameConnection == GAME::LAN && jjGameState == GAME::PREGAME)
		return true;
	return false;
}

uint GetActivePlayerCount() {
	uint ActivePlayerCount = 0;
	for (uint i = 0; i < 32; ++i)
		if (jjPlayers[i].isInGame)
			++ActivePlayerCount;
	return ActivePlayerCount;
}
uint GetOutPlayerCount() {
	uint OutPlayerCount = 0;
	for (uint i = 0; i < 32; ++i)
		if (jjPlayers[i].isActive && jjPlayers[i].isOut)
			++OutPlayerCount;
	return OutPlayerCount;
}

bool GameWasActive = true;
bool LocalPlayerWasBlinking = false;
jjPLAYER@ LocalPlayer = jjLocalPlayers[0];
void onMain() { //Go through and adjust certain objects to be destroyable (or not) depending on whether the game is in progress or paused/pregaming
	if (!DefiningLevelProperties) {
		if (LevelProperties != 0) {
			bool gameActive = GetGameActive();
			if (gameActive && !GameWasActive) {
				for (int i = 0; i < 32; ++i)
					if (jjPlayers[i].isActive && !jjPlayers[i].isSpectating && jjPlayers[i].isConnecting) {
						gameActive = false; //don't start the game if someone's connecting
						break;
					}
			}
			if (GameWasActive != gameActive) {
				GameWasActive = gameActive;
				if (gameActive) {
					for (int i = jjObjectCount - 1; i >= 1; --i) {
						jjOBJ@ obj = jjObjects[i];
						if (obj.behavior == BEHAVIOR::DESTRUCTSCENERY)
							obj.bulletHandling = HANDLING::DETECTBULLET;
						else if (obj.behavior == BEHAVIOR::CRATE)
							obj.playerHandling = HANDLING::SPECIAL;
					}
				} else {
					for (int i = jjObjectCount - 1; i >= 1; --i) {
						jjOBJ@ obj = jjObjects[i];
						if (obj.behavior == BEHAVIOR::DESTRUCTSCENERY)
							obj.bulletHandling = HANDLING::IGNOREBULLET;
						else if (obj.behavior == BEHAVIOR::CRATE)
							obj.playerHandling = HANDLING::SPECIALDONE;
					}
				}
			}
			
			const bool localPlayerBlinking = LocalPlayer.blink != 0;
			if (LocalPlayerWasBlinking != localPlayerBlinking) {
				for (int i = jjObjectCount - 1; i >= 1; --i) {
					jjOBJ@ obj = jjObjects[i];
					if (obj.behavior == BEHAVIOR::SPRING || obj.behavior == BEHAVIOR::BUMP || obj.behavior == BEHAVIOR::PADDLE) //stuff that you shouldn't be able to use while blinking
						obj.playerHandling = (localPlayerBlinking) ? HANDLING::SPECIALDONE : HANDLING::SPECIAL;
				}
				LocalPlayerWasBlinking = localPlayerBlinking;
			}
			
			if (GameWasActive) {
				if (SuddenDeathTriggerIDs.length != 0) {
					bool activateSuddenDeath = jjGameState == GAME::OVERTIME;
					if (!activateSuddenDeath) {
						if (SuddenDeathCounter == 0) {
							if (GetActivePlayerCount() <= 2) //== 2 might make more sense, but <= 2 is better for testing
								SuddenDeathCounter = 1; //start
						} else if (++SuddenDeathCounter == 70*30) { //wait half a minute
							activateSuddenDeath = true;
							SuddenDeathCounter = 70*24; //wait six seconds
						}
					}
					if (activateSuddenDeath) {
						jjTriggers[SuddenDeathTriggerIDs[0]] = true; //just the first one
						SuddenDeathTriggerIDs.removeAt(0);
					}
				}
			}
		}
	} else {
		DefinitionSequence::Do();
	}
}

enum PlayerState { Starting, Warping, Ingame };
array<PlayerState> PlayerStates(jjLocalPlayerCount);
bool PlayerWithinBoundaries(const jjPLAYER@ play, const array<uint16> &in boundaries) { //left, top, right, bottom
	const uint16 x = uint16(play.xPos), y = uint16(play.yPos);
	return x >= boundaries[0] && y >= boundaries[1] && x < boundaries[2] && y < boundaries[3];
}
void onPlayer(jjPLAYER@ play) {
	if (DefiningLevelProperties) {
		play.keyLeft = play.keyRight = play.keyDown = play.keyUp = /*play.keyFire =*/ play.keyRun = play.keySelect = play.keyJump = false;
		play.idle = 0;
		return;
	}
	if (play.health > 0 && LevelProperties != 0) {
		if (PlayerWithinBoundaries(play, WaitingAreaBoundaries) && PlayerStates[play.localPlayerID] == PlayerState::Starting) {
			play.keyLeft = play.keyRight = play.keyJump = play.keyFire = play.keyRun = false;
			play.doubleJumpCount = 0; //so you'll be able to double jump again if you revive
			play.frozen = 0; //don't delay level start
			
			const int arenaLeft = ArenaBoundaries[0];
			const int arenaTop = ArenaBoundaries[1];
			const int arenaRight = ArenaBoundaries[2] - jjSubscreenWidth;
			const int arenaBottom = ArenaBoundaries[3] - jjSubscreenHeight;
			const int arenaHalfWidth = (arenaRight - arenaLeft) / 2;
			const int arenaHalfHeight = (arenaBottom - arenaTop) / 2;
			const float intensityX = jjSin((arenaHalfWidth > arenaHalfHeight) ? jjRenderFrame/2 : jjRenderFrame*3);
			const float intensityY = jjSin((arenaHalfWidth > arenaHalfHeight) ? jjRenderFrame*3 : jjRenderFrame/2);
			play.cameraFreeze(
				arenaLeft + arenaHalfWidth + intensityX * arenaHalfWidth,
				arenaTop + arenaHalfHeight + intensityY * arenaHalfHeight,
				false, true
			);
			
			if (play.warpID == 0 && play.blink == 0 && GameWasActive) { //ready to begin
				if (StartWarpIDs.length != 0) {
					if (LevelHasProperty(LevelPropertyEnum::OneOrMoreStartWarpIDs))
						play.warpToID(StartWarpIDs[jjRandom() % StartWarpIDs.length]);
					else {
						const uint idx = (jjRandom() % StartWarpIDs.length) & ~1;
						play.warpToTile(StartWarpIDs[idx], StartWarpIDs[idx+1]);
					}
					PlayerStates[play.localPlayerID] = PlayerState::Warping;
					for (uint i = 0; i < GameTriggerIDs.length; ++i)
						jjTriggers[GameTriggerIDs[i]] = true;
					if (StartAmmo.length == 9) {
						for (uint i = 0; i < 9; ++i) {
							const int weaponID = i + 1;
							if (!jjWeapons[weaponID].infinite) { //don't bother giving ammo for blaster
								if ((play.ammo[weaponID] = (StartAmmo[i] & 0x7FFF)) > 0) {
									jjWeapons[weaponID].allowed = true;
									if (jjAutoWeaponChange)
										play.currWeapon = weaponID;
								}
							}
							if (play.powerup[weaponID] = (StartAmmo[i] & 0x8000) != 0) {
								jjWeapons[weaponID].allowedPowerup = true;
								jjSamplePriority(SOUND::COMMON_GLASS2);
							}
							else if (weaponID != WEAPON::BLASTER && play.ammo[weaponID] > 0)
								jjSamplePriority(SOUND::COMMON_PICKUPW1);
						}
					}
					if (WaterLevel != 0)
						jjSetWaterLevel(WaterLevel * 32, true);
					if (StartTextString >= 0)
						play.showText(StartTextString, 0);
					if (StartLighting != 0)
						play.lighting = StartLighting;
					if (RandomPowerups != 0) {
						jjSamplePriority(SOUND::COMMON_GLASS2);
						array<int> possibleAmmoTypes = {1, 2, 3, 4, 5, 6, 7, 8, 9};
						while (RandomPowerups-- != 0 && possibleAmmoTypes.length != 0) {
							const uint idx = jjRandom() % possibleAmmoTypes.length;
							const int ammoType = possibleAmmoTypes[idx];
							possibleAmmoTypes.removeAt(idx);
							if (jjWeapons[ammoType].allowedPowerup || (ammoType == WEAPON::TNT && jjWeapons[WEAPON::TNT].allowed)) {
								play.ammo[ammoType] = jjWeapons[ammoType].maximum == -1 ? 50 : jjWeapons[ammoType].maximum;
								play.powerup[ammoType] = jjWeapons[ammoType].allowedPowerup;
							} else if (play.fastfire != 6)
								play.fastfire = 6;
							else
								++RandomPowerups; //try again!
						}
					}
				} //else...?
			}
		} else if (PlayerWithinBoundaries(play, FailureAreaBoundaries)) {
			if (GameWasActive && PlayerStates[play.localPlayerID] == PlayerState::Ingame) {
				if (GetActivePlayerCount() > 1 || GetOutPlayerCount() == 0) { //don't kill yourself if you won
					PlayerStates[play.localPlayerID] = PlayerState::Starting;
					while (play.health > 0) play.hurt(10, true); //works better than jjPLAYER::kill
				}
			}
		} else { // if (PlayerWithinBoundaries(play, ArenaBoundaries)) { //warp target could be outside of arena boundaries as defined by where the actual destruct scenery events are
			if (play.health != jjMaxHealth && !(jjGameCustom == GAME::XLRS || jjGameState == GAME::OVERTIME)) //invincible, but in a way that allows players to be hit
				play.health = jjMaxHealth;
			if (PlayerStates[play.localPlayerID] == PlayerState::Warping) {
				if (!PlayerWithinBoundaries(play, WaitingAreaBoundaries)) {
					play.blink = 210; //don't bump into other players while entering
					play.cameraUnfreeze(false);
					if (MorphPlayerType >= 3 && MorphPlayerType <= 5 && play.warpID != 0) //not ready to morph yet
						return;
					PlayerStates[play.localPlayerID] = PlayerState::Ingame;
					switch (MorphPlayerType) {
						case 0:
							break;
						case 10:
							jjCharacters[CHAR::JAZZ].airJump = AIR::HELICOPTER;
						case 1:
							play.morphTo(CHAR::JAZZ, false);
							break;
						case 2:
							play.morphTo(CHAR::SPAZ, false);
							break;
						case 3:
							play.morphTo(CHAR::FROG, false);
							break;
						case 4:
							play.morphTo(CHAR::BIRD, false);
							break;
						case 5:
							play.morphTo(CHAR::BIRD2, false);
							break;
						case 6: //airboard
							play.fly = FLIGHT::AIRBOARD;
							break;
						case 7: //fly carrot
							play.fly = FLIGHT::FLYCARROT;
							break;
						case 8:
							play.morphTo(jjIsTSF ? CHAR::LORI : CHAR::JAZZ, false);
							break;
						case 9:
							play.morphTo(jjIsTSF ? CHAR::LORI : CHAR::SPAZ, false);
							break;
						case 11:
							jjCharacters[CHAR::JAZZ].airJump = AIR::HELICOPTER;
							if (jjIsTSF)
								jjCharacters[CHAR::LORI].airJump = AIR::HELICOPTER;
							//but don't morph!
							break;
					}
				}
			}
		}
	}
}

jjTEXTAPPEARANCE SpinningString(STRING::SPIN);
bool onDrawHealth(jjPLAYER@ play, jjCANVAS@ screen) {
	if (!DefiningLevelProperties) {
		if (LevelProperties != 0) //real ground force level
			if (play.isLocal) //not spectating
				if (PlayerStates[play.localPlayerID] == PlayerState::Starting)
					if (GameWasActive) { //reviving after death, preferably
						int intensity = int(abs(play.blink));
						if (intensity > 255) intensity = 255;
						screen.drawString(jjSubscreenWidth/2, jjSubscreenHeight/2 - 30, "Get Ready", STRING::LARGE, SpinningString, 0, SPRITE::BLEND_NORMAL, intensity);
					}
		
		return !(jjGameCustom == GAME::XLRS || jjGameState == GAME::OVERTIME);
	} else {
		DefinitionSequence::Draw(screen);
		return true;
	}
}

bool onDrawAmmo(jjPLAYER@, jjCANVAS@) { return DefiningLevelProperties; }
bool onDrawScore(jjPLAYER@, jjCANVAS@) { return DefiningLevelProperties; }
bool onDrawLives(jjPLAYER@, jjCANVAS@) { return DefiningLevelProperties; }


namespace DefinitionSequence {
	jjTEXTAPPEARANCE TLeft(STRING::NORMAL), TCenter(STRING::NORMAL);
	
	const string Brown = "", Green = "|", Red = "||", Blue = "|||", Orange = "||||";
	class Control {
		void Do() {}
		void Draw(jjCANVAS@ screen) const {}
	}
	class LocatedControl : Control {
		int XPos, YPos;
		int Left,Top,Right,Bottom;
		LocatedControl(int x, int y) { XPos = x; YPos = y; }
		void DrawString(jjCANVAS@ screen, const string &in text, STRING::Size size = STRING::MEDIUM, STRING::Alignment align = STRING::CENTER, int x = 0, int y = 0) const {
			screen.drawString(XPos + x, YPos + y, text, size, (align == STRING::CENTER) ? TCenter : TLeft);
		}
	}
	class Label : LocatedControl {
		string Text;
		STRING::Size Size;
		STRING::Alignment Alignment;
		Label(int x, int y, const string &in text, STRING::Size size = STRING::MEDIUM, STRING::Alignment align = STRING::CENTER) {
			super(x,y);
			Text = text;
			Size = size;
			Alignment = align;
		}
		void Draw(jjCANVAS@ screen) const override {
			DrawString(screen, Text, Size, Alignment);
		}
	}
	funcdef void ButtonPress(Button&);
	class Button : LocatedControl {
		ButtonPress@ OnPress;
		string DefaultColor;
		string Text;
		STRING::Size Size;
		STRING::Alignment Alignment;
		bool Hover;
		Control@ Tag;
		Button(int x, int y, const string &in defaultColor, const string &in text, ButtonPress@ onPress, STRING::Size size = STRING::MEDIUM, Control@ tag = null, STRING::Alignment align = STRING::CENTER) {
			super(x,y);
			DefaultColor = defaultColor;
			Text = text;
			@OnPress = @onPress;
			Size = size;
			Alignment = align;
			Top = y - 15;
			Bottom = y + ((size == STRING::MEDIUM) ? 20 : 10);
			if (align == STRING::CENTER) {
				const int width = jjGetStringWidth(text, size, STRING::NORMAL) / 2 + 10;
				Left = x - width;
				Right = x + width;
			} else {
				const int width = jjGetStringWidth(text, size, STRING::NORMAL);
				Left = x - 10;
				Right = x + width + 10;
			}
			@Tag = @tag;
		}
		void Do() override {
			Hover = jjMouseX >= Left && jjMouseX < Right && jjMouseY >= Top && jjMouseY < Bottom;
			if (Hover && Click) {
				Click = false;
				jjSamplePriority(SOUND::Sample(SOUND::MENUSOUNDS_SELECT0 + (jjRandom() % 7)));
				OnPress(this);
			}
		}
		void Draw(jjCANVAS@ screen) const override {
			if (!Hover || Alignment == STRING::LEFT)
				DrawString(screen, (Hover ? Orange : DefaultColor) + Text, Size, Alignment);
			else
				screen.drawString(XPos, YPos, "#" + Text, Size, SpinningString);
		}
	}
	FocusableControl@ FocusedControl;
	class FocusableControl : LocatedControl {
		FocusableControl(int x, int y) { super(x,y); }
		bool get_Focused() const { return FocusedControl is this; }
		bool Hover;
		bool GetHover() const { return false; }
		void Do() override {
			if (!Focused && GetHover() && Click)
				@FocusedControl = this;
		}
	}
	array<bool> NumberRelatedKeysPressed(10 + 1 + 1 + 2, false);
	enum NumericTextboxCameraFocus { None, Horizontal, Vertical };
	class NumericTextbox : FocusableControl {
		int Min, Max;
		int Value = 0;
		string ValueString;
		string Label;
		STRING::Size Size;
		NumericTextboxCameraFocus CameraFocus;
		NumericTextbox(int x, int y, int min, int max, NumericTextboxCameraFocus cameraFocus = DefinitionSequence::NumericTextboxCameraFocus::None, string label = "", STRING::Size size = STRING::MEDIUM) {
			super(x,y);
			Min = min;
			Max = max;
			if (Min > 0) Value = Min;
			else if (Max < 0) Value = Max;
			CameraFocus = cameraFocus;
			Label = label;
			Size = size;
			const int minWidth = jjGetStringWidth("" + Min, size, STRING::NORMAL);
			const int maxWidth = jjGetStringWidth("" + Max, size, STRING::NORMAL);
			const int width = (maxWidth > minWidth ? maxWidth : minWidth) / 2 + 10;
			Left = x - width;
			Top = y - ((size == STRING::MEDIUM) ? 15 : 7);
			Right = x + width;
			Bottom = y + ((size == STRING::MEDIUM) ? 20 : 10);
		}
		void Draw(jjCANVAS@ screen) const override {
			screen.drawRectangle(Left, Top, Right-Left, Bottom-Top, !Focused ? 15 : 32, SPRITE::BLEND_NORMAL, 127);
			string displayString = ValueString;
			if (Focused && jjGameTicks & 15 < 8) displayString += "_";
			DrawString(screen, displayString, Size, STRING::LEFT, x: Left-XPos);
			if (Label.length > 0)
				DrawString(screen, Label, STRING::SMALL, y: 30);
		}
		bool GetHover() const { return jjMouseX >= Left && jjMouseX < Right && jjMouseY >= Top && jjMouseY < Bottom; }
		void Do() override {
			if (Focused) {
				const int oldValue = Value;
				
				for (uint i = 0; i < 10; ++i) {
					const bool numberKeyPressed = jjKey[0x30 + i]; //0 through 9
					if (numberKeyPressed && !NumberRelatedKeysPressed[i]) {
						Value *= 10;
						if (ValueString.length > 0 && ValueString[0] == '-'[0]) Value -= i;
						else Value += i;
						if (Value > Max) Value = Max;
						else if (Value < Min) Value = Min;
						ValueString = "" + Value;
					}
					NumberRelatedKeysPressed[i] = numberKeyPressed;
				}
				
				if (Value == 0 && Min < 0) {
					const bool minusKeyPressed = jjKey[0x2D];
					if (minusKeyPressed && !NumberRelatedKeysPressed[10])
						ValueString = "-";
					NumberRelatedKeysPressed[10] = minusKeyPressed;
				} else NumberRelatedKeysPressed[10] = false;
				
				if (ValueString.length > 0) {
					const bool backspaceKeyPressed = jjKey[0x8];
					if (backspaceKeyPressed && !NumberRelatedKeysPressed[11]) {
						Value /= 10;
						if (Value > Max) Value = Max;
						else if (Value < Min) Value = Min;
						ValueString = (Value != 0) ? ("" + Value) : "";
					}
					NumberRelatedKeysPressed[11] = backspaceKeyPressed;
				} else NumberRelatedKeysPressed[11] = false;
				
				if (Value < Max) {
					const bool upKeyPressed = jjKey[0x26];
					if (upKeyPressed && !NumberRelatedKeysPressed[12])
						ValueString = (++Value) + "";
					NumberRelatedKeysPressed[12] = upKeyPressed;
				} else NumberRelatedKeysPressed[12] = false;
				
				if (Value > Min) {
					const bool upKeyPressed = jjKey[0x28];
					if (upKeyPressed && !NumberRelatedKeysPressed[13])
						ValueString = (--Value) + "";
					NumberRelatedKeysPressed[13] = upKeyPressed;
				} else NumberRelatedKeysPressed[13] = false;
				
				if (Value != oldValue) {
					AdjustCamera();
					jjSamplePriority(SOUND::MENUSOUNDS_TYPE);
				}
			} else {
				FocusableControl::Do();
				if (Focused) { //got focus just now
					for (uint i = 0; i < NumberRelatedKeysPressed.length; ++i)
						NumberRelatedKeysPressed[i] = false;
					AdjustCamera();
				}
			}
		}
		void AdjustCamera() const {
			if (CameraFocus == NumericTextboxCameraFocus::None)
				return;
			else for (int i = 0; i < jjLocalPlayerCount; ++i) {
				jjPLAYER@ play = jjLocalPlayers[i];
				if (CameraFocus == NumericTextboxCameraFocus::Horizontal)
					play.cameraFreeze(false, Value*32, true, false);
				else
					play.cameraFreeze(Value*32, false, true, false);
			}
		}
	}
	
	class Container : Control {
		array<Control@> Controls;
		Container(){}
		Container(array<Control@> controls) { Controls = controls; }
		void Do() {
			for (uint i = 0; i < Controls.length; ++i)
				Controls[i].Do();
		}
		void Draw(jjCANVAS@ screen) const {
			for (uint i = 0; i < Controls.length; ++i)
				Controls[i].Draw(screen);
		}
	}
	class YesNoPage : Container {
		YesNoPage(const string &in title, ButtonPress@ onYes, ButtonPress@ onNo) {
			Controls = array<Control@> = {
				Label(320, 100, title),
				Button(200, 300, Green, "Yes", onYes),
				Button(400, 300, Red, "No", onNo)
			};
		}
	}
	
	class RectanglePage : Container {
		array<uint16>@ Rectangle;
		LevelPropertyEnum Bit;
		bool initializedBoxes = false;
		RectanglePage(const string &in title, array<uint16>@ rectangle, LevelPropertyEnum bit) {
			Bit = bit;
			@Rectangle = @rectangle;
			Controls = array<Control@> = {
				Label(320, 60, Orange + title),
				Button(320, 380, Brown, "Continue", function(b){
					RectanglePage@ page = cast<RectanglePage@>(CurrentPage);
					
					bool setBit = false;
					for (uint i = 0; i < 4; ++i)
						if ((page.Rectangle[i] = cast<NumericTextbox@>(page.Controls[2 + i]).Value) != 0)
							setBit = true;
					if (setBit) {
						if (page.Rectangle[0] >= page.Rectangle[2] || page.Rectangle[1] >= page.Rectangle[3]) { //invalid dimensions
							//jjSamplePriority(SOUND::COMMON_HORN1);
							return;
						}
						LevelSetProperty(page.Bit);
					}
					
					Next();
				}),
				NumericTextbox(200, 200, 0, jjLayerWidth[4], NumericTextboxCameraFocus::Vertical, "Left"),
				NumericTextbox(320, 100, 0, jjLayerHeight[4], NumericTextboxCameraFocus::Horizontal, "Top"),
				NumericTextbox(440, 200, 0, jjLayerWidth[4], NumericTextboxCameraFocus::Vertical, "Right"),
				NumericTextbox(320, 300, 0, jjLayerHeight[4], NumericTextboxCameraFocus::Horizontal, "Bottom")
			};
		}
		void Do() override {
			if (!initializedBoxes) {
				initializedBoxes = true;
				for (uint i = 0; i < 4; ++i) {
					const uint value = Rectangle[i];
					if (value != 0) {
						NumericTextbox@ box = cast<NumericTextbox@>(Controls[i+2]);
						box.Value = value;
						box.ValueString = "" + value;
					}
				}
			}
			const int rectLeft = cast<NumericTextbox@>(Controls[2]).Value * 32;
			const int rectTop = cast<NumericTextbox@>(Controls[3]).Value * 32;
			const int rectRight = cast<NumericTextbox@>(Controls[4]).Value * 32;
			const int rectBottom = cast<NumericTextbox@>(Controls[5]).Value * 32;
			const uint8 color = rectLeft < rectRight && rectTop < rectBottom ? 16 : 24;
			if (color == 16 || (jjGameTicks & 7) < 3) {
				jjDrawRectangle(rectLeft, rectTop, 2, rectBottom - rectTop, color, SPRITE::NORMAL,0, -99);
				jjDrawRectangle(rectLeft, rectTop, rectRight - rectLeft, 2, color, SPRITE::NORMAL,0, -99);
				jjDrawRectangle(rectRight, rectTop, 2, rectBottom - rectTop, color, SPRITE::NORMAL,0, -99);
				jjDrawRectangle(rectLeft, rectBottom, rectRight - rectLeft + 2, 2, color, SPRITE::NORMAL,0, -99);
			}
			Container::Do();
		}
	}
	
	funcdef void ListItemGenerator(array<Control@>@);
	class ListPage : Container {
		uint8 ParameterLength;
		array<uint8> EventIDs;
		ListPage(const string &in title, uint min, uint max, ListItemGenerator@ generator, ButtonPress@ onContinue, STRING::Size size = STRING::MEDIUM, array<uint8> eventStuff = array<uint8>(0)) {
			Controls = array<Control@> = {
				Label(320, 70, Orange + title),
				List(240, 108, min, max, generator, size),
				Button(320, 400, Brown, "Continue", onContinue),
				Button(240, 0, Green, "+", function(b) {
					ListPage@ page = cast<ListPage@>(CurrentPage);
					cast<List@>(page.Controls[1]).Add();
					page.RepositionPlusButton();
				}, size)
			};
			RepositionPlusButton();
			if (eventStuff.length >= 2) {
				ParameterLength = eventStuff[0];
				eventStuff.removeAt(0);
				EventIDs = eventStuff;
			}
		}
		void RepositionPlusButton() {
			Button@ plusButton = cast<Button@>(Controls[3]);
			const List@ list = cast<List@>(Controls[1]);
			const uint itemCount = list.Items.length;
			const int yDiff = 108 + itemCount * list.ySep - plusButton.YPos;
			plusButton.YPos += yDiff;
			plusButton.Top += yDiff;
			plusButton.Bottom += yDiff;
			plusButton.Text = itemCount < list.Max ? "+" : "";
		}
		void Do() override {
			if (EventIDs.length != 0) {
				int xTile = int(jjLocalPlayers[0].cameraX) / 32;
				int yTile = int(jjLocalPlayers[0].cameraY) / 32;
				const int xTileMax = xTile + (jjSubscreenWidth + 63) / 32;
				const int yTileMax = yTile + (jjSubscreenHeight + 63) / 32;
				for (; xTile < xTileMax; ++xTile)
					for (int y = yTile; y < yTileMax; ++y)
						if (EventIDs.find(jjEventGet(xTile, y)) >= 0)
							jjDrawString(xTile * 32 + 16, y * 32 + 16, Blue + jjParameterGet(xTile, y, 0, ParameterLength), STRING::SMALL, TCenter, 0, SPRITE::PALSHIFT, 0, -99);
				
			}
			Container::Do();
		}
	}
	funcdef void ActionOnListItem(Container@, uint);
	class List : LocatedControl {
		uint Min, Max;
		array<Container@> Items;
		ListItemGenerator@ Generator;
		STRING::Size Size;
		uint ySep;
		bool TargetPositionList;
		List(int x, int y, uint min, uint max, ListItemGenerator@ generator, STRING::Size size) {
			super(x, y);
			@Generator = @generator;
			Min = min; Max = max;
			Size = size;
			ySep = (size == STRING::MEDIUM) ? 36 : 22;
			for (uint i = 0; i < Min; ++i)
				Add();
			TargetPositionList = Items.length > 0 && Items[0].Controls.length == 3 && cast<NumericTextbox@>(Items[0].Controls[1]) !is null;
		}
		void Add(int i = -1) {
			if (Items.length < Max) {
				if (i < 0) i = Items.length;
				Container listItem;
				if (uint(i) < Min)
					listItem.Controls.insertLast(Control()); //dummy
				else
					listItem.Controls.insertLast(Button(
						0, 0,
						Red, "-",
						function(b) {
							List@ list = cast<List@>(CurrentPage.Controls[1]);
							uint i = list.Items.findByRef(cast<Container@>(b.Tag));
							list.Items.removeAt(i);
							for (; i < list.Items.length; ++i)
								list.Reposition(i);
							cast<ListPage@>(CurrentPage).RepositionPlusButton();
						},
						Size,
						listItem
					));
				Generator(listItem.Controls);
				array<Control@> itemControls = listItem.Controls;
				for (uint j = 0; j < itemControls.length; ++j) {
					Control@ other = itemControls[j];
					LocatedControl@ otherLocated;
					if ((@otherLocated = cast<LocatedControl@>(other)) !is null) {
						otherLocated.XPos += XPos;
						otherLocated.Left += XPos;
						otherLocated.Right += XPos;
					}
				}
				Items.insertAt(i, @listItem);
				Reposition(i);
			}
		}
		void Reposition(uint i) const {
			array<Control@> itemControls = Items[i].Controls;
			for (uint j = 0; j < itemControls.length; ++j) {
				Control@ other = itemControls[j];
				LocatedControl@ otherLocated;
				if ((@otherLocated = cast<LocatedControl@>(other)) !is null) {
					const int yDiff = YPos + i * ySep - otherLocated.YPos;
					otherLocated.YPos += yDiff;
					otherLocated.Top += yDiff;
					otherLocated.Bottom += yDiff;
				}
			}
		}
		void Do() {
			for (uint i = 0; i < Items.length; ++i)
				Items[i].Do();
			if (TargetPositionList) {
				for (uint i = 0; i < Items.length; ++i) {
					const uint x = cast<NumericTextbox@>(Items[i].Controls[1]).Value * 32;
					const uint y = cast<NumericTextbox@>(Items[i].Controls[2]).Value * 32;
					jjDrawSprite(x+16,y+16, ANIM::SPAZ, RABBIT::TELEPORTFALL, 0, 1, (x+y) != 0 ? SPRITE::NORMAL : SPRITE::TRANSLUCENT, 0, -99);
				}
				
			}
		}
		void Draw(jjCANVAS@ screen) const {
			for (uint i = 0; i < Items.length; ++i)
				Items[i].Draw(screen);
		}
		void ForEach(ActionOnListItem@ action) {
			for (uint i = 0; i < Items.length; ++i)
				action(Items[i], i);
		}
	}
	class TriggerIDPage : ListPage {
		LevelPropertyEnum Bit;
		array<uint8>@ TriggerIDs;
		TriggerIDPage(const string &in timing, LevelPropertyEnum bit, array<uint8>@ ids) {
			super(
				"Trigger IDs to enable at " + timing,
				0,8,
				function(c) { c.insertLast(NumericTextbox(60,0,0,31)); },
				function(b) {
					TriggerIDPage@ page = cast<TriggerIDPage@>(CurrentPage);
					List@ list = cast<List@>(page.Controls[1]);
					if (list.Items.length >= 1) {
						LevelSetProperty(page.Bit);
						list.ForEach(function(li, i) {
							cast<TriggerIDPage@>(CurrentPage).TriggerIDs.insertLast(cast<NumericTextbox@>(li.Controls[1]).Value);
						});
					}
					Next();
				},
				eventStuff: array<uint8> = {5, AREA::TRIGGERZONE, OBJECT::TRIGGERCRATE, OBJECT::TRIGGERSCENERY}
			);
			Bit = bit;
			@TriggerIDs = @ids;
		}
	}
	class AmmoCheckbox : LocatedControl {
		bool Hover;
		const jjANIMATION@ NormalAnim, PowerupAnim;
		bool Checked = false;
		AmmoCheckbox(int x, int y, uint a1, uint a2) {
			super(x,y);
			@NormalAnim = jjAnimations[a1];
			@PowerupAnim = jjAnimations[a2];
			Left = x - 10;
			Top = y - 10;
			Right = x + 10;
			Bottom = y + 10;
		}
		void Do() override {
			Hover = jjMouseX >= Left && jjMouseX < Right && jjMouseY >= Top && jjMouseY < Bottom;
			if (Hover && Click) {
				jjSamplePriority(SOUND::Sample(SOUND::MENUSOUNDS_SELECT0 + (jjRandom() % 7)));
				Checked = !Checked;
			}
		}
		void Draw(jjCANVAS@ screen) const override {
			const jjANIMATION@ anim = Checked ? PowerupAnim : NormalAnim;
			int y = YPos;
			if (Hover) y += int(jjSin(jjGameTicks << 4) * 4);
			screen.drawSpriteFromCurFrame(XPos, y, anim + ((jjGameTicks/5)%anim.frameCount));
		}
	}
	
	array<Container@> Pages;
	array<string> PageInstructions;
	uint PageID = 0;
	Container@ CurrentPage;
	bool Click, Clicked;
	void Do() {
		Click = jjKey[1] && !Clicked;
		Clicked = jjKey[1];
		CurrentPage.Do();
	}
	void Draw(jjCANVAS@ screen) {
		screen.drawString(0,35, Blue + "gfv.mut Setup Mode");
		screen.drawString(0,52, Blue + "Page " + (PageID+1) + " of " + Pages.length);
		screen.drawRectangle(0,0, jjSubscreenWidth, jjSubscreenHeight, 0, SPRITE::SHADOW);
		CurrentPage.Draw(screen);
	}
	
	array<array<int>> AmmoIcons = {
		{-29,18},
		{25,24},
		{29,28},
		{34,33},
		{49,48},
		{57,56},
		{59,59},
		{62,61},
		{68,67}
	};
	void RadioButtonClick(Button& b) {
		for (uint i = 2; i < CurrentPage.Controls.length; ++i)
			cast<Button@>(CurrentPage.Controls[i]).DefaultColor = Brown;
		b.DefaultColor = Blue;
	}
	void Start() {
		jjSampleLoad(SOUND::COMMON_HORN1, "HH17_Null.wav");
		
		TLeft.align = STRING::LEFT;
		TCenter.align = STRING::CENTER;
		TLeft.newline = TCenter.newline = STRING::SPECIALSIGN;
	
		DefiningLevelProperties = true;
		Pages = array<Container@> = {
			YesNoPage(
				"This file wrote no .gf\nBegin writing one?",
				function(b){ Next(); },
				function(b){ DefiningLevelProperties = false; }
			),
			RectanglePage(
				"Define the boundaries of the Waiting Area",
				WaitingAreaBoundaries,
				LevelPropertyEnum::DefiniteWaitingAreaBoundaries
			),
			YesNoPage(
				"Is there a failure area with distinct\nboundaries from the waiting area?",
				function(b){ @CurrentPage = RectanglePage(
					"Define the boundaries of the Failure Area",
					FailureAreaBoundaries,
					LevelPropertyEnum::DefiniteFailureAreaBoundariesDistinctFromWaitingArea
				); },
				function(b){ Next(); }
			),
			RectanglePage(
				"Define the boundaries of the Arena",
				ArenaBoundaries,
				LevelPropertyEnum::DefiniteArenaBoundaries
			),
			YesNoPage(
				"Do players enter the arena by\none or more Warp Target events?",
				function(b){ @CurrentPage = ListPage(
					"List the Warp IDs used to enter the arena:",
					1,8,
					function(c) { c.insertLast(NumericTextbox(60,0,0,255)); },
					function(b) {
						LevelSetProperty(LevelPropertyEnum::OneOrMoreStartWarpIDs);
						cast<List@>(cast<ListPage@>(CurrentPage).Controls[1]).ForEach(function(li, i) {
							StartWarpIDs.insertLast(cast<NumericTextbox@>(li.Controls[1]).Value);
						});
						Next();
					},
					eventStuff: array<uint8> = {8, AREA::WARP, AREA::WARPTARGET}
				); },
				function(b){ @CurrentPage = ListPage(
					"List entrance points for the arena:",
					1,8,
					function(c) {
						c.insertLast(NumericTextbox(60,0,0,jjLayerWidth[4], NumericTextboxCameraFocus::Vertical));
						c.insertLast(NumericTextbox(130,0,0,jjLayerHeight[4], NumericTextboxCameraFocus::Horizontal));
					},
					function(b) {
						cast<List@>(cast<ListPage@>(CurrentPage).Controls[1]).ForEach(function(li, i) {
							for (uint j = 1; j <= 2; ++j)
								StartWarpIDs.insertLast(cast<NumericTextbox@>(li.Controls[j]).Value);
						});
						Next();
					}
				); }
			),
			TriggerIDPage("LEVEL start:", LevelPropertyEnum::TriggerIDsToTurnOnAtLevelStart, LevelTriggerIDs),
			TriggerIDPage("GAME start:", LevelPropertyEnum::TriggerIDsToTurnOnAtGameStart, GameTriggerIDs),
			TriggerIDPage("SUDDEN DEATH:", LevelPropertyEnum::TriggerIDsForSuddenDeath, SuddenDeathTriggerIDs),
			Container(array<Control@> = {
				Label(320, 140, Orange + "Set Water Level at game start?"),
				NumericTextbox(320,200,0,jjLayerHeight[4], NumericTextboxCameraFocus::Horizontal),
				Button(320, 260, Brown, "Continue", function(b) {
					WaterLevel = cast<NumericTextbox@>(CurrentPage.Controls[1]).Value;
					if (WaterLevel != 0)
						LevelSetProperty(LevelPropertyEnum::SetWaterLevelAtGameStart);
					Next();
				})
			}),
			Container(array<Control@> = {
				Label(320, 140, Orange + "Set Ambient Lighting at game start?"),
				NumericTextbox(320,200,0,255),
				Button(320, 260, Brown, "Continue", function(b) {
					StartLighting = cast<NumericTextbox@>(CurrentPage.Controls[1]).Value;
					if (StartLighting != 0)
						LevelSetProperty(LevelPropertyEnum::SetLightAtGameStart);
					Next();
				})
			}),
			ListPage(
				"Grant players ammo at game start?",
				10,10,
				function(c) {
					if (AmmoIcons.length != 0) {
						array<int> ammoIcons = DefinitionSequence::AmmoIcons[0];
						DefinitionSequence::AmmoIcons.removeAt(0);
						for (uint i = 0; i < 2; ++i)
							if (ammoIcons[i] >= 0) ammoIcons[i] += jjAnimSets[ANIM::AMMO];
							else ammoIcons[i] = jjAnimSets[ANIM::PICKUPS] - ammoIcons[i];
						c.insertLast(AmmoCheckbox(0,0,ammoIcons[0], ammoIcons[1]));
						c.insertLast(NumericTextbox(30,0,0,50, size: STRING::SMALL));
					} else {
						c.insertLast(NumericTextbox(30,0,0,9, size: STRING::SMALL));
						c.insertLast(Label(50,10, "random powerups", STRING::SMALL, STRING::LEFT));
					}
				},
				function(b) {
					cast<List@>(cast<ListPage@>(CurrentPage).Controls[1]).ForEach(function(li, i) {
						if (i < 9) {
							const bool powerup = cast<AmmoCheckbox@>(li.Controls[1]).Checked;
							const uint ammo = cast<NumericTextbox@>(li.Controls[2]).Value;
							if (powerup || ammo != 0) {
								LevelSetProperty(LevelPropertyEnum::GiveAmmoAtGameStart);
								if (StartAmmo.length == 0) StartAmmo.resize(9);
								StartAmmo[i] = ammo | (powerup ? 0x8000 : 0);
							}
						} else {
							RandomPowerups = cast<NumericTextbox@>(li.Controls[1]).Value;
							if (RandomPowerups != 0)
								LevelSetProperty(LevelPropertyEnum::GiveRandomizedPowerupsAtGameStart);
						}
					});
					Next();
				},
				STRING::SMALL
			),
			Container(array<Control@> = {
				Label(320, 100, Orange + "Show players text on game start?"),
				Button(320, 420, Brown, "Continue", function(b) {
					for (uint i = 2; i < CurrentPage.Controls.length; ++i) {
						const Button@ option = cast<Button@>(CurrentPage.Controls[i]);
						if (option.DefaultColor == Blue) {
							if (option.Text[0] >= '0'[0]) { //not "(none)"
								LevelSetProperty(LevelPropertyEnum::ShowTextStringAtGameStart);
								StartTextString = parseUInt(option.Text.substr(0,2));
							}
							break;
						}
					}
					Next();
				})
			}),
			Container(array<Control@> = {
				Label(320, 100, Orange + "Morph all players?"),
				Button(320, 420, Brown, "Continue", function(b) {
					for (uint i = 0; i < 9; ++i) {
						const Button@ option = cast<Button@>(CurrentPage.Controls[i + 2]);
						if (option.DefaultColor == Blue) {
							if (i != 0)
								LevelSetProperty(LevelPropertyEnum::MorphPlayerOnGameStart);
							MorphPlayerType = i;
							break;
						}
					}
					Next();
				}),
				Button(150, 130, Blue, "no", RadioButtonClick, STRING::SMALL, null, STRING::LEFT),
				Button(150, 150, Brown, "Jazz (double jump)", RadioButtonClick, STRING::SMALL, null, STRING::LEFT),
				Button(150, 170, Brown, "Spaz", RadioButtonClick, STRING::SMALL, null, STRING::LEFT),
				Button(150, 190, Brown, "Frog", RadioButtonClick, STRING::SMALL, null, STRING::LEFT),
				Button(150, 210, Brown, "Bird (ramming)", RadioButtonClick, STRING::SMALL, null, STRING::LEFT),
				Button(150, 230, Brown, "Bird (shooting)", RadioButtonClick, STRING::SMALL, null, STRING::LEFT),
				Button(150, 250, Brown, "Airboard", RadioButtonClick, STRING::SMALL, null, STRING::LEFT),
				Button(150, 270, Brown, "Fly Carrot", RadioButtonClick, STRING::SMALL, null, STRING::LEFT),
				Button(150, 290, Brown, "Lori (Jazz in 1.23)", RadioButtonClick, STRING::SMALL, null, STRING::LEFT),
				Button(150, 310, Brown, "Lori (Spaz in 1.23)", RadioButtonClick, STRING::SMALL, null, STRING::LEFT),
				Button(150, 330, Brown, "Jazz (copter ears)", RadioButtonClick, STRING::SMALL, null, STRING::LEFT),
				Button(150, 350, Brown, "no morph, but normal airjumps", RadioButtonClick, STRING::SMALL, null, STRING::LEFT)
			})
		};
		
		array<string> helpStrings = {"(none)"};
		for (uint i = 0; i < 16; ++i)
			if (jjHelpStrings[i].length != 0)
				helpStrings.insertLast(formatInt(i, '0', 2) + ": " + jjRegexReplace(jjHelpStrings[i], '@', ''));
		for (uint i = 0; i < helpStrings.length; ++i)
			Pages[Pages.length-2].Controls.insertLast(Button(150, 150 + i*20, (i == 0) ? Blue : Brown, helpStrings[i], RadioButtonClick, STRING::SMALL, null, STRING::LEFT));
		
		
		PageInstructions = array<string> = {
"""While players are in the waiting area, they will
watch a preview of the arena but not be able to move.
The level should not allow players to leave the
waiting area on their own without pressing any keys,
or else they may think the game has started prematurely.

To define the waiting area, select the four numeric
input boxes with the mouse and type in coordinates.
A green rectangle will be displayed on screen if the
coordinates are valid; otherwise, a flashing red one.
""",
"""The failure area is where players go after they
exit the arena, usually via a warp or sucker tube.
In this mutator, being in the failure area after the
game has started results in dying and losing a life.
Therefore this area must be defined or no one will win.

A common practice is for the waiting area and
failure area to be in the same part of the level...
for instance, in the Survivor pack, both are at the
very top of the level. In those cases, you can save
some time and reuse the waiting area's dimensions
by selecting No at the initial prompt.
""",
"""Defining the arena's boundaries is the least
important, because these numbers are only used for
displaying a preview of the arena before the game
has started. Unlike the waiting/failure area
boundaries, this has no gameplay effect.

The mutator will GUESS at the arena's boundaries
by searching the level for Destruct Scenery events.
These numbers may or may not be accurate for this
particular level, so you should double check them.
""",
"""Every level must define some way to ENTER
the arena. Usually this is done using Warp events
with one or more Warp IDs (clicking Yes). Most
Ground Force levels use only a single Warp ID to
enter, but some, e.g. Survivor02, use multiple IDs
as an additional way to randomize where in
the arena players start.

You can also (by clicking No) write your own list
of up to 8 different locations that players will
warp to, even if there are no Warp Target events
on those tiles. If the level usually has players
enter the arena using sucker tubes instead of
warps, for instance, you'll need this option.
""",
"""Provide a list of any Trigger IDs that should
be turned on IMMEDIATELY, before players even
enter the arena but while they are looking at a
preview of it. This is rarely needed... one example
is in Survivor13 (Safety Precaution), where the
trigger scenery blocks under the red springs are
never supposed to be visible.
""",
"""List any Trigger IDs that should be turned on
WHEN PLAYERS ENTER, e.g. to set up a timer for
something to happen later.
""",
"""Sudden Death triggers are turned on either
when the game enters Overtime or when there are
only two players left, after a brief delay,
and are intended to hasten the end of the game.
If the level usually has somebody hitting trigger
crates in a control room to affect the level, you
could use those Trigger IDs here instead.

If multiple Trigger IDs are included here, they
will be turned on in order from top to bottom,
spaced by four second delays.
""",
"""If there are any Set Water Level events that
players normally touch while entering the arena,
which this mutator prevents them from touching,
here is a chance to ensure that the water level is
set to that height anyway. If this box appears
blank, then the level contains no such events
and you can probably skip this page.
""",
"""Likewise, this page lets you use different
lighting for the arena than for the waiting area.
If there are no Set Light events in the level,
this box will be left blank initially.
""",
"""Some levels may choose to have players start
off with some amount of ammo, often using
elaborate systems of sucker tubes. Write any
non-zero ammo amounts you want players to have.

By clicking on the ammo pickup icons, you can
toggle each weapon from regular to powerup.

The "random powerups" box will draw only from
among those powerups that are found in the level.
Despite the name, it may also grant TNT if there
are TNT ammo pickups in the level, or fastfire.
""",
"""If any text but "(none)" is selected, players
will be shown that text upon warping into the
arena. This is a good last chance to warn them about
special areas that can kill them.
""",
"""By default, people may play as any rabbit
they choose (Jazz, Spaz, or sometimes Lori), but
will use the double jump ability instead of the
copter ears ability, because copter ears tend to
be overpowered in ground force levels. Select a
different option if you have some other/more specific
plan for how people should play this level.
"""
		};
		
		
		//guess arena boundaries
		ArenaBoundaries[0] = ArenaBoundaries[1] = 9999;
		for (uint xTile = jjLayerWidth[4] - 1; int(xTile) >= 0; --xTile)
			for (uint yTile = jjLayerHeight[4] - 1; int(yTile) >= 0; --yTile) {
				const int eventID = jjEventGet(xTile, yTile);
				if (eventID == OBJECT::DESTRUCTSCENERY) {
					if (xTile < ArenaBoundaries[0])
						ArenaBoundaries[0] = xTile;
					if (xTile > ArenaBoundaries[2])
						ArenaBoundaries[2] = xTile;
					if (yTile < ArenaBoundaries[1])
						ArenaBoundaries[1] = yTile;
					if (yTile > ArenaBoundaries[3])
						ArenaBoundaries[3] = yTile;
				}
				else if (eventID == AREA::WATERLEVEL) {
					auto@ box = cast<NumericTextbox@>(Pages[8].Controls[1]);
					box.Value = jjParameterGet(xTile,yTile, 0, 8);
					box.ValueString = "" + box.Value;
				}
				else if (eventID == AREA::SETLIGHT) {
					auto@ box = cast<NumericTextbox@>(Pages[9].Controls[1]);
					box.Value = jjParameterGet(xTile,yTile, 0, 7);
					box.ValueString = "" + box.Value;
				}
			}
		for (int i = 0; i < 4; ++i) //extend sides slightly
			if (i > 1)
				ArenaBoundaries[i] += 2;
			else if (ArenaBoundaries[i] > 9000)
				ArenaBoundaries[i] = 0;
			else
				ArenaBoundaries[i] -= 1;
			
		@CurrentPage = @Pages[0];
	}
	void Next() {
		@FocusedControl = null;
		for (int i = 0; i < jjLocalPlayerCount; ++i)
			jjLocalPlayers[i].cameraUnfreeze(false);
		if (++PageID < Pages.length) {
			@CurrentPage = @Pages[PageID];
			jjPrint("\n\n*** PAGE " + (PageID+1) + ": ***\n" + PageInstructions[0]);
			PageInstructions.removeAt(0);
		} else {
			DefiningLevelProperties = false;
			
			jjSTREAM output;
			output.push(LevelProperties);
			
			const array<array<uint16>@> AllBoundaries = {WaitingAreaBoundaries, FailureAreaBoundaries, ArenaBoundaries};
			for (uint i = 0; i < AllBoundaries.length; ++i)
				if (LevelHasProperty(LevelPropertyEnum(i)))
					for (int j = 0; j < 4; ++j)
						output.push(uint16(AllBoundaries[i][j] * 32));
			
			const array<array<uint8>@> AllIDs = {StartWarpIDs, LevelTriggerIDs, GameTriggerIDs};
			for (uint i = 0; i < AllIDs.length; ++i)
				if (i == 0 || LevelHasProperty(LevelPropertyEnum(i + LevelPropertyEnum::OneOrMoreStartWarpIDs))) {
					output.push(uint8(AllIDs[i].length));
					for (uint j = 0; j < AllIDs[i].length; ++j)
						output.push(uint8(AllIDs[i][j]));
				}
				
			if (LevelHasProperty(LevelPropertyEnum::SetWaterLevelAtGameStart))
				output.push(WaterLevel);
			if (LevelHasProperty(LevelPropertyEnum::GiveAmmoAtGameStart))
				for (uint i = 0; i < StartAmmo.length; ++i)
					output.push(StartAmmo[i]);
			if (LevelHasProperty(LevelPropertyEnum::ShowTextStringAtGameStart))
				output.push(StartTextString);
			if (LevelHasProperty(LevelPropertyEnum::SetLightAtGameStart))
				output.push(StartLighting);
			if (LevelHasProperty(LevelPropertyEnum::GiveRandomizedPowerupsAtGameStart))
				output.push(RandomPowerups);
			if (LevelHasProperty(LevelPropertyEnum::MorphPlayerOnGameStart))
				output.push(MorphPlayerType);
			if (LevelHasProperty(LevelPropertyEnum::TriggerIDsForSuddenDeath)) {
				output.push(uint8(SuddenDeathTriggerIDs.length));
				for (uint i = 0; i < SuddenDeathTriggerIDs.length; ++i)
					output.push(uint8(SuddenDeathTriggerIDs[i]));
			}
			
			output.save(jjLevelFileName.substr(0, jjLevelFileName.length-4) + ".gf.asdat");
			
			jjChat("/r");
		}
	}
}

/**
 * Minimal public interface support just so other mutators can detect this one's presence
 */
class gfvPublicClass : jjPUBLICINTERFACE { string getVersion() const { return "2.0"; } }
gfvPublicClass gfvPublicInstance;
gfvPublicClass@ onGetPublicInterface() { return gfvPublicInstance; }