Downloads containing charsNyanrush.asc

Downloads
Name Author Game Mode Rating
TSF with JJ2+ Only: Characters Cranky Mutator N/A Download file

File preview

#pragma require "Resize v10.asc"
#include "Resize v10.asc"
#pragma require "nyancat.bmp"

namespace CharsNyanRush
{
	// Per-player preference and live visual state. Neither uses jjPLAYER::invisibility.
	array<bool> enabled(32, false);
	array<bool> rushing(32, false);
	array<bool> visualActive(32, false);

	ANIM::Set nyanCatAnim;
	bool spriteReady = false;
	bool requestedInitialState = false;

	bool IsEnabled(int playerID)
	{
		return playerID >= 0 && playerID < int(enabled.length) && enabled[playerID];
	}

	bool IsActive(int playerID)
	{
		return playerID >= 0 && playerID < int(visualActive.length) && visualActive[playerID];
	}

	// Character, monster/object, hat, and ledge renderers use this guard.
	bool ShouldSuppressVisuals(int playerID)
	{
		return IsActive(playerID);
	}

	// Sugar Rush is exposed directly by jjPLAYERDRAW::sugarRush.
	// Do not infer it from getObjectHitForce() or animation IDs: those values
	// also occur during ordinary movement/landing transitions.

	void UpdateServerRushState(jjPLAYERDRAW &inout draw)
	{
		if(!jjIsServer) return;

		int playerID = draw.player.playerID;
		if(playerID < 0 || playerID >= 32) return;

		if(!draw.player.isActive || !draw.player.isInGame)
		{
			bool hadState = rushing[playerID] || visualActive[playerID];
			rushing[playerID] = false;
			visualActive[playerID] = false;
			if(hadState)
				SendState(playerID);
			return;
		}

		// This is the actual engine Sugar Rush state, not an approximation.
		bool newRushing = draw.sugarRush;
		bool newVisualActive = enabled[playerID] && newRushing;

		rushing[playerID] = newRushing;
		if(newVisualActive != visualActive[playerID])
		{
			visualActive[playerID] = newVisualActive;
			SendState(playerID);
		}
	}

	void LoadSprite()
	{
		TrueColor::ProcessPalette();
		TrueColor::EnableCaching();

		TrueColor::Bitmap spriteSheet("nyancat.bmp");
		jjPALCOLOR background = spriteSheet.pixels[0][0];
		jjPALCOLOR transparent = jjPALCOLOR(0, 0, 0);

		for(uint x = 0; x < spriteSheet.width; x++)
		{
			for(uint y = 0; y < spriteSheet.height; y++)
			{
				if(spriteSheet.pixels[x][y] == background)
					spriteSheet.pixels[x][y] = transparent;
			}
		}

		array<array<TrueColor::Coordinates>> frames = {
			{
				TrueColor::Coordinates(2, 12, 37, 22, -15, -10),
				TrueColor::Coordinates(48, 10, 40, 24, -15, -10),
				TrueColor::Coordinates(96, 11, 36, 22, -15, -10),
				TrueColor::Coordinates(138, 11, 37, 23, -15, -10),
				TrueColor::Coordinates(181, 11, 36, 22, -15, -10),
				TrueColor::Coordinates(220, 11, 36, 22, -15, -10)
			}
		};

		nyanCatAnim = TrueColor::FindCustomAnim();
		Resize::AllocateAndResizeTrueColorSpriteSheet(nyanCatAnim, spriteSheet, frames, 2);
		spriteReady = true;
	}

	void onLevelLoad()
	{
		spriteReady = false;
		requestedInitialState = false;
		LoadSprite();
	}

	void ApplyState(int playerID, bool newEnabled, bool newVisualActive)
	{
		if(playerID < 0 || playerID >= 32) return;
		enabled[playerID] = newEnabled;
		visualActive[playerID] = newVisualActive;
	}

	void SendState(int playerID, int toClientID = 0, bool joiningSnapshot = false)
	{
		if(playerID < 0 || playerID >= 32) return;

		jjSTREAM packet;
		packet.push((joiningSnapshot ? "!charsNyanRushStateBegin " : "!charsNyanRushState ")
			+ playerID + " " + (enabled[playerID] ? 1 : 0)
			+ " " + (visualActive[playerID] ? 1 : 0));

		if(toClientID == 0)
			jjSendPacket(packet);
		else
			jjSendPacket(packet, toClientID);
	}

	void SendAllStatesToClient(int clientID)
	{
		if(!jjIsServer || clientID == 0) return;
		for(int i = 0; i < 32; i++)
		{
			if(jjPlayers[i].isActive)
				SendState(i, clientID, true);
		}
	}

	void SetEnabledOnServer(int playerID, bool value)
	{
		if(!jjIsServer || playerID < 0 || playerID >= 32) return;

		enabled[playerID] = value;
		visualActive[playerID] = value && rushing[playerID];

		SendState(playerID);
	}

	bool SenderOwnsPlayer(int playerID, int fromClientID)
	{
		if(playerID < 0 || playerID >= 32 || !jjPlayers[playerID].isActive)
			return false;

		if(fromClientID == 0)
			return jjPlayers[playerID].isLocal;

		return jjPlayers[playerID].clientID == fromClientID;
	}

