Downloads containing player_collision_detector.mut

Downloads
Name Author Game Mode Rating
TSF with JJ2+ Only: Player-to-Player Collision... froducish Mutator N/A Download file

File preview

#pragma name "Player-to-Player Collision Detector"
#pragma require "player_collision_detector_header.asc"
#include "player_collision_detector_header.asc"

bool active = false;
array<COLLIDE::HOOK@> onCollides(32);

void onCollide(jjPLAYER@ victim, jjPLAYER@ collider, COLLIDE::Collide collision, uint8 eventID) {
	for (uint i = 0; i < onCollides.length; i++) {
		COLLIDE::HOOK@ hook = onCollides[i];
		if (hook !is null) {
			hook(victim, collider, collision, eventID);
		}
	}
}

class CollisionDetector : COLLIDE::DetectorI {
	void setHook(COLLIDE::HOOK@ hook = onCollide, uint moduleID = jjScriptModuleID) {
		if (moduleID >= onCollides.length()) {
			onCollides.resize(moduleID * 2);
		}
		@onCollides[moduleID] = hook;
	}
	
	void unsetHook(uint moduleID = jjScriptModuleID) {
		@onCollides[moduleID] = null;
	}
	
	void startDetecting() {
		sendOfTypeExcept(PACKET_ON, jjLocalPlayers[0].playerID);
		active = true;
	}
	
	void stopDetecting() {
		sendOfTypeExcept(PACKET_OFF, jjLocalPlayers[0].playerID);
		active = false;
	}
	
	string getVersion() const {
		return "2.0";
	}
};

CollisionDetector publicInstance;
CollisionDetector@ onGetPublicInterface() {
	return publicInstance;
}

namespace RESPONDER {
	abstract class Responder : jjBEHAVIORINTERFACE {
		protected jjPLAYER@ victim;
		protected jjPLAYER@ collider;
		protected jjOBJ@ bullet;
		protected jjOBJ@ ob;
		
		void onBehave(jjOBJ@ obj) {
			obj.delete();
		}
		
		// This is to be used *after* narrowing down a region of collision. Used for collisions that may hurt or pass through a player.
		protected bool bestSecondCheck() const final {
			uint curAnim = victim.curAnim - jjAnimSets[victim.setID].firstAnim;
			return !victim.isEnemy(collider) ||
				(bullet !is null && (victim.frozen != 0 /*|| victim.doesCollide(bullet, true)*/)) || // Trade off a little precision by excluding doesCollide to prevent issues with player methods like hurt(...)
				victim.blink != 0 ||
				((victim.charCurr == CHAR::JAZZ || victim.charCurr == CHAR::SPAZ || victim.charCurr == CHAR::LORI) && (curAnim == RABBIT::HURT || curAnim == RABBIT::DIE)) ||
				(victim.charCurr == CHAR::FROG && victim.curAnim == jjAnimSets[ANIM::FROG].firstAnim + 1) ||
				((victim.charCurr == CHAR::BIRD || victim.charCurr == CHAR::BIRD2) && (victim.curAnim == jjAnimSets[ANIM::BIRD].firstAnim + 5 || victim.curAnim == jjAnimSets[ANIM::BIRD].firstAnim + 10));
		}
		
		protected void spawnObject() final {
			@ob = jjObjects[jjAddObject(OBJECT::BEES, -1000, -1000, 0, CREATOR::OBJECT, BEHAVIOR::BEES)];
			ob.behavior = this;
			ob.counter = 0;
			ob.playerHandling = HANDLING::PARTICLE;
			ob.deactivates = false;
		}
	}

	class HurtingForce : Responder {
		private int force;
		private bool bulletDidCollide;
		
		HurtingForce(int force, jjPLAYER@ victim, jjPLAYER@ collider, jjOBJ@ bullet = null) {
			this.force = force;
			@this.victim = victim;
			@this.collider = collider;
			@this.bullet = bullet;
			spawnObject();
		}
		
