Downloads containing Multiflag.mut

Downloads
Name Author Game Mode Rating
JJ2+ Only: MultiflagFeatured Download Violet CLM Mutator 9.5 Download file

File preview

#pragma name "Multiflag"
#pragma description "Variant on Capture the Flag. Flags have no inherent team: bring one from the enemy base to your base to score a point, and it will stay at your base until recaptured. Roast an enemy flagholder to steal their flag, unless you're holding a flag already. Use the !flagsperteam command to add even more flags to the game."
#include "ArtificialArrow.asc"

enum SpecialPlayerID { Free = -1, BlueBase = -2, RedBase = -3 }
class Flag {
	float xPos, yPos;
	int playerID;
	int direction;
	int timeout;
	Flag(){}
}
array<Flag> Flags(32);
uint FlagCount = 2;
const int FreeFlagTimeout = 30 * 70;

const bool LimitAllFlagPoints = true;

class Point {	
	int xPos, yPos;
	Point(){}
	Point(int x, int y) { xPos = x * 32 + 15; yPos = y * 32 + 15; }
}
array<Point> BaseLocations(2);
array<int> BaseDirections(2);
array<jjOBJ@> BaseObjects(2, null);

array<int> NextCaptureTime(32, 5*70);

jjANIMSET@ CustomSet;
void onLevelLoad() {
	{
		//3, 4: blue
		//7, 8: red
		//ingame first, HUD second
		uint8 customSetID = 0;
		jjANIMSET@ flagSet = jjAnimSets[ANIM::FLAG];
		while (jjAnimSets[ANIM::CUSTOM[customSetID]] != 0) { ++customSetID; }
		@CustomSet = jjAnimSets[ANIM::CUSTOM[customSetID]];
		CustomSet.allocate(array<uint>={8,0,0,0,0});
		jjPIXELMAP emptySprite(1,1);
		for (uint i = 0; i < 8; ++i)
			emptySprite.save(jjAnimFrames[jjAnimations[CustomSet]+i]);
		jjAnimations[CustomSet+1] = jjAnimations[flagSet+3];
		jjAnimations[CustomSet+2] = jjAnimations[flagSet+7];
		jjAnimations[CustomSet+3] = jjAnimations[flagSet+4];
		jjAnimations[CustomSet+4] = jjAnimations[flagSet+8];
		jjAnimations[flagSet+3] = jjAnimations[flagSet+4] = jjAnimations[flagSet+7] = jjAnimations[flagSet+8] = jjAnimations[CustomSet];
	}
	
	uint levelAlreadyHasCTFBases = 0;
	for (int x = jjLayerWidth[4]; --x >= 0;)
		for (int y = jjLayerHeight[4]; --y >= 0;) {
			const auto eventID = jjEventGet(x,y);
			if (eventID == OBJECT::CTFBASE) {
				const uint teamID = jjParameterGet(x,y,0,1);
				BaseLocations[teamID] = Point(x,y);
				BaseDirections[teamID] = (jjParameterGet(x,y,1,1) * 2) - 1;
				levelAlreadyHasCTFBases |= (teamID + 1);
			}
		}
		
	if (levelAlreadyHasCTFBases != 3) { //0b11
		jjConsole("|WARNING: level does not have both CTF bases.");
	}
	
	if (jjIsServer) {
		jjSTREAM load("multiflag.asdat");
		if (!load.isEmpty()) {
			load.pop(FlagCount);
		}
	}
		
	for (uint i = 0; i < Flags.length; ++i) { //start
		const int teamID = int(i & 1);
		Flags[i].playerID = SpecialPlayerID::BlueBase - int(teamID);
	}
}

void onLevelBegin() {
	if (jjIsServer) {
		if (jjGameCustom != GAME::FR)
			jjChat("/fr");
	} else
		jjSendPacket(jjSTREAM());
	
	for (uint i = jjObjectCount; --i != 0;) {
		jjOBJ@ obj = jjObjects[i];
		if (obj.behavior == BEHAVIOR::FLAG)
			@BaseObjects[jjParameterGet(int(obj.xOrg) >> 5, int(obj.yOrg) >> 5, 0, 1)] = @obj;
	}
}