	void RequestEnabled(int playerID, bool value)
	{
		jjSTREAM packet;
		packet.push("!charsNyanRushRequest " + playerID + " " + (value ? 1 : 0));
		jjSendPacket(packet);
	}

	void RequestAllStates()
	{
		if(jjIsServer) return;
		jjSTREAM packet;
		packet.push("!charsNyanRushAll");
		jjSendPacket(packet);
	}

	void onReceive(jjSTREAM &in packet, int fromClientID)
	{
		string line = "";
		if(!packet.pop(line)) return;
		array<string> parts = line.split(" ");
		if(parts.length == 0) return;

		if((line == "!all" || line == "!charsNyanRushAll") && jjIsServer)
		{
			SendAllStatesToClient(fromClientID);
			return;
		}

		if(parts[0] == "!charsNyanRushRequest" && parts.length >= 3 && jjIsServer)
		{
			int playerID = parseInt(parts[1]);
			bool value = parseInt(parts[2]) != 0;
			if(SenderOwnsPlayer(playerID, fromClientID))
				SetEnabledOnServer(playerID, value);
			return;
		}

		if((parts[0] == "!charsNyanRushState" || parts[0] == "!charsNyanRushStateBegin")
			&& parts.length >= 4)
		{
			int playerID = parseInt(parts[1]);
			if(playerID >= 0 && playerID < 32)
				ApplyState(playerID, parseInt(parts[2]) != 0, parseInt(parts[3]) != 0);
		}
	}

	bool onLocalChat(string &in text, CHAT::Type)
	{
		array<string> parts = text.split(" ");
		if(parts.length == 0 || !jjRegexMatch(parts[0], "^!nyanrush$", true))
			return false;

		if(parts.length != 2 || !(jjRegexMatch(parts[1], "^(on|off|1|0)$", true)))
		{
			jjAlert("Usage: !nyanrush ON or !nyanrush OFF", false);
			return true;
		}

		bool value = jjRegexMatch(parts[1], "^(on|1)$", true);
		for(int i = 0; i < jjLocalPlayerCount; i++)
		{
			int playerID = jjLocalPlayers[i].playerID;
			if(jjIsServer)
				SetEnabledOnServer(playerID, value);
			else
				RequestEnabled(playerID, value);
		}

		jjAlert("Nyan Rush: " + (value ? "ON" : "OFF"), false);
		return true;
	}

	void onMainUpdate()
	{
		if(!jjIsServer)
		{
			if(!requestedInitialState && jjGameTicks >= 2)
			{
				requestedInitialState = true;
				RequestAllStates();
			}
			return;
		}

		for(int i = 0; i < 32; i++)
		{
			if(!jjPlayers[i].isActive)
			{
				bool hadState = enabled[i] || rushing[i] || visualActive[i];
				enabled[i] = false;
				rushing[i] = false;
				visualActive[i] = false;
				if(hadState)
					SendState(i);
			}
		}
	}

	void DrawPlayerName(jjPLAYER@ play)
	{
		jjTEXTAPPEARANCE style(STRING::NORMAL);
		style.align = STRING::CENTER;
		jjDrawString(
			play.xPos + 2,
			play.yPos - 27,
			"\xa71" + play.name,
			STRING::SMALL,
			style,
			0,
			SPRITE::NORMAL,
			0,
			1
		);
	}

	void AddTrailParticle(jjPLAYER@ player, int yOffset, int color)
	{
		jjPARTICLE@ part = jjAddParticle(PARTICLE::STAR);
		int direction = player.direction < 0 ? 1 : -1;
		part.xPos = player.xPos + 15 * direction;
		part.yPos = player.yPos + yOffset + (jjSin((jjGameTicks % 35) * (1024 / 35)) - 0.5) * 5;
		part.xSpeed = direction * 5;
		part.star.size = 12;
		part.star.color = color;
	}

	void DrawTrail(jjPLAYER@ player)
	{
		// Two stars every second tick, instead of the original six every tick.
		// This retains the rainbow trail without multiplying particle load when
		// several opted-in players rush at once.
		if((jjGameTicks & 1) != 0) return;

		int phase = (jjGameTicks / 2 + player.playerID) % 3;
		if(phase == 0)
		{
			AddTrailParticle(player, -18, 24);
			AddTrailParticle(player, 4, 19);
		}
		else if(phase == 1)
		{
			AddTrailParticle(player, -11, 43);
			AddTrailParticle(player, 11, 35);
		}
		else
		{
			AddTrailParticle(player, -4, 40);
			AddTrailParticle(player, 18, 93);
		}
	}

	void onMainDraw()
	{
		if(!spriteReady) return;

		for(int i = 0; i < 32; i++)
		{
			if(!IsActive(i) || !jjPlayers[i].isActive || !jjPlayers[i].isInGame)
				continue;

			jjPLAYER@ play = jjPlayers[i];
			DrawPlayerName(play);
			TrueColor::DrawSprite(
				play.xPos,
				play.yPos,
				nyanCatAnim,
				0,
				(jjGameTicks >> 3) * TrueColor::NumberOfFramesPerImage,
				play.direction
			);
			DrawTrail(play);
		}
	}

	void onPlayerDraw(jjPLAYERDRAW &inout draw)
	{
		UpdateServerRushState(draw);

		if(!ShouldSuppressVisuals(draw.player.playerID)) return;
		draw.sprite = false;
		draw.name = false;
	}
}