		void onBehave(jjOBJ@ obj) {
			if ((bullet is null && obj.counter >= 70) || (bullet !is null && !bullet.isActive)) {
				obj.delete();
			}
			
			// Used as a third check instead of a second check lol
			bulletDidCollide = bulletDidCollide || (bullet is null ? false : victim.doesCollide(bullet, true));
			
			if (bestSecondCheck()) {
				if (bullet is null) {
					sendCollision(collider.playerID, victim.playerID, force < 0 ? COLLIDE::SPECIALMOVE : COLLIDE::STOMP, 0);
				} else {
					float x = victim.xPos - bullet.xPos;
					float y = victim.yPos - bullet.yPos;
					// If they are enemies then be extra sure it REALLY is the bullet that affected this victim.
					if (!victim.isEnemy(collider) || bulletDidCollide || (((x*x)+(y*y)) <= (24 * 24) * 2)) {
						sendCollision(collider.playerID, victim.playerID, (bullet.var[6] & (1 << 3)) != 0 ? COLLIDE::BULLETPU : COLLIDE::BULLET, bullet.eventID);
					}
				}
				obj.delete();
			}
			
			obj.counter++;
		}
	}

	class SugarRush : Responder {
		SugarRush(jjPLAYER@ victim, jjPLAYER@ collider) {
			@this.victim = victim;
			@this.collider = collider;
			spawnObject();
		}
		
		void onBehave(jjOBJ@ obj) {
			if (bestSecondCheck()) {
				sendCollision(collider.playerID, victim.playerID, COLLIDE::SUGARRUSH, 0);
				obj.delete();
			} else if (obj.counter >= 70) {
				obj.delete();
			}
			obj.counter++;
		}
	}
	
	class Bump : Responder {
		private float xSpeedOld;
		private int victimOldDirection;
		private int colliderOldDirection;
		
		Bump(jjPLAYER@ collider, jjPLAYER@ victim) {
			@this.victim = victim;
			@this.collider = collider;
			xSpeedOld = collider.xSpeed;
			victimOldDirection = victim.direction;
			colliderOldDirection = collider.direction;
			spawnObject();
		}
		
		void onBehave(jjOBJ@ obj) {
			if (obj.state == STATE::DONE) {
				bumpDone.set(0, collider, false);
				obj.delete();
				return;
			}
			
			if (obj.counter >= 35) {
				obj.state = STATE::DONE;
			} else if ((victim.direction != victimOldDirection &&!isGlidingBird(victim)) || (collider.direction != colliderOldDirection && !isGlidingBird(collider))) {
				obj.state = STATE::WAIT;
			} else if (obj.state != STATE::WAIT && collider.idle < 2 && victim.idle < 2 && sgn(collider.xSpeed) != sgn(xSpeedOld) && sgn(collider.xSpeed) != 0){
				sendBump(collider.playerID, victim.playerID);
				obj.state = STATE::WAIT;
			}
			
			obj.counter++;
		}
		
		private int sgn(float num) const {
			return num < 0 ? -1 : num > 0 ? 1 : 0;
		}
		
		private bool isGlidingBird(const jjPLAYER@ player) const {
			return (player.charCurr == CHAR::BIRD || player.charCurr == CHAR::BIRD2) && player.curAnim < jjAnimSets[ANIM::BIRD].firstAnim + 4;
		}
	}
}

class BooleansPerLocal {
	private array<uint8> arr;
	
	BooleansPerLocal(uint capacity) {
		arr = array<uint8>(capacity, 0);
	}
	
	void set(uint idx, const jjPLAYER@ player, bool value) {
		uint8 mask = 1 << uint8(player.localPlayerID);
		arr[idx] &= ~mask;
		if (value) {
			arr[idx] |= mask;
		}
	}
	
	bool get(uint idx, const jjPLAYER@ player) const {
		uint8 mask = 1 << uint8(player.localPlayerID);
		return arr[idx] & mask != 0;
	}
}

BooleansPerLocal bulletDone(jjObjectMax);
BooleansPerLocal hitForceDone(32);
BooleansPerLocal bumpDone(1);

