// Backpack that only gives ammo for valid weapons
Class UnrealBackpack : BackpackItem replaces Backpack
{
	override Inventory CreateCopy( Actor other )
	{
		// Find every unique type of ammoitem. Give it to the player if
		// he doesn't have it already, and double its maximum capacity.
		for ( int i=0; i<AllActorClasses.Size(); i++ )
		{
			let type = (class<Ammo>)(AllActorClasses[i]);
			if ( !type || (type.GetParentClass() != 'Ammo') ) continue;
			// check that it's for a valid weapon
			bool isvalid = false;
			for ( int j=0; j<AllActorClasses.Size(); j++ )
			{
				let type2 = (class<Weapon>)(AllActorClasses[j]);
				if ( !type2 ) continue;
				let rep = GetReplacement(type2);
				if ( (rep != type2) && !(rep is "DehackedPickup") ) continue;
				readonly<Weapon> weap = GetDefaultByType(type2);
				if ( !other.player || !other.player.weapons.LocateWeapon(type2) || weap.bCheatNotWeapon ) continue;
				if ( (weap.AmmoType1 == type) || (weap.AmmoType2 == type) )
				{
					isvalid = true;
					break;
				}
			}
			if ( !isvalid ) continue;
			let ammoitem = Ammo(other.FindInventory(type));
			int amount = GetDefaultByType(type).BackpackAmount;
			// don't give these at all
			if ( (amount <= 0) && (GetDefaultByType(type).BackpackMaxAmount == GetDefaultByType(type).MaxAmount) ) continue;
			// extra ammo in baby mode and nightmare mode
			if ( !bIgnoreSkill ) amount = int(amount*G_SkillPropertyFloat(SKILLP_AmmoFactor));
			if ( amount < 0 ) amount = 0;
			if ( !ammoitem )
			{
				// The player did not have the ammoitem. Add it.
				ammoitem = Ammo(Spawn(type));
				ammoitem.Amount = bDepleted?0:amount;
				if ( ammoitem.BackpackMaxAmount > ammoitem.MaxAmount )
					ammoitem.MaxAmount = ammoitem.BackpackMaxAmount;
				if ( ammoitem.Amount > ammoitem.MaxAmount )
					ammoitem.Amount = ammoitem.MaxAmount;
				ammoitem.AttachToOwner(other);
			}
			else
			{
				// The player had the ammoitem. Give some more.
				if ( ammoitem.MaxAmount < ammoitem.BackpackMaxAmount )
					ammoitem.MaxAmount = ammoitem.BackpackMaxAmount;
				if ( !bDepleted && (ammoitem.Amount < ammoitem.MaxAmount) )
				{
					ammoitem.Amount += amount;
					if ( ammoitem.Amount > ammoitem.MaxAmount )
						ammoitem.Amount = ammoitem.MaxAmount;
				}
			}
		}
		return Inventory.CreateCopy(other);
	}
	override bool HandlePickup( Inventory item )
	{
		// Since you already have a backpack, that means you already have every
		// kind of ammo in your inventory, so we don't need to look at the
		// entire PClass list to discover what kinds of ammo exist, and we don't
		// have to alter the MaxAmount either.
		if ( item is 'BackpackItem' )
		{
			for ( let probe = Owner.Inv; probe; probe = probe.Inv )
			{
				if ( probe.GetParentClass() != 'Ammo' ) continue;
				if ( probe.Amount >= probe.MaxAmount && !sv_unlimited_pickup ) continue;
				int amount = Ammo(probe).Default.BackpackAmount;
				// extra ammo in baby mode and nightmare mode
				if ( !bIgnoreSkill )
					amount = int(amount*G_SkillPropertyFloat(SKILLP_AmmoFactor));
				probe.Amount += amount;
				if ( (probe.Amount > probe.MaxAmount) && !sv_unlimited_pickup )
					probe.Amount = probe.MaxAmount;
			}
			// The pickup always succeeds, even if you didn't get anything
			item.bPickupGood = true;
			return true;
		}
		return false;
	}
	override void DoPickupSpecial( Actor toucher )
	{
		Super.DoPickupSpecial(toucher);
		if ( gameinfo.gametype&GAME_DOOMCHEX )
		{
			static const Class<Inventory> xitems[] = {"Flare", "Seeds", "VoiceBox", "ForceField", "Dampener", "Peacemaker", "SentryGunItem"};
			int xitemn[7];
			xitemn[0] = max(0,Random[BackpackExtra](-1,3));
			xitemn[1] = max(0,Random[BackpackExtra](-1,3));
			xitemn[2] = max(0,Random[BackpackExtra](-2,1));
			xitemn[3] = max(0,Random[BackpackExtra](-2,1));
			xitemn[4] = max(0,Random[BackpackExtra](-1,1));
			xitemn[5] = max(0,Random[BackpackExtra](-2,1));
			xitemn[6] = max(0,Random[BackpackExtra](-2,1));
			// random doubling
			if ( !Random[BackpackExtra](0,4) ) xitemn[0] *= 2;
			if ( !Random[BackpackExtra](0,4) ) xitemn[1] *= 2;
			if ( !Random[BackpackExtra](0,7) ) xitemn[2] *= 2;
			if ( !Random[BackpackExtra](0,6) ) xitemn[3] *= 2;
			if ( !Random[BackpackExtra](0,5) ) xitemn[4] *= 2;
			if ( !Random[BackpackExtra](0,9) ) xitemn[5] *= 2;
			if ( !Random[BackpackExtra](0,9) ) xitemn[6] *= 2;
			for ( int i=0; i<(sting_proto?7:5); i++ )
			{
				if ( xitemn[i] <= 0 ) continue;
				toucher.GiveInventory(xitems[i],xitemn[i]);
			}
		}
	}
	Default
	{
		Tag "$T_BACKPACK";
		Inventory.PickupMessage "$I_BACKPACK";
		Inventory.RespawnTics 2100;
	}
	States
	{
	Spawn:
		BPAK A -1;
		Stop;
	}
}

Class UTranslator : UnrealInventory
{
	bool bNewMessage, bNotNewMessage;
	string NewMessage, Hint;
	Array<String> OldMessages, OldHints;

	override void Travelled()
	{
		Super.Travelled();
		NewMessage = Hint = "";
		OldMessages.Clear();
		OldHints.Clear();
	}

	override bool Use( bool pickup )
	{
		if ( pickup ) return false;
		if ( Owner.player == players[consoleplayer] )
			Menu.SetMenu('TranslatorMenu');
		bNewMessage = bNotNewmessage = false;
		return false;
	}

	void AddMessage( String msg, String hnt )
	{
		int found = -1;
		for ( int i=0; i<OldMessages.Size(); i++ )
		{
			if ( OldMessages[i] != msg ) continue;
			found = i;
		}
		if ( found != -1 )
		{
			OldMessages.Delete(found);
			OldHints.Delete(found);
		}
		if ( (NewMessage != msg) && (NewMessage.Length() > 0) )
		{
			OldMessages.Push(NewMessage);
			OldHints.Push(Hint);
		}
		NewMessage = msg;
		Hint = hnt;
	}

	Default
	{
		Tag "$T_TRANSLATOR";
		Inventory.PickupMessage "$I_TRANSLATOR";
		Inventory.Icon "I_Tran";
		Inventory.MaxAmount 1;
	}
	States
	{
	Spawn:
		TRNS A -1;
		Stop;
	}
}

// To be placed by mappers
// Uses two UDMF-settable user strings
// AMBUSH: only triggered by script activation, not touch
Class TranslatorEvent : Actor
{
	String user_message, user_hint;
	Array<Actor> Touchers;

	Default
	{
		Radius 40;
		Height 80;
		+SPECIAL;
		+NOGRAVITY;
		+INVISIBLE;
	}
	virtual void TriggerMessage( Actor who )
	{
		if ( special1 > gametic ) return;
		let Translator = UTranslator(who.FindInventory("UTranslator"));
		if ( !Translator ) return;
		special1 = gametic+8;
		if ( who.CheckLocalView() )
		{
			if ( special2 ) Console.Printf(StringTable.Localize("$TR_MSG"));
			else Console.Printf(StringTable.Localize("$TR_NEWMSG"));
			S_Sound("translator/event",CHAN_VOICE|CHAN_UI);
		}
		Translator.AddMessage(user_message,user_hint);
		if ( special2 ) Translator.bNotNewMessage = true;
		else Translator.bNewMessage = true;
		special2 = 1;
	}
	// GZDoom doesn't have Touch/UnTouch like UE1 so I have to improvise
	override void Tick()
	{
		Super.Tick();
		for ( int i=0; i<Touchers.Size(); i++ )
		{
			bool deletthis = false;
			if ( Touchers[i].pos.x+Touchers[i].radius < pos.x-radius ) deletthis = true;
			if ( Touchers[i].pos.x-Touchers[i].radius > pos.x+radius ) deletthis = true;
			if ( Touchers[i].pos.y+Touchers[i].radius < pos.y-radius ) deletthis = true;
			if ( Touchers[i].pos.y-Touchers[i].radius > pos.y+radius ) deletthis = true;
			if ( Touchers[i].pos.z+Touchers[i].height < pos.z ) deletthis = true;
			if ( Touchers[i].pos.z > pos.z+height ) deletthis = true;
			if ( !deletthis ) continue;
			Touchers.Delete(i);
			i--;
		}
	}
	override void Touch( Actor toucher )
	{
		if ( bAMBUSH || !toucher || !toucher.player ) return;
		for ( int i=0; i<Touchers.Size(); i++ ) if ( Touchers[i] == toucher ) return;
		Touchers.Push(toucher);
		TriggerMessage(toucher);
	}
	override void Activate( Actor activator )
	{
		if ( !activator || !activator.player ) return;
		TriggerMessage(activator);
	}
	States
	{
	Spawn:
		SMSG A -1;
		Stop;
	}
}

Class VoiceBox : UnrealInventory
{
	Actor box;

	Default
	{
		Tag "$T_VOICEBOX";
		Inventory.PickupMessage "$I_VOICEBOX";
		Inventory.Icon "I_VoiceB";
		Inventory.MaxAmount 2;
		UnrealInventory.Charge 600;
	}
	override void DoEffect()
	{
		Super.DoEffect();
		if ( !bActive ) return;
		if ( box ) Charge = box.ReactionTime;
		else
		{
			Charge = DefaultCharge;
			bActive = false;
			Amount--;
			if ( Amount <= 0 ) DepleteOrDestroy();
		}
	}
	override bool Use( bool pickup )
	{
		if ( pickup || bActive ) return false;
		Vector3 x, y, z;
		[x, y, z] = dt_CoordUtil.GetAxes(Owner.pitch,Owner.angle,Owner.roll);
		Vector3 origin = level.Vec3Offset(Owner.Vec2OffsetZ(0,0,Owner.player.viewz),x*20-z*8);
		box = Spawn("VoiceBoxActive",origin);
		box.ReactionTime = Charge;
		box.vel = x*9.;
		box.vel.z += 1.;
		box.target = Owner;
		box.angle = Owner.angle;
		box.pitch = Owner.pitch;
		bActive = true;
		return false;
	}
	States
	{
	Spawn:
		VBOX A -1;
		Stop;
	}
}

