Copy to Clipboard Test

Ice Floors [Combo] [2.55] Code

namespace IceFloors
{
	void UserOnEnterIce()
	{
		// Things that happen when entering ice go here
	}
	bool UserOnExitIce()
	{
		// Things that happen when exiting ice go here
		// If it returns true, the part that runs jumping velocity is skipped
		return false;
	}
	bool UserDisableIceMovement()
	{
		// Things that disable ice movement go here
		return false;
	}
	
	// For setting ice velocity from another script
	// (getting velocity is complicated but hopefully this has some uses)
	void UserSetIceVelocity(int vX, int vY)
	{
		genericdata gd = Game->LoadGenericData(Game->GetGenericScript("IceFloors_Generic"));
		gd->DataSize = IFG_SIZE;
		int id = gd->Data[IFG_DISCRIMINATOR];
		gd->Data[IFG_VX] = vX;
		gd->Data[IFG_VY] = vY;
	}
	
	float IceVX()
	{
		genericdata gd = Game->LoadGenericData(Game->GetGenericScript("IceFloors_Generic"));
		gd->DataSize = IFG_SIZE;
		return gd->Data[IFG_VX];
	}
	
	float IceVY()
	{
		genericdata gd = Game->LoadGenericData(Game->GetGenericScript("IceFloors_Generic"));
		gd->DataSize = IFG_SIZE;
		return gd->Data[IFG_VY];
	}
	
	// Returns true if on ice
	bool OnIce()
	{
		genericdata gd = Game->LoadGenericData(Game->GetGenericScript("IceFloors_Generic"));
		gd->DataSize = IFG_SIZE;
		return gd->Data[IFG_ON_ICE];
	}

	// Defaults for when "Custom Values" is not checked on the combo
	const float ICE_FLOOR_MAX_STEP = 1.5; // Top speed while on ice (1.5 for Link's default)
	const float ICE_FLOOR_START_STEP = 0.5; // Speed when stepping onto an ice combo
	const float ICE_FLOOR_ACCEL = 0.04; // Acceleration, when holding a direction on ice
	const float ICE_FLOOR_DECEL = 0.02; // Deceleration, when not holding a direction on ice

	// Knockback
	const bool ICE_FLOOR_SPECIAL_KNOCKBACK = true; // If true, getting hit will affect ice velocity
	const float ICE_FLOOR_KNOCKBACK_STEP = 1.5; // The speed at which Link is knocked back

	// Jumping behavior
	const bool ICE_FLOOR_AIR_VELOCITY_FORCE_POS = true; // If true, air velocity on ice combos will force Link to move, else it's added to engine movement
	const float ICE_FLOOR_AIR_VELOCITY_MULTIPLIER = 1.0; // Multiplier when converting ice velocity to air velocity
	const float ICE_FLOOR_AIR_VELOCITY_DECEL = 0.02; // Deceleration over time for air velocity

	// Other settings
	const bool ICE_FLOOR_STOP_AT_WALL = true; // If true, Link will lose all acceleration when hitting a wall

	enum IceFloorGenericIndices
	{
		IFG_VX,
		IFG_VY,
		IFG_AIRVX,
		IFG_AIRVY,
		IFG_FLINGX,
		IFG_FLINGY,
		IFG_LAUNCHED,
		IFG_ON_ICE,
		IFG_ON_ICE_INTERNAL, // This one is only valid when ice combos are on the screen
		IFG_DISCRIMINATOR,
		
		IFG_SIZE
	};

