#pragma name "Double Domination"
// Original concept from Unreal Tournament 2004
// Scripted by PurpleJazz and Sir Ementaler

/*******************************************************************/

const float PI = 3.1415927f;
const array<TEAM::Color> teams = {TEAM::BLUE, TEAM::RED, TEAM::GREEN, TEAM::YELLOW};

// A simple class to define a custom control point
class CONTROLPOINT {
	int xTile, yTile;
	float xPos, yPos;
	int counter = 0; // Time in ticks until the control point is claimed by the person who last touched it
	TEAM::Color teamID = TEAM::NEUTRAL; // Team currently in control of the point
	TEAM::Color teamLastTouchID = TEAM::NEUTRAL; // Team of the person who last touched the point
}

const int maxTimeUntilScore = 700;
const int maxTimeUntilNewRound = 560;

array<CONTROLPOINT> ControlPoints;
TEAM::Color dominatingTeam = TEAM::NEUTRAL;
int captureTime = 70;
int timeUntilScore = maxTimeUntilScore;
int timeUntilNewRound = maxTimeUntilNewRound;

// Obtains palette index of a team color; used in palette shifts.
uint8 getTeamColor(TEAM::Color team) {
	switch (team) {
		case TEAM::BLUE: return 32;
		case TEAM::RED: return 24;
		case TEAM::GREEN: return 16;
		case TEAM::YELLOW: return 40;
	}
	return 64;
}
// Obtains team name.
string getTeamName(TEAM::Color team) {
	switch (team) {
		case TEAM::BLUE: return "Blue";
		case TEAM::RED: return "Red";
		case TEAM::GREEN: return "Green";
		case TEAM::YELLOW: return "Yellow";
	}
	return "Neutral";
}
// Obtains pipe characters (|) that change chat color to that of a team.
string getTeamPipes(TEAM::Color team) {
	switch (team) {
		case TEAM::BLUE: return "||";
		case TEAM::RED: return "|";
		case TEAM::GREEN: return "";
		case TEAM::YELLOW: return "|||";
	}
	return "|||||";
}

// Returns whether a team is in control of the majority of control points.
// The mutator is designed primarily for levels with two control points but works even if there are more or fewer,
// in case of which domination is defined as holding more than half of the present control points.
bool isTeamDominating(TEAM::Color team) {
	uint pointsUnderControl = 0;
	for (uint i = 0; i < ControlPoints.length(); i++) {
		if (ControlPoints[i].teamID == team)
			pointsUnderControl++;
	}
	return pointsUnderControl * 2 > ControlPoints.length();
}

// Sends a packet from the server to all clients (or one client if the clientID argument is used).
// The packet contains IDs of a control point it regards and the team that currently controls it.
// See onReceive defined below to see how clients interpret this data when they receive it.
void updateControlPointOwner(int id, int clientID = 0) {
	jjSTREAM packet;
	packet.push(id);
	packet.push(ControlPoints[id].teamID);
	jjSendPacket(packet);
}

void onLevelLoad() {
	// Deactivate all CTF bases. The script uses CTF base events to determine where to place control points
	// and it doesn't need the side effect of the bases actually spawning.
	jjObjectPresets[OBJECT::CTFBASE].behavior = BEHAVIOR::INACTIVE;
}