Class VoiceBoxHitbox : Actor
{
	Default
	{
		Tag "$T_VOICEBOX";
		Radius 6;
		Height 9;
		Health 60;
		+SHOOTABLE;
		+NOGRAVITY;
		+NOCLIP;
		+DONTSPLASH;
		+NOBLOOD;
		+NOTELEPORT;
	}
	override void Tick()
	{
		Super.Tick();
		if ( !master )
		{
			Destroy();
			return;
		}
		SetOrigin(master.pos,true);
	}
	override void Die( Actor source, Actor inflictor, int dmgflags, Name MeansOfDeath )
	{
		if ( master ) master.ReactionTime = 0;
		Super.Die(source,inflictor,dmgflags,MeansOfDeath);
	}
}

Class VoiceBoxActive : Actor
{
	Actor b;
	double desiredangle, anglevel, oldangle;

	override void PostBeginPlay()
	{
		Super.PostBeginPlay();
		b = Spawn("VoiceBoxHitbox",pos);
		b.master = self;
		desiredangle = FRandom[Junk](0,360);
		anglevel = FRandom[Junk](5,15);
	}
	action void A_VoiceBoxPlay()
	{
		static const String BattleSounds[] =
		{
			"automag/shot",
			"utrl/explode",
			"automag/shot",
			"automag/shot",
			"flak/altfire",
			"utrl/explode",
			"flak/fire",
			"shock/fire",
			"stinger/hit2",
			"automag/shot"
		};
		if ( invoker.b ) invoker.b.A_AlertMonsters(0,AMF_TARGETEMITTER);
		for ( int i=0; i<10; i++ ) if ( !Random[Voicebox](0,30) ) A_PlaySound(BattleSounds[i],CHAN_AUTO,FRandom[Voicebox](0.5,1.0));
	}
	override void Tick()
	{
		oldangle = angle;
		Super.Tick();
		if ( deltaangle(angle,desiredangle) ~== 0 ) angle = desiredangle;
		else angle += clamp(deltaangle(angle,desiredangle),-anglevel,anglevel);
	}
	Default
	{
		Radius 6;
		Height 6;
		PROJECTILE;
		-NOGRAVITY;
		+SKYEXPLODE;
		+MOVEWITHSECTOR;
		+CANBOUNCEWATER;
		+BOUNCEAUTOOFF;
		+BOUNCEAUTOOFFFLOORONLY;
		+USEBOUNCESTATE;
		+INTERPOLATEANGLES;
		BounceType "Hexen";
		BounceFactor 0.5;
		BounceSound "transloc/bounce";
		WallBounceFactor 0.5;
		Gravity 0.35;
		ReactionTime 200;
	}
	States
	{
	Spawn:
		VBOX A -1;
		Stop;
	Bounce:
		VBOX A 0 { angle = invoker.oldangle; }
		Goto Spawn;
	Death:
		VBOX A 30
		{
			invoker.anglevel *= 0;
			A_PlaySound("voice/activate");
		}
		VBOX ABCDEFGHIJ 1
		{
			A_VoiceBoxPlay();
			ReactionTime--;
			return A_JumpIf(ReactionTime<=0,"Death2");
		}
		Goto Death+1;
	Death2:
		VBOX A 0
		{
			if ( invoker.b ) invoker.b.Destroy();
			A_PlaySound("flare/explode",CHAN_VOICE);
			A_NoGravity();
			A_Stop();
			A_SetRenderStyle(1.,STYLE_Add);
			SetZ(pos.z+16);
			Spawn("FlareXLight",pos);
			bMOVEWITHSECTOR = false;
			bFORCEXYBILLBOARD = true;
			A_SetScale(FRandomPick[ExploS](-1.5,1.5),FRandomPick[ExploS](-1.5,1.5));
			int numpt = Random[ExploS](15,30);
			for ( int i=0; i<numpt; i++ )
			{
				Vector3 pvel = (FRandom[ExploS](-1,1),FRandom[ExploS](-1,1),FRandom[ExploS](-1,1)).unit()*FRandom[ExploS](1,3);
				let s = Spawn("UTSmoke",pos);
				s.vel = pvel;
			}
			numpt = Random[ExploS](9,18);
			for ( int i=0; i<numpt; i++ )
			{
				Vector3 pvel = (FRandom[ExploS](-1,1),FRandom[ExploS](-1,1),FRandom[ExploS](-1,1)).unit()*FRandom[ExploS](2,6);
				let s = Spawn("UTSpark",pos);
				s.vel = pvel;
			}
			numpt = Random[ExploS](18,28);
			for ( int i=0; i<numpt; i++ )
			{
				Vector3 pvel = (FRandom[ExploS](-1,1),FRandom[ExploS](-1,1),FRandom[ExploS](-1,1)).unit()*FRandom[ExploS](2,12);
				let s = Spawn("UTChip",pos);
				s.vel = pvel;
				s.scale *= FRandom[ExploS](0.9,2.7);
			}
			return A_Jump(256,"Explo1","Explo2","Explo3","Explo4","Explo5");
		}
	Explo1:
		EXP1 ABCDEFGH 3 Bright;
		Stop;
	Explo2:
		EXP2 ABCDEFGH 3 Bright;
		Stop;
	Explo3:
		EXP3 ABCDEFGH 3 Bright;
		Stop;
	Explo4:
		EXP4 ABCDEFGH 3 Bright;
		Stop;
	Explo5:
		EXP5 ABCDEFGH 3 Bright;
		Stop;
	}
}

Class Flare : UnrealInventory
{
	Default
	{
		Tag "$T_FLARES";
		Inventory.PickupMessage "$I_FLARES";
		Inventory.Icon "I_Flare";
		Inventory.MaxAmount 20;
		+UNREALINVENTORY.UNLIMITEDCOPIES;
	}
	override bool Use( bool pickup )
	{
		if ( pickup ) return false;
		Vector3 x, y, z;
		[x, y, z] = dt_CoordUtil.GetAxes(Owner.pitch,Owner.angle,Owner.roll);
		Vector3 origin = level.Vec3Offset(Owner.Vec2OffsetZ(0,0,Owner.player.viewz),x*10-z*8);
		let a = Spawn("FlareThrown",origin);
		a.target = Owner;
		a.angle = Owner.angle;
		a.pitch = Owner.pitch;
		a.vel += x*a.speed;
		return true;
	}
	States
	{
	Spawn:
		FLAR A -1;
		Stop;
	}
}

Class FlareThrown : Actor
{
	double pitchvel, anglevel, rollvel;
	double desiredangle;
	bool rotatetodesired;
	double lastpitch, lastangle, lastroll;
	Actor b;
	Default
	{
		Radius 6;
		Height 6;
		+NOBLOCKMAP;
		+MISSILE;
		+MOVEWITHSECTOR;
		+THRUACTORS;
		+USEBOUNCESTATE;
		+INTERPOLATEANGLES;
		+BOUNCEAUTOOFF;
		+BOUNCEAUTOOFFFLOORONLY;
		+NOTELEPORT;
		+FORCERADIUSDMG;
		+NODAMAGETHRUST;
		DamageType "Exploded";
		Speed 12;
		VSpeed 2;
		Mass 1;
		Gravity 0.35;
		BounceType "Hexen";
		WallBounceFactor 0.6;
		BounceFactor 0.6;
		ReactionTime 350;
	}
	override void PostBeginPlay()
	{
		Super.PostBeginPlay();
		pitchvel = FRandom[Junk](5,15)*RandomPick[Junk](-1,1);
		anglevel = FRandom[Junk](5,15)*RandomPick[Junk](-1,1);
		rollvel = FRandom[Junk](5,15)*RandomPick[Junk](-1,1);
	}
	override void Tick()
	{
		lastpitch = pitch;
		lastangle = angle;
		lastroll = roll;
		Super.Tick();
		if ( rotatetodesired )
		{
			if ( deltaangle(pitch,0) ~== 0 ) pitch = 0;
			else pitch += clamp(deltaangle(pitch,0),-pitchvel,pitchvel);
			if ( deltaangle(angle,desiredangle) ~== 0 ) angle = desiredangle;
			else angle += clamp(deltaangle(angle,desiredangle),-anglevel,anglevel);
			if ( deltaangle(roll,0) ~== 0 ) roll = 0;
			else roll += clamp(deltaangle(roll,0),-rollvel,rollvel);
		}
		else
		{
			angle += anglevel;
			pitch += pitchvel;
			roll += rollvel;
		}
		if ( ReactionTime <= 0 ) return;
		if ( GetAge() < 9 ) return;
		if ( waterlevel > 0 )
		{
			if ( tracer ) tracer.Destroy();
			if ( b ) b.Destroy();
			A_StopSound(CHAN_VOICE);
			ReactionTime = 0;
			return;
		}
		if ( !b )
		{
			A_PlaySound("flare/on");
			A_PlaySound("flare/loop",CHAN_VOICE,.5,true);
			tracer = Spawn("FlareThrownX",pos);
			tracer.angle = angle;
			tracer.pitch = pitch;
			tracer.target = self;
			b = Spawn("FlareHitbox",pos);
			b.master = self;
		}
		b.A_AlertMonsters(0,AMF_TARGETEMITTER);
		ReactionTime--;
	}
	States
	{
	Spawn:
		FLAR A 1 A_JumpIf(ReactionTime<=0,"Death");
		Wait;
	Bounce:
		FLAR A 0
		{
			pitch = lastpitch;
			angle = lastangle;
			roll = lastroll;
			rotatetodesired = true;
			desiredangle = FRandom[Junk](0,360);
			pitchvel = abs(pitchvel)*0.75;
			anglevel = abs(anglevel)*0.75;
			rollvel = abs(rollvel)*0.75;
		}
		Goto Spawn;
	Death:
		FLAR A 0 { anglevel *= 0; }
		FLAR A 1
		{
			if ( waterlevel > 0 )
			{
				if ( tracer ) tracer.Destroy();
				if ( b ) b.Destroy();
				A_StopSound(CHAN_VOICE);
				return ResolveState("Fizz");
			}
			return A_JumpIf(ReactionTime<=0,1);
		}
		Wait;
		FLAR A 0
		{
			if ( tracer ) tracer.Destroy();
			if ( b ) b.Destroy();
			if ( waterlevel > 0 )
			{
				return ResolveState("Fizz");
			}
			A_StopSound(CHAN_VOICE);
			A_PlaySound("flare/explode");
			A_Explode(50,50);
			A_NoGravity();
			A_Stop();
			A_SetRenderStyle(1.,STYLE_Add);
			SetZ(pos.z+9);
			Spawn("FlareXLight",pos);
			bMOVEWITHSECTOR = false;
			bFORCEXYBILLBOARD = true;
			A_SetScale(FRandomPick[ExploS](-1.5,1.5),FRandomPick[ExploS](-1.5,1.5));
			int numpt = Random[ExploS](10,20);
			for ( int i=0; i<numpt; i++ )
			{
				Vector3 pvel = (FRandom[ExploS](-1,1),FRandom[ExploS](-1,1),FRandom[ExploS](-1,1)).unit()*FRandom[ExploS](1,3);
				let s = Spawn("UTSmoke",pos);
				s.vel = pvel;
			}
			numpt = Random[ExploS](6,15);
			for ( int i=0; i<numpt; i++ )
			{
				Vector3 pvel = (FRandom[ExploS](-1,1),FRandom[ExploS](-1,1),FRandom[ExploS](-1,1)).unit()*FRandom[ExploS](2,6);
				let s = Spawn("UTSpark",pos);
				s.vel = pvel;
			}
			numpt = Random[ExploS](15,20);
			for ( int i=0; i<numpt; i++ )
			{
				Vector3 pvel = (FRandom[ExploS](-1,1),FRandom[ExploS](-1,1),FRandom[ExploS](-1,1)).unit()*FRandom[ExploS](2,12);
				let s = Spawn("UTChip",pos);
				s.vel = pvel;
				s.scale *= FRandom[ExploS](0.9,2.7);
			}
			return A_Jump(256,"Explo1","Explo2","Explo3","Explo4","Explo5");
		}
	Explo1:
		EXP1 ABCDEFGH 3 Bright;
		Stop;
	Explo2:
		EXP2 ABCDEFGH 3 Bright;
		Stop;
	Explo3:
		EXP3 ABCDEFGH 3 Bright;
		Stop;
	Explo4:
		EXP4 ABCDEFGH 3 Bright;
		Stop;
	Explo5:
		EXP5 ABCDEFGH 3 Bright;
		Stop;
	Fizz:
		FLAR A 1
		{
			special1++;
			if ( special1 > 150 ) A_FadeOut();
		}
		Wait;
	}
}