	@Flag0("Custom Values"),
	@FlagHelp0("Check to enable custom speed values for this combo"),
	@Flag1("Affect Jump Velocity"),
	@FlagHelp1("Check to make jumping off the combo maintain velocity in the air"),
	@Attribyte0("Discriminator"),
	@AttribyteHelp0("A unique ID used to tell apart two instances of the script on the same screen. \nUse when you have ice combos with different physics used together."),
	@Attribute0("Max Step"),
	@AttributeHelp0("The max step speed while on ice (Link's base step is 1.5)"),
	@Attribute1("Start Step"),
	@AttributeHelp1("The starting step speed when stepping on ice (Link's base step is 1.5)"),
	@Attribute2("Acceleration"),
	@AttributeHelp2("Step speed of accelerating on ice (Link's base step is 1.5)"),
	@Attribute3("Deceleration"),
	@AttributeHelp3("Step speed of decelerating on ice when not pushing a direction (Link's base step is 1.5)")
	combodata script IceFloors
	{
		void run()
		{
			int id = this->Attribytes[0];
			
			mapdata lyr = Game->LoadTempScreen(this->Layer);
			// Only run if this is the first instance of the combo on the layer
			for(int i=0; i<176; ++i)
			{
				combodata cd = Game->LoadComboData(lyr->ComboD[i]);
				// Check based on script and discriminator to see if this is the first
				if(cd->Script==this->Script&&cd->Attribytes[0]==id)
				{
					if(i!=this->Pos)
						Quit();
					else
						break;
				}
			}
			
			genericdata hitEvents;
			// Ice floor knockback uses a generic script to get collision angles, so set that up
			if(ICE_FLOOR_SPECIAL_KNOCKBACK)
			{
				hitEvents = Game->LoadGenericData(Game->GetGenericScript("IceFloors_HitEvents"));
				hitEvents->DataSize = IFHE_SIZE;
				hitEvents->Running = true;
			}
			
			// Also another generic script for handling Link in the air
			genericdata genScript = Game->LoadGenericData(Game->GetGenericScript("IceFloors_Generic"));
			genScript->DataSize = IFG_SIZE;
			genScript->Running = true;
			
			bool customValues = this->Flags[0];
			bool affectJumpVelocity = this->Flags[1];
			
			float maxStep = this->Attributes[0];
			float startStep = this->Attributes[1];
			float accel = this->Attributes[2];
			float decel = this->Attributes[3];
			
			// If values aren't customized, set them to defaults
			if(!customValues)
			{
				maxStep = ICE_FLOOR_MAX_STEP;
				startStep = ICE_FLOOR_START_STEP;
				accel = ICE_FLOOR_ACCEL;
				decel = ICE_FLOOR_DECEL;
			}
			
			int iceX = Link->X;
			int iceY = Link->Y;
			bool wasOnIce = OnIce(this, lyr, id);
			genScript->Data[IFG_ON_ICE_INTERNAL] = OnGlobalIce(this);
						
			while(true)
			{
				PreventScrollBug(genScript->Data[IFG_VX], genScript->Data[IFG_VY]);
				
				// Get X and Y stick inputs
				int iX = (Link->InputLeft?-1:0) + (Link->InputRight?1:0);
				int iY = (Link->InputUp?-1:0) + (Link->InputDown?1:0);
				if(iX!=0&&iY!=0)
				{
					iX *= 0.7071;
					iY *= 0.7071;
				}
				
				if(OnIce(this, lyr, id))
				{
					genScript->Data[IFG_ON_ICE] = true;
					
					// If just entered ice
					if(!wasOnIce)
					{
						UserOnEnterIce();
						genScript->Data[IFG_DISCRIMINATOR] = id;
						
						// Step velocity based on startStep
						// Only when stepping onto ice
						if(!genScript->Data[IFG_ON_ICE_INTERNAL])
						{
							genScript->Data[IFG_VX] = iX * startStep;
							genScript->Data[IFG_VY] = iY * startStep;
						}
						iceX = Link->X;
						iceY = Link->Y;
						
						genScript->Data[IFG_ON_ICE_INTERNAL] = true;
						wasOnIce = true;
					}
					
					if(!InterruptIce()&&!UserDisableIceMovement())
					{
						// Handle special knockback behavior on ice
						if(ICE_FLOOR_SPECIAL_KNOCKBACK)
						{
							if(hitEvents->Data[IFHE_HITFLAG])
							{
								genScript->Data[IFG_VX] = VectorX(ICE_FLOOR_KNOCKBACK_STEP, hitEvents->Data[IFHE_HITANGLE]);
								genScript->Data[IFG_VY] = VectorY(ICE_FLOOR_KNOCKBACK_STEP, hitEvents->Data[IFHE_HITANGLE]);
								// The hit flag is in theory only being read by one combo script isntance
								// per frame, so we can unset it here and wait on the next time Link is hit.
								hitEvents->Data[IFHE_HITFLAG] = false;
							}
						}
						
						if(genScript->Data[IFG_DISCRIMINATOR]==id)
						{
							// Player input (X)
							genScript->Data[IFG_VX] += iX * accel;
							
							// Automatic deceleration (X)
							if(genScript->Data[IFG_VX]<0&&iX==0)
								genScript->Data[IFG_VX] = Decelerate(genScript->Data[IFG_VX], decel);
							if(genScript->Data[IFG_VX]>0&&iX==0)
								genScript->Data[IFG_VX] = Decelerate(genScript->Data[IFG_VX], decel); 
							
							// Y stuff doesn't happen in sideview
							if(!IsSideview())
							{
								// Player input (Y)
								genScript->Data[IFG_VY] += iY * accel;
							
								// Automatic deceleration (Y)
								if(genScript->Data[IFG_VY]<0&&iY==0)
									genScript->Data[IFG_VY] = Decelerate(genScript->Data[IFG_VY], decel);
								if(genScript->Data[IFG_VY]>0&&iY==0)
									genScript->Data[IFG_VY] = Decelerate(genScript->Data[IFG_VY], decel);
							}
						
							genScript->Data[IFG_VX] = Clamp(genScript->Data[IFG_VX], -maxStep, maxStep);
							genScript->Data[IFG_VY] = Clamp(genScript->Data[IFG_VY], -maxStep, maxStep);
							
							Link->MoveXY(genScript->Data[IFG_VX], genScript->Data[IFG_VY]);
							if(ICE_FLOOR_STOP_AT_WALL)
							{
								if(!Link->CanMoveXY(Sign(genScript->Data[IFG_VX]), 0))
									genScript->Data[IFG_VX] = 0;
								if(!Link->CanMoveXY(0, Sign(genScript->Data[IFG_VY])))
									genScript->Data[IFG_VY] = 0;
							}
						}
					}
					
					if(UserDisableIceMovement())
					{
						genScript->Data[IFG_VX] = 0;
						genScript->Data[IFG_VY] = 0;
					}
					
					// Record Link's old position to roll back his engine movement
					iceX = Link->X;
					iceY = Link->Y;
				}
				else
				{
					// Leaving ice
					if(wasOnIce)
					{
						if(!UserOnExitIce())
						{
							if(affectJumpVelocity)
							{
								// Set some data for the generic script to read
								genScript->Data[IFG_LAUNCHED] = true;
								genScript->Data[IFG_FLINGX] = Link->X;
								genScript->Data[IFG_FLINGY] = Link->Y;
								genScript->Data[IFG_AIRVX] = genScript->Data[IFG_VX] * ICE_FLOOR_AIR_VELOCITY_MULTIPLIER;
								genScript->Data[IFG_AIRVY] = genScript->Data[IFG_VY] * ICE_FLOOR_AIR_VELOCITY_MULTIPLIER;
							}
						}
						// It's possible we left this ice combo to a different type of ice, so check
						genScript->Data[IFG_ON_ICE_INTERNAL] = OnGlobalIce(this);
						wasOnIce = false;
					}
				}
				
				Waitdraw();
				
				if(genScript->Data[IFG_DISCRIMINATOR]==id)
				{
					// If on ice, roll back engine movement
					if(wasOnIce)
					{
						if(!UserDisableIceMovement())
						{
							Link->X = iceX;
							if(!IsSideview())
								Link->Y = iceY;
						}
					}
					else
					{
						genScript->Data[IFG_VX] = 0;
						genScript->Data[IFG_VY] = 0;
						iceX = Link->X;
						iceY = Link->Y;
					}
				}
				
				Waitframe();
			}
		}
		// Returns true if Link is on ice with the current script's discriminator
		bool OnIce(combodata this, mapdata lyr, int id)
		{
			if(Link->Falling)
				return false;
			if(Link->Action==LA_DROWNING)
				return false;
			
			// We're checking for two things:
			//  * That Link is not in the air
			//  * That he's on an ice combo with this script's discriminator
			if(!IsSideview())
			{
				if(Link->Z>0||Link->FakeZ>0)
					return false;
				combodata underLink = Game->LoadComboData(lyr->ComboD[ComboAt(Link->X+8, Link->Y+12)]);
				if(underLink->Script==this->Script&&underLink->Attribytes[0]==id)
					return true;
			}
			else
			{
				// If Link is rising or falling in sideview, assume he's not on solid ground
				if(Link->Jump!=0||Link->FakeJump!=0)
					return false;
					
				combodata underLink;
				// Link's position needs to be rounded in case new quest rules are on
				int linkX = Round(Link->X);
				int linkY = Round(Link->Y);
				underLink = Game->LoadComboData(lyr->ComboD[ComboAt(linkX+3, linkY+16)]);
				if(underLink->Script==this->Script&&underLink->Attribytes[0]==id)
					return true;
				underLink = Game->LoadComboData(lyr->ComboD[ComboAt(linkX+15-3, linkY+16)]);
				if(underLink->Script==this->Script&&underLink->Attribytes[0]==id)
					return true;
				underLink = Game->LoadComboData(lyr->ComboD[ComboAt(linkX+8, linkY+16)]);
				if(underLink->Script==this->Script&&underLink->Attribytes[0]==id)
					return true;
			}
			return false;
		}
		// Same as above, but ignores the discriminator
		bool OnGlobalIce(combodata this)
		{
			if(Link->Falling)
				return false;
			if(Link->Action==LA_DROWNING)
				return false;
			
			for(int i=0; i<=2; ++i)
			{
				mapdata lyr = Game->LoadTempScreen(i);
				// We're checking for two things:
				//  * That Link is not in the air
				//  * That he's on an ice combo with this script's discriminator
				if(!IsSideview())
				{
					if(Link->Z>0||Link->FakeZ>0)
						return false;
					combodata underLink = Game->LoadComboData(lyr->ComboD[ComboAt(Link->X+8, Link->Y+12)]);
					if(underLink->Script==this->Script)
						return true;
				}
				else
				{
					// If Link is rising or falling in sideview, assume he's not on solid ground
					if(Link->Jump!=0||Link->FakeJump!=0)
						return false;
						
					combodata underLink;
					// Link's position needs to be rounded in case new quest rules are on
					int linkX = Round(Link->X);
					int linkY = Round(Link->Y);
					underLink = Game->LoadComboData(lyr->ComboD[ComboAt(linkX+3, linkY+16)]);
					if(underLink->Script==this->Script)
						return true;
					underLink = Game->LoadComboData(lyr->ComboD[ComboAt(linkX+15-3, linkY+16)]);
					if(underLink->Script==this->Script)
						return true;
					underLink = Game->LoadComboData(lyr->ComboD[ComboAt(linkX+8, linkY+16)]);
					if(underLink->Script==this->Script)
						return true;
				}
			}
			return false;
		}
		bool InterruptIce()
		{
			// Only allow Link to slide around during approved actions
			switch(Link->Action)
			{
				case LA_NONE:
				case LA_WALKING:
				case LA_ATTACKING:
				case LA_GOTHURTLAND:
				case LA_CHARGING:
				case LA_SPINNING:
					break;
				default:
					return true;
			}
			return false;
		}
		// Decelerates step values towards 0
		float Decelerate(float val, float dec)
		{
			if(val>0)
				return Max(val-Abs(dec), 0);
			else
				return Min(val+Abs(dec), 0);
		}
		// Prevents scrolling the screen against the direction Link is being pushed by the ice
		void PreventScrollBug(int vx, int vy)
		{
			if(vx>0&&Link->X<=Ceiling(Abs(vx)))
				Link->InputLeft = false;
			if(vx<0&&Link->X>=240-Ceiling(Abs(vx)))
				Link->InputRight = false;
			if(vy>0&&Link->Y<=Ceiling(Abs(vy)))
				Link->InputUp = false;
			if(vy<0&&Link->Y>=160-Ceiling(Abs(vy)))
				Link->InputDown = false;
		}
	}