void onPlayer(jjPLAYER@ player) {
	if (active) {
		
		for (int i = 0; i < 32; i++) {
			jjPLAYER@ collider = jjPlayers[i];
			if (collider.isInGame && collider !is player) {
				if (playerForces[i] == 0 && hitForceDone.get(i, player)) {
					hitForceDone.set(i, player, false);
				}
				
				float x = collider.xPos - player.xPos;
				float y = collider.yPos - player.yPos;
				float radiusSquared = (x*x) + (y*y);
				
				if (!hitForceDone.get(i, player) && playerForces[i] != 0 && radiusSquared <= (32 * 32)) {
					hitForceDone.set(i, player, true);
					RESPONDER::HurtingForce(playerForces[i], player, collider);
				} else if (hasSugarRush[i] && radiusSquared <= (32 * 32)) {
					RESPONDER::SugarRush(player, collider);
				}
				
				// Actually in this case the player takes responsiblity as the bumper, not the bumpee
				if (playerForces[player.playerID] == 0 && playerForces[i] == 0 && (radiusSquared <= (32 * 32)) && (jjGameMode != GAME::CTF || player.isEnemy(collider)) && player.blink == 0 && collider.blink == 0 && !bumpDone.get(0, player)) {
					bumpDone.set(0, player, true);
					RESPONDER::Bump(player, collider);
				}
			}
		}
		
		for (int i = 1; i < jjObjectCount; i++) {
			jjOBJ@ obj = jjObjects[i];
			if (!bulletDone.get(i, player) && obj.isActive && (obj.eventID < OBJECT::SMOKERING || obj.eventID == OBJECT::TNT) && obj.creatorType == CREATOR::PLAYER && int(obj.creatorID) != player.playerID) {
				float x = player.xPos - obj.xPos;
				float y = player.yPos - obj.yPos;
				if (((x*x)+(y*y)) <= (20 * 20) * 2) {
					bulletDone.set(i, player, true);
					RESPONDER::HurtingForce(0, player, jjPlayers[obj.creatorID], obj);
				}
			} else if (bulletDone.get(i, player) && !obj.isActive) {
				bulletDone.set(i, player, false);
			}
		}
	}
}

// Forces are modified to exclude sugar rush from the constant 1. Sugar rush will be checked separately.
array<int> playerForces(32, 0);
void onPlayerInput(jjPLAYER@ player) {
	if (active) {
		uint curAnim = player.curAnim - jjAnimSets[p.setID].firstAnim;
		bool isStomping = (player.charCurr == CHAR::JAZZ || player.charCurr == CHAR::SPAZ || player.charCurr == CHAR::LORI) && (curAnim == RABBIT::FALLBUTTSTOMP || curAnim == RABBIT::BUTTSTOMPLAND);
		int force = player.getObjectHitForce() != -1 ? (isStomping ? 1 : 0) : -1;
		int8 origin = player.playerID;
		// Only if hit force actually changed locally so we don't bombard others with packets.
		if (playerForces[origin] != force) {
			updateStatus(origin, force);
		}
	}
}

array<bool> hasSugarRush(32, false);
// Why is this the only hook to use a reference (&) instead of a nullable reference (@)? :S
void onPlayerDraw(jjPLAYERDRAW& playerdraw) {
	if (active) {
		bool rush = playerdraw.sugarRush;
		int8 origin = playerdraw.player.playerID;
		if (hasSugarRush[origin] != rush) {
			updateStatus(origin, rush);
		}
	}
}

void onMain() {
	if (jjIsServer && active) {
		for (int i = 0; i < 32; i++) {
			if (!jjPlayers[i].isInGame && (playerForces[i] != 0 || hasSugarRush[i])) {
				updateStatus(i, 0);
				updateStatus(i, false);
			}
		}
	}
}

enum PacketType { 
	PACKET_UPDATE,
	PACKET_COLLIDE,
	PACKET_BUMP,
	PACKET_ON,
	PACKET_OFF
};