Class FlareXLight : PaletteLight
{
	Default
	{
		ReactionTime 25;
		Args 0,0,0,80;
	}
}

Class FlareHitbox : VoiceBoxHitbox
{
	Default
	{
		Tag "$T_FLARES";
		Radius 4;
		Height 6;
		Health 1;
	}
}

Class FlareThrownX : Actor
{
	Default
	{
		RenderStyle "Add";
		+NOGRAVITY;
		+NOCLIP;
		+DONTSPLASH;
		+INTERPOLATEANGLES;
		Radius 0.1;
		Height 0;
	}
	override void Tick()
	{
		Super.Tick();
		if ( !target )
		{
			Destroy();
			return;
		}
		SetOrigin(target.pos,true);
		angle = target.angle;
		pitch = target.pitch;
	}
	States
	{
	Spawn:
		FLAR A -1 Bright;
		Stop;
	}
}

Class BetaFlare : UnrealInventory
{
	Class<Actor> ThrownClass;

	Property ThrownClass : ThrownClass;

	override bool TryPickup( in out Actor toucher )
	{
		if ( !sting_proto || !sting_flares ) return false;	// not allowed
		return Super.TryPickup(toucher);
	}
	override void Tick()
	{
		Super.Tick();
		if ( sting_proto && sting_flares ) return;
		if ( Owner ) Owner.RemoveInventory(self);
		Destroy();
	}
	override bool Use( bool pickup )
	{
		if ( pickup || bActive || (charge < defaultcharge) ) return false;
		Vector3 x, y, z;
		[x, y, z] = dt_CoordUtil.GetAxes(Owner.pitch,Owner.angle,Owner.roll);
		Vector3 origin = level.Vec3Offset(Owner.Vec2OffsetZ(0,0,Owner.player.viewz),x*10-z*8);
		let a = Spawn(ThrownClass,origin);
		a.target = Owner;
		a.angle = Owner.angle;
		a.pitch = Owner.pitch;
		a.vel += x*a.speed;
		for ( Inventory i=Owner.Inv; i; i=i.Inv )
		{
			if ( !(i is 'BetaFlare') ) continue;
			UnrealInventory(i).bActive = true;
			UnrealInventory(i).Charge = 0;
			i.tracer = a;
		}
		return false;
	}
	override void DoEffect()
	{
		Super.DoEffect();
		if ( !bActive )
		{
			charge = min(defaultcharge,charge+1);
			return;
		}
		else if ( !tracer ) bActive = false;
	}
	Default
	{
		Inventory.MaxAmount 1;
		UnrealInventory.Charge 100;
		+INVENTORY.UNDROPPABLE;
		+INVENTORY.UNTOSSABLE;
	}
}

Class LightFlare : BetaFlare
{
	Default
	{
		Tag "$T_LFLARES";
		Inventory.Icon "I_FlarBL";
		BetaFlare.ThrownClass "LightFlareThrown";
	}
}

Class DarkFlare : BetaFlare
{
	Default
	{
		Tag "$T_DFLARES";
		Inventory.Icon "I_FlarBD";
		BetaFlare.ThrownClass "DarkFlareThrown";
	}
}

Class BetaFlareThrown : Actor
{
	double pitchvel, anglevel, rollvel;
	double desiredangle;
	bool rotatetodesired;
	double lastpitch, lastangle, lastroll;
	Actor b;
	Default
	{
		Radius 6;
		Height 6;
		+NOBLOCKMAP;
		+MISSILE;
		+MOVEWITHSECTOR;
		+THRUACTORS;
		+USEBOUNCESTATE;
		+INTERPOLATEANGLES;
		+BOUNCEAUTOOFF;
		+BOUNCEAUTOOFFFLOORONLY;
		+NOTELEPORT;
		+FORCERADIUSDMG;
		+NODAMAGETHRUST;
		DamageType "Exploded";
		Speed 12;
		VSpeed 2;
		Mass 1;
		Gravity 0.35;
		BounceType "Hexen";
		WallBounceFactor 0.6;
		BounceFactor 0.6;
		ReactionTime 350;
	}
	virtual void A_SpawnEffect()
	{
	}
	override void PostBeginPlay()
	{
		Super.PostBeginPlay();
		pitchvel = FRandom[Junk](5,15)*RandomPick[Junk](-1,1);
		anglevel = FRandom[Junk](5,15)*RandomPick[Junk](-1,1);
		rollvel = FRandom[Junk](5,15)*RandomPick[Junk](-1,1);
	}
	override void Tick()
	{
		lastpitch = pitch;
		lastangle = angle;
		lastroll = roll;
		Super.Tick();
		if ( rotatetodesired )
		{
			if ( deltaangle(pitch,0) ~== 0 ) pitch = 0;
			else pitch += clamp(deltaangle(pitch,0),-pitchvel,pitchvel);
			if ( deltaangle(angle,desiredangle) ~== 0 ) angle = desiredangle;
			else angle += clamp(deltaangle(angle,desiredangle),-anglevel,anglevel);
			if ( deltaangle(roll,0) ~== 0 ) roll = 0;
			else roll += clamp(deltaangle(roll,0),-rollvel,rollvel);
		}
		else
		{
			angle += anglevel;
			pitch += pitchvel;
			roll += rollvel;
		}
		if ( ReactionTime <= 0 ) return;
		if ( GetAge() < 9 ) return;
		if ( waterlevel > 0 )
		{
			if ( tracer ) tracer.Destroy();
			if ( b ) b.Destroy();
			A_StopSound(CHAN_VOICE);
			ReactionTime = 0;
			return;
		}
		if ( !b )
		{
			A_PlaySound("flare/on");
			A_PlaySound("flare/loop",CHAN_VOICE,.5,true);
			b = Spawn("FlareHitbox",pos);
			b.master = self;
		}
		b.A_AlertMonsters(0,AMF_TARGETEMITTER);
		ReactionTime--;
	}
	States
	{
	Spawn:
		FLAR A 1
		{
			if ( GetAge() >= 9 ) A_SpawnEffect();
			return A_JumpIf(ReactionTime<=0,"Death");
		}
		Wait;
	Bounce:
		FLAR A 0
		{
			pitch = lastpitch;
			angle = lastangle;
			roll = lastroll;
			rotatetodesired = true;
			desiredangle = FRandom[Junk](0,360);
			pitchvel = abs(pitchvel)*0.75;
			anglevel = abs(anglevel)*0.75;
			rollvel = abs(rollvel)*0.75;
		}
		Goto Spawn;
	Death:
		FLAR A 0 { anglevel *= 0; }
		FLAR A 1
		{
			if ( waterlevel > 0 )
			{
				if ( b ) b.Destroy();
				A_StopSound(CHAN_VOICE);
				return ResolveState("Fizz");
			}
			A_SpawnEffect();
			return A_JumpIf(ReactionTime<=0,1);
		}
		Wait;
		FLAR A 0
		{
			if ( b ) b.Destroy();
			if ( waterlevel > 0 ) return ResolveState("Fizz");
			A_RemoveLight('PLight');
			A_StopSound(CHAN_VOICE);
			A_PlaySound("flare/explode");
			A_Explode(50,50);
			A_NoGravity();
			A_Stop();
			A_SetRenderStyle(1.,STYLE_Add);
			SetZ(pos.z+9);
			Spawn("FlareXLight",pos);
			bMOVEWITHSECTOR = false;
			bFORCEXYBILLBOARD = true;
			A_SetScale(FRandomPick[ExploS](-1.5,1.5),FRandomPick[ExploS](-1.5,1.5));
			int numpt = Random[ExploS](10,20);
			for ( int i=0; i<numpt; i++ )
			{
				Vector3 pvel = (FRandom[ExploS](-1,1),FRandom[ExploS](-1,1),FRandom[ExploS](-1,1)).unit()*FRandom[ExploS](1,3);
				let s = Spawn("UTSmoke",pos);
				s.vel = pvel;
			}
			numpt = Random[ExploS](6,15);
			for ( int i=0; i<numpt; i++ )
			{
				Vector3 pvel = (FRandom[ExploS](-1,1),FRandom[ExploS](-1,1),FRandom[ExploS](-1,1)).unit()*FRandom[ExploS](2,6);
				let s = Spawn("UTSpark",pos);
				s.vel = pvel;
			}
			numpt = Random[ExploS](15,20);
			for ( int i=0; i<numpt; i++ )
			{
				Vector3 pvel = (FRandom[ExploS](-1,1),FRandom[ExploS](-1,1),FRandom[ExploS](-1,1)).unit()*FRandom[ExploS](2,12);
				let s = Spawn("UTChip",pos);
				s.vel = pvel;
				s.scale *= FRandom[ExploS](0.9,2.7);
			}
			return A_Jump(256,"Explo1","Explo2","Explo3","Explo4","Explo5");
		}
	Explo1:
		EXP1 ABCDEFGH 3 Bright;
		Stop;
	Explo2:
		EXP2 ABCDEFGH 3 Bright;
		Stop;
	Explo3:
		EXP3 ABCDEFGH 3 Bright;
		Stop;
	Explo4:
		EXP4 ABCDEFGH 3 Bright;
		Stop;
	Explo5:
		EXP5 ABCDEFGH 3 Bright;
		Stop;
	Fizz:
		FLAR A 0 A_RemoveLight('PLight');
		FLAR A 1
		{
			special1++;
			if ( special1 > 150 ) A_FadeOut();
		}
		Wait;
	}
}

Class LightFlareThrown : BetaFlareThrown
{
	override void A_SpawnEffect()
	{
		Vector3 x, y, z;
		double a, s;
		[x, y, z] = dt_CoordUtil.GetAxes(pitch,angle,roll);
		for ( int i=0; i<4; i++ )
		{
			a = FRandom[BFlare](0,360);
			s = FRandom[BFlare](0,0.15);
			let t = Spawn("LightFlareParticle",pos+x*4+z);
			t.vel = (x+y*cos(a)*s+z*sin(a)*s).unit()*FRandom[BFlare](1,4);
		}
	}
	override void Tick()
	{
		Super.Tick();
		if ( GetAge() == 9 ) A_AttachLightDef('PLight','LIGHTFLARE');
	}
}