	enum IceFloors_HitEvents_Data
	{
		IFHE_HITFLAG,
		IFHE_HITANGLE,
		
		IFHE_SIZE
	};

	// This script listens for the player hit event and 
	// stores an angle in its data array for the combodata script to read
	generic script IceFloors_HitEvents
	{
		void run()
		{
			this->EventListen[GENSCR_EVENT_HERO_HIT_2] = true;
			while(true)
			{
				switch(WaitEvent())
				{
					case GENSCR_EVENT_HERO_HIT_2:
					{
						// When a collision happens, get the angle from the object
						// and set the hit flag
						switch(Game->EventData[GENEV_HEROHIT_HITTYPE])
						{
							case OBJTYPE_NPC:
							{
								npc n = Game->EventData[GENEV_HEROHIT_HITOBJ];
								int cx = n->X+n->HitXOffset+n->HitWidth/2;
								int cy = n->Y+n->HitYOffset+n->HitHeight/2;
								this->Data[IFHE_HITFLAG] = true;
								this->Data[IFHE_HITANGLE] = Angle(cx, cy, CenterLinkX(), CenterLinkY());
								break;
							}
							case OBJTYPE_EWPN:
							{
								eweapon e = Game->EventData[GENEV_HEROHIT_HITOBJ];
								int cx = e->X+e->HitXOffset+e->HitWidth/2;
								int cy = e->Y+e->HitYOffset+e->HitHeight/2;
								this->Data[IFHE_HITFLAG] = true;
								this->Data[IFHE_HITANGLE] = Angle(cx, cy, CenterLinkX(), CenterLinkY());
								break;
							}
							case OBJTYPE_COMBODATA:
							{
								this->Data[IFHE_HITFLAG] = true;
								// Combos just use the hit direction for their angle, because we have
								// no way to determine where the combo was on screen yet.
								this->Data[IFHE_HITANGLE] = DirAngle(Game->EventData[GENEV_HEROHIT_HITDIR]);
								break;
							}
						}
						break;
					}
				}
			}
		}
	}

