Downloads containing TimeTrial.mut

Downloads
Name Author Game Mode Rating
JJ2+ Only: Time TrialFeatured Download PurpleJazz Mutator 10 Download file

File preview

#pragma name "Time Trial"

// Scripted by PurpleJazz with help from Sir Ementaler

/*******************************************************************/
//*MUTATOR DECLARATIONS*//

class checkpoint {
	int xTile, yTile;
	float xPos, yPos;
}

array<checkpoint> Checkpoints;

array<bool> StartedRace(32, false);
array<bool> TouchedCheckpoint(32, false);

array<uint64> Time(32, 0);
array<uint64> BestTime(32, 252000);

string formatTime(uint64 time) {
	return (time / (70 * 60)) + ':' + formatInt(time / 70 % 60, '0', 2) + ':' + formatInt(time * 100 / 7 % 1000, '0', 3);
}

uint8 goalColor = 240;
uint8 cpColor = 248;

int goalTextOffset = 24;
int cpTextOffset = 52;

string goalText = "START";
string cpText = "CHECKPOINT";

const float PI = 3.1415927f;
int CTFArrowTimer = 0;

bool playSample = false;
bool hasBases = false;

/*******************************************************************/
//*RECORD SAVING DECLARATIONS*/- Based on SE's Test Manager code/

string lowercase(string &in text) {
	for (int i = text.length() - 1; i >= 0; i--) {
		if (text[i] > 64 && text[i] < 91)
			text[i] ^= 32;
	}
	return text;
}

class record {
	string name;
	int time;
	bool opEquals(const record &in other) const {
		return lowercase(this.name) == lowercase(other.name);
	}
	int opCmp(const record &in other) const {
		return this.time - other.time;
	}
}

array<record> records;
string filename;

bool load(const string &in name) {
	jjSTREAM file(name);
	uint16 count;
	if (!file.pop(count))
		return false;
	records.resize(count);
	for (uint i = 0; i < count; i++) {
		record@ rec = records[i];
		if (!file.pop(rec.name) || !file.pop(rec.time)) {
			records.resize(0);
			return false;
		}
	}
	return true;
}

void save(const string &in name) {
	jjSTREAM file;
	file.push(uint16(records.length()));
	for (uint i = 0; i < records.length(); i++) {
		const record@ rec = records[i];
		file.push(rec.name);
		file.push(rec.time);
	}
	file.save(name);
}

/*******************************************************************/
//*MAIN SCRIPT FUNCTIONS*//

void onLevelLoad() {
	jjObjectPresets[OBJECT::CTFBASE].behavior = BEHAVIOR::INACTIVE;
	jjObjectPresets[OBJECT::RFBULLET].behavior = jjObjectPresets[OBJECT::RFBULLETPU].behavior = rf;
	
	for (int i = 1; i <= 9; i++) jjWeapons[i].infinite = true;
	
	jjANIMATION@ anim = jjAnimations[jjAnimSets[ANIM::FLAG] + 3];
	for (uint i = 0; i < anim.frameCount; i++) {
		jjANIMFRAME@ frame = jjAnimFrames[anim + i];
		jjPIXELMAP flag(frame);
		for (uint x = 0; x < flag.width; x++) {
			for (uint y = 0; y < flag.height; y++) {
				if (flag[x, y] == 22)
					flag[x, y] = 86;
				else if (flag[x, y] == 23)
					flag[x,y] = 87;
			}
		}
		flag.save(frame);
	}
	
	for (int x = 0; x < jjLayerWidth[4]; x++) {
		for (int y = 0; y < jjLayerHeight[4]; y++) {
			if (jjEventGet(x, y) == OBJECT::CTFBASE) {
				hasBases = true;
				checkpoint newCheckpoint;
				newCheckpoint.xTile = x;
				newCheckpoint.yTile = y;
				newCheckpoint.xPos = x * 32 + 16;
				newCheckpoint.yPos = y * 32 + 16;
				Checkpoints.insertLast(newCheckpoint);
			}
			if (jjEventGet(x,y) == OBJECT::RFAMMO3 || jjEventGet(x,y) == OBJECT::RFAMMO15) {
				jjWeapons[WEAPON::RF].allowed = true;
			}
			if (jjEventGet(x,y) == OBJECT::RFPOWERUP) {
				jjWeapons[WEAPON::RF].allowedPowerup = true;
			}
		}
	}
	
	if (jjIsServer) { //thanks to SE for this code
		uint32 sum1 = 0, sum2 = 0;
		for (int i = 0; i < jjLayerHeight[4]; i++) {
			for (int j = 0; j < jjLayerWidth[4]; j++) {
				sum1 += jjTileGet(4, j, i);
				sum2 += sum1;
			}
			sum1 %= 0xFFFF;
			sum2 %= 0xFFFF;
		}
		load(filename = "TimeTrial-" + formatInt(sum2 << 16 | sum1, '0H', 8) + ".asdat");
		
		record rec;
		rec.name = jjLocalPlayers[0].name;
		int id = records.find(rec);
		if (id >= 0) BestTime[jjLocalPlayers[0].playerID] = records[id].time;
	}
	else jjSendPacket(jjSTREAM());
}