Class DarkFlareThrown : BetaFlareThrown
{
	override void A_SpawnEffect()
	{
		Vector3 x, y, z;
		double a, s;
		[x, y, z] = dt_CoordUtil.GetAxes(pitch,angle,roll);
		for ( int i=0; i<3; i++ )
		{
			a = FRandom[BFlare](0,360);
			s = FRandom[BFlare](0,0.5);
			let t = Spawn("DarkFlareParticle",pos+x*4+z);
			t.vel = (x+y*cos(a)*s+z*sin(a)*s).unit()*FRandom[BFlare](4,8);
		}
	}
	override void Tick()
	{
		Super.Tick();
		if ( GetAge() == 9 ) A_AttachLightDef('PLight','DARKFLARE');
	}
}

Class LightFlareParticle : UTSmoke
{
	Default
	{
		StencilColor "FFFFFF";
		Scale 0.2;
	}
}
Class DarkFlareParticle : UTSmoke
{
	Default
	{
		StencilColor "000000";
		Scale 2.2;
	}
}

Class Dampener : UnrealInventory
{
	static bool Active( Actor Owner )
	{
		let d = Dampener(Owner.FindInventory("Dampener"));
		if ( d && d.bActive ) return true;
		return false;
	}
	override bool Use( bool pickup )
	{
		if ( pickup ) return false;
		bActive = !bActive;
		Owner.A_PlaySound(bActive?"dampener/on":"dampener/off",CHAN_ITEM);
		return false;
	}
	override void DoEffect()
	{
		Super.DoEffect();
		if ( !bActive ) return;
		if ( DrainCharge(1) )
		{
			Owner.A_PlaySound("dampener/off",CHAN_ITEM);
			if ( Owner.CheckLocalView() ) Console.Printf(StringTable.Localize("$D_DAMPENER"));
			if ( Amount <= 0 ) DepleteOrDestroy();
		}
	}
	Default
	{
		Tag "$T_DAMPENER";
		Inventory.PickupMessage "$I_DAMPENER";
		Inventory.Icon "I_Dampen";
		Inventory.MaxAmount 3;
		UnrealInventory.Charge 1000;
	}
	States
	{
	Spawn:
		DAMP A 8 A_CheckProximity(1,"PlayerPawn",80,1,CPXF_ANCESTOR|CPXF_CHECKSIGHT);
		Wait;
		DAMP ABC 8;
		DAMP DEFGHIJ 3;
		DAMP D 0 A_CheckProximity(1,"PlayerPawn",80,0,CPXF_ANCESTOR|CPXF_CHECKSIGHT|CPXF_EXACT);
		Goto Spawn+4;
		DAMP CBA 8;
		Goto Spawn;
	}
}

Class Forcefield : UnrealInventory
{
	Default
	{
		Tag "$T_FORCEFIELD";
		Inventory.PickupMessage "$I_FORCEFIELD";
		Inventory.Icon "I_ForceF";
		Inventory.MaxAmount 3;
	}
	override bool Use( bool pickup )
	{
		if ( pickup ) return false;
		Vector3 origin = Owner.Vec2OffsetZ(0,0,Owner.player.viewz);
		FLineTraceData d;
		Owner.LineTrace(Owner.angle,90,Owner.pitch,TRF_ABSPOSITION,origin.z,origin.x,origin.y,data:d);
		if ( d.HitType != TRACE_HitNone ) origin = level.Vec3Offset(d.HitLocation,-d.HitDir*(GetDefaultByType("ForceFieldEffect").radius+8));
		else origin = d.HitLocation;
		Owner.LineTrace(0,GetDefaultByType("ForceFieldEffect").height/2,90,TRF_ABSPOSITION,origin.z,origin.x,origin.y,data:d);
		origin = d.HitLocation;
		let a = Spawn("ForceFieldEffect",origin);
		if ( !a.TestMobjLocation() )
		{
			if ( Owner.CheckLocalView() ) Console.Printf(StringTable.Localize("$M_FFNOROOM"));
			a.Destroy();
			return false;
		}
		a.target = Owner;
		a.angle = Owner.angle;
		return true;
	}
	override void PostBeginPlay()
	{
		Super.PostBeginPlay();
		tracer = Spawn("ForcefieldX",pos);
		tracer.angle = angle;
		tracer.target = self;
	}
	States
	{
	Spawn:
		FFPK A -1;
		Stop;
	}
}

Class ForcefieldX : AsmdAmmoX
{
	States
	{
	Spawn:
		FFPK A -1 Bright;
		Stop;
	}
}

Class ForceFieldLight : DynamicLight
{
	double cdown;
	Default
	{
		DynamicLight.Type "Point";
		+DYNAMICLIGHT.ATTENUATE;
		Args 0,0,0,100;
	}
	override void Tick()
	{
		Super.Tick();
		if ( !target )
		{
			Destroy();
			return;
		}
		SetOrigin(target.Vec3Offset(0,0,target.height/2.),true);
		if ( isFrozen() ) return;
		args[LIGHT_RED] = int(255*cdown);
		args[LIGHT_BLUE] = int(255*cdown);
		if ( target.bSOLID )
		{
			cdown = min(1.,cdown+1./27);
			return;
		}
		cdown = max(0.,cdown-1./35);
		if ( cdown <= 0. ) Destroy();
	}
}

Class ForcefieldEffect : Actor
{
	double nvol;
	int lct;

	Default
	{
		Tag "$T_FORCEFIELD";
		RenderStyle "Add";
		+NOGRAVITY;
		+NOTELEPORT;
		+DONTSPLASH;
		+CANNOTPUSH;
		+SHOOTABLE;
		+SOLID;
		+NODAMAGE;
		+NOBLOOD;
		Mass int.max;
		Health int.max;
		Radius 15;
		Height 45;
	}
	override int DamageMobj( Actor inflictor, Actor source, int damage, Name mod, int flags, double angle )
	{
		A_PlaySound("ffield/hit",CHAN_BODY);
		return Super.DamageMobj(inflictor,source,damage,mod,flags,angle);
	}
	override void PostBeginPlay()
	{
		Super.PostBeginPlay();
		A_PlaySound("ffield/on",CHAN_ITEM);
		A_PlaySound("ffield/active",CHAN_VOICE,0.6,true);
		let tracer = Spawn("ForceFieldLight",pos);
		tracer.target = self;
		lct = 24;
	}
	States
	{
	Spawn:
		FFLD ABCDEFGHIJ 3 Bright;
		#### # 700 Bright;
		#### # 35 Bright
		{
			A_UnsetShootable();
			A_UnsetSolid();
		}
		FFLD A 1 Bright A_PlaySound("ffield/hit",CHAN_VOICE);
		Stop;
	}
}

Class UFlashLight1 : DynamicLight
{
	int basecolor[3];
	Default
	{
		DynamicLight.Type "Point";
		+DynamicLight.SPOT;
		+DynamicLight.ATTENUATE;
		+DynamicLight.DONTLIGHTSELF;
		args 0,0,0,560;
		DynamicLight.SpotInnerAngle 3;
		DynamicLight.SpotOuterAngle 10;
	}
	override void Tick()
	{
		Super.Tick();
		if ( !target || !master )
		{
			Destroy();
			return;
		}
		if ( target.player ) SetOrigin(target.Vec2OffsetZ(0,0,target.player.viewz),true);
		else SetOrigin(target.vec3Offset(0,0,target.height*0.75),true);
		A_SetAngle(target.angle,SPF_INTERPOLATE);
		A_SetPitch(target.pitch,SPF_INTERPOLATE);
		if ( UnrealInventory(master) )
		{
			args[LIGHT_RED] = int(basecolor[0]*clamp(UnrealInventory(master).charge/1400.,0.,1.));
			args[LIGHT_GREEN] = int(basecolor[1]*clamp(UnrealInventory(master).charge/1400.,0.,1.));
			args[LIGHT_BLUE] = int(basecolor[2]*clamp(UnrealInventory(master).charge/1400.,0.,1.));
		}
		else
		{
			args[LIGHT_RED] = basecolor[0];
			args[LIGHT_GREEN] = basecolor[1];
			args[LIGHT_BLUE] = basecolor[2];
		}
		bDORMANT = (target.health <= 0);
		if ( Inventory(target) && target.bInvisible ) bDORMANT = true;
		// alert monsters hit by the light
		if ( GetClass() != "UFlashLight1" ) return;
		if ( !bDORMANT && target.player && (target.health > 0) )
		{
			BlockThingsIterator bt = BlockThingsIterator.Create(target,args[LIGHT_INTENSITY]);
			while ( bt.Next() )
			{
				if ( !bt.Thing || (Distance3D(bt.Thing) > args[LIGHT_INTENSITY]) ) continue;
				Vector3 aimdir = (cos(angle)*cos(pitch),sin(angle)*cos(pitch),-sin(pitch));
				Vector3 reldir = Vec3To(bt.Thing).unit();
				if ( (acos(aimdir dot reldir) < SpotOuterAngle+5) && bt.Thing.CheckSight(target) ) bt.Thing.LastHeard = target;
			}
		}
	}
}
Class UFlashLight2 : UFlashLight1
{
	Default
	{
		args 0,0,0,600;
		DynamicLight.SpotInnerAngle 0;
		DynamicLight.SpotOuterAngle 20;
	}
}

Class UFlashlight : UnrealInventory
{
	UFlashLight1 lt[2];

	Default
	{
		Tag "$T_FLASHLIGHT";
		Inventory.Icon "I_Flashl";
		Inventory.MaxAmount 3;
		Inventory.PickupMessage "$I_FLASHLIGHT";
		UnrealInventory.Charge 2800;
	}
	virtual void SetupLights()
	{
		lt[0].basecolor[0] = 255;
		lt[0].basecolor[1] = 240;
		lt[0].basecolor[2] = 224;
		lt[1].basecolor[0] = 128;
		lt[1].basecolor[1] = 120;
		lt[1].basecolor[2] = 112;
	}
	override bool Use( bool pickup )
	{
		if ( pickup ) return false;
		bActive = !bActive;
		Owner.A_PlaySound(bActive?"lite/pickup":"lite/off",CHAN_ITEM);
		if ( bActive )
		{
			if ( !lt[0] ) lt[0] = UFlashLight1(Spawn("UFlashLight1",owner.pos));
			lt[0].target = owner;
			lt[0].master = self;
			if ( !lt[1] ) lt[1] = UFlashLight1(Spawn("UFlashLight2",owner.pos));
			lt[1].target = owner;
			lt[1].master = self;
			SetupLights();
		}
		else
		{
			if ( lt[0] ) lt[0].Destroy();
			if ( lt[1] ) lt[1].Destroy();
		}
		return false;
	}
	override void DoEffect()
	{
		Super.DoEffect();
		if ( !bActive ) return;
		if ( DrainCharge(1) )
		{
			Owner.A_PlaySound("lite/off",CHAN_ITEM);
			if ( Owner.CheckLocalView() ) Console.Printf(StringTable.Localize("$D_FLASHLIGHT"));
			if ( Amount <= 0 ) DepleteOrDestroy();
			else
			{
				if ( lt[0] ) lt[0].Destroy();
				if ( lt[1] ) lt[1].Destroy();
			}
		}
	}
	override void DetachFromOwner()
	{
		if ( lt[0] ) lt[0].Destroy();
		if ( lt[1] ) lt[1].Destroy();
		Super.DetachFromOwner();
	}
	override void OwnerDied()
	{
		if ( lt[0] ) lt[0].Destroy();
		if ( lt[1] ) lt[1].Destroy();
		Super.OwnerDied();
	}
	override void Travelled()
	{
		Super.Travelled();
		if ( !bActive ) return;
		if ( !lt[0] ) lt[0] = UFlashLight1(Spawn("UFlashLight1",owner.pos));
		lt[0].target = owner;
		lt[0].master = self;
		if ( !lt[1] ) lt[1] = UFlashLight1(Spawn("UFlashLight2",owner.pos));
		lt[1].target = owner;
		lt[1].master = self;
		SetupLights();
	}
	States
	{
	Spawn:
		SLIT A -1;
		Stop;
	}
}

