// general movement code extend Class Demolitionist { bool IsRunning() { // for analog move, check for full sprint speed if ( isAnalogMoving ) return (NormalizedMove().length() >= (gameinfo.normforwardmove[1]*256.)); return !!(player.cmd.buttons&BT_RUN); } // directional movement without straferunning Vector2 NormalizedMove() { if ( !(player.cmd.forwardmove|player.cmd.sidemove) ) return (0,0); int idx = !!(player.cmd.buttons&BT_RUN); // ratio between forwardmove and sidemove (depending on BT_RUN state) double fs = gameinfo.normforwardmove[idx]/gameinfo.normsidemove[idx]; // raw axes scaled to 1:1 ratio Vector2 mvec = (player.cmd.forwardmove,-player.cmd.sidemove*fs); // should we use raw analog movement? if ( isAnalogMoving ) { // straferun cap (lol sorry) if ( mvec.length() > gameinfo.normforwardmove[idx]*256. ) mvec = mvec.unit()*gameinfo.normforwardmove[idx]*256.; return mvec; // passed as-is otherwise } // multiply unit vector back to "raw" running speed (as TweakSpeed handles the "true" scaling for us later) return mvec.unit()*gameinfo.normforwardmove[1]*256.; } double TweakSpeed() { double fact; // when analog moving, lerp between factors (should feel ok?) if ( isAnalogMoving ) { double mscale = NormalizedMove().length(); double rspeed = gameinfo.normforwardmove[0]*256.; double sspeed = gameinfo.normforwardmove[1]*256.; if ( mscale < rspeed ) // between walking/running fact = SWWMUtility.MapRange(0,rspeed,.08,.4,mscale); else if ( mscale < sspeed ) // between running/sprinting fact = SWWMUtility.MapRange(rspeed,sspeed,.4,1.1,mscale); else fact = 1.1; // sprinting } else fact = bWalking?.08:IsRunning()?1.1:.4; for ( Inventory i=Inv; i; i=i.Inv ) fact *= i.GetSpeedFactor(); return fact; } void LeaveFootprint( double yofs, bool foot ) { Vector3 checkpos = level.Vec3Offset((pos.xy,floorz),(RotateVector((0,yofs*.25*radius),angle),0)); let s = level.PointInSector(checkpos.xy); // part 1: leave a footprint if ( footblood[foot] > 0. ) { double zatstep = s.floorplane.ZAtPoint(checkpos.xy); for ( int i=0; i checkpos.z ) continue; else if ( ffz < zatstep ) continue; zatstep = ffz; break; } if ( abs(floorz-zatstep) <= 8. ) { let bs = Spawn('mkBloodStep',(checkpos.xy,zatstep)); bs.angle = angle; bs.frame = foot; bs.A_ChangeModel("",0,"","",0,"models/extra",String.Format("BloodFoot%d.png",Random[Blood](1,4))); bs.alpha = min(1.,footblood[foot]); bs.SetShade(footbloodcol[foot]); } footblood[foot] = max(0.,footblood[foot]*.95-.05); } // part 2: check for stepping on a blood pool, make feet bloody if ( !hnd ) hnd = SWWMHandler(EventHandler.Find('SWWMHandler')); for ( mkBloodPool p=hnd.bloodpools; p; p=p.nextpool ) { // vertical distance fast check if ( abs(checkpos.z-p.pos.z) > 8. ) continue; // horizontal distance slower check double distsq = level.Vec2Diff(checkpos.xy,p.pos.xy).lengthsquared(); if ( distsq > (p.radius*p.radius+.25*radius*radius) ) continue; // muck it up, with some color mixing too double oldblood = footblood[foot]; footblood[foot] = 2.; footbloodcol[foot] = SWWMUtility.LerpColor(p.stepcol,footbloodcol[foot],min(1.,oldblood)*.75); } } void PlayFootstep( double yofs, int run = 0, double vol = .3, bool nosplash = false ) { if ( run == 2 ) { A_StartSound("demolitionist/run",CHAN_FOOTSTEP,CHANF_OVERLAP,vol); if ( !nosplash ) { let b = Spawn('InvisibleSplasher',level.Vec3Offset(pos,(RotateVector((0,yofs*.25*radius),angle),0))); b.target = self; b.A_CheckTerrain(); } } else if ( run == 1 ) { A_StartSound("demolitionist/walk",CHAN_FOOTSTEP,CHANF_OVERLAP,vol*.5); if ( !nosplash ) { let b = Spawn('InvisibleSplasher',level.Vec3Offset(pos,(RotateVector((0,yofs*.25*radius),angle),0))); b.target = self; b.A_CheckTerrain(); } } else { A_StartSound("demolitionist/walk",CHAN_FOOTSTEP,CHANF_OVERLAP,vol*.2); if ( !nosplash ) { let b = Spawn('SmolInvisibleSplasher',level.Vec3Offset(pos,(RotateVector((0,yofs*.25*radius),angle),0))); b.target = self; b.A_CheckTerrain(); } } LeaveFootprint(yofs,yofs>0); } override void CalcHeight() { double defviewh = viewheight+player.crouchviewdelta; oldbob = player.bob; if ( bFlyCheat || (player.cheats&CF_NOCLIP2) ) player.bob = 0.; else player.bob = min((player.vel dot player.vel)*player.GetMoveBob(),MAXBOB); if ( player.cheats&CF_NOVELOCITY ) { player.viewz = pos.z+defviewh; if ( player.viewz > ceilingz-4 ) player.viewz = ceilingz-4; SetViewPos((0.,0.,0.)); return; } // adjust viewheight if ( player.playerstate == PST_LIVE ) { player.viewheight += player.deltaviewheight; if ( player.viewheight > defviewh ) { player.viewheight = defviewh; player.deltaviewheight = 0.; } else if ( player.viewheight < (defviewh/2.) ) { player.viewheight = defviewh/2.; if ( player.deltaviewheight <= 0. ) player.deltaviewheight = 1./65536.; } if ( player.deltaviewheight ) { player.deltaviewheight += .25; if ( !player.deltaviewheight ) player.deltaviewheight = 1./65536.; } } // apply bobbing (formula adapted from Unreal) oldbobtime = bobtime; double vel2d = vel.xy dot vel.xy; bobtime += (1.8+.4*min(vel2d/100.,3.))/GameTicRate; Vector2 bob = (sin(bobtime*180.),.5*sin(bobtime*360.))*player.bob*.35; if ( player.morphtics ) bob = (0.,0.); else if ( player.bob && !multiplayer && !(player.cheats&CF_CHASECAM) && player.onground && !bNoGravity && (player.crouchdir != -1) && (player.cmd.forwardmove|player.cmd.sidemove) ) { // bob-based footsteps in first person int m = int(2.*M_PI+oldbobtime); int n = int(2.*M_PI+bobtime); if ( m != n ) { int side = (sin(bobtime*180.)>0.)?1:-1; int spd = bWalking?0:IsRunning()?2:1; PlayFootstep(side,spd); } } // set up viewz player.viewz = pos.z+player.viewheight+(bob.y*clamp(viewbob,0.,1.5)); // handle smooth step down (hacky but looks ok) player.viewz += ssup; ssup = max(0,(ssup*.7)-.25); if ( floorclip && (player.playerstate != PST_DEAD) && (pos.z <= floorz) ) player.viewz -= floorclip; if ( player.viewz > ceilingz-4 ) player.viewz = ceilingz-4; if ( player.viewz < floorz+4 ) player.viewz = floorz+4; // add viewpos Y for side bob (currently breaks SCALEDNOLERP) SetViewPos((0.,bob.x*clamp(viewbob,0.,1.5),0.)); } override void CheckPitch() { if ( bFly && !bFlyCheat && !(player.cheats&CF_NOCLIP2) ) { if ( player.cmd.pitch == -32768 ) player.centering = true; return; // the rest is handled in moveplayer } // fun fact: we can't override freelook disabling // so... just call super, I guess Super.CheckPitch(); } override void CheckMoveUpDown() { if ( InStateSequence(CurState,FindState('Dash')) ) player.cmd.upmove = 0; if ( bFly && !bFlyCheat && !(player.cheats&CF_NOCLIP2) ) { double fs = TweakSpeed(); let [x, y, z] = SWWMUtility.GetAxes(angle,pitch,roll); Vector3 accel; if ( (player.cmd.upmove == -32768) || sendtoground ) { sendtoground = true; player.centering = true; accel = (0,0,-4096); } else accel = z*player.cmd.upmove*8.; accel *= fs/128.; vel = vel+accel/GameTicRate; if ( sendtoground ) vel.xy *= .6; if ( (pos.z <= floorz) || bOnMobj ) sendtoground = false; if ( vel.length() > 50. ) vel = vel.unit()*50.; return; } else sendtoground = false; Super.CheckMoveUpDown(); } private bool ShouldDecelerate( Sector s ) { // check if we can apply fast decel while standing on this sector // (important to not break certain intended vanilla effects) if ( bNOFRICTION ) return false; // we don't have friction at all (e.g.: while dashing) if ( bWINDTHRUST && (s.special >= 40) && (s.special <= 51) ) return false; // wind if ( (s.special == 84) || (s.special == 118) || ((s.special >= 201) && (s.special <= 244)) ) return false; // current // TODO check for scroller thinkers if that ever gets exposed to ZScript return true; } private Vector3 GetGroundNormal() { // find closest 3d floor for its normal F3DFloor ff; for ( int i=0; i 40. ) vel = (vel+accel/GameTicRate).unit()*spd; else vel = vel+accel/GameTicRate; vel *= .95; player.vel += RotateVector(nmove,angle)*cos(pitch)*16.; player.jumptics = -2; if ( !(player.cheats & CF_PREDICTING) && (player.cmd.forwardmove|player.cmd.sidemove) ) PlayRunning(); if ( player.cheats&CF_REVERTPLEASE ) { player.cheats &= ~CF_REVERTPLEASE; player.camera = player.mo; } } else { if ( player.turnticks ) { player.turnticks--; A_SetAngle(angle+(180./TURN180_TICKS),SPF_INTERPOLATE); } else angle += player.cmd.yaw*(360./65536.); player.onground = (pos.z<=floorz)||bOnMobj||bMBFBouncer||(player.cheats&CF_NOCLIP2); let [friction, movefactor] = GetFriction(); if ( player.cmd.forwardmove|player.cmd.sidemove ) { double bobfactor; bobfactor = (friction maxspd ) spd -= (spd-maxspd)*.1; vel = (vel+accel).unit()*spd; } player.vel += RotateVector(nmove,angle)*bobfactor*cos(pitch)*16.*spdup; } else if ( player.onground ) { Vector2 accel = RotateVector(nmove,angle); double maxspd = 16.*TweakSpeed(); // account for special cases where max speed is zero // also, slippery if waist-high or deeper double spdup = 1.; if ( (waterlevel > 1) || (maxspd <= 0.) ) vel.xy += accel; else { // factors for slippery floors (make steering harder) double fact1 = SWWMUtility.MapRange(.90625,.97265625,2.,1.,friction); fact1 = clamp(fact1,1.,2.); double fact2 = SWWMUtility.MapRange(.90625,.97265625,1.,4.,friction); fact2 = clamp(fact2,1.,4.); if ( CanCrouch() && (player.crouchfactor != 1) ) maxspd *= player.crouchfactor; double spd = vel.xy.length(); // quicker speedup spdup = clamp(maxspd/max(.01,spd),1.,fact1); vel.xy += accel*spdup; // additional steering + quicker slow down spd = vel.xy.length(); if ( spd > maxspd ) spd -= (spd-maxspd)*.1; vel.xy = (vel.xy*fact2+accel).unit()*spd; } player.vel += RotateVector(nmove,angle)*bobfactor*16.*spdup; } else player.vel *= .75; // override air control because we REALLY need the extra mobility if ( !player.onground && !bNOGRAVITY ) { nmove = NormalizedMove(); double fs = TweakSpeed(); if ( CanCrouch() && (player.crouchfactor != -1) ) fs *= player.crouchfactor; Vector2 accel = RotateVector(nmove,angle); accel *= fs/320.; double spd = vel.xy.length(); double maxspd = fs*12.; if ( spd > maxspd ) vel.xy = (vel.xy+accel/GameTicRate).unit()*spd; else vel.xy = vel.xy+accel/GameTicRate; } if ( !(player.cheats&CF_PREDICTING) && (nmove.length() > 0.) ) PlayRunning(); if ( player.cheats&CF_REVERTPLEASE ) { player.cheats &= ~CF_REVERTPLEASE; player.camera = player.mo; } } else if ( player.onground && ShouldDecelerate(floorsector) ) { // quickly decelerate if we're not holding movement keys // the default friction is mapped to a 70% reduction, // while slippery floors (above 95%) will have no reduction at all double fact = SWWMUtility.MapRange(.90625,.97265625,.7,1.,friction); fact = clamp(fact,.6,1.); // ensure this doesn't go too far out of range (especially, prevent endless acceleration) vel *= fact; player.vel *= fact; } if ( abs(roll) > 0. ) roll += clamp(deltaangle(roll,0),-3.,3.); } guideangle *= .9; guidepitch *= .9; guideroll *= .9; // anchor to ground when going down steps if ( !hnd ) hnd = SWWMHandler(EventHandler.Find('SWWMHandler')); if ( lastground && !player.onground && !bFly && !bFlyCheat && (abs(pos.z-floorz) <= maxdropoffheight) && (player.jumptics == 0) && (vel.z <= 0) && !isdashing && !hnd.nogroundanchor ) { // test for gap crossing (i.e: climbing up platforms with holes between them) Vector3 storepos = pos; double storefloorz = floorz; bool crossgap = false; for ( int i=1; i<=4; i++ ) // test up to 4 steps ahead, should be enough for most cases { SetOrigin(Vec3Offset(vel.x,vel.y,vel.z),true); if ( floorz < storepos.z ) continue; crossgap = true; break; } SetOrigin(storepos,true); floorz = storefloorz; if ( !crossgap ) { ssup = max(0,(pos.z-floorz)); SetZ(floorz); lastground = player.onground = true; } } if ( player.onground ) lastgroundtic = level.maptime; else lastairtic = level.maptime; if ( !(player.cheats & CF_PREDICTING) && !(player.cmd.forwardmove|player.cmd.sidemove) ) PlayIdle(); if ( !(player.cmd.buttons&BT_USER2) || (gamestate != GS_LEVEL) || (dashcooldown > 0) || (dashfuel < 20.) ) return; Vector3 dodge = (0,0,0); let [x, y, z] = SWWMUtility.GetAxes(angle,pitch,roll); int fm = player.cmd.forwardmove; int sm = player.cmd.sidemove; if ( !(fm|sm) ) fm = 1; if ( fm ) dodge += (fm>0)?X:-X; if ( sm ) dodge += (sm>0)?Y:-Y; if ( dodge == (0,0,0) ) dodge = X; // do we really need this? if ( player.onground && !bNOGRAVITY && !(player.cheats&CF_NOCLIP2) ) { Vector3 fnorm = GetGroundNormal(); if ( dodge dot fnorm <= 0. ) { // recalc and project onto floor normal dodge = (0,0,0); Vector2 xdir = (cos(angle),sin(angle)); Vector2 ydir = (sin(angle),-cos(angle)); if ( fm ) dodge.xy += (fm>0)?xdir:-xdir; if ( sm ) dodge.xy += (sm>0)?ydir:-ydir; dodge = dodge-(dodge dot fnorm)*fnorm; } } if ( dodge.length() <= 0. ) return; // not sure if this can happen, really fullfuel = (dashfuel >= default.dashfuel); dashdir = dodge.unit(); dashcooldown = 10; dashboost = 20.; bOnMobj = false; if ( player.cheats & CF_REVERTPLEASE ) { player.cheats &= ~CF_REVERTPLEASE; player.camera = player.mo; } vel += dashdir*dashboost; vel.z += clamp(-vel.z*.4,0.,30.); player.jumptics = -1; SetStateLabel('Dash'); A_StartSound("demolitionist/jet",CHAN_JETPACK,CHANF_LOOP); lastbump *= .95; mystats.dashcount++; BumpView(5.,vel); } override void CheckJump() { if ( InStateSequence(CurState,FindState('Dash')) ) return; // do not if ( !(player.cmd.buttons&BT_JUMP) || (gamestate != GS_LEVEL) ) return; Vector2 walldir = AngleToVector(angle); bool walljump = false, wallclimb = false; double climbvelz = 0.; FLineTraceData d; Actor jumpactor = null; for ( int i=-4; i last_jump_held) && (((dashfuel > 10.) && (boostcooldown <= 0)) || walljump || wallclimb)) ) { if ( !player.onground && (((walljump || wallclimb) && (level.maptime < last_kick+8)) || (!(walljump || wallclimb) && (level.maptime < last_boost+8))) ) return; double jumpvelz = JumpZ*35./GameTicRate; double jumpfac = 0; for ( let p=Inv; p; p=p.Inv ) { let pp = PowerHighJump(p); if ( pp ) { double f = pp.Strength; if ( f > jumpfac ) jumpfac = f; } } if ( jumpfac > 0 ) jumpvelz *= jumpfac; bool raging = FindInventory('RagekitPower'); if ( raging ) jumpvelz *= 2.; double pvelz = vel.z; if ( !player.onground && !(player.cheats&CF_PREDICTING) ) { // check for wall stuff if ( walljump ) { if ( vel.z < 10. ) vel.z += 2.*jumpvelz+clamp(-pvelz*.6,0.,30.); vel.xy += walldir*20*Speed; lastbump *= .95; } else if ( wallclimb ) { if ( vel.z < 10. ) vel.z += climbvelz+clamp(-pvelz*.95,0.,30.); vel.xy += walldir*10*Speed; lastbump *= .97; } if ( walljump && jumpactor && jumpactor.bSHOOTABLE ) { SWWMUtility.DoKnockback(jumpactor,(-walldir,0),12000); int dmg = jumpactor.DamageMobj(self,self,10,'Jump'); if ( raging ) { let ps = Spawn('BigPunchSplash',pos); ps.damagetype = 'Jump'; ps.target = self; ps.special1 = dmg; } } if ( walljump || wallclimb ) { last_kick = level.maptime+1; SWWMUtility.AchievementProgressInc("jump",1,player); } } bOnMobj = false; player.jumpTics = -1; if ( (dashfuel > 10.) && !player.onground && !walljump && !wallclimb ) { dashboost = 3.; boostcooldown = 20; if ( vel.z < 10. ) vel.z += jumpvelz+clamp(-pvelz*.4,0.,30.); A_StartSound("demolitionist/jet",CHAN_JETPACK,CHANF_LOOP); lastbump *= .95; mystats.boostcount++; last_boost = level.maptime+1; BumpView(3.,vel); SetStateLabel('Boost'); } else { A_StartSound(walljump?"demolitionist/kick":"demolitionist/runstart",CHAN_FOOTSTEP,CHANF_OVERLAP); dashboost = 0.; // bunnyhop time if ( !walljump && !wallclimb ) { if ( !bWalking && (level.maptime < (lastairtic+10)) ) { SWWMUtility.AchievementProgressInc("bune",1,player); // bhop, z vel relative to vel size if ( vel.z < 25. ) // don't ramp up too hard { vel.z += jumpvelz*((IsRunning()?1.2:.65)+vel.length()*.01); // add part of last landing z velocity too vel.z += max(0,-landvelz*(raging?.45:.35)); } // accelerate vel.xy += (RotateVector(NormalizedMove(),angle)/2400.)*(1.+vel.length()*.025)*TweakSpeed(); } else { // first jump if ( vel.z < 10. ) // don't ramp up too hard { vel.z += jumpvelz*(bWalking?.75:IsRunning()?1.25:1.); // add part of last landing z velocity too vel.z += max(0,-landvelz*(raging?.35:.25)); } // long jump if running/sprinting if ( !walljump && !wallclimb && !bWalking ) vel.xy += (RotateVector(NormalizedMove(),angle)/1500.)*(raging?2.:1.)*TweakSpeed(); } } BumpView(clamp((vel.length()-10)/12.,1.,20.),vel); if ( swwm_mutevoice < 4 ) { String myvoice = CVar.GetCVar('swwm_voicetype',player).GetString(); int loudlv = swwm_voiceamp; int maxjump = StringTable.Localize("$SWWM_"..myvoice.."_NJUMP").ToInt(); int idx = (maxjump<=1)?1:Random[DemoLines](1,maxjump); A_StartSound(String.Format("voice/%s/jump%d",myvoice,idx),CHAN_DEMOVOICE,CHANF_OVERLAP); if ( loudlv > 1 ) A_StartSound(String.Format("voice/%s/jump%d",myvoice,idx),CHAN_DEMOVOICEAUX,CHANF_OVERLAP); if ( loudlv > 2 ) A_StartSound(String.Format("voice/%s/jump%d",myvoice,idx),CHAN_DEMOVOICEAUX2,CHANF_OVERLAP); if ( loudlv > 3 ) A_StartSound(String.Format("voice/%s/jump%d",myvoice,idx),CHAN_DEMOVOICEAUX3,CHANF_OVERLAP); } SetStateLabel('Jump'); } } last_jump_held = level.maptime+1; } bool AllowCrouch() { if ( player.cmd.buttons&BT_JUMP ) return false; if ( InStateSequence(CurState,FindState('Dash')) ) return false; // no crouch during dash return true; } // Imagine having to duplicate two functions only to change a couple values in both // I sure love constants override void CrouchMove( int direction ) { double defaultheight = FullHeight; double savedheight = Height; double crouchspeed = direction*CROUCHSPEED*((direction>0)?.65:.9); // match animation speed double oldheight = player.viewheight; player.crouchdir = direction; player.crouchfactor += crouchspeed; // check whether the move is ok Height = defaultheight; // actually test the full height, or it'll look weird if ( !TryMove(pos.xy,false) ) { Height = savedheight; if ( direction > 0 ) { // doesn't fit player.crouchfactor -= crouchspeed; player.crouchdir = -1; // force crouch return; } } Height = savedheight; player.crouchfactor = clamp(player.crouchfactor,.3,1.); player.viewheight = ViewHeight*player.crouchfactor; player.crouchviewdelta = player.viewheight-ViewHeight; // Check for eyes going above/below fake floor due to crouching motion. CheckFakeFloorTriggers(pos.z+oldheight,true); } override void CheckCrouch( bool totallyfrozen ) { // crouch to swim/float down if ( !totallyfrozen && (player.cmd.buttons&BT_CROUCH) && bNOGRAVITY ) vel.z = -3; bool wascrouching = !!(player.cmd.buttons&BT_CROUCH); if ( !AllowCrouch() ) player.cmd.buttons &= ~BT_CROUCH; if ( CanCrouch() && (player.health > 0) ) { if ( !totallyfrozen ) { int crouchdir = player.crouching; if ( !crouchdir ) crouchdir = (player.cmd.buttons&BT_CROUCH)?-1:1; else if ( player.cmd.buttons&BT_CROUCH ) player.crouching = 0; if ( (crouchdir == 1) && (player.crouchfactor < 1) && (pos.z+height < ceilingz) ) CrouchMove(1); else if ( (crouchdir == -1) && (player.crouchfactor > .3) ) CrouchMove(-1); } } else player.Uncrouch(); player.crouchoffset = -(ViewHeight)*(1-player.crouchfactor); // we need the crouch button state to be preserved for other functions if ( wascrouching ) player.cmd.buttons |= BT_CROUCH; } // let's customize our gravity override void FallAndSink( double grav, double oldfloorz ) { if ( !player || (player.mo != self) || (player.cheats&CF_TOTALLYFROZEN) ) { Super.FallAndSink(grav,oldfloorz); return; } // do nothing if standing on ground or "floating" if ( player.onground || bNOGRAVITY ) return; // ensure we don't pass terminal velocity just from falling if ( vel.z < -50 ) return; // we don't care about "the doom way" here, gravity is // ALWAYS in effect when not standing on solid ground if ( waterlevel > 1 ) { // sink faster grav *= .35; } // reduce gravity while we're boosting else if ( InStateSequence(CurState,FindState('Dash')) || InStateSequence(CurState,FindState('Boost')) ) grav *= .25; vel.z -= grav; } }