void onPlayer(jjPLAYER@ player) {
	array<int> flagLocations;
	uint8 spriteParam = player.teamRed ? 32 : 24;
	if (DoesPlayerHaveFlag(player) !is null) {
		flagLocations = array<int> = { BaseLocations[player.team].xPos, BaseLocations[player.team].yPos };
		spriteParam ^= (32+24);
	} else {
		for (uint flagID = 0; flagID < FlagCount; ++flagID) {
			const Flag@ flag = Flags[flagID];
			if (flag.playerID == SpecialPlayerID::Free || flag.playerID == (player.teamRed ? SpecialPlayerID::BlueBase : SpecialPlayerID::RedBase) || (flag.playerID >= 0 && jjPlayers[flag.playerID].teamRed != player.teamRed)) {
				flagLocations.insertLast(int(flag.xPos));
				flagLocations.insertLast(int(flag.yPos));
			}
		}
	}
	ArtificialArrow::DrawArrow(flagLocations, player, SPRITE::SINGLEHUE, spriteParam);
}

void AssignFlagToBase(Flag@ flag, int teamID, bool score = true) {
	flag.playerID = SpecialPlayerID::BlueBase - teamID;
	const Point@ homeBase = BaseLocations[teamID];
	if (score) {
		if (teamID == 1)
			jjChat("/redscore " + (jjTeamScore[TEAM::RED] + 1));
		else
			jjChat("/bluescore " + (jjTeamScore[TEAM::BLUE] + 1));
	}
}

void onRoast(jjPLAYER@ victim, jjPLAYER@ killer) {
	if (jjIsServer) {
		NextCaptureTime[victim.playerID] = jjGameTicks + 7*70;
		Flag@ flag = DoesPlayerHaveFlag(victim);
		if (flag !is null) {
			if (victim !is killer) { //roast
				if (DoesPlayerHaveFlag(killer) is null) {
					flag.playerID = killer.playerID;
					if (!killer.teamRed)
						jjAlert("||" + killer.nameUnformatted + " ||||stole a flag from |||" + victim.nameUnformatted, true, STRING::MEDIUM);
					else
						jjAlert("|" + killer.nameUnformatted + " |||||stole a flag from ||||" + victim.nameUnformatted, true, STRING::MEDIUM);
				} else {
					flag.playerID = SpecialPlayerID::Free;
					flag.timeout = jjGameTicks + FreeFlagTimeout;
				}
			} else { //pit death
				AssignFlagToBase(flag, victim.team ^ 1, false);
			}
			SendFlagData();
		}
	}
}

void onReceive(jjSTREAM &in packet, int clientID) {
	if (packet.isEmpty()) { //ping
		if (jjIsServer)
			SendFlagData(clientID);
		else
			AutoScoreCountdown = 5*70;
	} else { //flag data
		uint8 fc;
		packet.pop(fc);
		FlagCount = fc;
		for (uint flagID = 0; flagID < FlagCount; ++flagID) {
			Flag@ flag = Flags[flagID];
			int8 pid;
			packet.pop(pid);
			flag.playerID = pid;
			if (pid == SpecialPlayerID::Free) {
				int16 pos;
				packet.pop(pos);
				flag.xPos = pos;
				packet.pop(pos);
				flag.yPos = pos;
				flag.timeout = jjGameTicks + FreeFlagTimeout; //approximate
			}
		}
	}
}