Class USearchlight : UFlashlight
{
	Default
	{
		Tag "$T_SEARCHLIGHT";
		Inventory.Icon "I_BigFl";
		Inventory.MaxAmount 1;
		Inventory.PickupMessage "$I_SEARCHLIGHT";
		Inventory.RespawnTics 1050;
		UnrealInventory.Charge 70000;
	}
	override void DoEffect()
	{
		Super.DoEffect();
		if ( !bActive ) return;
		if ( DrainCharge(1) )
		{
			Owner.A_PlaySound("lite/off",CHAN_ITEM);
			if ( Owner.CheckLocalView() ) Console.Printf(StringTable.Localize("$D_SEARCHLIGHT"));
			if ( Amount <= 0 ) DepleteOrDestroy();
			else
			{
				if ( lt[0] ) lt[0].Destroy();
				if ( lt[1] ) lt[1].Destroy();
			}
		}
	}
	override void SetupLights()
	{
		lt[0].basecolor[0] = 255;
		lt[0].basecolor[1] = 224;
		lt[0].basecolor[2] = 192;
		lt[0].SpotInnerAngle = 20;
		lt[0].SpotOuterAngle = 35;
		lt[0].args[3] = 750;
		lt[1].basecolor[0] = 128;
		lt[1].basecolor[1] = 112;
		lt[1].basecolor[2] = 96;
		lt[1].SpotOuterAngle = 50;
		lt[1].args[3] = 780;
	}
}

Class SentryItem : UnrealInventory
{
	Default
	{
		Tag "$T_SENTRY";
		Inventory.Icon "I_Sentry";
		Inventory.MaxAmount 1;
		Inventory.PickupMessage "$I_SENTRY";
		Inventory.RespawnTics 1050;
		UnrealInventory.Charge MinigunSentryBase.sentryhealth;
		+UNREALINVENTORY.DRAWSPECIAL;
	}
	override bool TryPickup( in out Actor toucher )
	{
		if ( !sting_msentry ) return false;	// not allowed
		return Super.TryPickup(toucher);
	}
	override void Tick()
	{
		Super.Tick();
		if ( sting_msentry ) return;
		if ( Owner ) Owner.RemoveInventory(self);
		else
		{
			let r = Spawn("Berserk",pos,ALLOW_REPLACE);
			r.spawnangle = spawnangle;
			r.spawnpoint = spawnpoint;
			r.angle = angle;
			r.pitch = pitch;
			r.roll = roll;
			r.special = special;
			r.args[0] = args[0];
			r.args[1] = args[1];
			r.args[2] = args[2];
			r.args[3] = args[3];
			r.args[4] = args[4];
			r.ChangeTid(tid);
			r.SpawnFlags = SpawnFlags&~MTF_SECRET;
			r.HandleSpawnFlags();
			r.SpawnFlags = SpawnFlags;
			r.bCountSecret = SpawnFlags&MTF_SECRET;
			r.vel = vel;
			r.master = master;
			r.target = target;
			r.tracer = tracer;
			r.bDropped = bDropped;
		}
		Destroy();
	}
	override bool HandlePickup( Inventory item )
	{
		if ( item.GetClass() == GetClass() ) return true;	// can never get more than one
		return Super.HandlePickup(item);
	}
	static bool TransferOwnership( Actor newowner, Actor sentry )
	{
		if ( sentry.master == newowner ) return false;
		if ( sentry.master ) sentry.master.TakeInventory("SentryItem",1);
		sentry.master = newowner;
		sentry.SetTag(String.Format(StringTable.Localize("$T_OWNEDSENTRY"),newowner.player.GetUserName()));
		sentry.tracer.A_ClearTarget();
		sentry.tracer.bFRIENDLY = true;
		sentry.tracer.SetFriendPlayer(newowner.player);
		let si = SentryItem(newowner.FindInventory("SentryItem"));
		if ( si )
		{
			si.bActive = true;
			si.tracer = sentry;
		}
		else
		{
			newowner.GiveInventory("SentryItem",1);
			let si = SentryItem(newowner.FindInventory("SentryItem"));
			si.bActive = true;
			si.tracer = sentry;
		}
		return true;
	}
	override void AttachToOwner( Actor other )
	{
		special1 = MinigunSentryBase.sentryammo;
		Super.AttachToOwner(other);
	}
	override bool Use( bool pickup )
	{
		if ( pickup ) return false;
		if ( bActive )
		{
			if ( Owner.Distance3D(tracer) > 80 )
			{
				if ( Owner.CheckLocalView() ) Console.Printf(StringTable.Localize("$M_NSTOOFAR"));
				return false;
			}
			tracer.SetStateLabel("PackUp");
			return false;
		}
		Vector3 origin = Owner.Vec2OffsetZ(0,0,Owner.player.viewz);
		FLineTraceData d;
		Owner.LineTrace(Owner.angle,80,Owner.pitch,TRF_ABSPOSITION,origin.z,origin.x,origin.y,data:d);
		if ( d.HitType != TRACE_HitNone )
		{
			Vector3 normal = -d.HitDir;
			if ( d.HitType == TRACE_HitFloor )
			{
				if ( d.Hit3DFloor ) normal = -d.Hit3DFloor.top.Normal;
				else normal = d.HitSector.floorplane.Normal;
			}
			else if ( d.HitType == TRACE_HitCeiling )
			{
				if ( d.Hit3DFloor ) normal = -d.Hit3DFloor.bottom.Normal;
				else normal = d.HitSector.ceilingplane.Normal;
			}
			else if ( d.HitType == TRACE_HitWall )
			{
				normal = (-d.HitLine.delta.y,d.HitLine.delta.x,0).unit();
				if ( !d.LineSide ) normal *= -1;
			}
			origin = level.Vec3Offset(d.HitLocation,normal*20);
		}
		else origin = level.Vec3Offset(d.HitLocation,-d.HitDir*20);
		Owner.LineTrace(0,56,90,TRF_ABSPOSITION,origin.z,origin.x,origin.y,data:d);
		origin = d.HitLocation;
		let a = Spawn("MinigunSentryBase",origin);
		if ( !a.TestMobjLocation() )
		{
			if ( Owner.CheckLocalView() ) Console.Printf(StringTable.Localize("$M_MSNOROOM"));
			a.Destroy();
			return false;
		}
		if ( a.pos.z-a.floorz > 50 )
		{
			if ( Owner.CheckLocalView() ) Console.Printf(StringTable.Localize("$M_MSNOFLOOR"));
			a.Destroy();
			return false;
		}
		F3DFloor ff = null;
		for ( int i=0; i<a.floorsector.Get3DFloorCount(); i++ )
		{
			if ( a.floorsector.Get3DFloor(i).top.ZAtPoint(pos.xy) > floorz ) continue;
			ff = a.floorsector.Get3DFLoor(i);
			break;
		}
		Vector3 normal;
		if ( ff ) normal = -ff.top.Normal;
		else normal = a.floorsector.floorplane.Normal;
		if ( normal.z < 0.9 )
		{
			if ( Owner.CheckLocalView() ) Console.Printf(StringTable.Localize("$M_MSNOFLAT"));
			a.Destroy();
			return false;
		}
		bActive = true;
		bUNTOSSABLE = true;
		bUNDROPPABLE = true;
		tracer = a;
		a.Health = Charge;
		a.special1 = special1;
		a.master = Owner;
		a.angle = Owner.angle;
		a.pitch = 0;
		a.roll = 0;
		return false;
	}
	override void DoEffect()
	{
		Super.DoEffect();
		if ( !bActive )
		{
			if ( !(level.maptime%10) ) Charge = min(DefaultCharge,Charge+1);
			return;
		}
		if ( !tracer )
		{
			bActive = false;
			bUNTOSSABLE = false;
			bUNDROPPABLE = false;
			return;
		}
		Charge = tracer.Health;
		special1 = tracer.special1;
		if ( Charge <= 0 ) Destroy();
	}
	override void Travelled()
	{
		Super.Travelled();
		if ( bActive && !tracer )
		{
			bUNTOSSABLE = false;
			bUNDROPPABLE = false;
			Destroy();
		}
	}
	States
	{
	Spawn:
		SENT A -1;
		Stop;
	}
}

// overlay for muzzle flash
Class MinigunSentryX : Actor
{
	Default
	{
		RenderStyle "Add";
		+NOGRAVITY;
		+NOBLOCKMAP;
		+INTERPOLATEANGLES;
	}
	override void Tick()
	{
		Super.Tick();
		if ( !tracer || !tracer.InStateSequence(tracer.CurState,tracer.FindState("MissileLoop")) )
		{
			Destroy();
			return;
		}
		SetOrigin(tracer.pos,true);
		angle = tracer.angle;
		pitch = tracer.pitch;
		roll = tracer.roll;
	}
	States
	{
	Spawn:
		SENF A 1 Bright;
		TNT1 A 2;
		SENF D 1 Bright;
		TNT1 A 2;
		SENF G 1 Bright;
		TNT1 A 2;
		SENF J 1 Bright;
		TNT1 A 2;
		SENF M 1 Bright;
		TNT1 A 2;
		SENF P 1 Bright;
		TNT1 A 2;
		Stop;
	}
}

