// 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[i]); if ( !type || (type.GetParentClass() != 'Ammo') ) continue; // check that it's for a valid weapon bool isvalid = false; for ( int j=0; j)(AllActorClasses[j]); if ( !type2 ) continue; let rep = GetReplacement(type2); if ( (rep != type2) && !(rep is "DehackedPickup") ) continue; readonly 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 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 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 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 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 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 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 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 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 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; +DONTGIB; 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; r.bNeverRespawn = bNeverRespawn; } 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 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 ( !master || !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 ) { if ( !master ) return; 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() { if ( !master ) return; 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"); if ( master ) 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); if ( master ) { 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"); if ( master ) 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"); if ( master ) { 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= 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 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; +NOICEDEATH; +DONTGIB; } 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