void onLevelBegin() {
	jjConsole("|||Custom Mode:  Double Domination");
	// Look through the entire layer 4 and wherever you find a CTF base event, place a control point.
	for (int x = 0; x < jjLayerWidth[4]; x++) {
		for (int y = 0; y < jjLayerHeight[4]; y++) {
			if (jjEventGet(x, y) == OBJECT::CTFBASE) {
				CONTROLPOINT newControlPoint;
				newControlPoint.xTile = x;
				newControlPoint.yTile = y;
				newControlPoint.xPos = x * 32 + 16;
				newControlPoint.yPos = y * 32 + 16;
				ControlPoints.insertLast(newControlPoint);
			}
		}
	}
	// The Anims.j2a blade gun animation we use for control points has several stray pixels of color indices 15 and 72.
	// This code looks for them in every frame of its animation and replaces them with color 64 instead, so that they
	// look good when drawn in SPRITE::PALSHIFT mode.
	jjANIMATION@ anim = jjAnimations[jjAnimSets[ANIM::AMMO] + 66];
	for (uint i = 0; i < anim.frameCount; i++) {
		jjANIMFRAME@ frame = jjAnimFrames[anim + i];
		jjPIXELMAP picture(frame);
		for (uint x = 0; x < picture.width; x++) {
			for (uint y = 0; y < picture.height; y++) {
				if (picture[x, y] == 15 || picture[x, y] == 72)
					picture[x, y] = 64;
			}
		}
		picture.save(frame);
	}
	// Here we send an empty packet from the client to the server.
	// The server will interpret it as a request to update the client on the states of all control points.
	// You can see how that is done in onReceive defined below.
	if (!jjIsServer)
		jjSendPacket(jjSTREAM());
}

void onMain() {
	// Draw all control points, and if you're the server, decrease their counter value and potentially change owner.
	string letter = "A";
	for (uint i = 0; i < ControlPoints.length(); i++) {
		CONTROLPOINT@ cp = ControlPoints[i];
		if (jjIsServer && (jjGameState == GAME::STARTED || jjGameState == GAME::OVERTIME) && --cp.counter == 0) {
			cp.teamID = cp.teamLastTouchID;
			updateControlPointOwner(i);
		}
		// Draw the control point.
		jjDrawResizedSprite(cp.xPos, cp.yPos, ANIM::AMMO, 66, jjGameTicks / 6 % 9, 1.5, 1.5, SPRITE::PALSHIFT, getTeamColor(cp.teamID) - 64);
		// Draw a letter indicating which control point it is.
		jjDrawString(cp.xPos - 7, cp.yPos + 3, letter, STRING::SMALL, STRING::PALSHIFT, getTeamColor(cp.teamID) - 64);
		// The below may be surprising syntax. "letter[0]++;" essentially means "increase the ASCII code of the first character in the string by 1".
		// If you take a look at an ASCII table, e.g. at http://en.cppreference.com/w/cpp/language/ascii, you may notice that letters are sorted
		// alphabetically, so incrementing 'A' will give us 'B', then 'C', etc. Of course things become slightly more grim past 'Z' so we just make
		// the assumption that there will be no more than 26 control points, which sounds fairly reasonable anyway.
		letter[0]++;
	}
	if (jjIsServer && (jjGameState == GAME::STARTED || jjGameState == GAME::OVERTIME)) {
		// timeUntilScore < 0 would mean the game is currently awaiting a new round.
		if (timeUntilScore >= 0) {
			// Check if anybody is touching a control point and update its teamLastTouchID if so.
			for (int i = 0; i < 32; i++) {
				jjPLAYER@ play = jjPlayers[i];
				if (play.isActive) {
					for (uint j = 0; j < ControlPoints.length(); j++) {
						CONTROLPOINT@ cp = ControlPoints[j];
						if (cp.teamLastTouchID != play.team && int(play.xPos) / 32 == cp.xTile && int(play.yPos) / 32 == cp.yTile) {
							cp.teamLastTouchID = play.team;
							cp.counter = captureTime; 
						}
					}
				}
			}
			// If there is no dominating team or if the dominating team has just lost domination, reset the timeUntilScore counter and keep
			// checking if another domination team pops up.
			if (dominatingTeam == TEAM::NEUTRAL || !isTeamDominating(dominatingTeam)) {
				dominatingTeam = TEAM::NEUTRAL;
				timeUntilScore = maxTimeUntilScore;
				for (uint i = 0; i < teams.length(); i++) {
					if (isTeamDominating(teams[i])) {
						dominatingTeam = teams[i];
						break;
					}
				}
			}
			// If there is a dominating team, decrement the timeUntilScore counter.
			if (dominatingTeam != TEAM::NEUTRAL) {
				switch (timeUntilScore--) {
					case maxTimeUntilScore: jjAlert(getTeamPipes(dominatingTeam) + getTeamName(dominatingTeam) + " team dominating!", true, STRING::MEDIUM); break;
					case 350: jjAlert(getTeamPipes(dominatingTeam) + getTeamName(dominatingTeam) + " team scores in 5...", true, STRING::MEDIUM); break;
					case 280: jjAlert(getTeamPipes(dominatingTeam) + "4...", true, STRING::MEDIUM); break;
					case 210: jjAlert(getTeamPipes(dominatingTeam) + "3...", true, STRING::MEDIUM); break;
					case 140: jjAlert(getTeamPipes(dominatingTeam) + "2...", true, STRING::MEDIUM); break;
					case 70: jjAlert(getTeamPipes(dominatingTeam) + "1...", true, STRING::MEDIUM); break;
					case 0:
						jjChat('/' + getTeamName(dominatingTeam) + "score " + (jjTeamScore[dominatingTeam] + 1));
						for (uint i = 0; i < ControlPoints.length(); i++) {
							CONTROLPOINT@ cp = ControlPoints[i];
							cp.teamID = TEAM::NEUTRAL;
							cp.teamLastTouchID = TEAM::NEUTRAL;
							cp.counter = 0;
							updateControlPointOwner(i);
						}
						break;
				}
			}
		} else {
			// Countdown until a new round. Decrement timeUntilNewRound until 0, then reset counters.
			switch (timeUntilNewRound--) {
				case 350: jjAlert("||||||New round begins in 5...", true, STRING::MEDIUM); break;
				case 280: jjAlert("||||||4...", true, STRING::MEDIUM); break;
				case 210: jjAlert("||||||3...", true, STRING::MEDIUM); break;
				case 140: jjAlert("||||||2...", true, STRING::MEDIUM); break;
				case 70: jjAlert("||||||1...", true, STRING::MEDIUM); break;
				case 0:
					jjAlert("||||||Control points can be recaptured!", true, STRING::MEDIUM);
					timeUntilScore = maxTimeUntilScore;
					timeUntilNewRound = maxTimeUntilNewRound;
					dominatingTeam == TEAM::NEUTRAL;
					break;
			}
		}
	}
}