// The "head" of the sentry, attaches to the body
Class MinigunSentry : Actor
{
	const maxangle = 80;
	const maxpitch = 40;

	Default
	{
		+NOGRAVITY;
		+NOBLOCKMAP;
		+INTERPOLATEANGLES;
		+FRIENDLY;
	}
	override void Tick()
	{
		Super.Tick();
		if ( !master )
		{
			Destroy();
			return;
		}
		Vector3 x, y, z;
		[x, y, z] = dt_CoordUtil.GetAxes(master.pitch,master.angle,master.roll);
		SetOrigin(level.Vec3Offset(master.pos,z*38),true);
	}
	double _PitchTo( Actor other )
	{
		if ( !other ) return 0;
		Vector3 otherpos = level.Vec3Diff(pos,other.Vec3Offset(0,0,other.height/2));
		double dist = otherpos.length();
		if ( dist > 0 ) return -asin(otherpos.z/dist);
		return 0;
	}
	bool TargetVisible()
	{
		if ( !target || (target.Health <= 0) || !CheckSight(target) ) return false;
		double angledelta = DeltaAngle(master.angle,AngleTo(target));
		double pitchdelta = DeltaAngle(master.pitch,_PitchTo(target));
		if ( (abs(angledelta) > maxangle) || (abs(pitchdelta) > maxpitch) ) return false;
		return true;
	}
	void A_SentryFaceTarget()
	{
		if ( !TargetVisible() ) return;
		double angledelta = DeltaAngle(angle,AngleTo(target));
		double pitchdelta = DeltaAngle(pitch,_PitchTo(target));
		double angleturn = clamp(abs(angledelta)*0.1,1,5);
		double pitchturn = clamp(abs(pitchdelta)*0.1,1,5);
		angle = clamp(angle+clamp(angledelta,-angleturn,angleturn),master.angle-maxangle,master.angle+maxangle);
		pitch = clamp(pitch+clamp(pitchdelta,-pitchturn,pitchturn),master.pitch-maxpitch,master.pitch+maxpitch);
	}
	void A_SentryFaceDir( double dest, statelabel next )
	{
		double angledelta = DeltaAngle(angle,master.angle+dest);
		double pitchdelta = DeltaAngle(pitch,master.pitch);
		if ( max(abs(angledelta),abs(pitchdelta)) < 2 )
		{
			angle = master.angle+dest;
			pitch = master.pitch;
			SetStateLabel(next);
			return;
		}
		angle = clamp(angle+clamp(angledelta,-2,2),master.angle-maxangle,master.angle+maxangle);
		pitch = clamp(pitch+clamp(pitchdelta,-2,2),master.pitch-maxpitch,master.pitch+maxpitch);
	}
	void A_SentryAttack()
	{
		master.special1 = special1 = max(0,special1-1);
		if ( (special1 <= 0) && master.master && master.master.CheckLocalView() ) Console.Printf(StringTable.Localize("$M_SENTRYDRY"));
		A_SentryFaceTarget();
		master.A_AlertMonsters(0,AMF_TARGETEMITTER);
		A_PlaySound("sentry/fire",CHAN_WEAPON);
		Vector3 x, y, z, origin;
		[x, y, z] = dt_CoordUtil.GetAxes(pitch,angle,roll);
		origin = level.Vec3Offset(pos,x*24+z*8);
		double a = FRandom[Sentry](0,360), s = FRandom[Sentry](0,0.04);
		Vector3 dir = (x+y*cos(a)*s+z*sin(a)*s).unit();
		FLineTraceData d;
		master.LineTrace(atan2(dir.y,dir.x),10000,asin(-dir.z),TRF_ABSPOSITION,origin.z,origin.x,origin.y,d);
		if ( d.HitType == TRACE_HitActor )
		{
			int dmg = 17;
			dmg = d.HitActor.DamageMobj(self,master,dmg,'shot',DMG_USEANGLE|DMG_THRUSTLESS,atan2(d.HitDir.y,d.HitDir.x));
			double mm = 3000;
			if ( FRandom[Sentry](0,1) < 0.2 ) mm *= 5;
			UTMainHandler.DoKnockback(d.HitActor,d.HitDir,mm);
			if ( d.HitActor.bNOBLOOD )
			{
				let p = Spawn("BulletImpact",d.HitLocation);
				p.angle = atan2(d.HitDir.y,d.HitDir.x)+180;
				p.pitch = asin(d.HitDir.z);
			}
			else
			{
				d.HitActor.TraceBleed(dmg,self);
				d.HitActor.SpawnBlood(d.HitLocation,atan2(d.HitDir.y,d.HitDir.x)+180,dmg);
			}
		}
		else if ( d.HitType != TRACE_HitNone )
		{
			Vector3 hitnormal = -d.HitDir;
			if ( d.HitType == TRACE_HitFloor ) hitnormal = d.HitSector.floorplane.Normal;
			else if ( d.HitType == TRACE_HitCeiling ) hitnormal = d.HitSector.ceilingplane.Normal;
			else if ( d.HitType == TRACE_HitWall )
			{
				hitnormal = (-d.HitLine.delta.y,d.HitLine.delta.x,0).unit();
				if ( !d.LineSide ) hitnormal *= -1;
			}
			let p = Spawn("BulletImpact",d.HitLocation+hitnormal*0.01);
			p.angle = atan2(hitnormal.y,hitnormal.x);
			p.pitch = asin(-hitnormal.z);
			if ( d.HitLine ) d.HitLine.RemoteActivate(self,d.LineSide,SPAC_Impact,d.HitLocation);
		}
		for ( int i=0; i<3; i++ )
		{
			let s = Spawn("UTSmoke",origin);
			s.alpha *= 0.5;
		}
		let c = Spawn("UCasing",level.Vec3Offset(pos,-x*3+y*2+z*1.5));
		c.vel = x*FRandom[Junk](-1.5,1.5)+y*FRandom[Junk](2,4)+z*FRandom[Junk](2,3);
	}
	States
	{
	Spawn:
		SENT A 15;
		SENT A 0
		{
			A_PlaySound("sentry/raise");
			master.A_AlertMonsters(0,AMF_TARGETEMITTER);
		}
		SENR ABCDEFGHIJKLMNO 3;
		Goto Idle;
	Idle:
		SENI A 0
		{
			A_PlaySound("sentry/move",CHAN_BODY,0.4,true,pitch:0.8);
			if ( specialf1 > 0 ) specialf1 = -maxangle;
			else specialf1 = maxangle;
			special2 = 0;
		}
		SENI A 1
		{
			A_LookEx(LOF_NOSOUNDCHECK|LOF_NOJUMP);
			A_SentryFaceDir(specialf1,"IdleStop");
			return A_JumpIf(TargetVisible(),"See");
		}
		Wait;
	IdleStop:
		SENI A 0 A_PlaySound("sentry/movestop",CHAN_BODY,0.4,pitch:0.8);
		SENI A 1
		{
			A_LookEx(LOF_NOSOUNDCHECK|LOF_NOJUMP);
			if ( special2++ > 20 ) return ResolveState("Idle");
			return A_JumpIf(TargetVisible(),"See");
		}
		Wait;
	See:
		SENI A 0 A_PlaySound("sentry/movestop",CHAN_BODY,0.4,pitch:0.8);
		SENI A 1
		{
			if ( !TargetVisible() )
			{
				A_ClearTarget();
				return ResolveState("Idle");
			}
			if ( special1 > 0 ) A_Chase(null,"Missile",flags:CHF_DONTMOVE|CHF_NODIRECTIONTURN|CHF_DONTTURN);
			A_SentryFaceTarget();
			return ResolveState(null);
		}
		Wait;
	Missile:
		SENW A 0
		{
			A_PlaySound("sentry/wind",looping:true);
			master.A_AlertMonsters(0,AMF_TARGETEMITTER);
			master.SetStateLabel("Missile");
		}
		SENW ABCDEFGHIJKLMNOPQR 1 A_SentryFaceTarget();
		Goto MissileLoop;
	MissileLoop:
		SENF A 0
		{
			let f = Spawn("MinigunSentryX",pos);
			f.angle = angle;
			f.pitch = pitch;
			f.roll = roll;
			f.tracer = self;
		}
		SENF A 0 A_JumpIf(!TargetVisible()||(special1<=0),"MissileEnd");
		SENF A 1 A_SentryAttack();
		SENF BC 1 A_SentryFaceTarget();
		SENF D 0 A_JumpIf(!TargetVisible()||(special1<=0),"MissileEnd");
		SENF D 1 A_SentryAttack();
		SENF EF 1 A_SentryFaceTarget();
		SENF G 0 A_JumpIf(!TargetVisible()||(special1<=0),"MissileEnd");
		SENF G 1 A_SentryAttack();
		SENF HI 1 A_SentryFaceTarget();
		SENF J 0 A_JumpIf(!TargetVisible()||(special1<=0),"MissileEnd");
		SENF J 1 A_SentryAttack();
		SENF KL 1 A_SentryFaceTarget();
		SENF M 0 A_JumpIf(!TargetVisible()||(special1<=0),"MissileEnd");
		SENF M 1 A_SentryAttack();
		SENF NO 1 A_SentryFaceTarget();
		SENF P 0 A_JumpIf(!TargetVisible()||(special1<=0),"MissileEnd");
		SENF P 1 A_SentryAttack();
		SENF QR 1 A_SentryFaceTarget();
		Loop;
	MissileEnd:
		SENU A 0
		{
			A_LookEx(LOF_NOSOUNDCHECK|LOF_NOJUMP);
			if ( TargetVisible() && (special1>0) ) return ResolveState("MissileLoop");
			A_PlaySound("sentry/unwind");
			master.SetStateLabel("MissileEnd");
			return ResolveState(null);
		}
		SENU ABCDEFGHIJKLMNOPQR 1 A_SentryFaceTarget();
		SENI A 0 A_JumpIf(TargetVisible(),"See");
		Goto Idle;
	PackUp:
		SENI A 1 A_SentryFaceDir(0,1);
		Wait;
		SENI A 0
		{
			A_PlaySound("sentry/raise");
			master.A_AlertMonsters(0,AMF_TARGETEMITTER);
			master.SetStateLabel("DoPackUp");
		}
		SENR ONMLKJIHGFEDCBA 3;
		Stop;
	}
}

Class SentryFragment : Actor
{
	int deadtimer;
	double rollvel, anglevel, pitchvel;
	double heat;

	Default
	{
		Radius 2;
		Height 2;
		+NOBLOCKMAP;
		+MISSILE;
		+MOVEWITHSECTOR;
		+THRUACTORS;
		+NOTELEPORT;
		+DONTSPLASH;
		+INTERPOLATEANGLES;
		+USEBOUNCESTATE;
		BounceType "Doom";
		BounceFactor 0.3;
		Gravity 0.35;
	}
	override void PostBeginPlay()
	{
		Super.PostBeginPlay();
		deadtimer = 0;
		anglevel = FRandom[Junk](10,30)*RandomPick[Junk](-1,1);
		pitchvel = FRandom[Junk](10,30)*RandomPick[Junk](-1,1);
		rollvel = FRandom[Junk](10,30)*RandomPick[Junk](-1,1);
		frame = Random[Junk](0,5);
		scale *= Frandom[Junk](0.8,1.2);
		heat = 1.5;
	}
	override void Tick()
	{
		Super.Tick();
		if ( isFrozen() ) return;
		if ( heat > 0 )
		{
			heat -= FRandom[Junk](0.003,0.006);
			let s = Spawn("UTSmoke",pos);
			s.alpha *= min(1.,heat)*0.6*alpha;
		}
		if ( InStateSequence(CurState,ResolveState("Death")) )
		{
			deadtimer++;
			if ( deadtimer > 300 ) A_FadeOut(0.05);
			return;
		}
	}
	States
	{
	Spawn:
		CHIP # 1
		{
			angle += anglevel;
			pitch += pitchvel;
			roll += rollvel;
		}
		Loop;
	Bounce:
		CHIP # 0
		{
			anglevel = FRandom[Junk](10,30)*RandomPick[Junk](-1,1);
			pitchvel = FRandom[Junk](10,30)*RandomPick[Junk](-1,1);
			rollvel = FRandom[Junk](10,30)*RandomPick[Junk](-1,1);
			vel = (vel.unit()+(FRandom[Junk](-.2,.2),FRandom[Junk](-.2,.2),FRandom[Junk](-.2,.2))).unit()*vel.length();
			A_PlaySound("vfrag/bounce",CHAN_BODY,min(1.,vel.length()*0.1),pitch:FRandom[Junk](0.6,1.4));
		}
		Goto Spawn;
	Death:
		CHIP # -1
		{
			pitch = int(pitch/180)*180;
			roll = int(roll/180)*180;
		}
		Stop;
	Dummy:
		CHIP ABCDEF -1;
		Stop;
	}
}

Class SentryXLight : PaletteLight
{
	Default
	{
		ReactionTime 30;
		Args 0,0,0,120;
	}
}

