// Lämp Class LampMoth : Actor { Actor lamp; Vector3 trail, ofs; int lifespan; SWWMHandler hnd; Default { Tag "$T_MOTH"; Radius 2; Height 4; Speed 2; DamageFunction 1; MeleeRange 16; Mass 10; Health 50; DeathSound "moth/die"; BloodColor "20 10 10"; MONSTER; -COUNTKILL; +THRUACTORS; +NOGRAVITY; +NOTELEPORT; +FLOAT; +NOPAIN; +FRIENDLY; +LOOKALLAROUND; +QUICKTORETALIATE; +INTERPOLATEANGLES; +NOBLOCKMONST; } override string GetObituary( Actor victim, Actor inflictor, Name mod, bool playerattack ) { if ( inflictor && (inflictor != self) ) { if ( inflictor == master ) return StringTable.Localize("$O_MOTHSELF"); // not likely to happen else return StringTable.Localize("$O_MOTH"); } return StringTable.Localize("$O_MOTH2"); } override void PostBeginPlay() { Super.PostBeginPlay(); A_StartSound("moth/fly",CHAN_BODY,CHANF_LOOP,.02,4.,FRandom[Moth](.8,1.2)); if ( master && master.player ) SetFriendPlayer(master.player); else bFRIENDLY = false; } override int DamageMobj( Actor inflictor, Actor source, int damage, Name mod, int flags, double angle ) { // no hurt moff if ( source && IsFriend(source) ) damage = 0; return Super.DamageMobj(inflictor,source,damage,mod,flags,angle); } bool isEntranced() { if ( !lamp && CurSector ) // CurSector can be null somehow? { // look for nearby lamps double mindist = 62500.; if ( !hnd ) hnd = SWWMHandler(EventHandler.Find('SWWMHandler')); foreach ( s:level.Sectors ) { // don't check sectors that aren't within bounds, saves some time if ( !hnd.BoxInSectorBounds(s,pos.xy,250,CurSector.PortalGroup) ) continue; for ( Actor a=s.thinglist; a; a=a.snext ) { if ( !(a is 'CompanionLamp') ) continue; double dist = Distance3DSquared(a); if ( (a.frame == 0) || (dist > mindist) && !CheckSight(a,SF_IGNOREVISIBILITY|SF_IGNOREWATERBOUNDARY) ) continue; mindist = dist; lamp = a; master = a.target; if ( CompanionLamp(lamp).moff.Find(self) == CompanionLamp(lamp).moff.Size() ) CompanionLamp(lamp).moff.Push(self); if ( master && master.player ) SetFriendPlayer(master.player); else bFRIENDLY = false; } } } if ( !lamp || (lamp.frame == 0) || (Distance3DSquared(lamp) > 62500) || !CheckSight(lamp,SF_IGNOREVISIBILITY|SF_IGNOREWATERBOUNDARY) ) return false; if ( target && (target.Health > 0) && CheckSight(target) ) return false; return true; } void A_SmoothWander() { if ( level.Vec3Diff(pos,trail).length() < speed ) { double ang = FRandom[Moth](0,360); double pt = FRandom[Moth](-30,30); double dist = FRandom[Moth](20,40); ofs = SWWMUtility.Vec3FromAngles(ang,pt)*dist; } Vector3 newpos = level.Vec3Offset(pos,ofs); if ( level.IsPointInLevel(newpos) ) trail = newpos; if ( vel.length() > 0 ) { Vector3 uvel = vel.unit(); angle += Clamp(deltaangle(angle,atan2(uvel.y,uvel.x)),-5.,5.); pitch += Clamp(deltaangle(pitch,asin(-uvel.z)),-5.,5.); } vel *= .8; Vector3 dir = level.Vec3Diff(pos,trail); if ( dir.length() > 0 ) vel += dir.unit()*clamp(dir.length()*.05,.4*speed,.5*speed); } void A_SmoothChase() { if ( !target || (target.Health <= 0) || target.IsFriend(self) ) { A_ClearTarget(); SetStateLabel('Spawn'); return; } if ( CheckMeleeRange() ) { SetStateLabel('Melee'); return; } Vector3 dest = target.Vec3Offset(0,0,target.height*.75); Vector3 dir = level.Vec3Diff(pos,dest); if ( dir.length() > 0 ) { Vector3 dirunit = dir.unit(); FLineTraceData d; LineTrace(atan2(dirunit.y,dirunit.x),dir.length(),asin(-dirunit.z),data:d); if ( (d.HitType != TRACE_HitActor) && (d.HitActor != target) ) { A_Chase(); return; } } if ( vel.length() > 0 ) { Vector3 uvel = vel.unit(); angle = atan2(uvel.y,uvel.x); pitch = asin(-uvel.z); } vel *= .8; if ( dir.length() > 0 ) vel += dir.unit()*clamp(dir.length()*.02,.3*speed,2.*speed); } void A_FollowLamp() { if ( !lamp ) { SetStateLabel('Spawn'); return; } double dst = level.Vec3Diff(pos,trail).length(); if ( (dst < speed) || (dst > 50) ) { double ang = FRandom[Moth](0,360); double pt = FRandom[Moth](-30,30); double dist = FRandom[Moth](20,30); ofs = SWWMUtility.Vec3FromAngles(ang,pt)*dist; } Vector3 newpos = level.Vec3Offset(lamp.Vec3Offset(0,0,lamp.height/2),ofs); if ( level.IsPointInLevel(newpos) ) trail = newpos; if ( vel.length() > 0 ) { Vector3 uvel = vel.unit(); angle = atan2(uvel.y,uvel.x); pitch = asin(-uvel.z); } vel *= .8; Vector3 dir = level.Vec3Diff(pos,trail); if ( dir.length() > 0 ) vel += dir.unit()*clamp(dir.length()*.02,.4*speed,2.*speed); Vector3 diff = level.Vec3Diff(pos,lamp.pos); if ( (diff.x > -8) && (diff.x < 8) && (diff.y > -8) && (diff.y < 8) && (diff.z > -4) && (diff.z < lamp.height+4) ) { if ( diff.x < 0 ) vel.x -= .2; else vel.x += .2; if ( diff.y < 0 ) vel.y -= .2; else vel.y += .2; if ( diff.z < 0 ) vel.z -= .2; else vel.z += .2; } } void A_SmoothMove() { if ( vel.length() > 0 ) { Vector3 uvel = vel.unit(); angle = atan2(uvel.y,uvel.x); pitch = asin(-uvel.z); } vel *= .8; } void A_Scrape() { if ( CheckMeleeRange() ) { A_FaceTarget(0,0); lifespan -= 5; Vector3 awaydir = level.Vec3Diff(target.Vec3Offset(0,0,target.height),pos).unit(); vel += awaydir*8.; int dmg = target.DamageMobj(self,master?master:Actor(self),GetMissileDamage(0,0),'Melee',Random[Moth](0,8)?DMG_NO_PAIN:0); if ( (dmg > 0) && target && !target.bNOBLOOD && !target.bDORMANT && !target.bINVULNERABLE ) { target.TraceBleed(dmg,self); target.SpawnBlood(pos,atan2(awaydir.y,awaydir.x)+180,dmg); } A_StartSound("moth/scrape",CHAN_WEAPON,CHANF_OVERLAP,.2,2.5); DamageMobj(target,target,1,'Melee'); } } override void Tick() { Super.Tick(); if ( isFrozen() || (freezetics > 0) ) return; if ( isEntranced() ) { lifespan = 100; return; } if ( target && (target.Health > 0) ) lifespan = max(20,lifespan); lifespan--; if ( lifespan <= 0 ) { let s = SWWMAnimSprite.SpawnAt('SWWMSmallSmoke',pos); s.alpha *= .3; Destroy(); } } States { Spawn: XZW1 B 0 A_JumpIf(isEntranced(),'See.Entranced'); XZW1 BC 1 { A_SmoothWander(); A_Look(); } Loop; See: // go for enemies XZW1 B 0 A_JumpIf(isEntranced(),'See.Entranced'); XZW1 BC 1 A_SmoothChase(); Loop; See.Entranced: // follow the lamp XZW1 B 0 A_JumpIf(!isEntranced(),'Spawn'); XZW1 BC 1 { A_FollowLamp(); // allow moths to still target enemies while following the lamp, but only if they get really close A_LookEx(LOF_NOSOUNDCHECK|LOF_NOJUMP,maxseedist:150); } Loop; Melee: XZW1 B 0 A_Scrape(); XZW1 BCBC 1 A_SmoothMove(); Goto See; Death: TNT1 A 1 { A_StartSound("moth/die",CHAN_VOICE,CHANF_OVERLAP,.6,2.5); let s = SWWMAnimSprite.SpawnAt('SWWMSmallSmoke',pos); s.alpha *= .3; } Stop; } } Class LampMoth2 : LampMoth { Default { Tag "$T_WMOTH"; DamageFunction 3; Speed 3; Scale 1.5; Health 200; } } Class CompanionLamp : Actor { Vector3 Trail; Array moff; Actor parent; bool justteleport; Default { Tag "$T_LAMP"; +NOGRAVITY; +NOTELEPORT; +DONTSPLASH; +INTERPOLATEANGLES; +LOOKALLAROUND; +FRIENDLY; +NOBLOCKMONST; Radius 4; Height 16; } // random chance to spawn moths void A_Moth() { // count up special1++; for ( int i=0; i= 30) ) return; // spawn a moth at a random offset double ang = FRandom[Moth](0,360); double pt = FRandom[Moth](-30,30); double dist = FRandom[Moth](10,30); Vector3 ofs = SWWMUtility.Vec3FromAngles(ang,pt)*dist; Vector3 spawnpos = level.Vec3Offset(Vec3Offset(0,0,height/2),ofs); if ( !level.IsPointInLevel(spawnpos) ) return; // higher chance of white moths if carrying the plush int mchance = parent.FindInventory('MothPlushy')?3:9; let m = LampMoth(Spawn(Random[Moth](0,mchance)?'LampMoth':'LampMoth2',spawnpos)); if ( !m.TestMobjLocation() ) { m.Destroy(); return; } let s = SWWMAnimSprite.SpawnAt('SWWMSmallSmoke',m.pos); s.alpha *= .3; m.master = parent; m.lamp = self; m.trail = m.pos; moff.Push(m); SWWMUtility.AchievementProgressInc("moth",1,parent.player); } override void PostBeginPlay() { Super.PostBeginPlay(); if ( !parent || !SWWMLamp(master) ) { Destroy(); return; } Spawn('SWWMItemFog',pos); Trail = pos; } override void Tick() { Super.Tick(); if ( !parent || !SWWMLamp(master) ) { Destroy(); return; } if ( isFrozen() || (freezetics > 0) ) return; // update trailing position bool foundspot = false; for ( int i=0; i<180; i+=5 ) { for ( int j=1; j>=-1; j-=2 ) { double ang = (parent.angle-180)+i*j; Vector3 testpos = level.Vec3Offset(parent.pos,SWWMUtility.RotateVector3((32,0,parent.height-8+1.5*sin(level.maptime*3.)),ang)); if ( !level.IsPointInLevel(testpos) ) continue; Vector3 oldpos = pos; Vector3 oldprev = prev; Actor oldblockingmobj = blockingmobj; Line oldblockingline = blockingline; Sector oldblockingfloor = blockingfloor, oldblockingceiling = blockingceiling; SetOrigin(testpos,false); if ( !TestMobjLocation() || SWWMUtility.BlockingLineIsBlocking(self,Line.ML_BLOCKING|Line.ML_BLOCKEVERYTHING) || BlockingFloor || BlockingCeiling ) { SetOrigin(oldpos,false); prev = oldprev; blockingmobj = oldblockingmobj; blockingline = oldblockingline; blockingfloor = oldblockingfloor; blockingceiling = oldblockingceiling; continue; } SetOrigin(oldpos,false); prev = oldprev; blockingmobj = oldblockingmobj; blockingline = oldblockingline; blockingfloor = oldblockingfloor; blockingceiling = oldblockingceiling; Trail = testpos; foundspot = true; } // check at most for a 45 degree offset if ( foundspot && (i > 45) ) break; } Vector3 diff = level.Vec3Diff(pos,parent.pos); if ( (diff.length() > 400) || justteleport ) { Vector3 rel = level.Vec3Diff(pos,trail); justteleport = false; Actor f = Spawn('SWWMItemFog',pos); f.A_StartSound("lamp/disappear",CHAN_VOICE); // carry over the moths foreach ( m:moff ) { if ( !m ) continue; Vector3 whereto = level.Vec3Offset(m.pos,rel); if ( !level.IsPointInLevel(whereto) ) continue; Vector3 oldp = m.pos; m.SetOrigin(whereto,false); if ( !m.TestMobjLocation() ) m.SetOrigin(oldp,false); } SetOrigin(trail,false); angle = AngleTo(parent); vel *= 0.; f = Spawn('SWWMItemFog',pos); f.A_StartSound("lamp/appear",CHAN_VOICE); return; } angle += Clamp(deltaangle(angle,AngleTo(parent)),-5.,5.); vel *= .8; bool blocked = false; if ( SWWMUtility.BlockingLineIsBlocking(self,Line.ML_BLOCKING|Line.ML_BLOCKEVERYTHING) ) { // push away from wall Vector3 normal = (-BlockingLine.delta.y,BlockingLine.delta.x,0).unit(); if ( !Level.PointOnLineSide(pos.xy,BlockingLine) ) normal *= -1; vel += 4.*normal; blocked = true; } if ( BlockingFloor ) { // push away from floor Vector3 normal = BlockingFloor.floorplane.Normal; // find closest 3d floor for its normal for ( int i=0; i -16) && (diff.x < 16) && (diff.y > -16) && (diff.y < 16) && (diff.z > -16) && (diff.z < parent.height+8) ) { if ( diff.x < 0 ) vel.x -= .2; else vel.x += .2; if ( diff.y < 0 ) vel.y -= .2; else vel.y += .2; if ( diff.z < 0 ) vel.z -= .2; else vel.z += .2; blocked = true; } if ( blocked ) return; Vector3 dir = level.Vec3Diff(pos,trail); if ( dir.length() > 0 ) vel += dir.unit()*min(dir.length()*.05,20.); } States { Spawn: XZW1 A 1 { if ( SWWMLamp(master) && SWWMLamp(master).bActive ) { A_StartSound("lamp/on",CHAN_ITEMEXTRA,CHANF_OVERLAP); return ResolveState('Active'); } return ResolveState(null); } Wait; Active: XZW1 B 1 { A_Moth(); if ( !SWWMLamp(master) || !SWWMLamp(master).bActive ) { A_StartSound("lamp/off",CHAN_ITEMEXTRA,CHANF_OVERLAP); return ResolveState('Spawn'); } return ResolveState(null); } Wait; } } Class SWWMLamp : Inventory { Mixin SWWMOverlapPickupSound; Mixin SWWMUseToPickup; Mixin SWWMRespawn; Mixin SWWMRotatingPickup; Mixin SWWMPickupGlow; Mixin SWWMUnrealStyleDrop; bool bActive, bActivated; TextureID OnIcon; Actor thelamp; int charge; int inactivetime; Property Charge : charge; override Inventory CreateCopy( Actor other ) { // additional lore SWWMLoreLibrary.Add(other.player,"MothLamp"); return Super.CreateCopy(other); } override bool HandlePickup( Inventory item ) { // add charge if ( item.GetClass() == GetClass() ) { if ( (Charge >= Default.Charge) && (Amount+item.Amount > MaxAmount) ) { // sell excess if ( Owner.player ) { int sellprice = abs(Stamina)/2; SWWMCredits.Give(Owner.player,sellprice); if ( Owner.player == players[consoleplayer] ) { SWWMScoreObj.SpawnAtActorBunch(sellprice,Owner); Console.Printf(StringTable.Localize(SWWMUtility.SellFemaleItem(item)?"$SWWM_SELLEXTRA_FEM":"$SWWM_SELLEXTRA"),GetTag(),sellprice); } else Console.Printf(StringTable.Localize(SWWMUtility.SellFemaleItem(item)?"$SWWM_SELLEXTRAREM_FEM":"$SWWM_SELLEXTRAREM"),Owner.player.GetUserName(),GetTag(),sellprice); } } else if ( Charge > 0 ) { int AddCharge = Charge+SWWMLamp(item).Charge; Charge = min(Default.Charge,AddCharge); // if there's charge to spare, increase amount if ( AddCharge > Charge ) { if ( (Amount > 0) && (Amount+item.Amount < 0) ) Amount = int.max; Amount = min(MaxAmount,Amount+item.Amount); Charge = AddCharge-Charge; } } else { if ( (Amount > 0) && (Amount+item.Amount < 0) ) Amount = int.max; // new copy, increase and take its charge Amount = min(MaxAmount,Amount+item.Amount); Charge = SWWMLamp(item).Charge; } item.bPickupGood = true; return true; } return Super.HandlePickup(item); } override bool Use( bool pickup ) { if ( pickup && !deathmatch ) return false; bActivated = true; bActive = !bActive; if ( !OnIcon ) OnIcon = TexMan.CheckForTexture("graphics/HUD/Icons/I_Lamp.png"); Icon = bActive?OnIcon:default.Icon; // don't consume on use Amount++; return true; } override bool ShouldSpawn() { if ( deathmatch ) return false; return Super.ShouldSpawn(); } override void PreTravelled() { // remove the lamp if ( thelamp ) thelamp.Destroy(); } override void DoEffect() { Super.DoEffect(); if ( !thelamp && bActivated ) { thelamp = Spawn('CompanionLamp',level.Vec3Offset(Owner.pos,SWWMUtility.RotateVector3((20,0,24),Owner.angle))); CompanionLamp(thelamp).parent = Owner; thelamp.master = self; let f = Spawn('SWWMItemFog',thelamp.pos); f.A_StartSound("lamp/appear",CHAN_VOICE); } if ( bActive && !(level.maptime%35) && !isFrozen() ) Charge--; if ( bActive || !thelamp ) inactivetime = 0; else if ( thelamp ) { inactivetime++; if ( inactivetime > 350 ) // hide the lamp after 10 seconds of inactivity, so it doesn't get in your way { let f = Spawn('SWWMItemFog',thelamp.pos); f.A_StartSound("lamp/disappear",CHAN_VOICE); thelamp.Destroy(); bActivated = false; } } if ( Charge <= 0 ) { Amount--; if ( Amount <= 0 ) DepleteOrDestroy(); else Charge = default.Charge; } } override void DetachFromOwner() { Super.DetachFromOwner(); if ( thelamp ) { let f = Spawn('SWWMItemFog',thelamp.pos); f.A_StartSound("lamp/disappear",CHAN_VOICE); thelamp.Destroy(); } Icon = default.Icon; bActive = false; bActivated = false; } clearscope bool isBlinking() const { return ( (Charge < 10) && (level.maptime&8) ); } Default { Tag "$T_LAMP"; Inventory.Icon "graphics/HUD/Icons/I_LampOff.png"; Inventory.PickupSound "misc/p_pkup"; Inventory.PickupMessage "$I_LAMP"; Inventory.Amount 1; Inventory.MaxAmount 5; Inventory.InterHubAmount 5; Inventory.PickupFlash 'SWWMPurplePickupFlash'; +INVENTORY.ALWAYSPICKUP; +INVENTORY.AUTOACTIVATE; +INVENTORY.INVBAR; +COUNTITEM; +INVENTORY.BIGPOWERUP; +FLOATBOB; +DONTGIB; FloatBobStrength 0.25; SWWMLamp.Charge 100; Stamina 70000; } States { Spawn: XZW1 A -1; Stop; } }