array<uint> FlagsPerTeam(2);
uint AutoScoreCountdown = 0;
bool someTeamHasAllFlags = false;
array<int> LastTeamScore(2, 0);
void onMain() {
	someTeamHasAllFlags = false;
	FlagsPerTeam[0] = FlagsPerTeam[1] = 0;
	const bool gameActive = (jjGameState == GAME::STARTED || jjGameState == GAME::OVERTIME);
	
	array<int> baseFlagAngles(2, 0);
	for (uint flagID = 0; flagID < FlagCount; ++flagID) {
		Flag@ flag = Flags[flagID];
		if (flag.playerID >= 0) {
			const jjPLAYER@ holder = jjPlayers[flag.playerID];
			flag.xPos = holder.xPos;
			flag.yPos = holder.yPos;
			flag.direction = -holder.direction;
		} else if (flag.playerID != SpecialPlayerID::Free) {
			uint teamID = flag.playerID == SpecialPlayerID::BlueBase ? 0 : 1;
			flag.xPos = BaseLocations[teamID].xPos;
			flag.yPos = BaseLocations[teamID].yPos;
			flag.direction = BaseDirections[teamID];
		}
		if (jjIsServer && gameActive) {
			if (flag.playerID >= 0) {
				const jjPLAYER@ holder = jjPlayers[flag.playerID];
				if (holder.health <= 0 || !holder.isInGame) {
					flag.playerID = (jjTeamScore[TEAM::BLUE] > jjTeamScore[TEAM::RED]) ? SpecialPlayerID::BlueBase : SpecialPlayerID::RedBase;
					SendFlagData();
				} else {
					const auto teamID = holder.team;
					const Point@ homeBase = BaseLocations[teamID];
					if (abs(flag.xPos - homeBase.xPos) <= 32 && abs(flag.yPos - homeBase.yPos) <= 32) {
						const string pointScore = (teamID == 1 ? "|" : "||") + holder.nameUnformatted + " scored the point";
						AssignFlagToBase(flag, teamID);
						jjAlert(pointScore, true);
						SendFlagData();
					}
				}
			} else {
				if (flag.playerID == SpecialPlayerID::Free && jjGameTicks >= flag.timeout) {
					flag.playerID = (jjTeamScore[TEAM::BLUE] > jjTeamScore[TEAM::RED]) ? SpecialPlayerID::BlueBase : SpecialPlayerID::RedBase;
					SendFlagData();
					jjAlert("A flag timed out and returned to the " + (flag.playerID == SpecialPlayerID::BlueBase ? "blue" : "red") + " base", true);
				} else for (uint playerID = 0; playerID < 32; ++playerID) {
					const jjPLAYER@ player = jjPlayers[playerID];
					if (player.isInGame && NextCaptureTime[playerID] < jjGameTicks && (flag.playerID == SpecialPlayerID::Free || ((flag.playerID == SpecialPlayerID::BlueBase) == (player.teamRed))) && DoesPlayerHaveFlag(player) is null) {
						if (abs(flag.xPos - player.xPos) <= 32 && abs(flag.yPos - player.yPos) <= 32) {
							flag.playerID = playerID;
							jjAlert((player.teamRed ? "|" : "||") + player.nameUnformatted + " captured a flag", true, STRING::MEDIUM);
							NextCaptureTime[playerID] = jjGameTicks + 3*70; //why not
							SendFlagData();
							break; //don't be captured twice at once
						}
					}
				}
			}
		}
		if (flag.playerID >= SpecialPlayerID::Free) {
			jjDrawSpriteFromCurFrame(flag.xPos, flag.yPos+6, jjAnimations[CustomSet.firstAnim + (flag.playerID == SpecialPlayerID::BlueBase ? 1 : 2)].firstFrame + ((jjGameTicks >> 3) & 7), flag.direction, SPRITE::SINGLEHUE, 64);
			if (flag.playerID == SpecialPlayerID::Free) {
				jjDrawSpriteFromCurFrame(flag.xPos - 16, flag.yPos - 20, jjObjectPresets[OBJECT::STOPWATCH].curFrame + ((jjGameTicks >> 3) & 7), -1);
				jjDrawString(flag.xPos, flag.yPos - 20, "" + ((flag.timeout - jjGameTicks) / 70 + 1), STRING::MEDIUM);
			}
		} else {
			const int teamID = flag.playerID == SpecialPlayerID::BlueBase ? 0 : 1;
			jjDrawRotatedSpriteFromCurFrame(flag.xPos, flag.yPos+21, jjAnimations[CustomSet.firstAnim + teamID + 1].firstFrame + (((jjGameTicks >> 3) + flagID) & 7), flag.direction * baseFlagAngles[teamID], flag.direction,1, SPRITE::NORMAL);
			baseFlagAngles[teamID] -= 32;
			FlagsPerTeam[teamID] += 1;
			if (FlagsPerTeam[teamID] == FlagCount) {
				if ((!LimitAllFlagPoints) || (jjTeamScore[TEAM::Color(teamID)] <= jjTeamScore[TEAM::Color(teamID ^ 1)])) //don't let a team get too big of a lead
					someTeamHasAllFlags = true;
			}
		}
	}
	if (someTeamHasAllFlags && gameActive) {
		if (AutoScoreCountdown != 0) {
			AutoScoreCountdown -= 1;
			if (jjIsServer && AutoScoreCountdown == 0) {
				jjSamplePriority(SOUND::COMMON_BELL_FIRE2);
				if (FlagsPerTeam[0] == 0)
					jjChat("/redscore " + (jjTeamScore[TEAM::RED] + 1));
				else
					jjChat("/bluescore " + (jjTeamScore[TEAM::BLUE] + 1));
				AutoScoreCountdown = 5*70;
				SendTimePacket();
			}
		}
		if (jjIsServer) {
			if (AutoScoreCountdown == 0) {
				AutoScoreCountdown = 5*70;
				jjSamplePriority(SOUND::COMMON_BELL_FIRE2);
				jjAlert((FlagsPerTeam[0] == 0 ? "|Red" : "||Blue") + " team has all flags", true, STRING::MEDIUM);
				SendTimePacket();
			}
		}
	} else
		AutoScoreCountdown = 0;
	for (int teamID = 0; teamID < 2; ++teamID) {
		if (jjTeamScore[TEAM::Color(teamID)] != LastTeamScore[teamID]) {
			if (jjTeamScore[TEAM::Color(teamID)] > LastTeamScore[teamID] && BaseObjects[teamID] !is null) {
				BaseObjects[teamID].var[4] = 1; //make eva jump
				BaseObjects[teamID].counter = 0; //
			}
			LastTeamScore[teamID] = jjTeamScore[TEAM::Color(teamID)];
		}
	}
	
	for (uint playerID = 0; playerID < 32; ++playerID) {
		jjPLAYER@ player = jjPlayers[playerID];
		player.iconSet((player.isInGame && DoesPlayerHaveFlag(player) !is null) ? FlagIcon : null);
	}
}
jjANIMFRAME@ FlagIcon = jjAnimFrames[jjAnimations[jjAnimSets[ANIM::PLUS_PLAYERSTATEICONS]] + 1];