	// This script is called by the ice floors to launch Link in the air, potentially across screens
	generic script IceFloors_Generic
	{
		void run()
		{
			while(true)
			{
				// Using FFC timing so it knows when the screen is scrolling
				WaitTo(SCR_TIMING_POST_FFCS);
				
				// Only handle launch logic when actually in the air
				if(this->Data[IFG_LAUNCHED])
				{
					if(InAir())
					{
						IceFloors.PreventScrollBug(this->Data[IFG_AIRVX], this->Data[IFG_AIRVY]);
						
						// Don't move Link during scroll animations...
						if(Link->Action!=LA_SCROLLING)
						{
							// Things that interrupt ice movement will also interrupt this
							if(!IceFloors.InterruptIce())
							{
								this->Data[IFG_AIRVX] = IceFloors.Decelerate(this->Data[IFG_AIRVX], ICE_FLOOR_AIR_VELOCITY_DECEL);
								
								if(!IsSideview())
								{
									this->Data[IFG_AIRVY] = IceFloors.Decelerate(this->Data[IFG_AIRVY], ICE_FLOOR_AIR_VELOCITY_DECEL);
								}
								
								Link->MoveXY(this->Data[IFG_AIRVX], this->Data[IFG_AIRVY]);
								if(ICE_FLOOR_STOP_AT_WALL)
								{
									if(!Link->CanMoveXY(Sign(this->Data[IFG_AIRVX]), 0))
										this->Data[IFG_AIRVX] = 0;
									if(!Link->CanMoveXY(0, Sign(this->Data[IFG_AIRVY])))
										this->Data[IFG_AIRVX] = 0;
								}
								this->Data[IFG_FLINGX] = Link->X;
								this->Data[IFG_FLINGY] = Link->Y;
							}
						}
						// ... but do track his position for when he enters the next screen
						else
						{
							this->Data[IFG_FLINGX] = Link->X;
							this->Data[IFG_FLINGY] = Link->Y;
						}
					}
					else
						this->Data[IFG_LAUNCHED] = false;
				}
				
				// Wait on the passive subscreen script because it runs just before combodata
				WaitTo(SCR_TIMING_POST_DMAPDATA_PASSIVESUBSCREEN);
				
				// Unset the on ice state for the frame
				this->Data[IFG_ON_ICE] = false;
				
				WaitTo(SCR_TIMING_WAITDRAW);
				
				if(this->Data[IFG_LAUNCHED])
				{
					if(InAir())
					{
						// If the setting is enabled, force Link's position
						if(ICE_FLOOR_AIR_VELOCITY_FORCE_POS)
						{
							Link->X = this->Data[IFG_FLINGX];
							if(!IsSideview())
							{
								Link->Y = this->Data[IFG_FLINGY];
								// If unable to scroll off the screen, clamp Link's position to the screen
								if(Game->FFRules[qr_NO_SCROLL_WHILE_IN_AIR])
								{
									Link->X = Clamp(Link->X, 0, 240);
									Link->Y = Clamp(Link->Y, 0, 160);
								}
							}
						}
					}
					// End the launch on touching the ground
					else
						this->Data[IFG_LAUNCHED] = false;
				}
			}
		}
		bool InAir()
		{
			// This prevents infinite falling into pits
			// because of the script continuing to put him 
			// where it thinks he should be
			if(Link->Falling)
				return false;
			if(Link->Action==LA_DROWNING)
				return false;
				
			// Some of this might be redundant tbh. 
			// Link->Standing on its own was not enough 
			// to determine when he was airborne.
			if(!Link->Standing)
				return true;
			if(Link->Jump!=0||Link->FakeJump!=0)
				return true;
			if(!IsSideview())
			{
				if(Link->Z>0||Link->FakeZ>0)
					return true;
			}
			return false;
		}
	}
}