// Eviscerator projectiles and effects Class EvisceratorChunkLight : PointLightAttenuated { Default { Args 255,224,128,16; } override void Tick() { Super.Tick(); if ( !EvisceratorChunk(target) || EvisceratorChunk(target).justdied ) { Destroy(); return; } if ( isFrozen() || (freezetics > 0) ) return; SetOrigin(target.pos,true); double intst = clamp((.7-EvisceratorChunk(target).lifetime)/.7,0.,1.); args[LIGHT_RED] = int(255*intst); args[LIGHT_GREEN] = int(224*intst); args[LIGHT_BLUE] = int(128*intst); } } Class ChunkImpact : SWWMNonInteractiveActor { override void PostBeginPlay() { Super.PostBeginPlay(); A_SprayDecal("WallCrack",-20); int numpt = Random[Eviscerator](-1,2); Vector3 x = SWWMUtility.Vec3FromAngles(angle,pitch); for ( int i=0; i 0 ) { freezetics--; return; } if ( isFrozen() ) return; if ( !EvisceratorChunk(target) || EvisceratorChunk(target).justdied ) { Scale *= .9; A_FadeOut(); return; } SetOrigin(target.pos,true); alpha = clamp((.7-EvisceratorChunk(target).lifetime)/.7,0.,1.); if ( alpha <= 0. ) Destroy(); } States { Spawn: ETRL A -1 Bright; Stop; } } Class EvisceratorChunkTrail : SWWMNonInteractiveActor { Default { RenderStyle "Add"; XScale 8.; +FORCEXYBILLBOARD; } override void Tick() { if ( freezetics > 0 ) { freezetics--; return; } if ( isFrozen() ) return; A_SetScale(scale.x*(.6+specialf1),scale.y); A_FadeOut(.1+specialf2); } States { Spawn: XZW1 ABCDEFGHIJ -1 Bright; Stop; } } Class EvisceratorChunk : Actor { Mixin SWWMMissileFix; Actor lasthit; double anglevel, pitchvel, rollvel; double lifetime, lifespeed; Vector3 oldvel; bool justdied; int trailcolor; Default { Obituary "$O_EVISCERATOR"; Radius 2; Height 4; Speed 50; DamageFunction int(clamp((vel.length()-5)*.2,0,15)+(max(0,1-lifetime)**5)*10); DamageType 'Shrapnel'; BounceFactor 1.0; WallBounceFactor 1.0; PROJECTILE; +USEBOUNCESTATE; +BOUNCEONWALLS; +BOUNCEONFLOORS; +BOUNCEONCEILINGS; +ALLOWBOUNCEONACTORS; +NODAMAGETHRUST; +DONTBOUNCEONSKY; +CANBOUNCEWATER; +INTERPOLATEANGLES; +ROLLSPRITE; +ROLLCENTER; Scale 0.4; } override void PostBeginPlay() { Super.PostBeginPlay(); let l = Spawn("EvisceratorChunkLight",pos); l.target = self; let t = Spawn("EvisceratorChunkGlow",pos); t.target = self; lifespeed = FRandom[Eviscerator](0.01,0.02)*clamp((vel.length()/75.)**2,1.,2.); anglevel = FRandom[Eviscerator](50,100)*RandomPick[Eviscerator](-1,1); pitchvel = FRandom[Eviscerator](50,100)*RandomPick[Eviscerator](-1,1); rollvel = FRandom[Eviscerator](50,100)*RandomPick[Eviscerator](-1,1); scale *= Frandom[Eviscerator](0.8,1.2); frame = Random[Eviscerator](0,7); } override void Tick() { static const name tls[] = { 'HotMetal0', 'HotMetal1', 'HotMetal2', 'HotMetal3', 'HotMetal4', 'HotMetal5', 'HotMetal6', 'HotMetal7' }; oldvel = vel; Super.Tick(); // somehow checking the state does not work all the time // so instead I have to set a bool at the start of XDeath, // otherwise there is a single puff of smoke at the LAST tic // of the state, there is no logical explanation for this, // I guess I can blame graf, randi, or whoever else if ( isFrozen() || (freezetics > 0) || justdied ) return; lifetime += lifespeed; if ( waterlevel > 0 ) lifetime = max(.7,lifetime); A_SetTranslation(tls[clamp(int(lifetime*10),0,7)]); if ( !Random[Eviscerator](0,2) && (lifetime < .7) ) { let s = Spawn("SWWMHalfSmoke",pos); s.vel = .2*vel+SWWMUtility.Vec3FromAngles(FRandom[Eviscerator](0,360),FRandom[Eviscerator](-90,90)); s.scale *= .5; s.alpha *= scale.x*max(0,.7-lifetime)*1.5; } if ( !InStateSequence(CurState,FindState("Death")) ) { angle += anglevel; pitch += pitchvel; roll += rollvel; double alph = clamp((.7-lifetime)/.7,0,1.); if ( alph < 0 ) return; Vector3 dir = level.Vec3Diff(pos,prev); double dist = dir.length(); if ( dist < 1. ) return; dir /= dist; let t = Spawn("EvisceratorChunkTrail",pos); t.alpha = alph; t.scale.y = dist; t.angle = atan2(dir.y,dir.x); t.pitch = asin(-dir.z)+90; t.SetState(t.SpawnState+trailcolor); if ( trailcolor > 0 ) { // custom trails last longer t.specialf1 = .3; t.specialf2 = -.05; } } } void A_HandleBounce() { Vector3 HitNormal = (0,0,0); F3DFloor ff; if ( BlockingFloor ) { // find closest 3d floor for its normal for ( int i=0; i 0. ) HitNormal = vel.unit(); if ( swwm_omnibust ) BusterWall.ProjectileBust(self,GetMissileDamage(0,0),oldvel.unit()); // undo the bounce, we need to hook in our own vel = oldvel; // re-do the bounce with our formula Vector3 RealHitNormal = HitNormal; double dfact = clamp((oldvel.length()/75.)**2.,.02,.6); HitNormal = (HitNormal+SWWMUtility.Vec3FromAngles(FRandom[Eviscerator](0,360),FRandom[Eviscerator](-90,90))*dfact).unit(); if ( (HitNormal dot RealHitNormal) < 0 ) HitNormal *= -.5; vel = FRandom[Eviscerator](.8,.95)*((vel dot HitNormal)*HitNormal*(FRandom[Eviscerator](-1.8,-1.))+vel); bHITOWNER = true; lasthit = null; if ( (vel.length() > 20) && !Random[Eviscerator](0,2) ) { let l = Spawn("ChunkImpact",pos); l.angle = atan2(RealHitNormal.y,RealHitNormal.x); l.pitch = asin(-RealHitNormal.z); A_StartSound("eviscerator/hith",CHAN_WEAPON,CHANF_OVERLAP,.5); } A_Gravity(); gravity = clamp(.35-vel.length()/200.,.15,.35); anglevel = FRandom[Eviscerator](50,100)*RandomPick[Eviscerator](-1,1)*(vel.length()/speed); pitchvel = FRandom[Eviscerator](50,100)*RandomPick[Eviscerator](-1,1)*(vel.length()/speed); rollvel = FRandom[Eviscerator](50,100)*RandomPick[Eviscerator](-1,1)*(vel.length()/speed); A_StartSound("eviscerator/hit",CHAN_WEAPON,CHANF_OVERLAP,.3); if ( vel.length() < 3 ) { A_Stop(); ClearBounce(); ExplodeMissile(); } } override bool CanCollideWith( Actor other, bool passive ) { // safer to do here if ( !(other.bSHOOTABLE && other.bSOLID) || ((vel.length() <= 5) && other.bSHOOTABLE) || ((other == target) && !bHITOWNER) || (other == lasthit) ) return false; return true; } override int SpecialMissileHit( Actor victim ) { // directly bounce off shootable solids if ( !victim.bSHOOTABLE ) { if ( bSOLID ) { BlockingMobj = victim; A_HandleBounce(); lasthit = victim; } return 1; } // with this we can guarantee that the chunk won't just keep on dealing damage // this is something I wish Unreal's boulders did lasthit = victim; // don't knock back if already dead int oldamt = SWWMDamageAccumulator.GetAmount(victim); if ( victim.health-oldamt > 0 ) SWWMUtility.DoKnockback(victim,vel.unit(),15000); // gather damage int dmg = GetMissileDamage(0,0); SWWMDamageAccumulator.Accumulate(victim,dmg,self,target,damagetype); int amt = SWWMDamageAccumulator.GetAmount(victim); // pass through if it's already dead // + random chance relative to health int posthealth = victim.health-amt; double hratio = posthealth/double(victim.GetSpawnHealth()); if ( (posthealth <= 0) || (FRandom[Eviscerator](hratio,1.) < .7) ) { if ( !victim.bNOBLOOD && !victim.bDORMANT && !victim.bINVULNERABLE ) { victim.SpawnBlood(pos,AngleTo(victim),dmg); A_StartSound("eviscerator/hitf",CHAN_WEAPON,CHANF_OVERLAP,.1); } else { let l = Spawn("ChunkImpact",pos); l.angle = angle+180; l.pitch = -pitch; A_StartSound("eviscerator/hith",CHAN_WEAPON,CHANF_OVERLAP,.1); } vel *= FRandom[Eviscerator](.8,.9); // reduce velocity as it rips A_Gravity(); gravity = clamp(.35-vel.length()/200.,.15,.35); return 1; } // HACK if ( !victim.bNOBLOOD && !victim.bDORMANT && !victim.bINVULNERABLE ) { victim.SpawnBlood(pos,AngleTo(victim),dmg); A_StartSound("eviscerator/hitf",CHAN_WEAPON,CHANF_OVERLAP,.1); ExplodeMissile(null,victim); } else { BlockingMobj = victim; A_HandleBounce(); lasthit = victim; } return 1; } States { Spawn: XZW1 # -1; Stop; Bounce: XZW1 # 0 A_HandleBounce(); Goto Spawn; Death: XZW2 # 0 { pitch = 0; roll = 0; bMOVEWITHSECTOR = true; A_SetTics(Random[Eviscerator](30,50)); } XZW2 # 1 A_FadeOut(); Wait; XDeath: TNT1 A 35 { invoker.justdied = true; } Stop; } } Class EvisceratorProjSmoke : SWWMNonInteractiveActor { double lifetime, lifespeed; override void PostBeginPlay() { lifetime = 0; lifespeed = FRandom[Eviscerator](0.004,0.008); } override void Tick() { if ( freezetics > 0 ) { freezetics--; return; } if ( isFrozen() ) return; lifetime += lifespeed; let s = Spawn("SWWMSmoke",pos); s.vel = SWWMUtility.Vec3FromAngles(FRandom[Eviscerator](0,360),FRandom[Eviscerator](-90,90))*.5; s.vel.z += 2.; s.alpha = scale.x; s.SetShade(Color(1,1,1)*Random[Eviscerator](160,255)); scale.x = max(0,1-lifetime); if ( scale.x <= 0 ) Destroy(); } } Class EvisceratorProjLight : PaletteLight { Default { Args 0,0,0,140; ReactionTime 20; } } Class EvisceratorProj : Actor { Mixin SWWMMissileFix; double heat; Vector3 startpos; Default { Obituary "$O_EVISCERATOR"; DamageType 'Explosive'; Radius 2; Height 4; Gravity 0.35; Speed 60; PROJECTILE; -NOGRAVITY; +EXPLODEONWATER; +HITTRACER; +FORCERADIUSDMG; +NODAMAGETHRUST; } override void PostBeginPlay() { Super.PostBeginPlay(); startpos = pos; if ( waterlevel <= 0 ) vel.z += 3; heat = 1.5; } action void A_EvisExplode() { if ( target && tracer && (tracer.bIsMonster||tracer.player) && tracer.IsHostile(target) ) { double dist = level.Vec3Diff(pos,invoker.startpos).length(); SWWMUtility.AchievementProgress("lead",int(dist),target.player); } bForceXYBillboard = true; A_SetRenderStyle(1.0,STYLE_Add); A_SprayDecal("BigRocketBlast",50); A_NoGravity(); A_SetScale(3.); SWWMUtility.DoExplosion(self,120,120000,150,80); A_QuakeEx(6,6,6,20,0,1200,"",QF_RELATIVE|QF_SCALEDOWN|QF_3D,falloff:300,rollIntensity:.7); A_StartSound("eviscerator/shell",CHAN_WEAPON,attenuation:.5); A_StartSound("eviscerator/shell",CHAN_VOICE,attenuation:.3); A_AlertMonsters(swwm_uncapalert?0:3000); if ( !Tracer ) Spawn("EvisceratorProjSmoke",pos); Spawn("EvisceratorProjLight",pos); Vector3 x, y, z; double a, s; [x, y, z] = swwm_CoordUtil.GetAxes(pitch,angle,roll); EvisceratorChunk p; Vector3 spawnofs; if ( BlockingMobj ) spawnofs = (0,0,0); else if ( BlockingFloor ) spawnofs = BlockingFloor.floorplane.Normal*4; else if ( BlockingCeiling ) spawnofs = BlockingCeiling.ceilingplane.Normal*4; else if ( BlockingLine ) { spawnofs = (-BlockingLine.delta.y,BlockingLine.delta.x,0).unit()*4; if ( !SWWMUtility.PointOnLineSide(pos.xy,BlockingLine) ) spawnofs *= -1; } int trail = 0; if ( target && target.player ) trail = CVar.GetCVar('swwm_funtrails',target.player).GetInt(); if ( trail == 8 ) trail = Random[Eviscerator](0,7); for ( int i=0; i<30; i++ ) { p = EvisceratorChunk(Spawn("EvisceratorChunk",level.Vec3Offset(pos,spawnofs))); p.bHITOWNER = true; a = FRandom[Eviscerator](0,360); s = FRandom[Eviscerator](0,.4); Vector3 dir = SWWMUtility.ConeSpread(x,y,z,a,s); p.angle = atan2(dir.y,dir.x); p.pitch = -asin(dir.z); p.vel = SWWMUtility.Vec3FromAngles(p.angle,p.pitch)*(p.speed+FRandom[Eviscerator](-5,20)); p.target = target; if ( trail < 8 ) p.trailcolor = max(0,trail); else if ( trail == 9 ) p.trailcolor = (i%6)+2; else if ( trail == 10 ) { switch ( i%6 ) { case 0: case 3: p.trailcolor = 8; break; case 1: case 4: p.trailcolor = 9; break; case 2: p.trailcolor = 1; break; } } } int numpt = Random[Eviscerator](10,15); for ( int i=0; i 8 ) return; int numpt = Random[Eviscerator](0,8-special1); double ang, pt; for ( int i=0; i 0 ) { let s = Spawn("SWWMHalfSmoke",pos); s.alpha *= heat; } } Wait; Death: TNT1 A 0 A_EvisExplode(); XSEX ABCDEFGHIJKLMNOPQRS 2 Bright A_Subexpl(); Stop; } } Class EvisceratorSubExpl : SWWMNonInteractiveActor { Default { RenderStyle "Add"; Scale 2.; Alpha .6; +FORCEXYBILLBOARD; } States { Spawn: XSEX ABCDEFGHIJKLMNOPQRS 1 Bright; Stop; } } Class EvisceratorRing : SWWMNonInteractiveActor { Default { RenderStyle "Add"; Scale 4.; +FORCEXYBILLBOARD; } States { Spawn: XRG0 ABCDEFGHIJKLMNOPQRSTUVWX 1 Bright A_SetScale(scale.x*1.01); Stop; } } Class EvisceratorCasing : SWWMCasing { Default { Mass 10; BounceFactor 0.4; WallBounceFactor 0.4; BounceSound "eviscerator/casing"; } States { Death: XZW1 BC -1 { bINTERPOLATEANGLES = false; pitch = roll = 0; angle = FRandom[Junk](0,360); frame = RandomPick[Junk](1,2); } Stop; } }