[RFBULLET] void rf(jjOBJ@ obj) { //this is so your RF explosions won't affect other players
	if (obj.creatorType == CREATOR::PLAYER && !jjPlayers[obj.creatorID].isLocal) {
		obj.objType = HANDLING::EXPLOSION;
		if (obj.state == STATE::EXPLODE) jjDrawSprite(obj.xPos, obj.yPos, ANIM::AMMO, 3, obj.curFrame, obj.direction, SPRITE::TRANSLUCENT);
	}
	obj.behave(BEHAVIOR::RFBULLET, obj.state == STATE::EXPLODE && !jjPlayers[obj.creatorID].isLocal? false:true);
}

void onLevelBegin() {	
	if (jjIsServer) {
		if (jjGameMode != GAME::CTF || jjGameCustom != GAME::TB) jjChat("/tb");
		
		if (!jjEnabledTeams[TEAM::BLUE]) jjChat("/teams blue on");
		
		array<TEAM::Color> TeamsToDisable = {TEAM::RED, TEAM::YELLOW, TEAM::GREEN};
		array<string> TeamColors = {"red", "yellow", "green"};
		
		for (int i = 0; i < 3; i++) {
			if (jjEnabledTeams[TeamsToDisable[i]]) jjChat("/teams " + TeamColors[i] + " off");
		}
	}
	
	if (jjIsServer || jjIsAdmin) {
		if (jjGameState == GAME::STOPPED) jjAlert("|The game must be started in order to play this mode!");
	}
	
	jjAlert("||Type !help for a list of commands.");
}

bool onDrawHealth(jjPLAYER@ play, jjCANVAS@ canvas) {
	canvas.drawString(
	jjSubscreenWidth-796,
	jjSubscreenHeight-520,
	"||||" + formatTime(Time[play.playerID]),
	STRING::MEDIUM,
	STRING::NORMAL
	);
	
	if (BestTime[play.playerID] < 252000)
	canvas.drawString(
	jjSubscreenWidth-796,
	jjSubscreenHeight-496,
	"|PB: " + formatTime(BestTime[play.playerID]),
	STRING::SMALL,
	STRING::NORMAL
	);
	return false;
}