void onPlayer(jjPLAYER@ play) {
	// Draws arrows pointing at control points
	string letter = "A";
	for (uint i = 0; i < ControlPoints.length(); i++) {
		const CONTROLPOINT@ cp = ControlPoints[i];
		// atan2 is an incredibly useful math function that lets you find an angle of a vector with specified coordinates.
		// It's common in programming languages so you shouldn't have trouble finding documentation for it to find out how it works.
		// Its result, however, is in radians, so we convert it to the JJ2 units by multiplying it by (512 / PI).
		int angle = int(atan2(cp.yPos - play.yPos, cp.xPos - play.xPos) * (512 / PI));
		// Draw the arrow.
		jjDrawRotatedSprite(play.xPos + 32 * jjCos(angle), play.yPos + 32 * jjSin(angle), ANIM::FLAG, 0, 0, 970 - angle, 1, 1, SPRITE::PALSHIFT, getTeamColor(cp.teamID) - 32, 1);
		// Draw a letter indicating which control point it points at.
		jjDrawString(play.xPos + (play.xPos > cp.xPos ? 48 : 32) * jjCos(angle), play.yPos + (play.yPos > cp.yPos ? 32 : 48) * jjSin(angle), letter, STRING::SMALL, STRING::PALSHIFT, getTeamColor(cp.teamID) - 64, 1);
		// The below may be surprising syntax. "letter[0]++;" essentially means "increase the ASCII code of the first character in the string by 1".
		// If you take a look at an ASCII table, e.g. at http://en.cppreference.com/w/cpp/language/ascii, you may notice that letters are sorted
		// alphabetically, so incrementing 'A' will give us 'B', then 'C', etc. Of course things become slightly more grim past 'Z' so we just make
		// the assumption that there will be no more than 26 control points, which sounds fairly reasonable anyway.
		letter[0]++;
	}
}