Class SentryBoom : Actor
{
	Default
	{
		RenderStyle "Add";
		Scale 3.2;
		+NOGRAVITY;
		+NOBLOCKMAP;
		+NODAMAGETHRUST;
		+FORCERADIUSDMG;
		+FORCEXYBILLBOARD;
	}
	override void PostBeginPlay()
	{
		Super.PostBeginPlay();
		A_Explode(80,250);
		A_PlaySound("sentry/explode");
		UTMainHandler.DoBlast(self,250,70000);
		double ang, pt;
		for ( int i=0; i<16; i++ )
		{
			let f = Spawn("SentryFragment",Vec3Offset(FRandom[EFrag](-8,8),FRandom[EFrag](-8,8),FRandom[EFrag](4,40)));
			ang = FRandom[EFrag](0,360);
			pt = FRandom[EFrag](-90,90);
			f.vel = (cos(ang)*cos(pt),sin(ang)*cos(pt),-sin(pt))*FRandom[EFrag](8,15);
		}
		Scale.x *= RandomPick[EFrag](-1,1);
		Scale.y *= RandomPick[EFrag](-1,1);
		int numpt = Random[ExploS](20,25);
		for ( int i=0; i<numpt; i++ )
		{
			Vector3 pvel = (FRandom[ExploS](-1,1),FRandom[ExploS](-1,1),FRandom[ExploS](-1,1)).unit()*FRandom[ExploS](1,3);
			let s = Spawn("UTSmoke",pos);
			s.vel = pvel;
		}
		numpt = Random[ExploS](10,20);
		for ( int i=0; i<numpt; i++ )
		{
			Vector3 pvel = (FRandom[ExploS](-1,1),FRandom[ExploS](-1,1),FRandom[ExploS](-1,1)).unit()*FRandom[ExploS](2,6);
			let s = Spawn("UTSpark",pos);
			s.vel = pvel;
		}
		numpt = Random[ExploS](20,30);
		for ( int i=0; i<numpt; i++ )
		{
			Vector3 pvel = (FRandom[ExploS](-1,1),FRandom[ExploS](-1,1),FRandom[ExploS](-1,1)).unit()*FRandom[ExploS](2,12);
			let s = Spawn("UTChip",pos);
			s.vel = pvel;
			s.scale *= FRandom[ExploS](0.9,2.7);
		}
		Spawn("FlareXLight",pos);
	}
	States
	{
	Spawn:
		TNT1 A 0 NoDelay A_Jump(256,"Explo1","Explo2","Explo3","Explo4","Explo5");
	Explo1:
		EXP1 ABCDEFGH 3 Bright;
		Stop;
	Explo2:
		EXP2 ABCDEFGH 3 Bright;
		Stop;
	Explo3:
		EXP3 ABCDEFGH 3 Bright;
		Stop;
	Explo4:
		EXP4 ABCDEFGH 3 Bright;
		Stop;
	Explo5:
		EXP5 ABCDEFGH 3 Bright;
		Stop;
	}
}

// The body of the sentry
Class MinigunSentryBase : Actor
{
	const sentryammo = 200;
	const sentryhealth = 500;
	int rememberedplayer;

	Default
	{
		Tag "$T_SENTRY";
		Health sentryhealth;
		Mass int.max;
		Radius 8;
		Height 46;
		+SOLID;
		+SHOOTABLE;
		+NOBLOOD;
		+DONTTHRUST;
	}
	override void PostBeginPlay()
	{
		Super.PostBeginPlay();
		if ( master && master.player )
		{
			SetTag(String.Format(StringTable.Localize("$T_OWNEDSENTRY"),master.player.GetUserName()));
			rememberedplayer = master.playernumber();
		}
		else
		{
			rememberedplayer = -1;
			special1 = sentryammo;	// rogue sentries need ammo assigned
		}
		tracer = Spawn("MinigunSentry",pos);
		tracer.special1 = special1;
		tracer.master = self;
		tracer.angle = angle;
		tracer.pitch = pitch;
		tracer.roll = roll;
		Vector3 x, y, z;
		[x, y, z] = dt_CoordUtil.GetAxes(pitch,angle,roll);
		tracer.SetOrigin(level.Vec3Offset(pos,z*38),false);
		if ( !deathmatch )
		{
			if ( !master || !master.player ) tracer.bFRIENDLY = bFRIENDLY;
			bFRIENDLY = false;
			return;
		}
		if ( master && master.player ) tracer.SetFriendPlayer(master.player);
		else tracer.bFRIENDLY = bFRIENDLY;
		bFRIENDLY = false;
	}
	override void Tick()
	{
		Super.Tick();
		if ( !sting_msentry )
		{
			if ( tracer ) tracer.Destroy();
			Destroy();
			return;
		}
		// hub return support
		if ( !master && (rememberedplayer != -1) && playeringame[rememberedplayer] )
		{
			master = players[rememberedplayer].mo;
			let si = SentryItem(master.FindInventory("SentryItem"));
			if ( si )
			{
				si.bActive = true;
				si.tracer = self;
			}
			else
			{
				master.GiveInventory("SentryItem",1);
				si = SentryItem(master.FindInventory("SentryItem"));
				si.bActive = true;
				si.tracer = self;
			}
		}
	}
	override string GetObituary( Actor victim, Actor inflictor, Name mod, bool playerattack )
	{
		if ( victim == master ) return StringTable.Localize("$O_OWNSENTRY");
		else if ( master && master.player ) return String.Format(StringTable.Localize("$O_SENTRY"),GetTag());
		return StringTable.Localize("$O_ROGUESENTRY");
	}
	override bool Used( Actor user )
	{
		if ( !user.player || !InStateSequence(CurState,FindState("Idle")) ) return false;
		if ( abs(DeltaAngle(angle,AngleTo(user))) < 120 ) return false;
		if ( !master || ((user != master) && master.IsHostile(user)) )
		{
			if ( master && master.CheckLocalView() )
				Console.Printf(StringTable.Localize("$M_SENTRYHIJACK"));
			if ( SentryItem.TransferOwnership(user,self) )
				return false;	// on first touch only transfer ownership
		}
		let amo = user.FindInventory("UMiniAmmo");
		if ( !amo || (amo.Amount <= 0) || (tracer.special1 >= sentryammo) ) return false;
		A_PlaySound("misc/i_pkup",CHAN_ITEM);
		int xammo = min(sentryammo-tracer.special1,amo.Amount);
		special1 = tracer.special1 += xammo;
		amo.Amount -= xammo;
		return false;
	}
	override int DamageMobj( Actor inflictor, Actor source, int damage, Name mod, int flags, double angle )
	{
		if ( Health-damage <= 0 )
		{
			if ( master && master.CheckLocalView() )
				Console.Printf(StringTable.Localize("$M_SENTRYDOWN"));
			if ( master ) master.TakeInventory("SentryItem",1);
			if ( tracer ) tracer.Destroy();
		}
		int dmg = Super.DamageMobj(inflictor,source,damage,mod,flags,angle);
		if ( tracer && !tracer.target ) tracer.target = target;
		return dmg;
	}
	States
	{
	Spawn:
		SENT A 15;
		SENR ABCDEFGHIJKLMNO 3;
		Goto Idle;
	Idle:
		SENI A 1;
		Wait;
	Missile:
		SENW ABCDEFGHIJKLMNOPQR 1;
		Goto MissileLoop;
	MissileLoop:
		SENF ABCDEFGHIJKLMNOPQR 1;
		Loop;
	MissileEnd:
		SENU ABCDEFGHIJKLMNOPQR 1;
		Goto Idle;
	PackUp:
		SENI A -1
		{
			tracer.SetStateLabel("PackUp");
		}
		Stop;
	DoPackUp:
		SENR ONMLKJIHGFEDCBA 3;
		TNT1 A 1
		{
			if ( !master ) return ResolveState(null);
			let si = SentryItem(master.FindInventory("SentryItem"));
			if ( si )
			{
				si.charge = Health;
				si.special1 = special1;
			}
			else
			{
				master.GiveInventory("SentryItem",1);
				let si = SentryItem(master.FindInventory("SentryItem"));
				si.charge = Health;
				si.special1 = special1;
			}
			return ResolveState(null);
		}
		Stop;
	Death:
		TNT1 A 1 Spawn("SentryBoom",Vec3Offset(0,0,height/2));
		Stop;
	}
}

// original fun-size stationary version
Class SentryGunItem : UnrealInventory
{
	Default
	{
		Tag "$T_OSENTRY";
		Inventory.Icon "I_OSntry";
		Inventory.MaxAmount 2;
		Inventory.PickupMessage "$I_OSENTRY";
		Inventory.RespawnTics 1050;
	}
	override bool TryPickup( in out Actor toucher )
	{
		if ( !sting_proto ) return false;	// not allowed
		return Super.TryPickup(toucher);
	}
	override void Tick()
	{
		Super.Tick();
		if ( sting_proto ) return;
		if ( Owner ) Owner.RemoveInventory(self);
		Destroy();
	}
	override bool Use( bool pickup )
	{
		if ( pickup ) return false;
		Vector3 origin = Owner.Vec2OffsetZ(0,0,Owner.player.viewz);
		FLineTraceData d;
		Owner.LineTrace(Owner.angle,60,Owner.pitch,TRF_ABSPOSITION,origin.z,origin.x,origin.y,data:d);
		if ( d.HitType != TRACE_HitNone )
		{
			Vector3 normal = -d.HitDir;
			if ( d.HitType == TRACE_HitFloor )
			{
				if ( d.Hit3DFloor ) normal = -d.Hit3DFloor.top.Normal;
				else normal = d.HitSector.floorplane.Normal;
			}
			else if ( d.HitType == TRACE_HitCeiling )
			{
				if ( d.Hit3DFloor ) normal = -d.Hit3DFloor.bottom.Normal;
				else normal = d.HitSector.ceilingplane.Normal;
			}
			else if ( d.HitType == TRACE_HitWall )
			{
				normal = (-d.HitLine.delta.y,d.HitLine.delta.x,0).unit();
				if ( !d.LineSide ) normal *= -1;
			}
			origin = level.Vec3Offset(d.HitLocation,normal*20);
		}
		else origin = level.Vec3Offset(d.HitLocation,-d.HitDir*20);
		Owner.LineTrace(0,56,90,TRF_ABSPOSITION,origin.z,origin.x,origin.y,data:d);
		origin = d.HitLocation;
		let a = Spawn("SentryGun",origin);
		if ( !a.TestMobjLocation() )
		{
			if ( Owner.CheckLocalView() ) Console.Printf(StringTable.Localize("$M_MSNOROOM"));
			a.Destroy();
			return false;
		}
		if ( a.pos.z-a.floorz > 50 )
		{
			if ( Owner.CheckLocalView() ) Console.Printf(StringTable.Localize("$M_MSNOFLOOR"));
			a.Destroy();
			return false;
		}
		F3DFloor ff = null;
		for ( int i=0; i<a.floorsector.Get3DFloorCount(); i++ )
		{
			if ( a.floorsector.Get3DFloor(i).top.ZAtPoint(pos.xy) > floorz ) continue;
			ff = a.floorsector.Get3DFLoor(i);
			break;
		}
		Vector3 normal;
		if ( ff ) normal = -ff.top.Normal;
		else normal = a.floorsector.floorplane.Normal;
		if ( normal.z < 0.9 )
		{
			if ( Owner.CheckLocalView() ) Console.Printf(StringTable.Localize("$M_MSNOFLAT"));
			a.Destroy();
			return false;
		}
		tracer = a;
		a.master = Owner;
		a.angle = Owner.angle;
		a.pitch = 0;
		a.roll = 0;
		return true;
	}
	States
	{
	Spawn:
		SENT A -1;
		Stop;
	}
}

