swwmgz_m/zscript/swwm_inventory.zsc
Marisa Kirisame af1df2e18d Allow selling excess weapons/powerups.
Change of plans for one DLC weapon choice.
Various small fixes and tweaks.
Doubled price of Ynykron, since it was too cheap.
Fixed Candygun spares not being picked up in coop.
Add option to toggle menu fuzz, for streamers concerned about bitrate.
Add MAP07 enemies to the vanilla boss enhancer.
2020-05-19 14:30:35 +02:00

1217 lines
35 KiB
Text

// Inventory stuff
Mixin Class SWWMAutoUseFix
{
override bool HandlePickup( Inventory item )
{
if ( GetClass() == item.GetClass() )
{
// sell excess if there's a price
if ( bALWAYSPICKUP && (Amount+item.Amount > MaxAmount) && (Stamina > 0) )
{
int sellprice = SWWMUtility.Round100(Stamina*.7);
SWWMScoreObj.Spawn(sellprice,Owner.Vec3Offset(FRandom[ScoreBits](-8,8),FRandom[ScoreBits](-8,8),FRandom[ScoreBits](-8,8)+Owner.Height/2),Font.CR_GOLD);
SWWMCredits.Give(Owner.player,sellprice);
if ( Owner.player )
Console.Printf(StringTable.Localize("$SWWM_SELLEXTRA"),Owner.player.GetUserName(),GetTag(),sellprice);
}
if ( Use(true) ) Amount--;
}
return Super.HandlePickup(item);
}
}
// Base class for all SWWM Armors
Class SWWMArmor : Armor abstract
{
int priority;
String drainmsg;
Class<SWWMSpareArmor> parent;
Property ArmorPriority : priority;
Property DrainMessage : drainmsg;
Property GiverArmor : parent;
Default
{
+INVENTORY.AUTOACTIVATE;
+INVENTORY.UNTOSSABLE;
+INVENTORY.UNDROPPABLE;
+INVENTORY.KEEPDEPLETED;
+INVENTORY.ALWAYSPICKUP;
}
override void AttachToOwner( Actor other )
{
Super.AttachToOwner(other);
// find last armor that's better than us
Inventory found = null;
bool foundarmor = false;
for ( Inventory i=other.Inv; i; i=i.Inv )
{
if ( (i is 'SWWMArmor') && (i != self) ) foundarmor = true;
if ( !(i is 'SWWMArmor') || (i == self) || (SWWMArmor(i).priority < priority) ) continue;
found = i;
}
if ( !found && !foundarmor )
{
// check if first item in inventory is health or a sandwich
if ( (other.Inv is 'SWWMHealth') || (other.Inv is 'GrilledCheeseSandwich') )
{
// place ourselves before it
Inv = other.Inv;
other.Inv = self;
return;
}
// find first item with health or sandwich after it
for ( Inventory i=other.Inv; i; i=i.Inv )
{
if ( (i == self) || (!(i.Inv is 'SWWMHealth' ) && !(i.Inv is 'GrilledCheeseSandwich')) ) continue;
found = i;
break;
}
}
if ( !found && !foundarmor )
{
// find last of either invinciball or ragekit power
for ( Inventory i=other.Inv; i; i=i.Inv )
{
if ( !(i is 'InvinciballPower') && !(i is 'RagekitPower') ) continue;
found = i;
}
}
if ( !found ) return;
// place ourselves right after it
Inventory saved = found.Inv;
found.Inv = self;
other.Inv = Inv;
Inv = saved;
}
// for subclasses
virtual int HandleDamage( int damage, Name damageType, int flags )
{
return damage;
}
// sadly we can'y use the extra AbsorbDamage arguments until GZDoom gets updated
//override void AbsorbDamage( int damage, Name damageType, out int newdamage, Actor inflictor, Actor source, int flags )
override void ModifyDamage( int damage, Name damageType, out int newdamage, bool passive, Actor inflictor, Actor source, int flags )
{
if ( !passive ) return;
if ( (damagetype == 'Drowning') || (damagetype == 'Falling') || (damagetype == 'Poison') || (damagetype == 'PoisonCloud') )
return; // these go through armor and get ignored by the player
int saved;
if ( (amount > 0) && !DamageTypeDefinition.IgnoreArmor(damageType) && (damage > 0) )
{
SWWMHandler.DoFlash(Owner,Color(int(clamp(damage*.15,1,16)),255,224,192),3);
Owner.A_StartSound("armor/hit",CHAN_BODY,CHANF_DEFAULT,clamp(damage*.03,0.,1.),2.5);
saved = HandleDamage(damage,damageType,flags);
int healed = max(0,saved-damage);
saved = min(saved,damage);
if ( amount <= saved ) saved = amount;
newdamage -= saved;
if ( healed > 0 ) Owner.GiveBody(healed);
amount -= saved;
damage = newdamage;
bool shouldautouse = false;
if ( swwm_enforceautousearmor == 1 ) shouldautouse = true;
else if ( swwm_enforceautousearmor == -1 ) shouldautouse = false;
else shouldautouse = CVar.GetCVar('swwm_autousearmor',Owner.player).GetBool();
if ( (amount <= (MaxAmount-default.Amount)) && (Owner.CountInv(parent) > 0) && shouldautouse )
{
if ( GetDefaultByType(parent).UseSound ) Owner.A_StartSound(GetDefaultByType(parent).UseSound,CHAN_ITEMEXTRA,CHANF_DEFAULT,.6);
int tgive = 0;
bool acc = CVar.GetCVar('swwm_accdamage',players[consoleplayer]).GetBool();
while ( (amount <= (MaxAmount-default.Amount)) && (Owner.CountInv(parent) > 0) )
{
if ( acc ) tgive += default.Amount;
else SWWMScoreObj.Spawn(default.Amount,Owner.Vec3Offset(FRandom[ScoreBits](-8,8),FRandom[ScoreBits](-8,8),FRandom[ScoreBits](-8,8)+Owner.Height/2),Font.CR_GREEN);
Amount += default.Amount;
Owner.TakeInventory(parent,1);
// absorb the extra damage too
saved = HandleDamage(damage,damageType,flags);
healed = max(0,saved-damage);
saved = min(saved,damage);
if ( amount <= saved ) saved = amount;
newdamage -= saved;
if ( healed > 0 ) Owner.GiveBody(healed);
amount -= saved;
damage = newdamage;
}
if ( acc ) SWWMScoreObj.Spawn(tgive,Owner.Vec3Offset(FRandom[ScoreBits](-8,8),FRandom[ScoreBits](-8,8),FRandom[ScoreBits](-8,8)+Owner.Height/2),Font.CR_GREEN);
}
else if ( amount <= 0 )
{
if ( damage > 0 ) newdamage = ApplyDamageFactors(GetClass(),damageType,damage,damage);
if ( Owner.CheckLocalView() && (drainmsg != "") ) Console.Printf(StringTable.Localize(drainmsg));
DepleteOrDestroy();
return;
}
}
if ( damage > 0 ) newdamage = ApplyDamageFactors(GetClass(),damageType,damage,damage);
}
}
// gives armor when used
Class SWWMSpareArmor : Inventory abstract
{
Mixin SWWMAutoUseFix;
Class<SWWMArmor> giveme;
Property GiveArmor : giveme;
override bool Use( bool pickup )
{
bool shouldautouse = false;
if ( swwm_enforceautousearmor == 1 ) shouldautouse = true;
else if ( swwm_enforceautousearmor == -1 ) shouldautouse = false;
else shouldautouse = CVar.GetCVar('swwm_autousearmor',Owner.player).GetBool();
if ( pickup && !shouldautouse ) return false;
let cur = Owner.FindInventory(giveme);
if ( !cur || (!pickup && (cur.Amount < cur.MaxAmount)) || (GetDefaultByType(giveme).Amount+cur.Amount <= cur.MaxAmount) )
{
if ( pickup && ((Owner.player == players[consoleplayer]) || bBigPowerup) ) Owner.A_StartSound(UseSound,CHAN_ITEMEXTRA);
Owner.GiveInventory(giveme,GetDefaultByType(giveme).Amount);
SWWMHandler.ArmorFlash(Owner.PlayerNumber());
SWWMScoreObj.Spawn(GetDefaultByType(giveme).Amount,Owner.Vec3Offset(FRandom[ScoreBits](-8,8),FRandom[ScoreBits](-8,8),FRandom[ScoreBits](-8,8)+Owner.Height/2),Font.CR_GREEN);
return true;
}
return false;
}
Default
{
+INVENTORY.INVBAR;
+INVENTORY.ISARMOR;
+INVENTORY.AUTOACTIVATE;
Inventory.MaxAmount 5;
Inventory.InterHubAmount 5;
+FLOATBOB;
FloatBobStrength 0.25;
}
}
Class SWWMHealth : Inventory abstract
{
Mixin SWWMAutoUseFix;
// can't use the Health class for whatever reason
// nice parser you got there I guess?
Class<Inventory> giveme;
Property GiveHealth : giveme;
override void AttachToOwner( Actor other )
{
Super.AttachToOwner(other);
// find last health item that's better than us
Inventory found = null;
for ( Inventory i=other.Inv; i; i=i.Inv )
{
if ( !(i is 'SWWMHealth') || (i == self) || (GetDefaultByType(SWWMHealth(i).giveme).Amount < GetDefaultByType(giveme).Amount) ) continue;
found = i;
}
if ( !found )
{
// find last armor item
for ( Inventory i=other.Inv; i; i=i.Inv )
{
if ( !(i is 'SWWMArmor') ) continue;
found = i;
}
}
if ( !found )
{
// check if the first item in inventory is a sandwich
if ( other.Inv is 'GrilledCheeseSandwich' )
{
// place ourselves before it
Inv = other.Inv;
other.Inv = self;
return;
}
// find first item next to a sandwich
for ( Inventory i=other.Inv; i; i=i.Inv )
{
if ( (i == self) || !(i.Inv is 'GrilledCheeseSandwich') ) continue;
found = i;
break;
}
}
if ( !found )
{
// find last of either invinciball or ragekit power
for ( Inventory i=other.Inv; i; i=i.Inv )
{
if ( !(i is 'InvinciballPower') && !(i is 'RagekitPower') ) continue;
found = i;
}
}
if ( !found ) return;
// place ourselves right after it
Inventory saved = found.Inv;
found.Inv = self;
other.Inv = Inv;
Inv = saved;
}
override bool Use( bool pickup )
{
bool shouldautouse = false;
if ( swwm_enforceautousehealth == 1 ) shouldautouse = true;
else if ( swwm_enforceautousehealth == -1 ) shouldautouse = false;
else shouldautouse = CVar.GetCVar('swwm_autousehealth',Owner.player).GetBool();
if ( pickup && !shouldautouse ) return false;
if ( Owner.Health >= GetDefaultByType(giveme).MaxAmount ) return false;
if ( pickup && ((Owner.player == players[consoleplayer]) || bBigPowerup) ) Owner.A_StartSound(UseSound,CHAN_ITEMEXTRA);
SWWMHandler.HealthFlash(Owner.PlayerNumber());
Owner.GiveInventory(giveme,GetDefaultByType(giveme).Amount);
SWWMScoreObj.Spawn(GetDefaultByType(giveme).Amount,Owner.Vec3Offset(FRandom[ScoreBits](-8,8),FRandom[ScoreBits](-8,8),FRandom[ScoreBits](-8,8)+Owner.Height/2),Font.CR_BLUE);
return true;
}
virtual void AutoUseExtra()
{
}
override void DoEffect()
{
Super.DoEffect();
if ( Amount <= 0 ) DepleteOrDestroy();
}
override void ModifyDamage( int damage, Name damageType, out int newdamage, bool passive, Actor inflictor, Actor source, int flags )
{
if ( damageType == 'EndLevel' ) return;
bool shouldautouse = false;
if ( swwm_enforceautousehealth == 1 ) shouldautouse = true;
else if ( swwm_enforceautousehealth == -1 ) shouldautouse = false;
else shouldautouse = CVar.GetCVar('swwm_autousehealth',Owner.player).GetBool();
if ( !shouldautouse && !bBIGPOWERUP ) return;
if ( passive && (Owner.Health-damage <= (GetDefaultByType(giveme).MaxAmount-GetDefaultByType(giveme).Amount)) )
{
newdamage = damage;
if ( ((Owner.player == players[consoleplayer]) || bBigPowerup) ) Owner.A_StartSound(UseSound,CHAN_ITEMEXTRA);
int tgive = 0;
bool acc = CVar.GetCVar('swwm_accdamage',players[consoleplayer]).GetBool();
while ( (Amount > 0) && (newdamage > 0) )
{
if ( acc ) tgive += GetDefaultByType(giveme).Amount;
else SWWMScoreObj.Spawn(GetDefaultByType(giveme).Amount,Owner.Vec3Offset(FRandom[ScoreBits](-8,8),FRandom[ScoreBits](-8,8),FRandom[ScoreBits](-8,8)+Owner.Height/2),Font.CR_BLUE);
newdamage = newdamage-GetDefaultByType(giveme).Amount;
if ( newdamage < 0 ) Owner.GiveBody(-newdamage,GetDefaultByType(giveme).MaxAmount);
newdamage = max(0,newdamage);
AutoUseExtra();
Amount--;
}
if ( acc ) SWWMScoreObj.Spawn(tgive,Owner.Vec3Offset(FRandom[ScoreBits](-8,8),FRandom[ScoreBits](-8,8),FRandom[ScoreBits](-8,8)+Owner.Height/2),Font.CR_BLUE);
}
else newdamage = damage;
}
Default
{
+INVENTORY.INVBAR;
+INVENTORY.ISHEALTH;
+INVENTORY.AUTOACTIVATE;
Inventory.MaxAmount 5;
Inventory.InterHubAmount 5;
Inventory.UseSound "misc/health_pkup";
+FLOATBOB;
FloatBobStrength 0.25;
}
}
// Base casing classes
Class SWWMCasing : Actor abstract
{
int deadtimer, numbounces;
double pitchvel, anglevel;
double heat;
Default
{
Radius 2;
Height 2;
+NOBLOCKMAP;
+MISSILE;
+MOVEWITHSECTOR;
+THRUACTORS;
+USEBOUNCESTATE;
+INTERPOLATEANGLES;
+NOTELEPORT;
+ROLLSPRITE;
+ROLLCENTER;
Mass 1;
Gravity 0.35;
BounceType "Hexen";
WallBounceFactor 0.65;
BounceFactor 0.65;
BounceSound "explodium/casing";
}
override void PostBeginPlay()
{
Super.PostBeginPlay();
deadtimer = 0;
pitchvel = FRandom[Junk](10,30)*RandomPick[Junk](-1,1);
anglevel = FRandom[Junk](10,30)*RandomPick[Junk](-1,1);
heat = 1.0;
}
override void Tick()
{
Super.Tick();
if ( isFrozen() ) return;
if ( InStateSequence(CurState,ResolveState("Death")) )
{
deadtimer++;
if ( deadtimer > 300 ) A_FadeOut(0.05);
return;
}
heat -= 0.05;
if ( heat <= 0 ) return;
let s = Spawn("SWWMSmallSmoke",pos);
s.alpha *= heat;
}
States
{
Spawn:
XZW1 A 1
{
angle += anglevel;
pitch += pitchvel;
}
Loop;
Bounce:
#### # 0
{
pitchvel = FRandom[Junk](10,30)*RandomPick[Junk](-1,1);
anglevel = 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();
if ( numbounces && ((numbounces > 3) || (Random[Junk](1,20) < 17) || (vel.z > -1.4)) )
{
ClearBounce();
ExplodeMissile();
}
numbounces++;
}
Goto Spawn;
Death:
#### # -1
{
pitch = roll = 0;
angle = FRandom[Junk](0,360);
}
Stop;
}
}
Class SWWMBulletImpact : Actor
{
Default
{
RenderStyle "Add";
Radius 0.1;
Height 0;
+NOGRAVITY;
+NOBLOCKMAP;
+DONTSPLASH;
+NOTELEPORT;
Scale 0.25;
}
override void PostBeginPlay()
{
Super.PostBeginPlay();
A_SprayDecal("Pock",-20);
int numpt = int(Random[Junk](5,10)*scale.x*4);
Vector3 x = (cos(angle)*cos(pitch),sin(angle)*cos(pitch),-sin(pitch));
for ( int i=0; i<numpt; i++ )
{
Vector3 pvel = (x+(FRandom[Junk](-.8,.8),FRandom[Junk](-.8,.8),FRandom[Junk](-.8,.8))).unit()*FRandom[Junk](0.1,1.2);
let s = Spawn("SWWMSmoke",pos+x*2);
s.vel = pvel;
s.SetShade(Color(1,1,1)*Random[Junk](128,192));
}
numpt = int(Random[Junk](3,8)*scale.x*4);
for ( int i=0; i<numpt; i++ )
{
Vector3 pvel = (FRandom[Junk](-1,1),FRandom[Junk](-1,1),FRandom[Junk](-1,1)).unit()*FRandom[Junk](2,8);
let s = Spawn("SWWMSpark",pos+x*2);
s.vel = pvel;
}
numpt = int(Random[Junk](2,5)*scale.x*4);
for ( int i=0; i<numpt; i++ )
{
Vector3 pvel = (FRandom[Junk](-1,1),FRandom[Junk](-1,1),FRandom[Junk](-1,1)).unit()*FRandom[Junk](2,8);
let s = Spawn("SWWMChip",pos+x*2);
s.vel = pvel;
}
if ( !Random[Junk](0,3) ) A_StartSound("bullet/ricochet",CHAN_VOICE,attenuation:2.5);
else A_StartSound("bullet/hit",CHAN_VOICE,attenuation:3.0);
Spawn("InvisibleSplasher",pos);
}
States
{
Spawn:
TNT1 A 1;
Stop;
}
}
Class SWWMWeaponLight : DynamicLight
{
int cnt;
Default
{
DynamicLight.Type "Point";
args 255,224,64,150;
}
override void Tick()
{
Super.Tick();
if ( !target )
{
Destroy();
return;
}
if ( target.player )
{
Vector3 x, y, z, origin;
[x, y, z] = swwm_CoordUtil.GetAxes(target.pitch,target.angle,target.roll);
origin = level.Vec3Offset(target.Vec2OffsetZ(0,0,target.player.viewz),x*12);
SetOrigin(origin,true);
}
else SetOrigin(target.pos,true);
if ( cnt++ > 2 ) Destroy();
}
}
Class PunchImpact : Actor
{
Default
{
Radius 0.1;
Height 0;
+NOGRAVITY;
+NOCLIP;
+DONTSPLASH;
+NOTELEPORT;
}
override void PostBeginPlay()
{
Super.PostBeginPlay();
A_QuakeEx(2,2,2,12,0,200,"",QF_RELATIVE|QF_SCALEDOWN,falloff:100,rollIntensity:.3);
A_StartSound("demolitionist/punch",CHAN_VOICE,CHANF_DEFAULT,bAMBUSH?.6:1.);
A_SprayDecal("WallCrack",-20);
int numpt = Random[Ponch](5,10);
if ( bAMBUSH ) numpt /= 3;
Vector3 x = (cos(angle)*cos(pitch),sin(angle)*cos(pitch),-sin(pitch));
for ( int i=0; i<numpt; i++ )
{
Vector3 pvel = (x+(FRandom[Ponch](-.8,.8),FRandom[Ponch](-.8,.8),FRandom[Ponch](-.8,.8))).unit()*FRandom[Ponch](.1,1.2);
let s = Spawn("SWWMSmoke",pos);
s.vel = pvel;
s.SetShade(Color(1,1,1)*Random[Ponch](128,192));
}
numpt = Random[Ponch](4,12);
if ( bAMBUSH ) numpt /= 3;
for ( int i=0; i<numpt; i++ )
{
Vector3 pvel = (FRandom[Ponch](-1,1),FRandom[Ponch](-1,1),FRandom[Ponch](-1,1)).unit()*FRandom[Ponch](2,8);
let s = Spawn("SWWMSpark",pos);
s.vel = pvel;
}
numpt = Random[Ponch](4,8);
if ( bAMBUSH ) numpt /= 3;
for ( int i=0; i<numpt; i++ )
{
Vector3 pvel = (FRandom[Ponch](-1,1),FRandom[Ponch](-1,1),FRandom[Ponch](-1,1)).unit()*FRandom[Ponch](2,8);
let s = Spawn("SWWMChip",pos);
s.vel = pvel;
}
Destroy();
}
}
Class BigPunchSplash : Actor
{
Default
{
DamageType 'Melee';
Radius 0.1;
Height 0;
+NOGRAVITY;
+NOCLIP;
+DONTSPLASH;
+NOTELEPORT;
+NODAMAGETHRUST;
+FORCERADIUSDMG;
}
override void PostBeginPlay()
{
Super.PostBeginPlay();
SWWMHandler.DoBlast(self,120,40000,target);
A_Explode(special1,120,0);
Destroy();
}
}
Class BigPunchImpact : Actor
{
Default
{
Radius 0.1;
Height 0;
+NOGRAVITY;
+NOCLIP;
+DONTSPLASH;
+NOTELEPORT;
}
override void PostBeginPlay()
{
Super.PostBeginPlay();
A_QuakeEx(8,8,8,18,0,600,"",QF_RELATIVE|QF_SCALEDOWN,falloff:200,rollIntensity:.9);
A_StartSound("pusher/althit",CHAN_VOICE,CHANF_DEFAULT,bAMBUSH?.6:1.);
A_SprayDecal("BigWallCrack",-20);
int numpt = Random[Ponch](9,16);
if ( bAMBUSH ) numpt /= 3;
Vector3 x = (cos(angle)*cos(pitch),sin(angle)*cos(pitch),-sin(pitch));
for ( int i=0; i<numpt; i++ )
{
Vector3 pvel = (x+(FRandom[Ponch](-.8,.8),FRandom[Ponch](-.8,.8),FRandom[Ponch](-.8,.8))).unit()*FRandom[Ponch](.1,1.2);
let s = Spawn("SWWMSmoke",pos);
s.vel = pvel;
s.SetShade(Color(1,1,1)*Random[Ponch](128,192));
}
numpt = Random[Ponch](9,15);
if ( bAMBUSH ) numpt /= 3;
for ( int i=0; i<numpt; i++ )
{
Vector3 pvel = (FRandom[Ponch](-1,1),FRandom[Ponch](-1,1),FRandom[Ponch](-1,1)).unit()*FRandom[Ponch](2,8);
let s = Spawn("SWWMSpark",pos);
s.vel = pvel;
}
numpt = Random[Ponch](9,16);
if ( bAMBUSH ) numpt /= 3;
for ( int i=0; i<numpt; i++ )
{
Vector3 pvel = (FRandom[Ponch](-1,1),FRandom[Ponch](-1,1),FRandom[Ponch](-1,1)).unit()*FRandom[Ponch](2,8);
let s = Spawn("SWWMChip",pos);
s.vel = pvel;
}
Destroy();
}
}
Class ReflectedBullet : SWWMCasing {}
Class ParriedBuff : Inventory
{
Default
{
+INVENTORY.UNDROPPABLE;
+INVENTORY.UNTOSSABLE;
Inventory.Amount 1;
Inventory.MaxAmount 1;
}
override void DoEffect()
{
Super.DoEffect();
if ( !Owner.bMISSILE && !Owner.bSKULLFLY )
{
// lost soul is no longer attacking
Destroy();
return;
}
if ( special1 <= 0 ) return;
// smoke trail
Actor s;
if ( special1&1 )
{
s = Spawn("SWWMSmoke",Owner.pos);
s.vel = Owner.vel*.3+(FRandom[Ponch](-1,1),FRandom[Ponch](-1,1),FRandom[Ponch](-1,1)).unit()*FRandom[Ponch](.1,.6);
s.scale *= 1.2;
s.alpha *= .3;
}
if ( special1 > 1 )
{
s = Spawn("SWWMSmoke",Owner.pos);
s.vel = Owner.vel*.3+(FRandom[Ponch](-1,1),FRandom[Ponch](-1,1),FRandom[Ponch](-1,1)).unit()*FRandom[Ponch](.1,1.2);
s.scale *= 2.;
s.A_SetRenderStyle(s.alpha,STYLE_AddShaded);
s.SetShade(Color(4,2,1)*Random[Ponch](32,63));
}
}
}
// amplifies damage of parried projectiles
Class ParryDamageChecker : Inventory
{
Default
{
+Inventory.UNDROPPABLE;
+Inventory.UNTOSSABLE;
+Inventory.UNCLEARABLE;
Inventory.Amount 1;
Inventory.MaxAmount 1;
}
override void ModifyDamage( int damage, Name damageType, out int newdamage, bool passive, Actor inflictor, Actor source, int flags )
{
Inventory buff;
if ( inflictor && (buff=inflictor.FindInventory("ParriedBuff")) )
{
double mult;
if ( buff.special1 <= 1 ) mult = 1.5;
else if ( buff.special1 >= 2 ) mult = 8.;
if ( buff.special1&1 ) mult *= 2.;
newdamage = int(damage*mult);
}
}
}
Class ParryRing : Actor
{
Default
{
RenderStyle "Add";
Scale .1;
Alpha .3;
Radius 0.1;
Height 0;
+NOGRAVITY;
+NOBLOCKMAP;
+FORCEXYBILLBOARD;
+NOTELEPORT;
}
States
{
Spawn:
XRG4 ABCDEFGHIJKLMNOPQRSTUVWX 1 A_SetScale(scale.x*(1+specialf1));
Stop;
}
}
Class ParryField : Actor
{
int lasteggtic;
Default
{
+NOGRAVITY;
+NOCLIP;
+DONTSPLASH;
+NOTELEPORT;
+SHOOTABLE;
+NONSHOOTABLE;
+NOBLOOD;
+DONTTHRUST;
Health int.max;
Mass int.max;
Radius 20;
Height 40;
}
static bool ValidMissile( Actor a )
{
// fuuuuuuuuuuuuck why can't you just subclass them all from one base class, also wtf hexen, what's with all these things having +MISSILE
if ( (a is 'Rock1') || (a is 'Rock2') || (a is 'Rock3') || (a is 'Dirt1') || (a is 'Dirt2') || (a is 'Dirt3') || (a is 'Dirt4') || (a is 'Dirt5') || (a is 'Dirt6') || (a is 'GlassShard') || (a is 'PotteryBit') || (a is 'CorpseBloodDrip') || (a is 'Leaf1') || (a is 'HWaterDrip') || (a is 'IceGuyWisp1') || (a is 'WraithFX3') || (a is 'WraithFX4') || (a is 'Bat') || (a is 'SorcBall') )
return false;
return true;
}
override void Tick()
{
Super.Tick();
if ( !master )
{
Destroy();
return;
}
Vector3 x, y, z, origin;
[x, y, z] = swwm_CoordUtil.GetAxes(master.pitch,master.angle,master.roll);
origin = level.Vec3Offset(master.Vec2OffsetZ(0,0,master.player.viewz),x*20-(0,0,20));
SetOrigin(origin,true);
// check for projectiles to deflect
let ti = ThinkerIterator.Create("Actor");
Actor a;
while ( a = Actor(ti.Next()) )
{
if ( !((a.bMISSILE && (a.target != master) && ValidMissile(a)) || a.bSKULLFLY) || a.bTHRUACTORS || (level.Vec3Diff(a.pos,Vec3Offset(0,0,20)).length() > 80) ) continue;
Vector3 vdir = a.vel;
Vector3 dir = level.Vec3Diff(master.Vec2OffsetZ(0,0,pos.z),a.pos).unit();
if ( dir dot vdir > 0 ) continue; // already moving away
if ( a.bMISSILE )
{
// deflect directly to target
if ( !Random[Parry](0,2) && a.target )
dir = level.Vec3Diff(a.pos,a.target.Vec3Offset(0,0,a.target.height/2)).unit();
// push away
if ( a.bSEEKERMISSILE ) a.tracer = a.target;
a.target = master;
}
a.GiveInventory("ParriedBuff",1);
let buff = a.FindInventory("ParriedBuff");
double mvel = a.vel.length();
double nspeed = min(100,mvel*FRandom[Parry](1.2,1.4)+20);
a.angle = atan2(dir.y,dir.x);
a.pitch = asin(-dir.z);
let raging = RagekitPower(master.FindInventory("RagekitPower"));
if ( raging )
{
buff.special1 = 2;
nspeed = min(100,nspeed*2.);
raging.DoHitFX();
}
a.vel = dir*nspeed;
if ( a.bMISSILE ) a.speed = nspeed;
let i = Spawn(raging?"BigPunchImpact":"PunchImpact",a.pos);
i.target = master;
i.angle = atan2(dir.y,dir.x);
i.pitch = asin(-dir.z);
i.bAMBUSH = true;
A_QuakeEx(3,3,3,10,0,64,"",QF_RELATIVE|QF_SCALEDOWN,rollIntensity:.2);
A_StartSound("demolitionist/parry",CHAN_WEAPON);
if ( (level.maptime > lasteggtic) && !Random[Parry](0,4) )
{
for ( int i=1; i<6; i++ )
{
let r = Spawn("ParryRing",a.pos);
r.specialf1 = i*.04;
}
buff.special1++;
A_StartSound("misc/soulsparry",CHAN_ITEM,CHANF_OVERLAP,1.,.5);
lasteggtic = level.maptime+5;
}
}
if ( --special1 <= 0 ) Destroy();
}
override bool CanCollideWith( Actor other, bool passive )
{
return false;
}
override int DamageMobj( Actor inflictor, Actor source, int damage, Name mod, int flags, double angle )
{
if ( (flags&DMG_INFLICTOR_IS_PUFF) && (source != master) )
{
Vector3 vdir = (cos(angle),sin(angle),0);
Vector3 vpos = inflictor.pos; // puff goes here
let raging = RagekitPower(master.FindInventory("RagekitPower"));
let i = Spawn(raging?"BigPunchImpact":"PunchImpact",vpos);
if ( raging ) raging.DoHitFX();
i.angle = angle+180;
i.bAMBUSH = true;
// deflect hitscan
A_StartSound("misc/ricochet",CHAN_VOICE,CHANF_OVERLAP,.7);
A_QuakeEx(3,3,3,10,0,64,"",QF_RELATIVE|QF_SCALEDOWN,rollIntensity:.2);
A_StartSound("demolitionist/parry",CHAN_WEAPON);
double mult = 1.5;
if ( raging ) mult = 8.;
if ( (level.maptime > lasteggtic) && !Random[Parry](0,4) )
{
for ( int i=1; i<6; i++ )
{
let r = Spawn("ParryRing",vpos);
r.specialf1 = i*.04;
}
mult *= 2.;
A_StartSound("misc/soulsparry",CHAN_ITEM,CHANF_OVERLAP,1.,.5);
lasteggtic = level.maptime+5;
}
// three options:
switch ( Random[Parry](0,7) )
{
case 0:
case 1:
case 2:
// 1. just block the bullet, making it bounce off
let b = Spawn("ReflectedBullet",vpos);
b.angle = angle;
b.target = master;
b.vel = (-vdir+(FRandom[Parry](-.4,.4),FRandom[Parry](-.4,.4),FRandom[Parry](-.4,.4))).unit()*FRandom[Parry](1,2)*mult+(0,0,4);
break;
case 3:
case 4:
case 5:
// 2. ricochet in random direction
Vector3 rdir = (-vdir+(FRandom[Parry](-.6,.6),FRandom[Parry](-.6,.6),FRandom[Parry](-.6,.6))).unit();
bSHOOTABLE = false;
master.LineAttack(atan2(rdir.y,rdir.x),8000,asin(-rdir.z),int(damage*mult),mod,inflictor.GetClass(),LAF_ABSPOSITION,null,vpos.z,vpos.x,vpos.y);
bSHOOTABLE = true;
break;
case 6:
case 7:
// 3. ricochet directly back to source
Vector3 tdir = source?(level.Vec3Diff(vpos,source.Vec3Offset(0,0,source.height/2))).unit():(-vdir);
bSHOOTABLE = false;
master.LineAttack(atan2(tdir.y,tdir.x),8000,asin(-tdir.z),int(damage*mult),mod,inflictor.GetClass(),LAF_ABSPOSITION,null,vpos.z,vpos.x,vpos.y);
bSHOOTABLE = true;
break;
}
}
return 0;
}
States
{
Spawn:
TNT1 A -1;
Stop;
}
}
Class UseList
{
Line hitline;
int hitside;
Actor hitactor;
Vector3 pos;
}
Class UseLineTracer : LineTracer
{
Array<UseList> uses;
Array<Line> glass;
override ETraceStatus TraceCallback()
{
if ( Results.HitType == TRACE_HitActor )
{
let u = new("UseList");
u.hitline = null;
u.hitactor = Results.HitActor;
u.pos = Results.HitPos;
uses.Push(u);
return TRACE_Skip;
}
if ( Results.HitType == TRACE_HitWall )
{
if ( Results.HitLine.Activation&(SPAC_Use|SPAC_UseThrough) )
{
let u = new("UseList");
u.hitline = Results.HitLine;
u.hitside = Results.Side;
u.hitactor = null;
u.pos = Results.HitPos;
uses.Push(u);
}
if ( Results.Tier == TIER_Middle )
{
if ( !Results.HitLine.sidedef[1] || (Results.HitLine.Flags&(Line.ML_BlockHitscan|Line.ML_BlockEverything|Line.ML_BlockUse)) )
return TRACE_Stop;
if ( Results.HitLine.special == GlassBreak ) // fuck glass
glass.Push(Results.HitLine);
return TRACE_Skip;
}
}
return TRACE_Stop;
}
}
// Base class for all SWWM Weapons
Class SWWMWeapon : Weapon abstract
{
private int SWeaponFlags;
Actor pfield; // instance of parry field for current melee attack
FlagDef NoFirstGive : SWeaponFlags, 0; // don't give ammo on first pickup (for weapons with a clip count)
override bool HandlePickup( Inventory item )
{
if ( (GetClass() == item.GetClass()) )
{
if ( Weapon(item).PickupForAmmo(self) )
item.bPickupGood = true;
if ( (Amount+item.Amount > MaxAmount) && (Stamina > 0) )
{
// sell excess
int sellprice = SWWMUtility.Round100(Stamina*.7);
SWWMScoreObj.Spawn(sellprice,Owner.Vec3Offset(FRandom[ScoreBits](-8,8),FRandom[ScoreBits](-8,8),FRandom[ScoreBits](-8,8)+Owner.Height/2),Font.CR_GOLD);
SWWMCredits.Give(Owner.player,sellprice);
if ( Owner.player )
Console.Printf(StringTable.Localize("$SWWM_SELLEXTRA"),Owner.player.GetUserName(),GetTag(),sellprice);
item.bPickupGood = true;
}
return true;
}
return Super.HandlePickup(item);
}
override void AttachToOwner( Actor other )
{
Inventory.AttachToOwner(other);
Ammo1 = AddAmmo(Owner,AmmoType1,bNoFirstGive?0:AmmoGive1);
Ammo2 = AddAmmo(Owner,AmmoType2,bNoFirstGive?0:AmmoGive2);
SisterWeapon = AddWeapon(SisterWeaponType);
if ( Owner.player )
{
if ( !Owner.player.GetNeverSwitch() && !bNo_Auto_Switch )
Owner.player.PendingWeapon = self;
if ( Owner.player.mo == players[consoleplayer].camera )
StatusBar.ReceivedWeapon(self);
}
GivenAsMorphWeapon = false;
}
override void DetachFromOwner()
{
Owner.A_StopSound(CHAN_WEAPON);
Owner.A_StopSound(CHAN_WEAPONEXTRA);
Super.DetachFromOwner();
}
override void OwnerDied()
{
if ( Owner.player && (Owner.player.ReadyWeapon == self) )
{
Owner.A_StopSound(CHAN_WEAPON);
Owner.A_StopSound(CHAN_WEAPONEXTRA);
}
A_ClearRefire();
Super.OwnerDied();
}
override String GetObituary( Actor victim, Actor inflictor, Name mod, bool playerattack )
{
if ( mod == 'Melee' ) return StringTable.Localize("$O_MELEE");
return Super.GetObituary(victim,inflictor,mod,playerattack);
}
// draw ammo on hud above weapon box
virtual ui void DrawWeapon( double TicFrac, double bx, double by, Vector2 hs, Vector2 ss )
{
}
// HUD-side ticking
virtual ui void HudTick()
{
}
// instant raise/lower
action void A_FullRaise()
{
if ( !player ) return;
if ( player.PendingWeapon != WP_NOCHANGE )
{
player.mo.DropWeapon();
return;
}
if ( !player.ReadyWeapon ) return;
let psp = player.GetPSprite(PSP_WEAPON);
psp.y = WEAPONTOP;
}
action void A_FullLower()
{
if ( !player ) return;
if ( !player.ReadyWeapon )
{
player.mo.BringUpWeapon();
return;
}
let psp = player.GetPSprite(PSP_WEAPON);
psp.y = WEAPONBOTTOM;
if ( player.playerstate == PST_DEAD )
{
// Player is dead, so don't bring up a pending weapon
// Player is dead, so keep the weapon off screen
player.SetPSprite(PSP_FLASH,null);
psp.SetState(player.ReadyWeapon.FindState('DeadLowered'));
return;
}
// [RH] Clear the flash state. Only needed for Strife.
player.SetPSprite(PSP_FLASH,null);
player.mo.BringUpWeapon();
}
action void A_Parry( int duration )
{
Vector3 x, y, z, origin;
[x, y, z] = swwm_CoordUtil.GetAxes(pitch,angle,roll);
origin = level.Vec3Offset(Vec2OffsetZ(0,0,player.viewz),x*20-(0,0,20));
if ( invoker.pfield ) invoker.pfield.Destroy();
invoker.pfield = Spawn("ParryField",origin);
invoker.pfield.master = self;
invoker.pfield.special1 = duration;
if ( !FindInventory("ParryDamageChecker") )
GiveInventory("ParryDamageChecker",1); // need this so parried projectiles deal extra damage
}
private action bool TryMelee( double angle, int dmg, String hitsound = "" )
{
FTranslatedLineTarget t;
double slope = AimLineAttack(angle,1.5*DEFMELEERANGE,t,0.,ALF_CHECK3D);
FLineTraceData d;
LineTrace(angle,1.5*DEFMELEERANGE,slope,0,player.viewheight,data:d);
bool raging = CountInv("RagekitPower");
if ( d.HitType == TRACE_HitActor )
{
bool bloodless = true;
double diff = deltaangle(self.angle,AngleTo(d.HitActor));
self.angle += clamp(diff,-5.,5.);
SWWMHandler.DoKnockback(d.HitActor,d.HitDir+(0,0,.2),dmg*2000);
if ( raging )
{
invoker.bEXTREMEDEATH = true;
invoker.bNOEXTREMEDEATH = false;
}
else
{
invoker.bEXTREMEDEATH = false;
invoker.bNOEXTREMEDEATH = true;
}
if ( !d.HitActor.bDORMANT ) // lol oops
d.HitActor.DaggerAlert(self);
int flg = DMG_USEANGLE|DMG_THRUSTLESS;
if ( raging ) flg |= DMG_FOILINVUL;
dmg = d.HitActor.DamageMobj(invoker,self,dmg,'Melee',flg,atan2(d.HitDir.y,d.HitDir.x));
invoker.bEXTREMEDEATH = invoker.default.bEXTREMEDEATH;
invoker.bNOEXTREMEDEATH = invoker.default.bEXTREMEDEATH;
int quakin = raging?8:2;
if ( d.HitActor.player ) d.HitActor.A_QuakeEx(quakin,quakin,quakin,6,0,1,"",QF_RELATIVE|QF_SCALEDOWN,rollIntensity:0.125*quakin);
if ( !d.HitActor.bNOBLOOD && (raging || !d.HitActor.bINVULNERABLE) )
{
d.HitActor.TraceBleed(dmg,invoker);
d.HitActor.SpawnBlood(d.HitLocation,atan2(d.HitDir.y,d.HitDir.x)+180,dmg);
bloodless = false;
}
else
{
let p = Spawn(raging?"BigPunchImpact":"PunchImpact",d.HitLocation);
p.angle = atan2(-d.HitDir.y,-d.HitDir.x);
}
if ( raging )
{
let ps = Spawn("BigPunchSplash",d.HitLocation);
ps.target = self;
ps.special1 = dmg;
}
A_QuakeEx(quakin/2,quakin/2,quakin/2,3,0,1,"",QF_RELATIVE|QF_SCALEDOWN,rollIntensity:0.06*quakin);
if ( raging ) A_StartSound(bloodless?"pusher/althit":"pusher/altmeat",CHAN_WEAPON,CHANF_OVERLAP);
else A_StartSound((hitsound!="")?hitsound:bloodless?"demolitionist/punch":"demolitionist/punchf",CHAN_WEAPON,CHANF_OVERLAP);
A_AlertMonsters(300);
return true;
}
return false;
}
action void A_Melee( int dmg = 40, String hitsound = "" )
{
// temporarily disable parry field so we can trace through
if ( invoker.pfield ) invoker.pfield.bSHOOTABLE = false;
// check for usables
let ut = new("UseLineTracer");
ut.uses.Clear();
ut.glass.Clear();
ut.Trace(Vec3Offset(0,0,player.viewheight),CurSector,(cos(angle)*cos(pitch),sin(angle)*cos(pitch),sin(-pitch)),DEFMELEERANGE,0);
for ( int i=0; i<ut.uses.Size(); i++ )
{
if ( ut.uses[i].hitactor )
{
if ( ut.uses[i].hitactor == self ) continue;
if ( ut.uses[i].hitactor.Used(self) ) break;
}
else if ( ut.uses[i].hitline )
{
int locknum = SWWMUtility.GetLineLock(ut.uses[i].hitline);
if ( !locknum || CheckKeys(locknum,false,true) )
ut.uses[i].hitline.RemoteActivate(self,ut.uses[i].hitside,SPAC_Use,ut.uses[i].pos);
if ( !(ut.uses[i].hitline.activation&SPAC_UseThrough) ) break;
}
}
// fuck glass
for ( int i=0; i<ut.glass.Size(); i++ )
{
ut.glass[i].Activate(self,0,SPAC_Impact);
}
let raging = RagekitPower(FindInventory("RagekitPower"));
int maxang = raging?18:12;
for ( int i=0; i<maxang; i++ )
{
if ( TryMelee(angle+i*(45./16),dmg,hitsound) || TryMelee(angle-i*(45./16),dmg,hitsound) )
{
if ( invoker.pfield ) invoker.pfield.bSHOOTABLE = true;
return;
}
}
if ( invoker.pfield ) invoker.pfield.bSHOOTABLE = true;
// check for walls instead
FTranslatedLineTarget t;
double slope = AimLineAttack(angle,DEFMELEERANGE,t,0.,ALF_CHECK3D);
FLineTraceData d;
LineTrace(angle,DEFMELEERANGE,slope,TRF_THRUACTORS,player.viewheight,data:d);
if ( d.HitType == TRACE_HitNone ) return;
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;
d.HitLine.RemoteActivate(self,d.LineSide,SPAC_Impact,d.HitLocation+HitNormal*4);
}
let p = Spawn(raging?"BigPunchImpact":"PunchImpact",d.HitLocation+HitNormal*4);
p.angle = atan2(HitNormal.y,HitNormal.x);
p.pitch = asin(-HitNormal.z);
if ( raging )
{
let ps = Spawn("BigPunchSplash",d.HitLocation+HitNormal*4);
ps.target = self;
ps.special1 = dmg;
}
int quakin = raging?4:1;
A_QuakeEx(quakin,quakin,quakin,3,0,1,"",QF_RELATIVE|QF_SCALEDOWN,rollIntensity:0.12*quakin);
A_StartSound(raging?"pusher/althit":(hitsound!="")?hitsound:"demolitionist/punch",CHAN_WEAPON,CHANF_OVERLAP);
A_AlertMonsters(100);
if ( raging ) raging.DoHitFX();
}
override void PlayUpSound( Actor origin )
{
if ( UpSound ) origin.A_StartSound(UpSound,CHAN_WEAPON,CHANF_OVERLAP);
}
action void A_SWWMFlash( StateLabel flashlabel = null )
{
if ( !player || !player.ReadyWeapon )
return;
Weapon weap = player.ReadyWeapon;
State flashstate = null;
if ( !flashlabel )
{
if ( weap.bAltFire )
flashstate = weap.FindState('AltFlash');
if ( !flashstate )
flashstate = weap.FindState('Flash');
}
else flashstate = weap.FindState(flashlabel);
player.SetPSprite(PSP_FLASH,flashstate);
A_OverlayFlags(PSP_FLASH,PSPF_RENDERSTYLE|PSPF_FORCESTYLE,true);
A_OverlayRenderStyle(PSP_FLASH,STYLE_Add);
}
// tells the SWWM HUD that this weapon has ammo available
virtual clearscope bool ReportHUDAmmo()
{
return (!Ammo1||(Ammo1.Amount>0)||(Ammo2&&(Ammo2.Amount>0)));
}
// tells the Embiggener that this weapon uses the specified ammo type
// even if it is not its primary one
virtual clearscope bool UsesAmmo( Class<Ammo> kind )
{
return (AmmoType1&&(kind is AmmoType1))||(AmmoType2&&(kind is AmmoType2));
}
override void ModifyDropAmount( int dropamount )
{
Super.ModifyDropAmount(dropamount);
if ( (AmmoGive1 <= 0) && (default.AmmoGive1 > 0) )
AmmoGive1 = 1;
if ( (AmmoGive2 <= 0) && (default.AmmoGive2 > 0) )
AmmoGive2 = 1;
}
Default
{
Weapon.BobStyle "Alpha";
Weapon.BobSpeed 3.0;
Weapon.BobRangeX 0.5;
Weapon.BobRangeY 0.2;
Weapon.YAdjust 0;
+WEAPON.NOALERT;
+WEAPON.NODEATHINPUT;
+FLOATBOB;
FloatBobStrength 0.25;
}
}