Flag@ DoesPlayerHaveFlag(const jjPLAYER@ player) {
	const int playerID = player.playerID;
	bool localPlayerHasFlag = false;
	for (uint flagID = 0; flagID < FlagCount; ++flagID) {
		Flag@ flag = Flags[flagID];
		if (flag.playerID == playerID)
			return flag;
	}
	return null;
}


void onChat(int clientID, string &in stringReceived, CHAT::Type chatType) {
	if (chatType == CHAT::TEAMCHAT && jjRegexMatch(stringReceived, "h\\??", true)) {
		if (DoesPlayerHaveFlag(jjLocalPlayers[0]) !is null)
			jjChat("" + jjLocalPlayers[0].health, true);
	} else if (jjIsServer && (clientID == 0 || jjPlayersWithClientID(clientID)[0].isAdmin)) {
		array<string> results;
		if (jjRegexMatch(stringReceived, "!flagsperteam\\s+(\\d\\d?)", results, true)) {
			uint proposedFlagCount = parseUInt(results[1]);
			if (proposedFlagCount >= 1 && proposedFlagCount <= Flags.length/2) {
				proposedFlagCount *= 2;
				if (proposedFlagCount != FlagCount) {
					for (uint lostFlag = FlagCount - 1; lostFlag >= proposedFlagCount; --lostFlag) {
						Flag@ flag = Flags[lostFlag];
						if (flag.playerID >= 0)
							jjConsole(jjPlayers[flag.playerID].name + "'s flag is gone.");
					}
					FlagCount = proposedFlagCount;
					jjSTREAM save;
					save.push(FlagCount);
					save.save("multiflag.asdat");
					SendFlagData();
					jjConsole("Flags Per Team has been set to " + (proposedFlagCount/2), true);
				} else {
					jjConsole("Flags Per Team is already " + (proposedFlagCount/2), true);
				}
			} else
				jjConsole("|Must be between 1 and " + (Flags.length/2) + " flags per team.", true);
		}
	}
}