void onReceive(jjSTREAM &in packet, int clientID) {
	uint8 type;
	packet.pop(type);
	if (jjIsServer) {
		switch (type) {
			case PACKET_UPDATE: {
				int8 origin;
				packet.pop(origin);
				if (jjPlayers[origin].clientID == clientID) {
					uint8 code;
					packet.pop(code);
					if (code == 0) {
						int force;
						packet.pop(force);
						updateStatus(origin, force);
					} else {
						bool rush;
						packet.pop(rush);
						updateStatus(origin, rush);
					}
				}
			}
			break;
			case PACKET_COLLIDE: {
				int8 victim;
				packet.pop(victim);
				if (jjPlayers[victim].clientID == clientID) {
					int8 origin;
					int collision;
					uint8 eventID;
					packet.pop(origin);
					packet.pop(collision);
					packet.pop(eventID);
					sendCollision(origin, victim, COLLIDE::Collide(collision), eventID);
				}
			}
			break;
			case PACKET_BUMP: {
				int8 attack;
				packet.pop(attack);
				if (jjPlayers[attack].clientID == clientID) {
					int8 victim;
					packet.pop(victim);
					sendBump(attack, victim);
				}
			} break;
			case PACKET_ON: {
				int8 playerID;
				packet.pop(playerID);
				if (jjPlayers[playerID].clientID == clientID) {
					active = true;
					sendOfTypeExcept(PACKET_ON, playerID);
				}
			}
			break;
			case PACKET_OFF: {
				int8 playerID;
				packet.pop(playerID);
				if (jjPlayers[playerID].clientID == clientID) {
					active = false;
					sendOfTypeExcept(PACKET_OFF, playerID);
				}
			}
			break;
		}
	} else {
		switch (type) {
			case PACKET_UPDATE: {
				int8 origin;
				packet.pop(origin);
				uint8 code;
				packet.pop(code);
				if (code == 0) {
					int force;
					packet.pop(force);
					playerForces[origin] = force;
				} else {
					bool rush;
					packet.pop(rush);
					hasSugarRush[origin] = rush;
				}
			}
			break;
			case PACKET_COLLIDE: {
				int8 victim, origin;
				int collision;
				uint8 eventID;
				packet.pop(victim);
				packet.pop(origin);
				packet.pop(collision);
				packet.pop(eventID);
				onCollide(jjPlayers[victim], jjPlayers[origin], COLLIDE::Collide(collision), eventID);
			}
			break;
			case PACKET_BUMP: {
				int8 attack, victim;
				packet.pop(attack);
				packet.pop(victim);
				onCollide(jjPlayers[victim], jjPlayers[attack], COLLIDE::BUMP, 0);
			} break;
			case PACKET_ON: {
				active = true;
			}
			break;
			case PACKET_OFF: {
				active = false;
			}
			break;
		}
	}
}

void updateStatus(int8 playerID, int force) {
	playerForces[playerID] = force;
	jjSTREAM packet;
	packet.push(uint8(PACKET_UPDATE));
	packet.push(playerID);
	packet.push(uint8(0));
	packet.push(force);
	jjSendPacket(packet, -jjPlayers[playerID].clientID);
}

void updateStatus(int8 playerID, bool rush) {
	hasSugarRush[playerID] = rush;
	jjSTREAM packet;
	packet.push(uint8(PACKET_UPDATE));
	packet.push(playerID);
	packet.push(uint8(1));
	packet.push(rush);
	jjSendPacket(packet, -jjPlayers[playerID].clientID);
}

void sendCollision(int8 attackID, int8 victimID, COLLIDE::Collide collision, uint8 eventID) {
	jjSTREAM packet;
	packet.push(uint8(PACKET_COLLIDE));
	packet.push(victimID);
	packet.push(attackID);
	packet.push(int(collision));
	packet.push(eventID);
	jjSendPacket(packet, -jjPlayers[victimID].clientID);
	onCollide(jjPlayers[victimID], jjPlayers[attackID], collision, eventID);
}

void sendBump(int8 attackID, int8 victimID) {
	jjSTREAM packet;
	packet.push(uint8(PACKET_BUMP));
	packet.push(attackID);
	packet.push(victimID);
	jjSendPacket(packet, -jjPlayers[attackID].clientID);
	onCollide(jjPlayers[victimID], jjPlayers[attackID], COLLIDE::BUMP, 0);
}

void sendOfTypeExcept(PacketType type, int8 playerID) {
	jjSTREAM packet;
	packet.push(uint8(type));
	packet.push(playerID);
	jjSendPacket(packet, -jjPlayers[playerID].clientID);
}