// Ynykron projectiles and effects // there was an enemy here, but it's gone now Class AshenRemains : SWWMNonInteractiveActor { override void Tick() { if ( freezetics > 0 ) { freezetics--; return; } if ( isFrozen() ) return; double fz = CurSector.floorplane.ZAtPoint(pos.xy); if ( fz != pos.z ) SetOrigin((pos.x,pos.y,fz),true); special1++; if ( special1 > 350 ) A_FadeOut(0.01); } override void PostBeginPlay() { if ( (waterlevel > 0) || GetFloorTerrain().isliquid ) { Destroy(); return; } double fz = CurSector.floorplane.ZAtPoint(pos.xy); SetZ(fz); prev.z = fz; SWWMUtility.SetToSlope(self,FRandom[Ynykron](0,360)); } default { RenderStyle "Shaded"; StencilColor "000000"; } States { Spawn: XZW1 A -1; Stop; } } // cheap way to let players know they just got erased from existence Class PlayerGone : PlayerChunk { int deadtimer; override void DeathThink() { deadtimer++; if ( (deadtimer == 60) && (player == players[consoleplayer]) ) A_StartSound("demolitionist/youdied",CHAN_DEMOVOICE,CHANF_OVERLAP|CHANF_UI); if ( multiplayer || level.AllowRespawn || sv_singleplayerrespawn || G_SkillPropertyInt(SKILLP_PlayerRespawn) ) { // standard behaviour, respawn normally if ( (((player.cmd.buttons&BT_USE) || ((deathmatch || alwaysapplydmflags) && sv_forcerespawn)) && !sv_norespawn) && ((Level.maptime >= player.respawn_time) || ((player.cmd.buttons&BT_USE) && !player.Bot)) ) { player.cls = null; player.playerstate = PST_REBORN; if ( special1 > 2 ) special1 = 0; } } else if ( (player.cmd.buttons&BT_USE) && (deadtimer > 120) ) { // reload save player.cls = null; player.playerstate = PST_ENTER; if ( special1 > 2 ) special1 = 0; } // no revive (for obvious reasons) } static Actor FeckOff( Actor p ) { // doesn't affect voodoo dolls (convenient, isn't it?) if ( !p.player || (p.player.mo != p) ) return p; let c = PlayerGone(Spawn("PlayerGone",(65535,65535,0))); c.player = p.player; c.Health = p.Health; p.player = null; c.ObtainInventory(p); if ( c.player ) { c.player.mo = c; c.player.damagecount = 0; c.player.bonuscount = 0; c.player.poisoncount = 0; } for ( int i=0; i 0) ) return; let ti = ThinkerIterator.Create("IDontFeelSoGood",STAT_USER); IDontFeelSoGood i; while ( i = IDontFeelSoGood(ti.Next()) ) if ( i.goner == whomst ) return; i = new("IDontFeelSoGood"); i.ChangeStatNum(STAT_USER); i.goner = whomst; i.silence = silence; } override void Tick() { if ( !goner ) { Destroy(); return; } if ( goner.Health > 0 ) { goner.bINVISIBLE = goner.default.bINVISIBLE; goner.A_ChangeLinkFlags(goner.default.bNOBLOCKMAP); Destroy(); return; } if ( silence ) goner.A_StopAllSounds(); goner.DamageType = 'Massacre'; // prevents enemies such as pain elementals from spawning anything // special handling for some bosses: // - D'Sparil does not spawn, he's already gone // - Korax doesn't leave ghosts if ( (goner.tics != -1) && !(goner is 'Sorcerer1') && !(goner is 'Korax') ) return; cnt++; if ( cnt < 15 ) return; if ( goner is 'Sorcerer1' ) { let h = Actor.Spawn("DSparilHax",goner.pos); h.CopyFriendliness(goner,true); } else if ( goner is 'Korax' ) level.ExecuteSpecial(80,goner,null,0,255); goner.Destroy(); } } Class YnykronImpactLight : PaletteLight { Default { Tag "WhiteExpl,1"; ReactionTime 60; Args 0,0,0,300; } } Class YnykronSubImpactLight : PaletteLight { Default { Tag "WhiteExpl,1"; ReactionTime 20; Args 0,0,0,120; } } Class YnykronShotLight : PaletteLight { Default { Tag "WhiteExpl"; ReactionTime 100; Args 0,0,0,800; } } Class YnykronBeamLight : PaletteLight { Default { Tag "WhiteExpl,1"; ReactionTime 50; Args 0,0,0,250; } } Class YnykronImpactArm : Actor { Default { PROJECTILE; +THRUACTORS; +BOUNCEONWALLS; +BOUNCEONFLOORS; +BOUNCEONCEILINGS; +CANBOUNCEWATER; +NODAMAGETHRUST; +FORCERADIUSDMG; -NOGRAVITY; +NOFRICTION; Gravity 0.35; BounceFactor 1.0; Radius 2; Height 4; } override void PostBeginPlay() { Super.PostBeginPlay(); reactiontime = Random[ExploS](10,20); double ang, pt; ang = FRandom[ExploS](0,360); pt = FRandom[ExploS](-90,90); vel = SWWMUtility.Vec3FromAngles(ang,pt)*FRandom[ExploS](20.,40.); } States { Spawn: TNT1 A 1 { A_CountDown(); if ( !(reactiontime%2) ) { Spawn("YnykronImpactTrail",pos); Vector3 pvel = SWWMUtility.Vec3FromAngles(FRandom[ExploS](0,360),FRandom[ExploS](-90,90))*FRandom[ExploS](1,5); let s = Spawn("SWWMHalfSmoke",pos); s.vel = pvel+vel*.2; s.special1 = Random[ExploS](1,3); s.scale *= 2.4; s.alpha *= 0.1+.4*(ReactionTime/15.); } } Wait; } } Class YnykronImpactTrail : SWWMNonInteractiveActor { Default { RenderStyle "Add"; +FORCEXYBILLBOARD; Scale 2.; Alpha .2; } override void PostBeginPlay() { Super.PostBeginPlay(); Scale *= FRandom[ExploS](0.8,1.1); Scale.x *= RandomPick[ExploS](-1,1); Scale.y *= RandomPick[ExploS](-1,1); } States { Spawn: MOXP ABCDEFGHIJKLMNOPQRSTUVWXYZ[\ 1 Bright; Stop; } } Class YnykronDelayedImpact : SWWMNonInteractiveActor { Vector3 ofs; override void Tick() { if ( freezetics > 0 ) { freezetics--; return; } if ( isFrozen() ) return; special1++; if ( special1 < 4 ) { if ( tracer ) SetOrigin(level.Vec3Offset(tracer.pos,ofs),false); return; } let b = Spawn("YnykronImpact",pos); b.tracer = tracer; b.target = target; b.master = master; b.angle = angle; b.pitch = pitch; b.special1 = special2; b.special2 = 1; b.args[0] = args[0]; Destroy(); } } Class YnykronImpact : SWWMNonInteractiveActor { int rad; Default { Obituary "$O_YNYKRON"; DamageType 'Ynykron'; RenderStyle "Add"; Scale 5.; +FORCEXYBILLBOARD; +NODAMAGETHRUST; +FORCERADIUSDMG; +EXTREMEDEATH; } private bool CmpDist( Actor a, Actor b ) { double dista = Vec3To(a).length(); double distb = Vec3To(b).length(); return (dista > distb); } // quicksort (candidates) private int partition_candidates( Array a, int l, int h ) { Actor pv = a[h]; int i = (l-1); for ( int j=l; j<=(h-1); j++ ) { if ( CmpDist(pv,a[j]) ) { i++; Actor tmp = a[j]; a[j] = a[i]; a[i] = tmp; } } Actor tmp = a[h]; a[h] = a[i+1]; a[i+1] = tmp; return i+1; } private void qsort_candidates( Array a, int l, int h ) { if ( l >= h ) return; int p = partition_candidates(a,l,h); qsort_candidates(a,l,p-1); qsort_candidates(a,p+1,h); } void FlashPlayer( int str, double rad ) { if ( !SWWMUtility.InPlayerFOV(players[consoleplayer],self,rad) ) return; let mo = players[consoleplayer].Camera; double dist = Distance3D(mo); str = int(str*(1.-(dist/rad))); SWWMHandler.DoFlash(mo,Color(str,255,255,255),2); SWWMHandler.DoFlash(mo,Color(str/2,255,255,255),10); } override void PostBeginPlay() { Super.PostBeginPlay(); rad = args[0]+300+10*clamp(special1/10,0,15); A_QuakeEx(4.,4.,4.,50,0,rad*4,"",QF_RELATIVE|QF_SCALEDOWN|QF_3D,falloff:rad*2,rollintensity:.6); FlashPlayer(60,1200); if ( tracer ) { // voodoo dolls just get erased (how convenient) // otherwise instantly vaporize the poor sap if ( tracer.player && (tracer.player.mo != tracer) ) { if ( tracer.pos.z < (tracer.floorz+64) ) { let r = Spawn("AshenRemains",tracer.pos); r.scale *= tracer.radius/16.; } tracer.Destroy(); } else if ( tracer.CountInv("GrilledCheeseSandwich") > 0 ) { // force use the sandwich let gc = GrilledCheeseSandwich(tracer.FindInventory("GrilledCheeseSandwich")); tracer.A_StartSound(gc.UseSound,CHAN_ITEMEXTRA); gc.DoTheThing(true); gc.Amount--; } else if ( !tracer.FindInventory("GrilledCheeseSafeguard") ) tracer.DamageMobj(self,target,int.max,'Ynykron',DMG_FORCED|DMG_THRUSTLESS); if ( tracer && (tracer.Health <= 0) ) { if ( tracer.player ) { if ( tracer == target ) { SWWMUtility.MarkAchievement("oopsie",tracer.player); target = PlayerGone.FeckOff(tracer); } else PlayerGone.FeckOff(tracer); } if ( tracer.FindState("YnykronDeath",true) ) tracer.SetStateLabel("YnykronDeath"); // dedicated state else { // poof away manually tracer.bINVISIBLE = true; tracer.A_ChangeLinkFlags(true); // remove from blockmap, should guarantee archviles not raising this IDontFeelSoGood.DeletThis(tracer); // ensures corpse is deleted too } if ( tracer.pos.z < (tracer.floorz+64) ) { let r = Spawn("AshenRemains",tracer.pos); r.scale *= tracer.radius/16.; } if ( (tracer.bIsMonster || tracer.player) && (!target || tracer.IsHostile(target)) && YnykronShot(master) ) YnykronShot(master).enemykills++; if ( target && tracer.FindInventory("EndgameBossMarker") ) SWWMUtility.MarkAchievement("ligma",target.player); } } if ( YnykronShot(master) ) { if ( YnykronShot(master).lastimpact < gametic ) { master.A_StartSound("ynykron/hit",CHAN_VOICE,CHANF_OVERLAP,special1?.4:1.,.0); YnykronShot(master).lastimpact = gametic+(special1?2:5); } } else A_StartSound("ynykron/hit",CHAN_VOICE,CHANF_OVERLAP,special1?.4:1.,.0); Scale *= FRandom[ExploS](0.8,1.1); Scale.x *= RandomPick[ExploS](-1,1); Scale.y *= RandomPick[ExploS](-1,1); int numpt = Random[ExploS](2,4); if ( special2 ) numpt -= 3; for ( int i=0; i candidates; candidates.Clear(); foreach ( s:level.Sectors ) for ( Actor t=s.thinglist; t; t=t.snext ) { if ( !t.bSHOOTABLE || !SWWMUtility.SphereIntersect(t,pos,rad) || (!SWWMUtility.SphereIntersect(t,pos,100) && !CheckSight(t,SF_IGNOREVISIBILITY|SF_IGNOREWATERBOUNDARY)) ) continue; if ( YnykronShot(master) && (YnykronShot(master).hitlist.Find(t) < YnykronShot(master).hitlist.Size()) ) continue; Vector3 dirto = level.Vec3Diff(pos,t.Vec3Offset(0,0,t.Height/2)); double dist = dirto.length(); if ( (dist > rad/2) && (t == target) ) continue; candidates.Push(t); } qsort_candidates(candidates,0,candidates.Size()-1); candidates.Resize(2); foreach ( t:candidates ) { if ( !t ) continue; Vector3 dirto = level.Vec3Diff(pos,t.Vec3Offset(0,0,t.Height/2)); double dist = dirto.length(); dirto /= dist; int trad = int(max(t.radius,t.height)); if ( t && YnykronShot(master) ) { YnykronShot(master).hitlist.Push(t); if ( t.bBOSS || t.FindInventory("BossMarker") ) YnykronShot(master).hitboss = true; } // spawn blast that will propagate let b = Spawn("YnykronDelayedImpact",t.pos); Vector3 ofs = level.Vec3Diff(t.pos,level.Vec3Offset(pos,dirto*min(rad,dist))); YnykronDelayedImpact(b).ofs = ofs; b.tracer = t; b.target = target; b.master = master; b.angle = atan2(dirto.y,dirto.x); b.pitch = asin(-dirto.z); b.special2 = special1+(Random[Ynykron](0,8)?1:0); b.special1 = Random[Ynykron](-5,0)-min(special1/2,10); b.args[0] = trad; if ( YnykronShot(master) ) YnykronShot(master).blastcount++; } } override void OnDestroy() { if ( YnykronShot(master) ) YnykronShot(master).blastcount--; Super.OnDestroy(); } States { Spawn: MOXP ABCDEFGHIJKLMNOPQRSTUVWXYZ[\ 3 Bright A_SetTics(special2?1:3); Stop; } } Class YnykronTracer : LineTracer { Array ShootThroughList; Array WaterHitList; Array HitList; override ETraceStatus TraceCallback() { // liquid splashes if ( Results.CrossedWater ) { let hl = new("WaterHit"); hl.hitpos = Results.CrossedWaterPos; WaterHitList.Push(hl); } else if ( Results.Crossed3DWater ) { let hl = new("WaterHit"); hl.hitpos = Results.Crossed3DWaterPos; WaterHitList.Push(hl); } if ( Results.HitType == TRACE_HitActor ) { if ( Results.HitActor.bSHOOTABLE || (Results.HitActor is 'YnykronSingularityHitbox') ) { let ent = new("HitListEntry"); ent.hitactor = Results.HitActor; ent.hitlocation = Results.HitPos; ent.x = Results.HitVector; hitlist.Push(ent); } return TRACE_Skip; } else if ( (Results.HitType == TRACE_HitWall) && (Results.Tier == TIER_Middle) ) { if ( !Results.HitLine.sidedef[1] || (Results.HitLine.Flags&Line.ML_BlockHitscan) ) return TRACE_Stop; ShootThroughList.Push(Results.HitLine); return TRACE_Skip; } return TRACE_Stop; } } Class YnykronInWallTracer : YnykronTracer { override ETraceStatus TraceCallback() { // liquid splashes if ( Results.CrossedWater ) { let hl = new("WaterHit"); hl.hitpos = Results.CrossedWaterPos; WaterHitList.Push(hl); } else if ( Results.Crossed3DWater ) { let hl = new("WaterHit"); hl.hitpos = Results.Crossed3DWaterPos; WaterHitList.Push(hl); } if ( Results.HitType == TRACE_HitActor ) { if ( Results.HitActor.bSHOOTABLE ) { let ent = new("HitListEntry"); ent.hitactor = Results.HitActor; ent.hitlocation = Results.HitPos; ent.x = Results.HitVector; hitlist.Push(ent); } return TRACE_Skip; } else if ( (Results.HitType == TRACE_HitWall) ) ShootThroughList.Push(Results.HitLine); return TRACE_Skip; } } Class YnykronBeam : SWWMNonInteractiveActor { bool nospread; void TraceOut() { Vector3 x = SWWMUtility.Vec3FromAngles(angle,pitch); let t = new("YnykronTracer"); t.ShootThroughList.Clear(); t.WaterHitList.Clear(); t.HitList.Clear(); t.Trace(pos,cursector,x,speed,0,ignore:target); foreach ( l:t.ShootThroughList ) { l.Activate(target,0,SPAC_PCross); l.Activate(target,0,SPAC_Impact); } foreach ( w:t.WaterHitList ) { let b = Spawn("InvisibleSplasher",w.hitpos); b.target = target; b.A_CheckTerrain(); } for ( int i=0; i= 25600 ) { // end of the line, dissipate int numpt = Random[Ynykron](4,8); for ( int i=0; i 0 ) { freezetics--; return; } if ( isFrozen() ) return; A_FadeOut(FRandom[Ynykron](.01,.02)); special2++; if ( special2 == 1 ) SpreadOut(); if ( !CheckNoDelay() || (tics == -1) ) return; if ( tics > 0 ) tics--; while ( !tics ) { if ( !SetState(CurState.NextState) ) return; } } override void OnDestroy() { if ( YnykronShot(master) ) YnykronShot(master).beamcount--; Super.OnDestroy(); } Default { Obituary "$O_YNYKRON"; DamageType 'Ynykron'; RenderStyle "Add"; Alpha .4; Speed 128; +FORCEXYBILLBOARD; +ROLLSPRITE; +ROLLCENTER; +NODAMAGETHRUST; +FORCERADIUSDMG; +FOILINVUL; +EXTREMEDEATH; } States { Spawn: XZW1 A -1 Bright NoDelay { return FindState("StarterDev")+Random[Ynykron](0,3)*2; } Stop; TrailSpawn: XZW2 A -1 Bright { return FindState("TrailerDev")+Random[Ynykron](0,3)*2; } Stop; StarterDev: #### # 50 Bright; XZW1 B -1 Bright; Stop; #### # 50 Bright; XZW1 C -1 Bright; Stop; #### # 50 Bright; XZW1 D -1 Bright; Stop; #### # 50 Bright; XZW1 E -1 Bright; Stop; TrailerDev: #### # 50 Bright; XZW2 B -1 Bright; Stop; #### # 50 Bright; XZW2 C -1 Bright; Stop; #### # 50 Bright; XZW2 D -1 Bright; Stop; #### # 50 Bright; XZW2 E -1 Bright; Stop; } } Class DelayedWallBeam : SWWMNonInteractiveActor { override void PostBeginPlay() { if ( YnykronShot(master) ) YnykronShot(master).beamcount++; } override void Tick() { if ( freezetics > 0 ) { freezetics--; return; } if ( isFrozen() ) return; special2--; if ( special2 > 0 ) return; if ( YnykronShot(master) ) YnykronShot(master).beamcount--; let next = Spawn("YnykronBeam",pos); next.angle = angle; next.pitch = pitch; next.roll = roll; next.target = target; next.master = master; next.special1 = special1; next.SetStateLabel("TrailSpawn"); // exit blast Vector3 x = SWWMUtility.Vec3FromAngles(angle,pitch); let b = Spawn("YnykronImpact",level.Vec3Offset(pos,x*16)); b.target = target; b.master = master; b.angle = atan2(x.y,x.x); b.pitch = asin(-x.z); b.A_SprayDecal("YnykronBlast",-172); if ( YnykronShot(master) ) YnykronShot(master).blastcount++; // trace back to get the proper "exit surface" so we can trigger lines if needed let at = new("AuxiliarySilverBulletTracer"); at.Trace(pos,CurSector,-x,2.,0,ignoreallactors:true); if ( at.Results.HitType == TRACE_HitWall ) { if ( at.Results.HitLine.sidedef[1] ) { at.Results.HitLine.Activate(target,0,SPAC_PCross); at.Results.HitLine.Activate(target,0,SPAC_Impact); } else at.Results.HitLine.Activate(tracer,at.Results.Side,SPAC_Impact); } if ( swwm_omnibust ) BusterWall.Bust(at.Results,int.max,target,-at.Results.HitVector,at.Results.HitPos.z); Destroy(); } } Class YnykronRing : SWWMNonInteractiveActor { Default { RenderStyle "Add"; Scale .6; Alpha .05; +FORCEXYBILLBOARD; +ROLLSPRITE; +ROLLCENTER; } States { Spawn: XRG4 ABCDEFGHIJKLMNOPQRSTUVWX 3 Bright; Stop; } } // non-model version, for impacts Class YnykronImpactRing : SWWMNonInteractiveActor { Default { RenderStyle "Add"; Scale 2.; +FORCEXYBILLBOARD; } override void Tick() { if ( freezetics > 0 ) { freezetics--; return; } if ( isFrozen() ) return; A_SetScale(scale.x*(special2?1.06:1.03)); if ( !CheckNoDelay() || (tics == -1) ) return; if ( tics > 0 ) tics--; while ( !tics ) { if ( !SetState(CurState.NextState) ) return; } } States { Spawn: XRG4 ABCDEFGHIJKLMNOPQRSTUVWX 1 Bright A_SetTics(special2?1:2); Stop; } } Class YnykronShot : SWWMNonInteractiveActor { Array hitlist; bool hitboss; int enemykills; int beamcount; int blastcount; int lastimpact; void FlashPlayer( int str, double rad ) { if ( !SWWMUtility.InPlayerFOV(players[consoleplayer],self,rad) ) return; let mo = players[consoleplayer].Camera; double dist = Distance3D(mo); str = int(str*(1.-(dist/rad))); SWWMHandler.DoFlash(mo,Color(str,255,255,255),3); SWWMHandler.DoFlash(mo,Color(str/2,255,255,255),30); } override void PostBeginPlay() { A_QuakeEx(6.,6.,6.,150,0,65535,"",QF_RELATIVE|QF_SCALEDOWN,falloff:65535,rollIntensity:1.); A_StartSound("ynykron/beam",CHAN_VOICE,CHANF_DEFAULT,1.,0.); FlashPlayer(240,8000); hitlist.Clear(); beamcount = 0; blastcount = 0; int rings = 1; Vector3 dir; double a, s; let [x, y, z] = SWWMUtility.GetAxes(angle,pitch,roll); for ( double i=0; i<.04; i+=.006 ) { for ( int j=0; j<360; j+=(360/rings) ) { if ( i==0 ) dir = x; // central beam always precise else { a = j+FRandom[Ynykron](-5.,5.); s = i+FRandom[Ynykron](-.02,.04); dir = SWWMUtility.ConeSpread(x,y,z,a,s); } let b = Spawn("YnykronBeam",pos); b.target = target; b.master = self; b.angle = atan2(dir.y,dir.x); b.pitch = asin(-dir.z); b.roll = FRandom[Ynykron](0,360); } rings += 2; } Spawn("YnykronShotLight",level.Vec3Offset(pos,x*30)); } override void Tick() { if ( freezetics > 0 ) { freezetics--; return; } if ( isFrozen() ) return; // spawn rings special1++; if ( !(special1%10) && (special1 <= 30) ) { Vector3 dir = SWWMUtility.Vec3FromAngles(angle,pitch); for ( int i=0; i<16; i++ ) { let r = Spawn("YnykronRing",level.Vec3Offset(pos,dir*(special1*16+i))); r.scale *= special1/8.; r.angle = angle; r.pitch = pitch; r.roll = FRandom[Ynykron](0,360); } } // wait until we're no longer needed and all effects are over if ( (beamcount > 0) || (blastcount > 0) ) return; if ( target && enemykills ) { if ( (enemykills == 1) && !hitboss ) SWWMUtility.MarkAchievement("oneguy",target.player); SWWMUtility.AchievementProgress("ezkill",enemykills,target.player); enemykills = 0; } if ( IsActorPlayingSound(CHAN_VOICE) ) return; // we're done here Destroy(); } }