void onReceive(jjSTREAM &in packet, int clientID) {
	if (jjIsServer) {
		// The server is only supposed to receive one type of packets, and it's when players join the game and request the current state
		// of all control points. So the reaction to that is, naturally, sending them the requested information.
		for (uint i = 0; i < ControlPoints.length(); i++) {
			updateControlPointOwner(i, clientID);
		}
	} else {
		// The clients receive packets when a control point team gets updated. So we obtain the id of the control point in question,
		// and then pop the team ID directly into the CONTROLPOINT class's property.
		int id;
		packet.pop(id);
		packet.pop(ControlPoints[id].teamID);
	}
}

void onChat(int clientID, string &in text, CHAT::Type type) {
	// The mutator enables two commands:
	// "!resetcp" - resets owners of all control points.
	// "!capturetime <time>" - sets time needed to claim a control point to <time> in seconds.
	// In the latter, fractional values such as "1.75" can be used with expected results.
	if (jjIsServer && type == CHAT::NORMAL) {
		// Check if the sender is an admin or the server. The script oughtn't react to commands from regular players.
		bool senderIsAdmin = false;
		for (int i = 0; i < 32; i++) {
			const jjPLAYER@ play = jjPlayers[i];
			if (play.clientID == clientID) {
				senderIsAdmin = play.isActive && (play.isAdmin || play.playerID == 0);
				break;
			}
		}
		if (senderIsAdmin) {
			array<string> results;
			// We could use a simple (text == "!resetcp") comparison to test whether the chat is our command.
			// However, jjRegexMatch has many advantages over that. Here, thanks to said function, we made the test case-insensitive
			// using true as the last argument, and forgiving to any whitespace characters following the command thanks to "\\s*".
			// This way, even chat such as "!RESETCP" or "!ResetCP  " will be accepted as a valid command.
			if (jjRegexMatch(text, "!resetcp\\s*", true)) {
				jjAlert("||||||Control points have been reset", true);
				dominatingTeam = TEAM::NEUTRAL;
				timeUntilScore = maxTimeUntilScore;
				timeUntilNewRound = maxTimeUntilNewRound;
				for (uint i = 0; i < ControlPoints.length(); i++) {
					CONTROLPOINT@ cp = ControlPoints[i];
					cp.counter = 0;
					cp.teamID = cp.teamLastTouchID = TEAM::NEUTRAL;
					updateControlPointOwner(i);
				}
			} else
			// Here we show the hugest benefits of regex. The expression "(\\d*\\.?\\d+)" matches any positive decimal number with or without
			// a decimal dot, e.g. "1", "0.5", "250", "12.450", and even ".25" (without a leading zero). Not only that, but the parentheses
			// actually allow capturing the match result of that matching in results[1].
			if (jjRegexMatch(text, "!capturetime\\s*(\\d*\\.?\\d+)\\s*", results, true)) {
				// parseFloat is an AngelScript function associated with the string add-on. It takes a string as an argument and returns
				// a floating-point value it represents - perfect for our purposes. Other similar functions worth attention are parseInt
				// for integers and the inverse functions, formatFloat and formatInt that instead take a value and return a string that
				// represents them. All have a variety of options; you can learn more about them from the official documentation at
				// http://www.angelcode.com/angelscript/sdk/docs/manual/doc_datatypes_strings.html
				float value = parseFloat(results[1]);
				if (value < 0.1f) {
					jjAlert("|||||||CP capture time cannot be less than 0.1 seconds!", true);
				} else if (value > 10) {
					jjAlert("|||||||CP capture time cannot be more than 10 seconds!", true);
				} else {
					captureTime = int(value * 70);
					jjAlert("||||||CP capture time has been set to " + value + " seconds", true);
				}
			}
		}
	}
}