void onMain() {
	for (uint i = 0; i < Checkpoints.length(); i++) {
		checkpoint@ cp = Checkpoints[i];
		jjDrawSprite(cp.xPos + (i == 1? 16:-16), cp.yPos + 16, ANIM::FLAG, 3, jjGameTicks / 6 % 8, i == 1? -1:1, SPRITE::PALSHIFT, i == 1? cpColor : goalColor, 4, 4);
	}
	
	if (hasBases) {
		jjDrawString(Checkpoints[0].xPos - goalTextOffset, Checkpoints[0].yPos - 42, goalText, STRING::SMALL, STRING::NORMAL);
		jjDrawString(Checkpoints[1].xPos - cpTextOffset, Checkpoints[1].yPos - 42, cpText, STRING::SMALL, STRING::NORMAL);
	}
	
	if (CTFArrowTimer > jjGameTicks + 280) CTFArrowTimer = jjGameTicks;
		if (CTFArrowTimer < jjGameTicks && hasBases) {
			if (CTFArrowTimer + 64 >= jjGameTicks) {
				int angle_A = int(atan2(Checkpoints[0].yPos - jjLocalPlayers[0].yPos, Checkpoints[0].xPos - jjLocalPlayers[0].xPos) * (512 / PI));
				int angle_B = int(atan2(Checkpoints[1].yPos - jjLocalPlayers[0].yPos, Checkpoints[1].xPos - jjLocalPlayers[0].xPos) * (512 / PI));
				const float scale = 64.f / (112.f - jjSin((jjGameTicks - CTFArrowTimer) << 3) * 64.f);
				if (!StartedRace[jjLocalPlayers[0].playerID] || TouchedCheckpoint[jjLocalPlayers[0].playerID]) jjDrawRotatedSprite(jjLocalPlayers[0].xPos + 32 * jjCos(angle_A), jjLocalPlayers[0].yPos + 32 * jjSin(angle_A), ANIM::FLAG, 0, 0, 970 - angle_A, scale, scale, SPRITE::PALSHIFT, goalColor, 1);
				else if (StartedRace[jjLocalPlayers[0].playerID] || !TouchedCheckpoint[jjLocalPlayers[0].playerID]) jjDrawRotatedSprite(jjLocalPlayers[0].xPos + 32 * jjCos(angle_B), jjLocalPlayers[0].yPos + 32 * jjSin(angle_B), ANIM::FLAG, 0, 0, 970 - angle_B, scale, scale, SPRITE::PALSHIFT, cpColor, 1);
			} else {
			CTFArrowTimer = jjGameTicks + 210;
		}
	}
	
	if (jjIsServer) {
		for (int i = 0; i < 32; i++) {
			jjPLAYER@ play = jjPlayers[i];
			if (play.isActive) {
				for (uint j = 0; j < Checkpoints.length(); j++) {
					checkpoint@ cp = Checkpoints[j];
					if (int(play.xPos) / 32 == cp.xTile && int(play.yPos) / 32 == cp.yTile && jjGameState == GAME::STARTED) {
						if (j == 0) {
							if (!TouchedCheckpoint[play.playerID]) {
								Time[play.playerID] = 0;
								StartedRace[play.playerID] = true;
								stream(i);
							}
							else {
								if (Time[play.playerID] < BestTime[play.playerID] && TouchedCheckpoint[play.playerID]) {
									BestTime[play.playerID] = Time[play.playerID];
									
									record rec;
									rec.name = play.name;
									rec.time = BestTime[play.playerID];
									int id = records.find(rec);
									if (id < 0) records.insertLast(rec);
									else records[id] = rec;
									records.sortAsc();
									save(filename);
									
									jjAlert("|||||||" + play.name + " ||||set a new personal best of||||| " + formatTime(BestTime[play.playerID]), true);
									
									if (play.isLocal) {
										switch (play.charCurr) {
											case CHAR::JAZZ: jjSamplePriority(SOUND::JAZZSOUNDS_JUMMY); break;
											case CHAR::SPAZ: jjSamplePriority(SOUND::SPAZSOUNDS_HAPPY); break;
											case CHAR::LORI: jjSamplePriority(SOUND::LORISOUNDS_WEHOO); break;
										}
									}
								}
								
								string newrecord = Time[play.playerID] <= BestTime[play.playerID]? "New record! " : "";
								if (play.isLocal) {
									jjSamplePriority(SOUND::COMMON_BELL_FIRE);
									jjAlert("|||" + newrecord + "You finished in |||||" + formatTime(Time[play.playerID]), false, STRING::MEDIUM);
								}
								
								StartedRace[play.playerID] = false;
								TouchedCheckpoint[play.playerID] = false;
								stream(i);
							}
						}
						if (j == 1 && StartedRace[play.playerID] && !TouchedCheckpoint[play.playerID]) {
							TouchedCheckpoint[play.playerID] = true;
							stream(i);
						}
					}
					if (StartedRace[play.playerID] && j != 0 && jjGameState == GAME::STARTED) Time[play.playerID]++;
					
					else if (jjGameState == GAME::STOPPED) {
						Time[play.playerID] = 0;
						StartedRace[play.playerID] = false;
						TouchedCheckpoint[play.playerID] = false;
					}
					
					if (play.invincibility < 0) {
						StartedRace[play.playerID] = false;
						TouchedCheckpoint[play.playerID] = false;
						Time[play.playerID] = 0;
						stream(i);
					}
				}
			}
		}
		if (TouchedCheckpoint[jjLocalPlayers[0].playerID]) {
			goalColor = 8;
			cpColor = 0;
			goalTextOffset = 16;
			cpTextOffset = 32;
			goalText = "GOAL";
			cpText = "RETURN";		
		}
		else {
			goalColor = 240;
			cpColor = 248;
			goalText = "START";
			cpText = "CHECKPOINT";
			cpTextOffset = 52;
			goalTextOffset = 24;
		}
	}
	else {
		if (StartedRace[jjLocalPlayers[0].playerID] && jjGameState == GAME::STARTED) Time[jjLocalPlayers[0].playerID]++;
		
		else if (jjGameState == GAME::STOPPED) {
			Time[jjLocalPlayers[0].playerID] = 0;
			StartedRace[jjLocalPlayers[0].playerID] = false;
			TouchedCheckpoint[jjLocalPlayers[0].playerID] = false;
		}
		
		if (jjLocalPlayers[0].invincibility < 0) {
			Time[jjLocalPlayers[0].playerID] = 0;
			StartedRace[jjLocalPlayers[0].playerID] = false;
			TouchedCheckpoint[jjLocalPlayers[0].playerID] = false;
		}
		
		if (playSample) {
			jjSamplePriority(SOUND::COMMON_BELL_FIRE);
			if (Time[jjLocalPlayers[0].playerID] < BestTime[jjLocalPlayers[0].playerID]) {
				switch (jjLocalPlayers[0].charCurr) {
					case CHAR::JAZZ: jjSamplePriority(SOUND::JAZZSOUNDS_JUMMY); break;
					case CHAR::SPAZ: jjSamplePriority(SOUND::SPAZSOUNDS_HAPPY); break;
					case CHAR::LORI: jjSamplePriority(SOUND::LORISOUNDS_WEHOO); break;
				}
			}
			playSample = false;
		}
		
		if (TouchedCheckpoint[jjLocalPlayers[0].playerID]) {
			goalColor = 8;
			cpColor = 0;
			goalTextOffset = 16;
			cpTextOffset = 32;
			goalText = "GOAL";
			cpText = "RETURN";		
		}
		else {
			goalColor = 240;
			cpColor = 248;
			goalText = "START";
			cpText = "CHECKPOINT";
			cpTextOffset = 52;
			goalTextOffset = 24;
		}
	}
}