Class SentryGunX : Actor
{
	Default
	{
		RenderStyle "Add";
		+NOGRAVITY;
		+NOBLOCKMAP;
		+INTERPOLATEANGLES;
	}
	override void Tick()
	{
		Super.Tick();
		if ( !tracer || !tracer.InStateSequence(tracer.CurState,tracer.FindState("Fire")) )
		{
			Destroy();
			return;
		}
		SetOrigin(tracer.pos,true);
		angle = tracer.angle;
		pitch = tracer.pitch;
		roll = tracer.roll;
	}
	States
	{
	Spawn:
		SENF A 2 Bright;
		TNT1 A 2;
		SENF C 2 Bright;
		TNT1 A 2;
		Stop;
	}
}

Class SentryGun : Actor
{
	int rememberedplayer;
	Default
	{
		Tag "$T_OSENTRY";
		Health 50;
		Mass int.max;
		Radius 6;
		Height 18;
		+SOLID;
		+SHOOTABLE;
		+NOBLOOD;
		+DONTTHRUST;
		+FRIENDLY;
	}
	override void PostBeginPlay()
	{
		Super.PostBeginPlay();
		if ( master && master.player )
		{
			SetTag(String.Format(StringTable.Localize("$T_OWNEDOSENTRY"),master.player.GetUserName()));
			rememberedplayer = master.playernumber();
		}
		else rememberedplayer = -1;
		if ( !deathmatch )
		{
			if ( !master || !master.player ) bFRIENDLY = false;
			return;
		}
		if ( master && master.player ) SetFriendPlayer(master.player);
		else bFRIENDLY = false;
	}
	override string GetObituary( Actor victim, Actor inflictor, Name mod, bool playerattack )
	{
		if ( victim == master ) return StringTable.Localize("$O_OWNOSENTRY");
		else if ( master && master.player ) return String.Format(StringTable.Localize("$O_OSENTRY"),GetTag());
		return StringTable.Localize("$O_ROGUEOSENTRY");
	}
	override void Tick()
	{
		Super.Tick();
		if ( !sting_proto )
		{
			Destroy();
			return;
		}
		if ( !master && (rememberedplayer != -1) && playeringame[rememberedplayer] )
			master = players[rememberedplayer].mo;
	}
	bool IsEnemy( Actor a )
	{
		if ( !a || !a.bSHOOTABLE || !a.bISMONSTER || (a.Health <= 0) ) return false;
		return IsHostile(a);
	}
	bool HasTarget()
	{
		// check for targets in range
		let ti = ThinkerIterator.Create("Actor");
		Actor a;
		while ( a = Actor(ti.Next()) )
		{
			if ( !IsEnemy(a) ) continue;
			Vector3 x = (cos(angle)*cos(pitch),sin(angle)*cos(pitch),-sin(pitch));
			Vector3 vecto = level.Vec3Diff(Vec3Offset(0,0,16),a.Vec3Offset(0,0,a.height/2));
			double distto = vecto.length();
			Vector3 dirto = vecto/distto;
			double angleto = atan2(dirto.y,dirto.x);
			double pitchto = asin(-dirto.z);
			if ( (distto < 6000) && (dirto dot x > 0.95) && !LineTrace(angleto,distto,pitchto,TRF_THRUACTORS,16) )
			{
				target = a;
				return true;
			}
		}
		// check for targets in a straight line
		FLineTraceData d;
		LineTrace(angle,200,pitch,0,16,data:d);
		if ( (d.HitType == TRACE_HitActor) && IsEnemy(d.HitActor) )
		{
			target = d.HitActor;
			return true;
		}
		target = null;
		return false;
	}
	// if there's a target and we have ammo, jump to first state (if set)
	// if there's no target but we still have ammo, jump to second state (if set)
	// if we have no ammo, jump to third state (if set)
	void A_SentryGunCheck( statelabel actstate = null, statelabel idlestate = null, statelabel failstate = null )
	{
		if ( !(GetAge()%35) ) special1++;
		if ( (special1 >= 200) && failstate ) SetStateLabel(failstate);
		else if ( HasTarget() && actstate ) SetStateLabel(actstate);
		else if ( !HasTarget() && idlestate ) SetStateLabel(idlestate);
	}
	void A_SentryGunAttack( statelabel failstate = null )
	{
		special1++;
		if ( ((special1 >= 200) || !HasTarget()) && failstate )
		{
			A_StopSound(CHAN_BODY);
			SetStateLabel(failstate);
			return;
		}
		A_AlertMonsters(0,AMF_TARGETEMITTER);
		A_PlaySound("sentry/fire",CHAN_WEAPON,pitch:1.6);
		Vector3 x, y, z, origin;
		[x, y, z] = dt_CoordUtil.GetAxes(pitch,angle,roll);
		origin = level.Vec3Offset(pos,x*12+z*16);
		double a = FRandom[Sentry](0,360), s = FRandom[Sentry](0,0.05);
		Vector3 nx = target?Vec3To(target).unit():x;
		Vector3 dir = (nx+y*cos(a)*s+z*sin(a)*s).unit();
		FLineTraceData d;
		LineTrace(atan2(dir.y,dir.x),10000,asin(-dir.z),TRF_ABSPOSITION,origin.z,origin.x,origin.y,d);
		if ( d.HitType == TRACE_HitActor )
		{
			int dmg = Random[OSentry](6,17);
			dmg = d.HitActor.DamageMobj(self,self,dmg,'shot',DMG_USEANGLE|DMG_THRUSTLESS,atan2(d.HitDir.y,d.HitDir.x));
			double mm = 3000;
			if ( FRandom[OSentry](0,1) < 0.2 ) mm *= 5;
			UTMainHandler.DoKnockback(d.HitActor,d.HitDir,mm);
			if ( d.HitActor.bNOBLOOD )
			{
				let p = Spawn("BulletImpact",d.HitLocation);
				p.angle = atan2(d.HitDir.y,d.HitDir.x)+180;
				p.pitch = asin(d.HitDir.z);
			}
			else
			{
				d.HitActor.TraceBleed(dmg,self);
				d.HitActor.SpawnBlood(d.HitLocation,atan2(d.HitDir.y,d.HitDir.x)+180,dmg);
			}
		}
		else if ( d.HitType != TRACE_HitNone )
		{
			Vector3 hitnormal = -d.HitDir;
			if ( d.HitType == TRACE_HitFloor ) hitnormal = d.HitSector.floorplane.Normal;
			else if ( d.HitType == TRACE_HitCeiling ) hitnormal = d.HitSector.ceilingplane.Normal;
			else if ( d.HitType == TRACE_HitWall )
			{
				hitnormal = (-d.HitLine.delta.y,d.HitLine.delta.x,0).unit();
				if ( !d.LineSide ) hitnormal *= -1;
			}
			let p = Spawn("BulletImpact",d.HitLocation+hitnormal*0.01);
			p.angle = atan2(hitnormal.y,hitnormal.x);
			p.pitch = asin(-hitnormal.z);
			if ( d.HitLine ) d.HitLine.RemoteActivate(self,d.LineSide,SPAC_Impact,d.HitLocation);
		}
		for ( int i=0; i<3; i++ )
		{
			let s = Spawn("UTSmoke",origin);
			s.alpha *= 0.5;
			s.scale *= 0.7;
			s.vel += x*2;
		}
		let c = Spawn("UCasing",level.Vec3Offset(pos,-x*8+y*0.6+z*16));
		c.scale *= 0.5;
		c.vel = x*FRandom[Junk](-1.5,1.5)+y*FRandom[Junk](2,4)+z*FRandom[Junk](2,3);
	}
	States
	{
	Spawn:
		SENT A 15;
		Goto StartUp;
	StartUp:
		SENR A 0
		{
			A_PlaySound("sentry/raise",pitch:1.6);
			A_AlertMonsters(0,AMF_TARGETEMITTER);
		}
		SENR ABCDE 4;
		Goto Idle;
	Idle:
		SENI A 1 A_SentryGunCheck("WindUp",null,"ShutDown");
		Wait;
	WindUp:
		SENW A 0
		{
			A_PlaySound("sentry/wind",looping:true,pitch:1.6);
			A_AlertMonsters(0,AMF_TARGETEMITTER);
		}
		SENW ABCDEFGHIJKLMN 1;
	Fire:
		SENF A 0
		{
			let f = Spawn("SentryGunX",pos);
			f.angle = angle;
			f.pitch = pitch;
			f.roll = roll;
			f.tracer = self;
		}
		SENF A 2 A_SentryGunAttack("Unwind");
		SENF B 2;
		SENF C 2 A_SentryGunAttack("Unwind");
		SENF D 2;
		SENU A 0;	// tweening hack
		Loop;
	Unwind:
		SENU A 0 A_PlaySound("sentry/unwind",pitch:1.6);
		SENU ABCDEFGHIJKLMN 1;
		Goto Idle;
	ShutDown:
		SEND A 0 A_PlaySound("sentry/raise",pitch:1.6);
		SEND ABCDE 4;
		SEND E 20;
		SEND E -1 A_Die();
		Stop;
	Death:
		TNT1 A 0
		{
			A_StopSound(CHAN_BODY);
			A_PlaySound("flare/explode",CHAN_VOICE);
			A_NoGravity();
			A_Stop();
			A_SetRenderStyle(1.,STYLE_Add);
			SetZ(pos.z+16);
			Spawn("FlareXLight",pos);
			bMOVEWITHSECTOR = false;
			bFORCEXYBILLBOARD = true;
			A_SetScale(FRandomPick[ExploS](-1.5,1.5),FRandomPick[ExploS](-1.5,1.5));
			int numpt = Random[ExploS](15,30);
			for ( int i=0; i<numpt; i++ )
			{
				Vector3 pvel = (FRandom[ExploS](-1,1),FRandom[ExploS](-1,1),FRandom[ExploS](-1,1)).unit()*FRandom[ExploS](1,3);
				let s = Spawn("UTSmoke",pos);
				s.vel = pvel;
			}
			numpt = Random[ExploS](9,18);
			for ( int i=0; i<numpt; i++ )
			{
				Vector3 pvel = (FRandom[ExploS](-1,1),FRandom[ExploS](-1,1),FRandom[ExploS](-1,1)).unit()*FRandom[ExploS](2,6);
				let s = Spawn("UTSpark",pos);
				s.vel = pvel;
			}
			numpt = Random[ExploS](18,28);
			for ( int i=0; i<numpt; i++ )
			{
				Vector3 pvel = (FRandom[ExploS](-1,1),FRandom[ExploS](-1,1),FRandom[ExploS](-1,1)).unit()*FRandom[ExploS](2,12);
				let s = Spawn("UTChip",pos);
				s.vel = pvel;
				s.scale *= FRandom[ExploS](0.9,2.7);
			}
			return A_Jump(256,"Explo1","Explo2","Explo3","Explo4","Explo5");
		}
	Explo1:
		EXP1 ABCDEFGH 3 Bright;
		Stop;
	Explo2:
		EXP2 ABCDEFGH 3 Bright;
		Stop;
	Explo3:
		EXP3 ABCDEFGH 3 Bright;
		Stop;
	Explo4:
		EXP4 ABCDEFGH 3 Bright;
		Stop;
	Explo5:
		EXP5 ABCDEFGH 3 Bright;
		Stop;
	}
}