bool onDrawGameModeHUD(jjPLAYER@, jjCANVAS@ canvas) {
	array<uint> evenHeldFlags = FlagsPerTeam;
	const bool wide = jjResolutionWidth > 400;
	const int x = wide ? 100 : 80;
	int y = wide ? 22 : 17;
	uint teamWithAllFlags = 99;
	
	if (someTeamHasAllFlags) {
		const bool redHasFlags = FlagsPerTeam[0] == 0;
		if ((!LimitAllFlagPoints) || (jjTeamScore[redHasFlags ? TEAM::RED : TEAM::BLUE] <= jjTeamScore[redHasFlags ? TEAM::BLUE : TEAM::RED])) {
			const int stringCenter = 0x8000 - (jjSubscreenWidth / 2) + 30;
			canvas.drawString(stringCenter, 60, (redHasFlags ? "||" : "|||") + ((AutoScoreCountdown + 69) / 70), STRING::LARGE);
			teamWithAllFlags = redHasFlags ? 1 : 0;
		}
	}
	if (((jjRenderFrame/17)&1) == 1) {
		for (uint flagID = 0; flagID < FlagCount; ++flagID) {
			const Flag@ flag = Flags[flagID];
			if (flag.playerID >= 0)
				evenHeldFlags[jjPlayers[flag.playerID].teamRed ? 1 : 0] += 1;
		}
	}
	for (uint teamID = 0; teamID < 2; ++teamID) {
		const auto firstFrame = jjAnimations[CustomSet + 3 + teamID].firstFrame;
		int xx = x, yy = y;
		if (teamWithAllFlags == teamID) {
			xx += jjRandom() & 3;
			yy += jjRandom() & 3;
		}
		for (uint flagID = 0; flagID < evenHeldFlags[teamID]; ++flagID)
			canvas.drawSpriteFromCurFrame(xx + flagID * 20, yy, firstFrame + (((jjGameTicks >> 3) + flagID) & 7), 1, flagID < FlagsPerTeam[teamID] ? SPRITE::NORMAL : SPRITE::SINGLEHUE, 64);
		/*
		for (uint flagID = 0; flagID < FlagCount; ++flagID) {
			const Flag@ flag = Flags[flagID];
			if (flag.playerID == SpecialPlayerID::Free || flag.playerID == (player.teamRed ? SpecialPlayerID::BlueBase : SpecialPlayerID::RedBase) || (flag.playerID >= 0 && jjPlayers[flag.playerID].teamRed != player.teamRed)) {*/
		y += wide ? 24 : 12;
	}
	return false;
}

void SendFlagData(int clientID = 0) {
	//there's little enough that it's easier on my poor tender head to send everything at once every dang time
	jjSTREAM packet;
	packet.push(uint8(FlagCount));
	for (uint flagID = 0; flagID < FlagCount; ++flagID) {
		const Flag@ flag = Flags[flagID];
		packet.push(int8(flag.playerID));
		if (flag.playerID == SpecialPlayerID::Free) {
			packet.push(int16(flag.xPos));
			packet.push(int16(flag.yPos));
		}
	}
	jjSendPacket(packet, clientID);
}
void SendTimePacket() {
	jjSendPacket(jjSTREAM());
}

void onHelp(array<jjHELPPAGE@>& pages) {
	for (int i = pages.length - 1; i >= 0; --i) { //remove any existing pages with the gamemode tag
		if (jjRegexSearch(pages[i].tags, "\\bgamemode\\b", true))
			pages.removeAt(i);
	}
	pages.insertLast(jjHELPPAGE(
		{ //beginning of array of jjHELPPARAGRAPH instances
			jjHELPPARAGRAPH(
				"||Multi|flag",
				textSize: STRING::MEDIUM,
				textAlignment: STRING::CENTER
			),
			jjHELPPARAGRAPH("When you bring a flag to your base, you score a point even if theres no flag there. The flag you brought then stays at your base until it's captured."),
			jjHELPPARAGRAPH("When you roast a player carrying a flag, you steal it. Or if you already had a flag, the victim's flag stays there for 30 seconds before automatically returning to the base of the team with more points. If another player finds the discarded flag before it returns, they capture it."),
			jjHELPPARAGRAPH("If all flags are at the same base, and that team isn't winning, every five seconds that team scores a free point."),
			jjHELPPARAGRAPH("Strategy tip: don't capture a flag when injured, because the enemy team can wait for you to capture it, roast you, steal it back, and quickly return it to their base for a point.")
		}, //end of array
		"mutator gamemode multiflag"
	));
	if (jjIsServer || jjLocalPlayers[0].isAdmin)
		pages.insertLast(jjHELPPAGE(
			{jjHELPPARAGRAPH("Use the \\4! flagsperteam\\6 command to add even more flags to the game, for wilder gameplay. It defaults to 1.")},
			"mutator gamemode multiflag commands",
			"!flagsperteam"
		));
}