swwmgz_m/zscript/weapons/swwm_baseweapon_melee.zsc
Marisa the Magician ab462cc05d Quadravol melee and stance swap fully implemented.
Fix quick melee not hitting world geometry (oops).
2022-08-19 21:58:44 +02:00

586 lines
18 KiB
Text

// melee/parrying stuff
Class ParriedBuff : Inventory
{
Vector3 oldvel;
Default
{
+INVENTORY.UNDROPPABLE;
+INVENTORY.UNTOSSABLE;
Inventory.Amount 1;
Inventory.MaxAmount 1;
}
override void DoEffect()
{
Super.DoEffect();
if ( Owner.bBLASTED ) oldvel = Owner.vel;
if ( !Owner.bMISSILE && !Owner.bSKULLFLY )
{
// lost soul is no longer attacking
// remove blasted flag just in case
Owner.bBLASTED = false;
Destroy();
return;
}
if ( special1 <= 0 ) return;
// smoke trail
Actor s;
if ( special1&1 )
{
s = Spawn("SWWMHalfSmoke",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("SWWMHalfSmoke",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));
}
}
override void ModifyDamage( int damage, Name damageType, out int newdamage, bool passive, Actor inflictor, Actor source, int flags )
{
// increase blast damage (way too tiny normally for lost souls)
if ( Owner.bBLASTED && (damageType == 'Melee') && !inflictor && !source )
{
newdamage = 20;
if ( special1&1 ) newdamage *= 2;
if ( special1 > 1 ) newdamage *= 8;
}
}
}
// 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;
+NOINTERACTION;
}
States
{
Spawn:
XRG4 ABCDEFGHIJKLMNOPQRSTUVWX 1 A_SetScale(scale.x*(1+specialf1));
Stop;
}
}
Class ParryField : Actor
{
bool critsnd;
Array<Actor> justparried;
Default
{
Radius .1;
Height 0.;
+NOGRAVITY;
+NOCLIP;
+DONTSPLASH;
+NOTELEPORT;
+NOINTERACTION;
}
override void 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);
SetOrigin(origin,false);
let raging = RagekitPower(master.FindInventory("RagekitPower"));
let s = Demolitionist(master).mystats;
// check for projectiles to deflect
let ti = ThinkerIterator.Create("Actor");
Actor a;
while ( a = Actor(ti.Next()) )
{
if ( (justparried.Find(a) < justparried.Size()) || !(SWWMUtility.ValidProjectile(a) || a.bSKULLFLY) || a.bTHRUACTORS || (level.Vec3Diff(a.pos,pos).length() > 80) ) continue;
if ( a is 'Whirlwind' ) SWWMUtility.MarkAchievement("tornado",master.player);
justparried.Push(a);
Vector3 vdir = a.vel;
Vector3 dir = level.Vec3Diff(master.Vec2OffsetZ(0,0,pos.z),a.pos).unit();
Vector3 hdir = dir;
Actor oldtarget = a.target;
if ( (a.target != master) && (a.bMISSILE || (a is 'HolySpirit')) ) // special wraithverge handling
{
// deflect directly to target
if ( a.target )
{
hdir = level.Vec3Diff(a.pos,a.target.Vec3Offset(0,0,a.target.height/2)).unit();
double theta = max(FRandom[Parry](0.,1.)**2.,.1);
dir = SWWMUtility.LerpVector3(dir,hdir,theta);
}
// push away
if ( a.bSEEKERMISSILE ) a.tracer = a.target;
a.target = master;
}
if ( a.bSKULLFLY ) a.bBLASTED = true; // blast lost souls
let buff = a.FindInventory("ParriedBuff");
if ( !buff )
{
buff = Inventory(Spawn("ParriedBuff"));
buff.AttachToOwner(a);
buff.tracer = oldtarget;
}
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);
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|QF_3D,rollIntensity:.2);
A_StartSound("demolitionist/parry",CHAN_WEAPON);
if ( special1 >= special2 ) // perfect parry
{
// increased homing
dir = dir*.4+hdir*.6;
nspeed = min(100,nspeed*1.5);
a.vel = dir*nspeed;
for ( int i=1; i<6; i++ )
{
let r = Spawn("ParryRing",a.pos);
r.specialf1 = i*.04;
}
buff.special1 |= 1;
if ( !critsnd )
{
A_StartSound("misc/soulsparry",CHAN_ITEM,CHANF_OVERLAP,1.,.5);
if ( s ) s.pparries++;
}
critsnd = true;
if ( (a is 'LostSoul') && (master.player.ReadyWeapon is 'SilverBullet') )
SWWMUtility.MarkAchievement("baseball",master.player);
}
if ( s ) s.parries++;
SWWMUtility.AchievementProgressInc("parry",1,master.player);
}
if ( --special1 <= 0 ) Destroy();
}
}
Class UseList
{
Line hitline;
int hitside, hitpart;
Actor hitactor;
Vector3 pos;
}
Class UseLineTracer : LineTracer
{
Array<UseList> uses;
static play bool TangibleLine( UseList u )
{
if ( u.hitpart != TIER_MIDDLE ) return true; // lower/upper/ffloor
Line l = u.HitLine;
if ( !l.sidedef[1] ) return true; // onesided line
Side s = l.sidedef[u.hitside];
if ( s.GetTexture(1).IsNull() ) return false; // no midtex
double ofs = s.GetTextureYOffset(1);
Vector2 siz = TexMan.GetScaledSize(s.GetTexture(1));
Vector2 tofs = TexMan.GetScaledOffset(s.GetTexture(1));
ofs += tofs.y;
ofs *= s.GetTextureYScale(1);
siz.y *= s.GetTextureYScale(1);
SecPlane ceil, flor;
if ( (l.frontsector.floorplane.ZatPoint(l.v1.p) > l.backsector.floorplane.ZatPoint(l.v1.p))
&& (l.frontsector.floorplane.ZatPoint(l.v2.p) > l.backsector.floorplane.ZatPoint(l.v2.p)) )
flor = l.frontsector.floorplane;
else flor = l.backsector.floorplane;
if ( (l.frontsector.ceilingplane.ZatPoint(l.v1.p) < l.backsector.ceilingplane.ZatPoint(l.v1.p))
&& (l.frontsector.ceilingplane.ZatPoint(l.v2.p) < l.backsector.ceilingplane.ZatPoint(l.v2.p)) )
ceil = l.frontsector.ceilingplane;
else ceil = l.backsector.ceilingplane;
double ceilpoint = max(ceil.ZatPoint(l.v1.p),ceil.ZatPoint(l.v2.p));
double florpoint = min(flor.ZatPoint(l.v1.p),flor.ZatPoint(l.v2.p));
if ( l.flags&Line.ML_DONTPEGBOTTOM )
{
if ( u.pos.z > florpoint+ofs+siz.y ) return false;
if ( u.pos.z < florpoint+ofs ) return false;
return true;
}
else
{
if ( u.pos.z > ceilpoint+ofs ) return false;
if ( u.pos.z < (ceilpoint+ofs)-siz.y ) return false;
return true;
}
return false;
}
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.hitpart = Results.FFloor?TIER_FFLOOR:Results.Tier;
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;
return TRACE_Skip;
}
}
return TRACE_Stop;
}
}
Class MHitList
{
Actor a;
Vector3 dir, pos;
}
Class MeleeTracer : LineTracer
{
Actor ignoreme;
Array<MHitList> hits;
Vector3 x; // used to get closest hit to center
bool dorip;
override ETraceStatus TraceCallback()
{
if ( Results.HitType == TRACE_HitActor )
{
if ( !Results.HitActor.bSHOOTABLE
|| (Results.HitActor == ignoreme)
|| Results.HitActor.FindInventory("ParriedBuff") ) return TRACE_Skip;
bool addme = true;
for ( int k=0; k<hits.Size(); k++ )
{
if ( hits[k].a != Results.HitActor ) continue;
if ( (hits[k].dir dot x) < (Results.HitVector dot x) )
{
// closer to centerpoint
hits[k].dir = Results.HitVector;
hits[k].pos = Results.HitPos;
}
addme = false;
break;
}
if ( addme )
{
let h = new("MHitList");
h.a = Results.HitActor;
h.dir = Results.HitVector;
h.pos = Results.HitPos;
hits.Push(h);
}
return dorip?TRACE_Skip:TRACE_Stop;
}
if ( Results.HitType == TRACE_HitWall )
{
if ( Results.Tier == TIER_Middle )
{
if ( !Results.HitLine.sidedef[1] || (Results.HitLine.Flags&(Line.ML_BlockHitscan|Line.ML_BlockEverything)) )
return TRACE_Stop;
return TRACE_Skip;
}
}
return TRACE_Stop;
}
}
extend Class SWWMWeapon
{
Actor pfield; // instance of parry field for current melee attack
bool wallponch; // is punching a wall (for activation checks)
transient MeleeTracer mt;
transient UseLineTracer ut;
// melee attack flags
enum EMeleeFlags
{
MELEE_Rip = 1, // trace will continue instead of stopping at the first hit (useful for swords)
MELEE_ForceSound = 2, // force the use of the provided hit sound while raging
MELEE_FleshSound = 4, // hit sound has a "flesh" variant (will append "f" suffix to hitsound string)
MELEE_ForceBust = 8, // bust walls even without Omnibusting enabled
MELEE_Vertical = 16, // ring is widened vertically, rather than horizontally
MELEE_Wider = 32, // ring is widened by 2.5x rather than 1.5x
MELEE_ExtraWide = 64, // if Wider is also specified, widen by 5x, otherwise widen by 3x
MELEE_HammerHit = 128, // is hammer melee (gibbing counts for "HAHA DAB" achievement)
MELEE_NoRage = 256, // unaffected by ragekit
MELEE_NoUse = 512 // do not pass use actions to this melee
};
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;
invoker.pfield.special2 = duration;
if ( !FindInventory("ParryDamageChecker") )
GiveInventory("ParryDamageChecker",1); // need this so parried projectiles deal extra damage
}
// multi-hit cone rather than the usual one-hit arc, more fun
private action bool TryMelee( double spread, int dmg, String hitsound, double rangemul, double kickmul, int flags )
{
Vector3 x, y, z, dir;
[x, y, z] = swwm_CoordUtil.GetAxes(pitch,angle,roll);
Vector3 origin = Vec2OffsetZ(0,0,player.viewz);
if ( !invoker.mt ) invoker.mt = new("MeleeTracer");
let mt = invoker.mt; // for convenience
mt.ignoreme = self;
mt.hits.Clear();
mt.dorip = !!(flags&MELEE_Rip);
int rings = 1;
double step = spread/20.;
double range = 1.5*DEFMELEERANGE*rangemul;
bool raging = (flags&MELEE_NoRage)?false:CountInv("RagekitPower");
double widemul = (flags&MELEE_ExtraWide)?(flags&MELEE_Wider)?5:3:(flags&MELEE_Wider)?2.5:1.5;
for ( double i=0; i<spread; i+=step )
{
for ( int j=0; j<360; j+=(360/rings) )
{
// wide ring
if ( flags&MELEE_Vertical ) dir = SWWMUtility.ConeSpread(x,y,z*widemul,j,i);
else dir = SWWMUtility.ConeSpread(x,y*widemul,z,j,i);
mt.Trace(origin,level.PointInSector(origin.xy),dir,range,0);
}
rings += 5;
}
// no targets
if ( mt.hits.Size() <= 0 ) return false;
bool blooded = false;
bool bloodless = false;
int flg = DMG_THRUSTLESS|DMG_INFLICTOR_IS_PUFF;
if ( raging ) flg |= DMG_FOILINVUL;
int quakin = raging?8:2;
double diff = 0.;
bool hdoomgal = false;
for ( int i=0; i<mt.hits.Size(); i++ )
{
if ( !mt.hits[i].a ) continue;
diff += deltaangle(self.angle,AngleTo(mt.hits[i].a));
SWWMUtility.DoKnockback(mt.hits[i].a,mt.hits[i].dir,dmg*2000*kickmul);
// lol oops
if ( !mt.hits[i].a.bDORMANT ) mt.hits[i].a.DaggerAlert(self);
if ( !mt.hits[i].a.bNOBLOOD && !mt.hits[i].a.bDORMANT && (raging || !mt.hits[i].a.bINVULNERABLE) ) blooded = true;
else bloodless = true;
if ( SWWMHDoomHandler.IsCuteGirl(mt.hits[i].a) ) hdoomgal = true;
let p = SWWMPuff.Setup(mt.hits[i].pos,mt.hits[i].dir,invoker,self,mt.hits[i].a);
int newdmg = mt.hits[i].a.DamageMobj(p,self,dmg,'Melee',flg);
// things can instantly cease to exist after taking damage (wow)
if ( mt.hits[i].a )
{
if ( mt.hits[i].a.player ) mt.hits[i].a.A_QuakeEx(quakin,quakin,quakin,6,0,1,"",QF_RELATIVE|QF_SCALEDOWN|QF_3D,rollIntensity:.125*quakin);
if ( !mt.hits[i].a.bNOBLOOD && !mt.hits[i].a.bDORMANT && (raging || !mt.hits[i].a.bINVULNERABLE) )
{
mt.hits[i].a.TraceBleed(newdmg,invoker);
mt.hits[i].a.SpawnBlood(mt.hits[i].pos,atan2(mt.hits[i].dir.y,mt.hits[i].dir.x)+180,newdmg);
}
else
{
let p = Spawn(raging?"BigPunchImpact":"PunchImpact",mt.hits[i].pos);
p.angle = atan2(mt.hits[i].dir.y,mt.hits[i].dir.x);
}
if ( (flags&MELEE_HammerHit) && (mt.hits[i].a.Health <= mt.hits[i].a.GetGibHealth()) )
SWWMUtility.AchievementProgressInc("dab",1,player);
}
if ( raging )
{
let ps = Spawn("BigPunchSplash",mt.hits[i].pos);
ps.target = self;
ps.special1 = dmg;
}
}
self.angle += clamp(diff/mt.hits.Size(),-5.,5.); // averaged reorient
A_QuakeEx(quakin/2,quakin/2,quakin/2,3,0,1,"",QF_RELATIVE|QF_SCALEDOWN|QF_3D,rollIntensity:0.06*quakin);
if ( flags&MELEE_HammerHit )
{
if ( hdoomgal )
{
flags |= MELEE_ForceSound;
flags &= ~MELEE_FleshSound;
hitsound = "misc/clonk6";
}
else if ( swwm_bonkhammer )
{
flags |= MELEE_ForceSound;
flags &= ~MELEE_FleshSound;
hitsound = "misc/clonk"..Random[Hammer](1,11);
}
}
if ( raging && !(flags&MELEE_ForceSound) )
{
if ( blooded ) A_StartSound("demolitionist/xpunchf",CHAN_WEAPON,CHANF_OVERLAP);
if ( bloodless ) A_StartSound("demolitionist/xpunch",CHAN_WEAPON,CHANF_OVERLAP);
}
else if ( hitsound == "" )
{
if ( blooded ) A_StartSound("demolitionist/punchf",CHAN_WEAPON,CHANF_OVERLAP);
if ( bloodless ) A_StartSound("demolitionist/punch",CHAN_WEAPON,CHANF_OVERLAP);
}
else if ( flags&MELEE_FleshSound )
{
if ( blooded ) A_StartSound(hitsound.."f",CHAN_WEAPON,CHANF_OVERLAP);
if ( bloodless ) A_StartSound(hitsound,CHAN_WEAPON,CHANF_OVERLAP);
}
else A_StartSound(hitsound,CHAN_WEAPON,CHANF_OVERLAP);
A_AlertMonsters(swwm_uncapalert?0:300);
A_BumpFOV(.96);
return true;
}
action bool A_Melee( int dmg = 40, String hitsound = "", double rangemul = 1., double spreadmul = 1., double kickmul = 1., int flags = 0 )
{
let raging = RagekitPower(FindInventory("RagekitPower"));
if ( flags&MELEE_NoRage ) raging = null;
if ( raging ) rangemul += .2;
Vector3 origin = Vec3Offset(0,0,player.viewheight);
Vector3 dir = SWWMUtility.Vec3FromAngles(angle,pitch);
// check for usables
if ( !(flags&MELEE_NoUse) )
{
if ( !invoker.ut ) invoker.ut = new("UseLineTracer");
let ut = invoker.ut; // for convenience
ut.uses.Clear();
ut.Trace(origin,level.PointInSector(origin.xy),dir,DEFMELEERANGE*rangemul,0);
invoker.wallponch = true;
for ( int i=0; i<ut.uses.Size(); i++ )
{
if ( ut.uses[i].hitactor )
{
// punching is not greeting/patting (that'd be weird)
if ( (ut.uses[i].hitactor == self) || (ut.uses[i].hitactor is 'Demolitionist')
|| (ut.uses[i].hitactor is 'HeadpatTracker')
|| (ut.uses[i].hitactor is 'FroggyChair') ) continue;
if ( ut.uses[i].hitactor.Used(self) ) break;
}
else if ( ut.uses[i].hitline && UseLineTracer.TangibleLine(ut.uses[i]) )
{
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;
}
}
invoker.wallponch = false;
// check for shootables
SWWMBulletTrail.DoTrail(self,origin,dir,DEFMELEERANGE*rangemul,0);
}
bool res = TryMelee((raging?.3:.2)*spreadmul,dmg,hitsound,rangemul,kickmul,flags);
if ( res && !(flags&MELEE_Rip) ) return res;
// check for walls instead
FTranslatedLineTarget t;
double slope = AimLineAttack(angle,DEFMELEERANGE*rangemul,t,0.,ALF_CHECK3D);
FLineTraceData d;
LineTrace(angle,DEFMELEERANGE*rangemul,slope,TRF_THRUACTORS,player.viewheight,data:d);
if ( d.HitType == TRACE_HitNone ) return res;
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;
if ( !(flags&MELEE_NoUse) ) 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 ( d.HitType == TRACE_HitFloor ) p.CheckSplash(40);
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|QF_3D,rollIntensity:0.12*quakin);
A_BumpFOV(.98);
A_StartSound((raging&&!(flags&MELEE_ForceSound))?"demolitionist/xpunch":(hitsound!="")?hitsound:"demolitionist/punch",CHAN_WEAPON,CHANF_OVERLAP);
A_AlertMonsters(swwm_uncapalert?0:100);
if ( raging ) raging.DoHitFX();
if ( (flags&MELEE_ForceBust) || swwm_omnibust )
{
if ( flags&MELEE_HammerHit ) dmg = dmg*2+200; // extra bust damage if it's the hammer
BusterWall.BustLinetrace(d,raging?(dmg*8):dmg,self,d.HitDir,d.HitLocation.z);
}
return true;
}
}