/*******************************************************************/
//*CHAT COMMANDS*//

void onChat(int clientID, string &in text, CHAT::Type) {
	array<string> results;
	if (jjRegexMatch(text, """!(.*\S)\s*""", results)) {
		text = results[1];
		if (jjIsServer) {
			if (jjRegexMatch(text, """pb(\s+(.+))?""", results, true) || jjRegexMatch(text, """rank(\s+(.+))?""", results, true)) {
				string name = results[2];
				record rec;
				rec.name = name;
				int id = records.find(rec);
				
				if (name.isEmpty()) {
					jjAlert("|Please provide a valid player name.", true);
				}
				else {
					if (id >= 0) jjAlert("|||||||" + records[id].name + " ||||has a personal best of |||||" + formatTime(records[id].time) + " ||- ranked " + (id + 1) + " of " + records.length(), true);
					else jjAlert("|That player does not have a best time for this level.", true);
				}
			}
			else if (jjRegexMatch(text, """top""", true)) {
				if (records.length() >= 5) {
					for (uint i = 0; i < 5; i++) {
						const record@ rec = records[i];
						jjAlert("||" + (i + 1) + ". |||||" + rec.name + "| - " + formatTime(rec.time), true);
					}
				}
				else if (records.length() < 5 && !records.isEmpty()) {
					for (uint i = 0; i < records.length(); i++) {
						const record@ rec = records[i];
						jjAlert("||" + (i + 1) + ". |||||" + rec.name + "| - " + formatTime(rec.time), true);
					}
				}
				else if (records.isEmpty()) {
					jjAlert("|No one has set any records for this level yet!", true);
				}
			}
			else if (jjRegexMatch(text, """whois\s*([0-9]\d*)""", results, true)) {
				uint id = parseInt(results[1]) - 1;
				if (id < records.length()) {
					const record@ rec = records[id];
					jjAlert("|||||||" + rec.name + " ||||is ranked||||||| " + (id + 1) + " of " + records.length() +  " |with a best time of |||||" + formatTime(rec.time), true);
				}
				else jjAlert("|No player has been found with that ranking.", true);
			}
			else if (jjRegexMatch(text, """avg""", true) || jjRegexMatch(text, """average""", true)) {
			    uint sum = 0;
				for (uint i = 0; i < records.length(); i++) {
					sum += records[i].time;
				}
				if (!records.isEmpty()) jjAlert("|||The average time recorded in this level is |||||" + formatTime(sum / records.length()), true);
				else jjAlert("|No average can be calculated because no times have been recorded!", true);
			}
			else if (jjRegexMatch(text, """delete(\s+(.+))?""", results, true) && (clientID == jjPlayers[0].clientID || jjPlayers[clientID].isAdmin)) {
				string name = results[2];
				record rec;
				rec.name = name;
				int id = records.find(rec);
				
				if (name.isEmpty()) {
					jjAlert("|Please provide a valid player name.", true);
				}
				else if (id >= 0) {
					jjAlert("|||" + records[id].name + "'s record of |||||" + formatTime(records[id].time) + " |||has been ||||||DELETED", true);
					records.removeAt(id);
					records.sortAsc();
					save(filename);
					
					for (int i = 0; i < 32; i++) {
						if (jjPlayers[i].name == name) {
							BestTime[i] = 252000;
							stream(i);
						}
					}
				}
				else jjAlert("|That player does not have a best time.", true);
			}
		}
	}
}

