// collectible items that may drop sometimes Class SWWMCollectible : Inventory abstract { Mixin SWWMUseToPickup; int avail; bool propagated; Property Availability : avail; // minimum gametype requirements enum EAvailability { AVAIL_Strife = GAME_Strife, AVAIL_Hexen = AVAIL_Strife|GAME_Hexen, AVAIL_Heretic = AVAIL_Hexen|GAME_Heretic, AVAIL_All = AVAIL_Heretic|GAME_DoomChex }; Default { Inventory.PickupSound "menu/buyinv"; Inventory.Amount 1; Inventory.MaxAmount 1; Inventory.PickupFlash "SWWMCyanPickupFlash"; SWWMCollectible.Availability AVAIL_All; +INVENTORY.UNTOSSABLE; +INVENTORY.UNDROPPABLE; +INVENTORY.UNCLEARABLE; +INVENTORY.AUTOACTIVATE; +FLOATBOB; FloatBobStrength 0.25; Radius 8; Height 24; } override void PostBeginPlay() { Super.PostBeginPlay(); // delet ourselves if wrong iwad if ( !(gameinfo.gametype&avail) ) Destroy(); } override bool CanPickup( Actor toucher ) { // no pickup if wrong iwad if ( !(gameinfo.gametype&avail) ) return false; return Super.CanPickup(toucher); } override string PickupMessage() { if ( Stamina > 0 ) return StringTable.Localize(PickupMsg)..String.Format(" \cj(\cg„\cf%d\cj)\c-",Stamina); return Super.PickupMessage(); } override bool Use( bool pickup ) { if ( Owner.player && !propagated ) { if ( pickup && CVar.GetCVar('swwm_collectanim',Owner.player) ) SWWMGesture.SetSpecialGesture(Owner.player.mo,self,true); else if ( !pickup ) SWWMGesture.SetSpecialGesture(Owner.player.mo,self); } // clean up the flag propagated = false; return false; } override void AttachToOwner( Actor other ) { Super.AttachToOwner(other); // we're only attaching to the other players if ( propagated ) return; // give credit if ( other.player && (Stamina > 0) ) { SWWMScoreObj.Spawn(Stamina,other.Vec3Offset(FRandom[ScoreBits](-8,8),FRandom[ScoreBits](-8,8),FRandom[ScoreBits](-8,8)+other.Height/2),Font.CR_GOLD); SWWMCredits.Give(other.player,Stamina); } // send to all other players for ( int i=0; i 0 ) { gest.whichgesture = GS_Null; gest.whichstate = gest.sstate[0]; gest.whichcaller = gest.scaller[0]; // push back gest.sstate.Delete(0); gest.scaller.Delete(0); let psp = player.FindPSprite(PSP_WEAPON); psp.caller = gest; psp.SetState(gest.ResolveState("Ready")); return; } if ( gest.queued ) { gest.whichstate = null; gest.whichgesture = gest.nextgesture; gest.queued = false; let psp = player.FindPSprite(PSP_WEAPON); psp.caller = gest; psp.SetState(gest.ResolveState("Ready")); return; } player.PendingWeapon = gest.formerweapon; let psp = player.FindPSprite(PSP_WEAPON); psp.caller = gest; psp.SetState(gest.ResolveState("Deselect")); } } // April Fools 2020 Class FroggyChair : Actor { int cdown; bool carried; Default { Tag "$T_FROGGY"; Radius 12; Height 16; +SOLID; +SHOOTABLE; +NODAMAGE; +NOBLOOD; +INTERPOLATEANGLES; } private void BeginCarry( Actor carrier ) { if ( !carrier ) return; carrier.A_StartSound("demolitionist/handsup",CHAN_ITEM,CHANF_OVERLAP); A_SetRenderStyle(.5,STYLE_Translucent); A_SetSize(32,32); // easier to deselect carried = true; bSOLID = false; bSHOOTABLE = false; bNOGRAVITY = true; vel *= 0; master = carrier; } private void EndCarry() { if ( master ) master.A_StartSound("demolitionist/handsdown",CHAN_ITEM,CHANF_OVERLAP); A_SetRenderStyle(1.,STYLE_Normal); A_SetSize(default.radius,default.height); carried = false; bSOLID = true; bSHOOTABLE = true; bNOGRAVITY = false; master = null; } override void Tick() { if ( cdown < 80 ) { cdown++; if ( cdown == 10 ) Console.MidPrint(newsmallfont,"$D_FROGGY1"); else if ( cdown == 80 ) Console.MidPrint(newsmallfont,"$D_FROGGY2"); } if ( carried ) { if ( !master || (master.Health <= 0) ) EndCarry(); else { Vector3 tomove = master.Vec2OffsetZ(cos(master.angle)*40.,sin(master.angle)*40.,master.player.viewz-32.); Vector3 dirto = level.Vec3Diff(pos,tomove); SetOrigin(level.Vec3Offset(pos,dirto*.3),true); double magvel = dirto.length(); dirto /= magvel; vel = dirto*min(20,magvel); double angleto = deltaangle(angle,AngleTo(master)); A_SetAngle(angle+angleto*.3,SPF_INTERPOLATE); } return; } Super.Tick(); if ( pos.z <= floorz ) vel.xy *= .9; // fast friction } override bool Used( Actor user ) { if ( carried ) { if ( user != master ) return false; if ( TestMobjLocation() && level.IsPointInLevel(pos) ) EndCarry(); } else BeginCarry(user); return true; } States { Spawn: XZW1 A -1; Stop; } } // The collectibles Class MothPlushy : SWWMCollectible { // TODO post-release :: 1/64 chance after 5th use, set countdown to 175 to activate contract int usetimes; int cdown; bool mashiro; // contract with white lady is active override void DoEffect() { Super.DoEffect(); if ( cdown > 0 ) { if ( Owner && Owner.CheckLocalView() ) { if ( cdown == 105 ) A_StartSound("mashiro/appear",CHAN_ITEMEXTRA,CHANF_OVERLAP|CHANF_UI,1.,0.); else if ( cdown == 35 ) Console.MidPrint(newsmallfont,"$D_MASHIRO"); } cdown--; if ( cdown <= 0 ) mashiro = true; } } Default { Tag "$T_MOTHPLUSH"; Inventory.PickupMessage "$T_MOTHPLUSH"; Stamina 4000; } } Class AkariProject : SWWMCollectible { Default { Tag "$T_AKARIPROJECT"; Inventory.PickupMessage "$T_AKARIPROJECT"; Stamina 2000; Radius 4; Height 22; } } Class LoveSignalsCD : SWWMCollectible { Default { Tag "$T_LOVESIGNALS"; Inventory.PickupMessage "$T_LOVESIGNALS"; Stamina 3000; Radius 4; Height 21; } } Class NutatcoBar : SWWMCollectible { Default { Tag "$T_NUTATCO"; Inventory.PickupMessage "$T_NUTATCO"; Stamina 200; Radius 3; Height 22; } } Class FrispyCorn : SWWMCollectible { Default { Tag "$T_FRISPYCORN"; Inventory.PickupMessage "$T_FRISPYCORN"; Stamina 400; Radius 5; Height 23; } } // Heretic Class DemoPlush : SWWMCollectible { Default { Tag "$T_DEMOPLUSH"; Inventory.PickupMessage "$T_DEMOPLUSH"; SWWMCollectible.Availability AVAIL_Heretic; Stamina 6000; Radius 12; Height 36; } } Class SayaBean : SWWMCollectible { Default { Tag "$T_SAYABEAN"; Inventory.PickupMessage "$T_SAYABEAN"; SWWMCollectible.Availability AVAIL_Heretic; Stamina 5000; } } // Hexen Class KirinCummies : SWWMCollectible { Default { Tag "$T_PEACH"; Inventory.PickupMessage "$T_PEACH"; SWWMCollectible.Availability AVAIL_Hexen; Stamina 300; Radius 3; Height 21; } } Class MilkBreads : SWWMCollectible { Default { Tag "$T_MILKBREAD"; Inventory.PickupMessage "$T_MILKBREAD"; SWWMCollectible.Availability AVAIL_Hexen; Stamina 900; Radius 4; Height 21; } } Class KirinManga : SWWMCollectible { Default { Tag "$T_KIRINMANGA"; Inventory.PickupMessage "$T_KIRINMANGA"; SWWMCollectible.Availability AVAIL_Hexen; Stamina 1600; Radius 4; Height 22; } } Class KirinPlush : SWWMCollectible { Default { Tag "$T_KIRINPLUSH"; Inventory.PickupMessage "$T_KIRINPLUSH"; SWWMCollectible.Availability AVAIL_Hexen; Stamina 8000; Radius 14; Height 37; } } // yay! Class FancyConfetti : Actor { int deadtimer; bool dead; double anglevel, pitchvel, rollvel; Sector tracksector; int trackplane; Default { Radius 2; Height 2; +NOBLOCKMAP; +DROPOFF; +THRUACTORS; +NOTELEPORT; +DONTSPLASH; +INTERPOLATEANGLES; +ROLLSPRITE; +ROLLCENTER; Gravity 0.05; FloatBobPhase 0; } override void PostBeginPlay() { deadtimer = Random[Junk](-30,30); anglevel = FRandom[Junk](3,12)*RandomPick[Junk](-1,1); pitchvel = FRandom[Junk](3,12)*RandomPick[Junk](-1,1); rollvel = FRandom[Junk](3,12)*RandomPick[Junk](-1,1); if ( bAMBUSH ) frame = 0; else frame = Random[Junk](0,9); scale *= Frandom[Junk](0.8,1.2); } override void Tick() { prev = pos; // for interpolation if ( isFrozen() ) return; if ( dead ) { // do nothing but follow floor movement if ( tracksector ) { double trackz; if ( trackplane ) trackz = tracksector.ceilingplane.ZAtPoint(pos.xy); else trackz = tracksector.floorplane.ZAtPoint(pos.xy); if ( trackz != pos.z ) { SetZ(trackz); UpdateWaterLevel(false); } } deadtimer++; if ( deadtimer > 300 ) A_FadeOut(0.05); return; } if ( pos.z > floorz ) vel.z -= GetGravity(); vel.xy *= .98; if ( vel.z > 0 ) vel.z *= .98; // linetrace-based movement (hopefully more reliable than traditional methods) Vector3 dir = vel; double spd = vel.length(); dir /= spd; double dist = spd; FLineTraceData d; Vector3 newpos = pos; int nstep = 0; while ( dist > 0 ) { // safeguard, too many bounces if ( nstep > MAXBOUNCEPERTIC ) { Destroy(); return; } double ang = atan2(dir.y,dir.x); double pt = asin(-dir.z); LineTrace(ang,dist,pt,TRF_THRUACTORS|TRF_THRUHITSCAN|TRF_ABSPOSITION,newpos.z,newpos.x,newpos.y,d); Vector3 hitnormal = -d.HitDir; if ( d.HitType == TRACE_HitFloor ) { if ( d.Hit3DFloor ) hitnormal = -d.Hit3DFloor.top.Normal; else hitnormal = d.HitSector.floorplane.Normal; } else if ( d.HitType == TRACE_HitCeiling ) { if ( d.Hit3DFloor ) hitnormal = -d.Hit3DFloor.bottom.Normal; else 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; } dist -= d.Distance; if ( (d.HitType != TRACE_HitNone) && (d.HitType != TRACE_HitFloor) ) { // should only happen if we bounced dir = d.HitDir-(1.2*hitnormal*(d.HitDir dot hitnormal)); vel = dir*spd; newpos = d.HitLocation+hitnormal; } else newpos = level.Vec3Offset(newpos,dir*spd); if ( (d.HitType == TRACE_HitFloor) || (newpos.z <= floorz) ) { // lose speed and die if ( d.Hit3DFloor ) { newpos.z = d.Hit3DFloor.top.ZAtPoint(newpos.xy); tracksector = d.Hit3DFloor.model; trackplane = 1; } else { // hacky workaround if ( !d.HitSector ) d.HitSector = floorsector; newpos.z = d.HitSector.floorplane.ZAtPoint(newpos.xy); tracksector = d.HitSector; trackplane = 0; } vel = (0,0,0); pitch = 0; roll = 0; dead = true; SetStateLabel("Death"); break; } nstep++; } SetOrigin(newpos,true); UpdateWaterLevel(); if ( waterlevel > 0 ) { anglevel *= .98; pitchvel *= .98; rollvel *= .98; } if ( !CheckNoDelay() || (tics == -1) ) return; if ( tics > 0 ) tics--; while ( !tics ) { if ( !SetState(CurState.NextState) ) return; } } States { Spawn: XZW1 # 1 { angle += anglevel; pitch += pitchvel; roll += rollvel; } Wait; Death: XZW1 # -1; Stop; Dummy: XZW1 ABCDEFGHIJ -1; Stop; } } Class SuperFancySparkle : Actor { Default { RenderStyle "Add"; Radius 0.1; Height 0; Scale .2; +NOGRAVITY; +NOBLOCKMAP; +DONTSPLASH; +ROLLSPRITE; +ROLLCENTER; +FORCEXYBILLBOARD; +NOINTERACTION; FloatBobPhase 0; } override void PostBeginPlay() { Scale *= FRandom[ExploS](.75,1.5); specialf1 = FRandom[ExploS](.95,.98); specialf2 = FRandom[ExploS](.005,.015); double ang = FRandom[ExploS](0,360); double pt = FRandom[ExploS](-90,30); vel = (cos(ang)*cos(pt),sin(ang)*cos(pt),sin(-pt))*FRandom[ExploS](4,20); frame = Random[ExploS](0,7); } override void Tick() { if ( isFrozen() ) return; A_SetScale(scale.x*specialf1); A_FadeOut(specialf2); Vector3 dir = vel; double magvel = dir.length(); magvel *= .99; if ( magvel > 0. ) { dir /= magvel; dir += .2*(FRandom[ExploS](-1,1),FRandom[ExploS](-1,1),FRandom[ExploS](-1,1)); vel = dir.unit()*magvel; } SetOrigin(level.Vec3Offset(pos,vel),true); } States { Spawn: BLPF # -1 Bright; Stop; } } Class SuperPartyLight : PaletteLight { Default { Tag "SpRainbow"; Args 0,0,0,100; ReactionTime 40; } override void PostBeginPlay() { SetTag(String.Format("SpRainbow,%d",Random[ExploS](0,7))); ReactionTime = Random[ExploS](30,50); double ang = FRandom[ExploS](0,360); double pt = FRandom[ExploS](-90,30); vel = (cos(ang)*cos(pt),sin(ang)*cos(pt),sin(-pt))*FRandom[ExploS](2,10); Super.PostBeginPlay(); } override void Tick() { Super.Tick(); if ( isFrozen() ) return; SetOrigin(level.Vec3Offset(pos,vel),true); } } Class PartyTime : Actor { bool ignite; Default { Radius .1; Height 0.; +NOBLOCKMAP; +NOTELEPORT; +DONTSPLASH; +NOINTERACTION; } override void PostBeginPlay() { if ( target ) specialf1 = target.Height/2.; else specialf1 = 16.; } override void Tick() { if ( ignite ) { // wait for the sound to stop if ( !IsActorPlayingSound(CHAN_ITEM) ) Destroy(); return; } if ( !target || (target.tics == -1) ) { // burst into treats! ignite = true; A_Confetti(); return; } SetOrigin(target.pos,false); } action void A_Confetti() { if ( !bAMBUSH ) A_StartSound("misc/tada",CHAN_ITEM); double ang, pt; int numpt = Random[ExploS](100,120); if ( bAMBUSH ) numpt *= 2; for ( int i=0; i 3 ) { // there's three boxes in the map already (plus ourselves) Destroy(); return; } let b = Spawn("Chancebox",pos); // copy all our stuff b.spawnangle = spawnangle; b.angle = angle; b.pitch = pitch; b.roll = roll; b.spawnpoint = spawnpoint; b.special = special; for ( int i=0; i<5; i++ ) b.Args[i] = Args[i]; b.spawnflags = spawnflags&~MTF_SECRET; b.HandleSpawnFlags(); b.bCOUNTSECRET = spawnflags&MTF_SECRET; b.ChangeTid(tid); Destroy(); } Default { Radius 12; Height 20; } } Class BoxSpawnSpot { Vector3 pos; double angle; } Class CBoxLight : SpotLightAttenuated { Default { Args 112,64,224,100; DynamicLight.SpotInnerAngle 20; DynamicLight.SpotOuterAngle 80; +INTERPOLATEANGLES; } override void Tick() { Super.Tick(); if ( !target || target.InStateSequence(target.CurState,target.FindState("BlowUp")) ) { Destroy(); return; } Vector2 ofs = ((special1<2)?8:-8,(special1%2)?12:-12); double ang = (special1<2)?0:180; angle = target.angle+ang; SetOrigin(target.Vec3Offset(ofs.x*cos(target.angle)-ofs.y*sin(target.angle),ofs.y*cos(target.angle)+ofs.x*sin(target.angle),10),true); } } // Collectible box (recycling of discarded "chance box" item) Class Chancebox : Actor { bool dud; static void SpawnChanceboxes() { // find all secret sectors, find potential spawn spots within them // after that, spawn up to 3 boxes total within them int tboxes = 0; for ( int i=0; i maxradius ) maxradius = v1len; if ( v2len > maxradius ) maxradius = v2len; } int rings = 1; bool spawned = false; Array spots; spots.Clear(); for ( double j=0.; j l.bbox[3] ) continue; if ( tbox[3] < l.bbox[2] ) continue; if ( tbox[0] < l.bbox[1] ) continue; if ( tbox[1] > l.bbox[0] ) continue; if ( SWWMUtility.BoxOnLineSide(tbox[0],tbox[1],tbox[2],tbox[3],l) != -1 ) continue; blocked = true; break; } if ( blocked ) continue; // check for 3D floors first int nffloor = s.Get3DFloorCount(); double bceil = ceil; for ( int l=0; l 32+bt.Thing.Radius ) continue; if ( abs(bt.Thing.pos.y-testpos.y) > 32+bt.Thing.Radius ) continue; blockedff = true; break; } if ( blockedff ) continue; let sp = new("BoxSpawnSpot"); sp.pos = (testpos.x,testpos.y,fz); sp.angle = k+180; spots.Push(sp); } // spawn at the real floor if ( bceil-testpos.z < 56 ) continue; // too short // don't spawn on sky or hurtfloors if there are 3D floors if ( (nffloor > 0) && ((s.GetTexture(0) == skyflatnum) || (s.damageamount > 0)) ) continue; BlockThingsIterator bt = BlockThingsIterator.CreateFromPos(testpos.x,testpos.y,testpos.z,56,256,false); while ( bt.Next() ) { if ( !bt.Thing ) continue; if ( abs(bt.Thing.pos.x-testpos.x) > 32+bt.Thing.Radius ) continue; if ( abs(bt.Thing.pos.y-testpos.y) > 32+bt.Thing.Radius ) continue; blocked = true; break; } if ( blocked ) continue; let sp = new("BoxSpawnSpot"); sp.pos = testpos; sp.angle = k+180; spots.Push(sp); } rings += 3; } if ( spots.Size() < 10 ) continue; int ws = Random[Chancebox](0,spots.Size()-1); let c = Spawn("ChanceboxSpawner",spots[ws].pos); c.angle = spots[ws].angle; tboxes++; if ( tboxes >= 3 ) break; // already spawned 3 boxes in one map (which is a lot) } } action void A_DropSomething() { Array > candidates; candidates.Clear(); for ( int i=0; i)(AllActorClasses[i]); if ( !c || (c == 'SWWMCollectible') ) continue; let def = GetDefaultByType(c); // check that we can collect it in this IWAD if ( !(gameinfo.gametype&def.avail) ) continue; candidates.Push(c); } let ti = ThinkerIterator.Create("SWWMCollectible"); SWWMCollectible c; while ( c = SWWMCollectible(ti.Next()) ) { int f = candidates.Find(c.GetClass()); if ( f < candidates.Size() ) candidates.Delete(f); } if ( (candidates.Size() <= 0) || invoker.dud ) { // no candidates? just burst into treats if ( Random[Chancebox](0,1) ) { let a = Spawn(!Random[Chancebox](0,2)?"GoldShell":Random[Chancebox](0,1)?"GrilledCheeseSandwich":"YnykronAmmo",pos); a.bDROPPED = true; a.bNOGRAVITY = false; a.vel.z = FRandom[Chancebox](2,4); } else if ( !Random[Chancebox](0,2) ) { for ( int i=0; i<=12; i++ ) { let a = Spawn((i==0)?"CandyGun":"CandyGunBullets",pos); a.bDROPPED = true; a.bNOGRAVITY = false; a.vel.z = FRandom[Chancebox](2,4); if ( i > 0 ) a.vel.xy = (cos(i*30),sin(i*30))*FRandom[Chancebox](1,2); } } else if ( !Random[Chancebox](0,2) ) { for ( int i=0; i<3; i++ ) { let a = Spawn("SilverBullets2",pos); a.bDROPPED = true; a.bNOGRAVITY = false; a.vel.z = FRandom[Chancebox](2,4); a.vel.xy = (cos(i*120),sin(i*120))*FRandom[Chancebox](1,2); } for ( int i=0; i<6; i++ ) { let a = Spawn("SilverBullets",pos); a.bDROPPED = true; a.bNOGRAVITY = false; a.vel.z = FRandom[Chancebox](2,4); a.vel.xy = (cos(i*60),sin(i*60))*FRandom[Chancebox](3,4); } } else if ( Random[Chancebox](0,1) ) { let a = Spawn("HellblazerWarheads",pos); a.bDROPPED = true; a.bNOGRAVITY = false; a.vel.z = FRandom[Chancebox](2,4); for ( int i=0; i<3; i++ ) { a = Spawn("HellblazerRavagers",pos); a.bDROPPED = true; a.bNOGRAVITY = false; a.vel.z = FRandom[Chancebox](2,4); a.vel.xy = (cos(i*120),sin(i*120))*FRandom[Chancebox](1,2); } for ( int i=0; i<5; i++ ) { a = Spawn("HellblazerCrackshots",pos); a.bDROPPED = true; a.bNOGRAVITY = false; a.vel.z = FRandom[Chancebox](2,4); a.vel.xy = (cos(i*72),sin(i*72))*FRandom[Chancebox](3,4); } for ( int i=0; i<8; i++ ) { a = Spawn("HellblazerMissiles",pos); a.bDROPPED = true; a.bNOGRAVITY = false; a.vel.z = FRandom[Chancebox](2,4); a.vel.xy = (cos(i*45),sin(i*45))*FRandom[Chancebox](5,6); } } else if ( Random[Chancebox](0,1) ) { let a = Spawn("BlackShell",pos); a.bDROPPED = true; a.bNOGRAVITY = false; a.vel.z = FRandom[Chancebox](2,4); for ( int i=0; i<3; i++ ) { a = Spawn("BlueShell",pos); a.bDROPPED = true; a.bNOGRAVITY = false; a.vel.z = FRandom[Chancebox](2,4); a.vel.xy = (cos(i*120),sin(i*120))*FRandom[Chancebox](1,2); } for ( int i=0; i<8; i++ ) { a = Spawn((i%2)?"WhiteShell":"PurpleShell",pos); a.bDROPPED = true; a.bNOGRAVITY = false; a.vel.z = FRandom[Chancebox](2,4); a.vel.xy = (cos(i*72),sin(i*72))*FRandom[Chancebox](3,4); } for ( int i=0; i<12; i++ ) { a = Spawn((i%2)?"RedShell":"GreenShell",pos); a.bDROPPED = true; a.bNOGRAVITY = false; a.vel.z = FRandom[Chancebox](2,4); a.vel.xy = (cos(i*30),sin(i*30))*FRandom[Chancebox](5,6); } } else if ( Random[Chancebox](0,1) ) { for ( int i=0; i<100; i++ ) { let a = Spawn("HealthNuggetItem",pos); a.bDROPPED = true; a.bNOGRAVITY = false; a.vel.z = FRandom[Chancebox](2,8); a.vel.xy = (cos(i*3.6),sin(i*3.6))*FRandom[Chancebox](1,8); } } else { for ( int i=0; i<=15; i++ ) { let a = Spawn((i==0)?"RefresherItem":(i%3)?"HealthNuggetItem":"ArmorNuggetItem",pos); a.bDROPPED = true; a.bNOGRAVITY = false; a.vel.z = FRandom[Chancebox](2,4); if ( i > 0 ) a.vel.xy = (cos(i*24),sin(i*24))*FRandom[Chancebox](1,2); } } } else { // pop one at random let a = Spawn(candidates[Random[Chancebox](0,candidates.Size()-1)],pos); a.bDROPPED = true; a.bNOGRAVITY = false; a.vel.z = FRandom[Chancebox](2,4); } int numpt = Random[ExploS](16,32); for ( int i=0; i rang ) return false; if ( CurState != FindState("Spawn") ) return false; if ( !dud ) { if ( !Random[Chancebox](0,2) ) { // all boxes are duds let ti = ThinkerIterator.Create("Chancebox"); Chancebox c; while ( c = Chancebox(ti.Next()) ) c.dud = true; } else if ( Random[Chancebox](0,2) ) { int nbox = 0, ndud = 0; // this one's a dud (unless all the others are) let ti = ThinkerIterator.Create("Chancebox"); Chancebox c; while ( c = Chancebox(ti.Next()) ) { if ( c == self ) continue; nbox++; if ( c.dud ) ndud++; } if ( ndud < nbox ) dud = true; } else { // the others are duds let ti = ThinkerIterator.Create("Chancebox"); Chancebox c; while ( c = Chancebox(ti.Next()) ) { if ( c == self ) continue; c.dud = true; // we found one, the others just drop goodies } } } if ( bCOUNTITEM ) { user.player.itemcount++; level.found_items++; bCOUNTITEM = false; } if ( bCOUNTSECRET ) { user.GiveSecret(); bCOUNTSECRET = false; } if ( special ) { user.A_CallSpecial(special,args[0],args[1],args[2],args[3],args[4]); special = 0; } SWWMLoreLibrary.Add(user.player,"Chancebox"); specialf2 = AngleTo(user); SetStateLabel("PreActive"); return true; } override void PostBeginPlay() { Super.PostBeginPlay(); for ( int i=0; i<4; i++ ) { let l = Spawn("CBoxLight",pos); l.special1 = i; l.target = self; } let ti = ThinkerIterator.Create("Chancebox"); Chancebox c; while ( c = Chancebox(ti.Next()) ) { if ( c.dud || (c.CurState == c.FindState("Spawn")) ) continue; // automatically become a dud if collectible has been found dud = true; break; } } Default { Tag "$T_CHANCEBOX"; Radius 12; Height 20; +MOVEWITHSECTOR; +ROLLSPRITE; +SOLID; +INTERPOLATEANGLES; +COUNTITEM; Species "Chancebox"; } States { Spawn: XZW1 A -1; Stop; PreActive: XZW1 A 1 { double delta = deltaangle(angle,specialf2); int sign = (delta>=0.)?1:-1; delta = clamp(abs(delta)*.15,.1,10.)*sign; angle += delta; return A_JumpIf(abs(deltaangle(angle,specialf2))<1.,"Active"); } Wait; Active: XZW1 A 1 { angle = specialf2; specialf1 = angle; A_StartSound("misc/drumroll",CHAN_WEAPON); } XZW1 A 1 { bINTERPOLATEANGLES = false; angle = specialf1+FRandom[Chancebox](-5,5); pitch = FRandom[Chancebox](-5,5); roll = FRandom[Chancebox](-5,5); special1++; return A_JumpIf(special1>40,"BlowUp"); } Wait; BlowUp: XZW2 A 1 { A_SetSize(12,3); A_QuakeEx(2,2,2,9,0,500,"",QF_RELATIVE|QF_SCALEDOWN,falloff:200,rollIntensity:.2); A_StartSound("chancebox/explode",CHAN_VOICE); angle = specialf1; pitch = roll = 0; let t = Spawn("ChanceboxTop",Vec3Offset(0,0,20)); t.angle = angle; let s1 = Spawn("ChanceboxSide",level.Vec3Offset(pos,(RotateVector((12,0),angle+90),0))); s1.angle = angle+90; let s2 = Spawn("ChanceboxSide",level.Vec3Offset(pos,(RotateVector((12,0),angle-90),0))); s2.angle = angle-90; A_DropSomething(); } XZW2 BCDEFGHIJKLMNO 1; XZW2 P -1 A_Confetti(); Stop; } } // top side of chancebox, shoots upwards Class ChanceboxTop : Actor { Default { Radius 12; Height 3; Species "Chancebox"; PROJECTILE; +THRUSPECIES; } override void PostBeginPlay() { Super.PostBeginPlay(); vel = (0,0,20); } States { Spawn: XZW1 A 1 { double magvel = vel.length(); if ( magvel > 0. ) { magvel = min(60,magvel*1.2); vel = vel.unit()*magvel; } } Wait; Death: TNT1 A 1 A_SpawnItemEx("ExplodiumBulletImpact"); Stop; } } // left/right side of chancebox, shoots forward Class ChanceboxSide : Actor { Default { Radius 12; Height 24; Species "Chancebox"; PROJECTILE; +THRUSPECIES; } override void PostBeginPlay() { Super.PostBeginPlay(); vel = (cos(angle)*cos(pitch),sin(angle)*cos(pitch),-sin(pitch))*20; } States { Spawn: XZW1 A 1 { double magvel = vel.length(); if ( magvel > 0. ) { magvel = min(60,magvel*1.2); vel = vel.unit()*magvel; } } Wait; Death: TNT1 A 1 A_SpawnItemEx("ExplodiumBulletImpact"); Stop; } }