bool onLocalChat(string &in text, CHAT::Type type) {
	if (type == CHAT::NORMAL && text == "!help") {
		jjAlert("|||!getrf ||- gives you RF ammo if obtainable; PU if a monitor is present");
		jjAlert("|||!top ||- shows the 5 fastest times and who recorded them");
		jjAlert("|||!rank <player name> ||- shows the best time and the rank of specific player");
		jjAlert("|||!whois <rank> ||- shows the best time of a player with that rank");
		jjAlert("|||!avg ||- calculates the average time taken by all player runs");
		if (jjIsServer || jjIsAdmin) {
			jjAlert("|||!delete <player name> ||- erases the record of a specified player");
		}
		
		return true;
	}
	
	if (type == CHAT::NORMAL && text == "!getrf" || text == "!getRF") {
		if (jjWeapons[WEAPON::RF].allowed) {
			jjLocalPlayers[0].ammo[WEAPON::RF] = 1;
			jjLocalPlayers[0].currWeapon = WEAPON::RF;
			if (!jjWeapons[WEAPON::RF].allowedPowerup) jjAlert(">> |||Received RF");
		}
		if (jjWeapons[WEAPON::RF].allowedPowerup) {
			jjLocalPlayers[0].powerup[WEAPON::RF] = true;
			jjLocalPlayers[0].ammo[WEAPON::RF] = 1;
			jjLocalPlayers[0].currWeapon = WEAPON::RF;
			jjAlert(">> |||Received RF");
		}
		
		else if (!jjWeapons[WEAPON::RF].allowed) jjAlert("|>> No RF ammo could be found in this level");
		return true;
	}
	
	return false;
}

/*******************************************************************/
//*SERVER-CLIENT NETWORKING*//

void stream(int id) {
	if (jjPlayers[id].clientID != 0) {
		jjSTREAM packet;
		packet.push(!StartedRace[id] && Time[id] > 0? true:false);
		packet.push(Time[id]);
		packet.push(BestTime[id]);
		packet.push(StartedRace[id]);
		packet.push(TouchedCheckpoint[id]);
		jjSendPacket(packet, jjPlayers[id].clientID);
	}
}

void onReceive(jjSTREAM &in packet, int clientID) {
	if (!jjIsServer) {
		bool x;
		uint64 time; 
		uint64 best_time;
		bool started_race;
		bool touched_checkpoint;
		
		packet.pop(x);
		packet.pop(time);
		packet.pop(best_time);
		packet.pop(started_race);
		packet.pop(touched_checkpoint);
			
		Time[jjLocalPlayers[0].playerID] = time;
		BestTime[jjLocalPlayers[0].playerID] = best_time;
		StartedRace[jjLocalPlayers[0].playerID] = started_race;
		TouchedCheckpoint[jjLocalPlayers[0].playerID] = touched_checkpoint;
		
		string newrecord = Time[jjLocalPlayers[0].playerID] <= BestTime[jjLocalPlayers[0].playerID]? "New record! " : "";
		if (x) {
			jjAlert("|||" + newrecord + "You finished in |||||" + formatTime(Time[jjLocalPlayers[0].playerID]), false, STRING::MEDIUM);
			playSample = true;
		}
	}
	else {
		record rec;
		rec.name = jjPlayers[clientID].name;
		int id = records.find(rec);
		if (id >= 0) BestTime[clientID] = records[id].time;
		stream(clientID);
	}
}