script, ui: added "sv_min_startmap_health" cvar
[k8vavoom.git] / progs / common / linespec / player / PlayerEx.vc
blobfb9d2056eee7d554c6ef51e67bcf685c6fd134d5
1 //**************************************************************************
2 //**
3 //**    ##   ##    ##    ##   ##   ####     ####   ###     ###
4 //**    ##   ##  ##  ##  ##   ##  ##  ##   ##  ##  ####   ####
5 //**     ## ##  ##    ##  ## ##  ##    ## ##    ## ## ## ## ##
6 //**     ## ##  ########  ## ##  ##    ## ##    ## ##  ###  ##
7 //**      ###   ##    ##   ###    ##  ##   ##  ##  ##       ##
8 //**       #    ##    ##    #      ####     ####   ##       ##
9 //**
10 //**  Copyright (C) 1999-2006 Jānis Legzdiņš
11 //**  Copyright (C) 2018-2023 Ketmar Dark
12 //**
13 //**  This program is free software: you can redistribute it and/or modify
14 //**  it under the terms of the GNU General Public License as published by
15 //**  the Free Software Foundation, version 3 of the License ONLY.
16 //**
17 //**  This program is distributed in the hope that it will be useful,
18 //**  but WITHOUT ANY WARRANTY; without even the implied warranty of
19 //**  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20 //**  GNU General Public License for more details.
21 //**
22 //**  You should have received a copy of the GNU General Public License
23 //**  along with this program.  If not, see <http://www.gnu.org/licenses/>.
24 //**
25 //**************************************************************************
26 //#define SPOTLIGHT_DISCO_TEST
27 //#define SPOTLIGHT_DISCO_CROWN_TEST
28 //#define CALCLIGHT_TEST
30 //#define DEBUG_3DPOBJ_JUMPS
31 //#define DEBUG_WATER_JUMPS
34 class PlayerEx : BasePlayer abstract;
36 // player internal flags, for cheats and debug
37 bitenum {
38   CF_NOCLIP, // No clipping, walk through barriers.
39   CF_GODMODE, // No damage, no health loss.
40   CF_REGENERATION, // Regenerate Health points.
41   CF_FRIGHTENING, // Scare monsters away.
42   CF_DOUBLEFIRINGSPEED, // Player owns a double firing speed artifact
43   CF_HIGHJUMP, // Player owns a high jump artifact
44   CF_TIMEFREEZE, // Player owns a time freeze artifact
45   CF_BUDDHA, // Normal play, but never looses last 1% of health
48 enum FlashlightLightId = 3_669_666;
50 const float BLINKTHRESHOLD = 4.0;
52 // 16 pixels of bob
53 const float MAXBOB = 16.0;
55 const int MAXHEALTH      = 100;
56 const int MAXMORPHHEALTH = 30;
58 // for screen flashing (red or bright)
59 float DamageFlash;
60 float BonusFlash;
61 name DamageFlashType;
62 transient int DamageFlashBlend; // for advdamage indicator
64 // base height above floor for viewz
65 float ViewHeight;
66 float PrevViewHeight; // this is used in crouching cheat (and saved there)
67 float DeltaViewHeight; // bob/squat speed
68 float Bob; // bounded/scaled total momentum
70 // who did damage (none for floors/ceilings)
71 EntityEx Attacker;
73 float JumpTime;
74 TVec LocalQuakeHappening; // for each axis
76 // for damaging flats: don't apply damage more than once per tick
77 // this is last tick when sector damage was applied
78 //FIXME: do not save this? it doesn't matter much
79 int LastSectorDamageTic;
81 float HazardTime;
82 float LastHazardTime;
84 // bit flags, for cheats and debug
85 // see cheat_t above
86 int /*[checkpoint]*/ Cheats;
88 Weapon /*[checkpoint]*/ ReadyWeapon;
89 Weapon PendingWeapon; // is none if not changing
91 // refired shots are less accurate (this is counter)
92 int Refire;
94 float FlyHeight;
96 array!name RevealedMaps;
98 Inventory InvFirst;
99 Inventory InvPtr;
100 float InventoryTime;
101 int ArtifactFlash;
102 int InvSize;
104 Inventory SavedInventory;
106 bool onground;
108 bool bRevertCamera; // revert camera if player moves
109 bool bFrozen; // player cannot move
110 bool bTotallyFrozen; // player cannot do anything except press use
111 bool /*[checkpoint]*/ bNoTarget; // monster don't target
112 bool /*[checkpoint]*/ bInstantWeaponSwitch; // switch weapons instantly
113 bool bFly; // player is flying
114 bool bInventoryAlwaysOpen; // inventory bar is always open
115 bool bAutoAim; // auto aiming enabled? (weapon can override this)
116 // used in ammo king ammo amount getter
117 bool bHasBackpack;
118 // for PROP_NOWEAPONSWITCH
119 bool bDisableWeaponSwitch;
120 // used to force crouching from entity physics
121 // reset in player ticker, or on player reborn
122 // value is game tick when it should be reset, or 0 for inactive
123 transient int bForceCrouchingDown;
125 // button down flags, etc.
126 transient bool bReloadQueued; // true if "reload" was pressed
127 transient bool bReloadDown; // `true` if button was down last tic
128 transient bool bZoomDown; // `true` if button was down last tic
129 transient bool bAltAttackDown; // `true` if button was down last tic
130 transient bool bButton5Down; // `true` if button was down last tic
131 transient bool bButton6Down; // `true` if button was down last tic
132 transient bool bButton7Down; // `true` if button was down last tic
133 transient bool bButton8Down; // `true` if button was down last tic
134 // for water jumps
135 transient bool bWaterThrusted;
137 int PoisonCount; // screen flash for poison damage
138 float LastPoisonTime;
139 EntityEx Poisoner; // none for non-player mobjs
140 PlayerEx PoisonerPlayer; // for KArena
142 int Objectives;
144 float MorphTime; // player is morphed into something if > 0
145 int MorphStyle;
146 class!Actor UnmorphFlash;
148 // moved to entity
150 int Accuracy;
151 int Stamina;
154 float BlendR;
155 float BlendG;
156 float BlendB;
157 float BlendA;
159 int ChickenPeck; // chicken peck countdown
161 Actor Rain1; // active rain maker 1
162 Actor Rain2; // active rain maker 2
164 const int MAX_MAPS_VISITED = 100;
166 name MapsVisited[MAX_MAPS_VISITED];
168 float FOV; // current Field Of Vision
169 float DesiredFOV; // desired Field Of Vision
171 ubyte LastWaterLevel;
174 bool /*[checkpoint]*/ k8ElvenGifted; // always set to `true` in checkpoints
175 transient float k8BossesDetected = 666; // seconds; <0: waiting; 0: show message; >0: detected
176 transient float k8ElvenGiftMessageTime = 666; // seconds; <0: waiting; 0: show message; >0: detected
179 // Health Accumulator
180 // if you want to show HA info in UI, replicate those vars
181 int k8HealthAccum_Amount; // amount of currently accumulated health
182 float k8HealthAccum_LastRegenTime = -10000;
183 float k8HealthAccum_LastBoostTime = -10000;
185 // this is used in network games; no need to save it
186 //transient bool k8HealthAccum_Enabled_NWClient;
188 bool bFlashlightOn;
189 transient bool bFlashlightButtonDown;
190 transient float k8NextSuperBulletTime;
192 transient subsector_t *lastSubSector;
193 //transient array!int subSeen; // do not bother dropping subsector info for those
195 transient float lastViewOrgZForPfx; // set in `CalcHeight()`, used in `PostfixViewHeight()`
196 transient float lastViewOrgVH;
197 transient sector_t *lastViewOrgZSector;
199 // meh, let it cheat a little
200 transient int LastRegenTicTime = 0;
202 // so it is independent from `XLevel.Time`
203 transient float BobbingTime = 0;
205 // used in `SetupSectorFlatDamage()` and friends
206 struct SectorDamageInfo {
207   name flatDamageType;
208   int flatDamage;
209   int flatDamageTimeout;
210   int flatDamageLeaky; // byte, probability
211   bool flatDamageExit;
212   bool flatDamageHitFloor;
216 #include "PlayerEx.cheats.vc"
218 #ifdef CALCLIGHT_TEST
219 transient int lastCalcLight = 0;
220 #endif
223 // bootprints helpers
224 // don't bother saving this, this is purely cosmetic feature anyway
225 transient TVec bootprintLastOrg;
226 transient float bootprintTimeTotal; // to calculate alpha
227 transient float bootprintTimeLeft; // <= 0: no bootprints
228 transient float bootprintNextPutTime; // delay, decaying
229 transient bool bootprintFlip;
231 transient VLevel::VBootPrintDecalParams bootprintParams;
233 // footsteps timing; no need to save
234 transient float lastFootstepSoundTime;
235 transient bool lastFootstepIsLeft;
238 //==========================================================================
240 //  EngineHelperGetSavedInventory
242 //==========================================================================
243 override Entity EngineHelperGetSavedInventory () {
244   return SavedInventory;
248 //==========================================================================
250 //  ClearSubSeenInfo
252 //==========================================================================
253 final void ClearSubSeenInfo () {
254   lastSubSector = nullptr;
255   //subSeen.reset();
259 //==========================================================================
261 //  AddSeenSubsector
263 //==========================================================================
264 final void AddSeenSubsector () {
265   if (!MO) return;
266   if (!MO.SubSector) return;
267   if (lastSubSector == MO.SubSector) return;
268   /*
269   int ssnum = MO.SubSector-&MO.XLevel.Subsectors[0];
270   if (subSeen.length != MO.XLevel.Subsectors.length) {
271     subSeen.length = MO.XLevel.Subsectors.length;
272     foreach (ref int ss; subSeen) ss = 0;
273   }
274   if (subSeen[ssnum]) return;
275   subSeen[ssnum] = 1;
276   */
277   lastSubSector = MO.SubSector;
278   foreach (auto i; 0..MAXPLAYERS) {
279     PlayerEx plr = PlayerEx(Level.Game.Players[i]);
280     if (!plr || !plr.bIsBot) continue;
281     plr.BotSendSubSectorChange(lastSubSector);
282   }
286 //==========================================================================
288 //  BotDumpNodes
290 //==========================================================================
291 void BotDumpNodes () {
295 //==========================================================================
297 //  BotTestFindPathTo
299 //==========================================================================
300 void BotTestFindPathTo (TVec dest) {
304 replication {
305   reliable if (!bIsClient)
306     Cheats, ReadyWeapon, InvFirst, InvPtr, InventoryTime, ArtifactFlash,
307     Objectives, MorphTime/*, Accuracy, Stamina*/, MapsVisited, DamageFlash, DamageFlashType,
308     BonusFlash, Attacker, bFrozen, bTotallyFrozen,/* DesiredFOV, FOV,*/
309     bReloadQueued, bReloadDown, bZoomDown, bAltAttackDown,
310     bButton5Down, bButton6Down, bButton7Down, bButton8Down,
311     bFlashlightOn;
313   unreliable if (!bIsClient)
314     ParticleEffect, ClientExplosion, ClientParticleExplosion,
315     ClientSparkParticles, ClientRailTrail,
316     DecalEffect, FlatDecalEffect,
317     // we need this to update UI, but this is not vital info
318     /*k8HealthAccum_Enabled_NWClient,*/ k8HealthAccum_Amount;
320   reliable if (!bIsClient)
321     ClientVoice, ClientSpeech, ClientFinaleType, ClientSlideshow1,
322     ClientSlideshow2;
324   // from client to server
325   reliable if (bIsClient)
326     bAutoAim;
330 //==========================================================================
332 //  GetUseRanges
334 //==========================================================================
335 void GetUseRanges (out float ur, out float utr) {
336   PlayerPawn pwn = PlayerPawn(MO);
337   if (pwn) {
338     //printdebug("%C: GetUseRanges: ur=%s; utr=%s", self, pwn.UseRange, pwn.UseThingRange);
339     ur = fmax(1.0f, pwn.UseRange);
340     utr = fmax(1.0f, pwn.UseThingRange);
341   } else {
342     ur = PlayerPawn::DEFAULT_USERANGE;
343     utr = PlayerPawn::DEFAULT_USETHINGRANGE;
344   }
348 #include "PlayerEx.healthbar.vc"
351 //==========================================================================
353 //  FixCheatFlags
355 //  fix various cheat flags
357 //==========================================================================
358 void FixCheatFlags () {
359   Level.bFrozen = !!(Cheats&CF_TIMEFREEZE);
360   if (MO) {
361     if (Cheats&CF_GODMODE) {
362       if (LineSpecialGameInfo(Level.Game).GOD_HEALTH) {
363         if (MO) MO.Health = LineSpecialGameInfo(Level.Game).GOD_HEALTH;
364         Health = LineSpecialGameInfo(Level.Game).GOD_HEALTH;
365       }
366     }
367     if (Cheats&CF_NOCLIP) {
368       MO.bColideWithThings = !(Cheats&CF_NOCLIP);
369       MO.bColideWithWorld = !(Cheats&CF_NOCLIP);
370     }
371   }
375 //==========================================================================
377 //  ResetToDefaults
379 //  this is called on save loading, etc.
380 //  reset every important field to default
382 //==========================================================================
383 override void ResetToDefaults () {
384   ::ResetToDefaults();
385   ResetPlayerOnSpawn(keepPlayerState:true);
386   k8HealthAccum_Amount = 0;
387   //print("*** RESET ***");
388   //print("k8ElvenGiftMessageTime=%s", k8ElvenGiftMessageTime);
392 //==========================================================================
394 //  eventOnSaveLoaded
396 //  this is called after savegame was loaded
398 //==========================================================================
399 override void eventOnSaveLoaded () {
400   ::eventOnSaveLoaded();
401   //print("*** LOADED ***");
402   //print("k8ElvenGiftMessageTime=%s", k8ElvenGiftMessageTime);
403   k8ElvenGiftMessageTime = 666; // checkpoint loads will set this, so reset it back
404   // but detect bosses
405   k8BossesDetected = (GetCvarB('k8ElvenDetect') ? -0.5 : 666);
409 //==========================================================================
411 //  eventOnBeforeSave
413 //==========================================================================
414 override void eventOnBeforeSave (bool isAutosave, bool isCheckpoint) {
415   ::eventOnBeforeSave(isAutosave, isCheckpoint);
416   //print("*** BEFORE SAVING (auto:%B; checkpoint:%B) ***", isAutosave, isCheckpoint);
420 //==========================================================================
422 //  eventOnAfterSave
424 //==========================================================================
425 override void eventOnAfterSave (bool isAutosave, bool isCheckpoint) {
426   ::eventOnAfterSave(isAutosave, isCheckpoint);
427   //print("*** SAVED (auto:%B; checkpoint:%B) ***", isAutosave, isCheckpoint);
431 //==========================================================================
433 //  IsCheckpointPossible
435 //  this should check player's inventory (and maybe some other things), and
436 //  return `true` if simple checkpoint-style save is possible
437 //  (i.e. loader can simply recreate player inventory and health)
439 //  for now, there is no way to inject custom data in checkpoints, so if
440 //  you have any, return `false` here
442 //==========================================================================
443 override bool IsCheckpointPossible () {
444   if (!MO) return false;
445   return EntityEx(MO).IsInventoryCheckpointPossible();
449 //==========================================================================
451 //  CalcFlyZ
453 //  TODO: this is time-dependent; fix it!
455 //==========================================================================
456 float CalcFlyZ (float currz) {
457   currz += sin(90.0*35.0/20.0*AngleMod360(WorldTimer))/2.0;
458   return currz;
462 //==========================================================================
464 //  QS_Save
466 //==========================================================================
467 override void QS_Save () {
468   QS_PutInt("Health", Health);
469   QS_PutInt("Cheats", Cheats);
471   QS_PutInt("bNoTarget", (bNoTarget ? 1 : 0));
472   QS_PutInt("bInstantWeaponSwitch", (bInstantWeaponSwitch ? 1 : 0));
473   QS_PutInt("bAutoAim", (bAutoAim ? 1 : 0));
475   int viscount = 0;
476   foreach (auto midx; 0..MAX_MAPS_VISITED; reverse) {
477     if (MapsVisited[midx]) {
478       viscount = midx+1;
479       break;
480     }
481   }
483   QS_PutInt("MAX_MAPS_VISITED", viscount);
484   foreach (auto midx; 0..viscount) QS_PutName(va("MapVisited.%d", midx), MapsVisited[midx]);
488 //==========================================================================
490 //  QS_Load
492 //==========================================================================
493 override void QS_Load () {
494   Health = QS_GetInt("Health");
495   MO.Health = Health;
496   Cheats = QS_GetInt("Cheats");
498   FixCheatFlags();
500   bNoTarget = !!QS_GetInt("bNoTarget");
501   bInstantWeaponSwitch = !!QS_GetInt("bInstantWeaponSwitch");
502   bAutoAim = !!QS_GetInt("bAutoAim");
504   int viscount = QS_GetInt("MAX_MAPS_VISITED", 0);
505   if (viscount < 0) Error("invalid number of visited maps in quicksave");
506   //if (MAX_MAPS_VISITED != QS_GetInt("MAX_MAPS_VISITED")) Error("invalid number of visited maps in quicksave");
507   //print("VISCOUNT=%d (%d)", viscount, MAX_MAPS_VISITED);
508   foreach (auto midx; 0..viscount) {
509     name mname = QS_GetName(va("MapVisited.%d", midx));
510     if (midx < MAX_MAPS_VISITED) {
511       MapsVisited[midx] = mname;
512     } else {
513       if (mname) Error("invalid number of visited maps in quicksave");
514     }
515   }
517   foreach (auto midx; viscount..MAX_MAPS_VISITED) MapsVisited[midx] = '';
519   k8ElvenGifted = true;
523 //==========================================================================
525 //  ClearEntityInventoryQS
527 //==========================================================================
529 override void ClearEntityInventoryQS () {
530   ReadyWeapon = none;
531   PendingWeapon = none;
532   ::ClearEntityInventoryQS();
537 //==========================================================================
539 //  ResetWeaponReloadRefire
541 //==========================================================================
542 void ResetWeaponReloadRefire () {
543   bReloadQueued = false;
544   Refire = 0;
548 //==========================================================================
550 //  eventSetPendingWeapon
552 //==========================================================================
553 override bool eventSetPendingWeapon (Entity ent) {
554   if (!MO) return false;
555   Weapon wpn = Weapon(ent);
556   if (!wpn) return false;
557   for (Inventory inv = EntityEx(MO).Inventory; inv; inv = inv.Inventory) {
558     if (ent == wpn) {
559       PendingWeapon = wpn;
560       return true;
561     }
562   }
563   return false;
567 //==========================================================================
569 //  eventSetReadyWeapon
571 //  currently this is used only in checkpoint loader, hence such logic
572 //  (no checks for the same weapon, and such)
574 //==========================================================================
575 override void eventSetReadyWeapon (Entity ent, bool instant) {
576   if (!MO) return;
577   SetWeapon(none);
578   Weapon wpn = Weapon(ent);
579   for (Inventory inv = EntityEx(MO).Inventory; inv; inv = inv.Inventory) {
580     if (ent == wpn) {
581       SetWeapon(wpn);
582       break;
583     }
584   }
585   BringUpWeapon(instant:instant, skipSound:instant);
589 //==========================================================================
591 //  eventIsReadyWeaponByName
593 //==========================================================================
594 override bool eventIsReadyWeaponByName (string classname, bool allowReplace) {
595   if (!classname || !ReadyWeapon) return false;
596   // try direct
597   auto wpnClass = class!Weapon(FindClassNoCaseStr(classname));
598   if (!wpnClass) return false;
599   if (wpnClass == ReadyWeapon.Class) return true;
600   // try replacement
601   if (allowReplace) {
602     // replacement
603     auto wpnRepl = class!Weapon(GetClassReplacement(wpnClass));
604     if (wpnRepl && wpnRepl != wpnClass && wpnRepl == ReadyWeapon.Class) return true;
605     // replacee
606     auto wpnSrc = class!Weapon(GetClassReplacee(ReadyWeapon.Class));
607     if (wpnSrc && stricmp(string(GetClassName(wpnSrc)), classname) == 0) return true;
608     // superclass chain
609     wpnClass = class!Weapon(ReadyWeapon.Class);
610     while (wpnClass) {
611       if (stricmp(string(GetClassName(wpnClass)), classname) == 0) return true;
612       wpnClass = class!Weapon(GetClassParent(wpnClass));
613     }
614   }
615   // not found
616   return false;
620 //==========================================================================
622 //  eventFindInventoryWeapon
624 //==========================================================================
625 override Entity eventFindInventoryWeapon (string classname, bool allowReplace) {
626   if (!classname) return none;
627   if (!MO) return none;
628   // try direct
629   auto wpnClass = class!Weapon(FindClassNoCaseStr(classname));
630   if (!wpnClass) return none;
631   for (Inventory inv = EntityEx(MO).Inventory; inv; inv = inv.Inventory) {
632     if (inv.Class == wpnClass) return Weapon(inv);
633   }
634   // try replacements
635   if (allowReplace) {
636     // replacement
637     auto wpnRepl = class!Weapon(GetClassReplacement(wpnClass));
638     if (wpnRepl && wpnRepl != wpnClass) {
639       for (Inventory inv = EntityEx(MO).Inventory; inv; inv = inv.Inventory) {
640         if (inv.Class == wpnRepl) return Weapon(inv);
641       }
642     }
643   }
644   // not found
645   return none;
649 //==========================================================================
651 //  GetAttackZOfs
653 //==========================================================================
654 float GetAttackZOfs () {
655   auto plrmo = PlayerPawn(MO);
656   if (!plrmo) return 8.0;
657   return plrmo.AttackZOffset*plrmo.crouchfactor;
661 //==========================================================================
663 //  ThrustPlayer
665 //  Moves the given origin along a given angle.
667 //==========================================================================
668 void ThrustPlayer (float angle, float move, float deltaTime) {
669   if ((!EntityEx(MO).FindInventory(PowerFlight) || MO.Origin.z <= MO.FloorZ) &&
670       ((EntityEx(MO).GetActorTerrain()->Friction &&
671         EntityEx(MO).GetActorTerrain()->Friction < EntityEx::FRICTION_NORMAL) ||
672       (MO.Sector->special&SECSPEC_BASE_MASK) == SECSPEC_FrictionLow))
673   {
674     move *= LineSpecialGameInfo(Level.Game).IceMoveFactor;
675   }
676   float s, c;
677   sincos(angle, out s, out c);
678   MO.Velocity.x += move*c*deltaTime;
679   MO.Velocity.y += move*s*deltaTime;
683 //==========================================================================
685 //  SanitizeViewOrgZ
687 //==========================================================================
688 void SanitizeViewOrgZ () {
689   if (!MO) return; // just in case
690   if (PlayerState != PST_DEAD && MO.bFloorClip && MO.Origin.z <= MO.FloorZ) ViewOrg.z -= MO.FloorClip;
691   if (ViewOrg.z < MO.FloorZ+3.0) ViewOrg.z = MO.FloorZ+3.0;
692   if (ViewOrg.z > MO.CeilingZ-3.0) ViewOrg.z = MO.CeilingZ-3.0;
696 //==========================================================================
698 //  SaveViewOrgFixInfo
700 //==========================================================================
701 void SaveViewOrgFixInfo () {
702   if (MO) {
703     lastViewOrgZForPfx = MO.Origin.z;
704     lastViewOrgZSector = MO.Sector;
705     lastViewOrgVH = ViewHeight;
706   } else {
707     lastViewOrgZSector = nullptr;
708   }
712 //==========================================================================
714 //  ClientSetViewOrg
716 //==========================================================================
717 override void ClientSetViewOrg (TVec neworg) {
718   ::ClientSetViewOrg(neworg);
719   SaveViewOrgFixInfo();
723 //==========================================================================
725 //  PostfixViewHeight
727 //  after player thinker was called, player can be moved by a lift,
728 //  for example. we have to "postfix" view origin to get rid of
729 //  "sinking into lift" effect.
731 //  this is called from `SetViewPos()`, which in turn is called from
732 //  main world thinker.
734 //==========================================================================
735 override void PostfixViewHeight () {
736   if (!MO) return; // just in case
737   if (!lastViewOrgZSector) return;
738   if (lastViewOrgZSector != MO.Sector) return;
739   if (lastViewOrgVH != ViewHeight) return;
741   float zdiff = MO.Origin.z-lastViewOrgZForPfx;
742   if (!zdiff) return; // nothing to do
743   lastViewOrgZSector = nullptr; // in case we will be called repeatedly
745   // fix flashlight origin, so it won't jerk on lifts
746   if (bFlashlightOn) MO.ShiftDlightHeight(FlashlightLightId, zdiff);
748   ViewOrg.z += zdiff;
749   //SanitizeViewOrgZ();
753 //==========================================================================
755 //  CalcHeight
757 //  calculate the walking / running height adjustment
758 //  called from player tickers (both alive and dead)
760 //==========================================================================
761 void CalcHeight (float deltaTime) {
762   float mvbobbob = GetCvarF('movebob');
764   if (mvbobbob > 0) {
765     // regular movement bobbing
766     // (needs to be calculated for gun swing even if not on ground)
767     if (MO.bFly && !onground) {
768       Bob = 0.5;
769     } else {
770       if (mvbobbob > 1) mvbobbob = 1;
771       Bob = MO.Velocity.x*MO.Velocity.x+MO.Velocity.y*MO.Velocity.y;
772       Bob /= (1.0/mvbobbob)*35.0*35.0;
773       if (Bob > MAXBOB) Bob = MAXBOB;
774     }
775   }
777   // when crouching, bobbing have to be reduced
778   if (PlayerPawn(MO)) {
779     auto plrmo = PlayerPawn(MO);
780     Bob *= plrmo.crouchfactor;
781   }
783   float angle = 180.0*35.0/10.0*Level.XLevel.Time;
784   mvbobbob = Bob/2.0*sin(angle);
786   // move viewheight
787   if (PlayerState == PST_LIVE) {
788     ViewHeight += DeltaViewHeight*deltaTime;
790     float plrVH = PlayerPawn(MO).GetPawnViewHeight*PlayerPawn(MO).crouchfactor;
792     if (ViewHeight > plrVH) {
793       ViewHeight = plrVH;
794       DeltaViewHeight = 0.0;
795     }
797     if (ViewHeight < plrVH/2.0) {
798       ViewHeight = plrVH/2.0;
799       if (DeltaViewHeight <= 0.0) DeltaViewHeight = 0.00001;
800     }
802     if (DeltaViewHeight) {
803       DeltaViewHeight += 256.0*deltaTime;
804       if (!DeltaViewHeight) DeltaViewHeight = 0.00001;
805     }
806   }
808   if (Level.XLevel.bIsBadApple) {
809     ViewOrg.z = MO.Origin.z+41;
810   } else {
811     ViewOrg.z = MO.Origin.z+ViewHeight+mvbobbob;
812   }
813   SaveViewOrgFixInfo();
815   SanitizeViewOrgZ();
819 //==========================================================================
821 //  SetNewCrouchFactor
823 //==========================================================================
824 void SetNewCrouchFactor (float newcrf) {
825   auto plrmo = PlayerPawn(MO);
826   if (!plrmo) return;
828   //scope(exit) PrevViewHeight = ViewHeight;
830   // check whether the move is ok
831   float savedheight = plrmo.Height; //HACK! we'd better use `GetHeight()` here, but...
832   float defaultheight = plrmo.GetRealHeight; // ...this saves `Height` to `RealHeight` too
833   plrmo.Height = defaultheight*newcrf; // we'd better use `SetHeight()` here
834   if (!plrmo.TryMove(plrmo.Origin, AllowDropOff:false)) {
835     // nope, restore player height
836     plrmo.Height = savedheight;
837     return;
838   }
840   // setup new view height, and remember current crouch factor
841   // apply view height delta
842   float prevVH = PrevViewHeight;
843   if (prevVH == -666) prevVH = ViewHeight;
844   float vhDelta = ViewHeight-prevVH;
845   ViewHeight = PlayerPawn(plrmo).GetPawnViewHeight*newcrf+vhDelta;
846   plrmo.crouchfactor = newcrf;
847   PrevViewHeight = ViewHeight;
849   // check for eyes going above/below fake floor due to crouching motion
850   // this seems to be done in TryMove
851   //FIXME
852   //TODO
853   //CheckFakeFloorTriggers(pos.Z + oldheight, true);
857 //==========================================================================
859 //  CrouchMove
861 //==========================================================================
862 void CrouchMove (float deltaTime, int direction) {
863   auto plrmo = PlayerPawn(MO);
864   if (!plrmo) return;
866   float crouchspeed = (bForceCrouchingDown && direction > 0 ? -EntityEx::CROUCHSPEED : direction*EntityEx::CROUCHSPEED)*deltaTime;
867   float newcrf = fclamp(plrmo.crouchfactor+crouchspeed, 0.5, 1.0);
868   if (newcrf == plrmo.crouchfactor) return; // nothing to do
869   //if (bForceCrouchingDown) printdebug("FORCED CROUCH! old=%s; new=%s; cspeed=%s", plrmo.crouchfactor, newcrf, crouchspeed);
870   //else if (newcrf != 1.0f) printdebug("CROUCH(dir=%s)! old=%s; new=%s; cspeed=%s", direction, plrmo.crouchfactor, newcrf, crouchspeed);
872   SetNewCrouchFactor(newcrf);
876 //==========================================================================
878 //  CanCrouch
880 //==========================================================================
881 bool CanCrouch () {
882   if (!IsCrouchEnabled()) return false;
883   return (!bFly || MO.Origin.z <= MO.FloorZ);
887 //==========================================================================
889 //  IsCrouchButtonCrouch
891 //==========================================================================
892 bool IsCrouchButtonCrouch () {
893   if (!IsCrouchEnabled()) return false;
894   if (Buttons&BT_CROUCH) {
895     return (!bFly || MO.Origin.z <= MO.FloorZ);
896   }
897   return false;
901 //==========================================================================
903 //  MovePlayer
905 //==========================================================================
906 void MovePlayer (float deltaTime) {
907   float forward;
908   float side;
909   float fly;
911   // yeah, we can start crouching mid-air
912   //auto ohgt = MO.Height;
913   //printdebug("%C: MovePlayer; dt=%s; bForceCrouchingDown=%s", self, deltaTime, bForceCrouchingDown);
914   CrouchMove(deltaTime, (IsCrouchButtonCrouch() ? -1 : 1));
915   //print("HEIGHT: %s  %s  crf=%s; vh=%s; dvh=%s", ohgt, MO.Height, EntityEx(MO).crouchfactor, ViewHeight, PlayerPawn(MO).default.ViewHeight);
917   // do not let the player control movement if not onground
918   onground = (MO.Origin.z <= MO.FloorZ || EntityEx(MO).bOnMobj);
920   //if (Buttons&BT_JUMP) printdebug("%C: onground=%s; JumpTime=%s", self, onground, JumpTime);
922   /*
923   if (spGetNormalZ(MO.EFloor) != 1.0) printdebug("%C: slope onground=%s", self, onground);
925   // for slopes
926   if (!onground && spGetNormalZ(MO.EFloor) != 1.0 && MO.Origin.z <= spGetPointZ(MO.EFloor, MO.Origin)) {
927     printdebug("%C: on a slope!", self);
928     onground = true;
929   }
930   */
932   forward = ForwardMove*5.0;
933   side = SideMove*5.0;
935   PlayerPawn(MO).AdjustSpeed(forward, side);
937   if (!onground && !MO.bNoGravity && !MO.WaterLevel) {
938     // not on ground, so we have little effect on velocity
939     forward *= Level.AirControl;
940     side *= Level.AirControl;
941   }
943   if (forward) ThrustPlayer(MO.Angles.yaw, forward, deltaTime);
944   if (side) ThrustPlayer(AngleMod360(MO.Angles.yaw-90.0), side, deltaTime);
946   if (forward || side) {
947     SetPlayerRunState();
948     if (bRevertCamera) {
949       Camera = MO;
950       bRevertCamera = false;
951     }
952   }
954   fly = FlyMove/16.0;
955   if (fly && (bFly || EntityEx(MO).FindInventory(PowerFlight))) {
956     if (FlyMove != TOCENTRE) {
957       FlyHeight = fly*2.0;
958       if (!MO.bFly) {
959         MO.bFly = true;
960         MO.bNoGravity = true;
961         if (MO.Velocity.z <= -39.0*35.0) {
962           // stop falling scream
963           MO.StopSound(CHAN_VOICE);
964         }
965       }
966     } else {
967       MO.bFly = false;
968       MO.bNoGravity = false;
969     }
970   } else if (fly > 0.0) {
971     UseFlyPower();
972   }
974   if (MO.bFly) {
975     /* old code
976     MO.Velocity.z = FlyHeight*35.0;
977     if (FlyHeight) FlyHeight /= 2.0;
978     */
979     if (fabs(FlyHeight) > 0.1) {
980       MO.Velocity.z = FlyHeight*35.0;
981       FlyHeight /= 2.0;
982       if (fabs(FlyHeight) <= 0.1) FlyHeight = 0;
983     } else {
984       // directional flight
985       if (forward) {
986         TVec vfdir;
987         AngleVector(MO.Angles, out vfdir);
988         vfdir.z *= forward*deltaTime;
989         MO.Velocity.z += vfdir.z;
990       } else {
991         if (MO.Velocity.z < 0) {
992           MO.Velocity.z = fmin(0, MO.Velocity.z+4);
993         } else if (MO.Velocity.z > 0) {
994           MO.Velocity.z = fmax(0, MO.Velocity.z-4);
995         }
996       }
997     }
998     if ((Buttons&BT_JUMP) && MO.Velocity.z < 160) MO.Velocity.z += 30;
999     if ((Buttons&BT_CROUCH) && MO.Velocity.z > -160) MO.Velocity.z -= 30;
1000     // sanitize
1001     float limit = ((Buttons&BT_SPEED) && IsRunEnabled() && IsWeaponRunEnabled() ? 400 : 200);
1002     MO.Velocity.z = fclamp(MO.Velocity.z, -limit, limit);
1003     //print("VZ=%s; FH=%s; fvwd=%s", MO.Velocity.z, FlyHeight, vforward);
1004   }
1006   if ((Buttons&BT_JUMP) && !bFly && onground && !JumpTime && IsJumpEnabled()) {
1007     MO.Velocity.z = (GetJumpVelZ()*(Cheats&CF_HIGHJUMP ? 2.0 : 1.0))*1.1;
1008     EntityEx(MO).bOnMobj = false;
1009     JumpTime = 0.5;
1010     MO.PlaySound('*jump', CHAN_VOICE);
1011     // add last polyobject speed, if there is any
1012     auto pawn = PlayerPawn(MO);
1013     if (pawn) {
1014       #ifdef DEBUG_3DPOBJ_JUMPS
1015       if (pawn.lastStand3DPObj) {
1016         printdebug("%C: jumping from pobj %s: vel=%s;  (FloorZ=%s; z=%s, onmobj=%B)", pawn, pawn.lastStand3DPObj.tag, pawn.lastStand3DPObjVel, MO.FloorZ, MO.Origin.z, EntityEx(MO).bOnMobj);
1017       }
1018       #endif
1019       if (pawn.lastStand3DPObj) {
1020         MO.Velocity += pawn.lastStand3DPObjVel;
1021         pawn.lastJump3DPObj = pawn.lastStand3DPObj;
1022         pawn.lastStand3DPObj = nullptr;
1023       } else {
1024         pawn.lastJump3DPObj = nullptr;
1025       }
1026     }
1027   }
1031 //==========================================================================
1033 //  CheckCanMoveTo
1035 //  check for a jump-out-of-water
1037 //==========================================================================
1038 final bool CheckCanMoveTo (TVec org) {
1039   EntityEx mo = EntityEx(MO);
1040   Entity::tmtrace_t tmt;
1041   if (!mo.CheckRelPosition(out tmt, org, noPickups:true, ignoreMonsters:true, ignorePlayers:true)) {
1042     return false;
1043   } else {
1044     return (org.z >= tmt.FloorZ && org.z+mo.Height < tmt.CeilingZ);
1045   }
1049 //==========================================================================
1051 //  CheckWaterJumpThrust
1053 //==========================================================================
1054 void CheckWaterJumpThrust () {
1055   EntityEx mo = EntityEx(MO);
1056   TVec vforward, start;
1058   // thrust
1059   #ifdef DEBUG_WATER_JUMPS
1060   printdebug("%C: CWJT: vel=%s : %s : rt=%s; wlev=%s; wtr=%B", self, mo.Velocity, mo.Velocity.xy.length,
1061              mo.ReactionTime, mo.WaterLevel, bWaterThrusted);
1062   #endif
1063   if (!bWaterThrusted && mo.WaterLevel < 2) {
1064     vforward = (AngleYawVector(mo.Angles.yaw)*ForwardMove+YawVectorRight(mo.Angles.yaw)*SideMove).Normalise;
1065     start = mo.Origin;
1066     if (CheckCanMoveTo(start)) {
1067       bWaterThrusted = true;
1068       mo.bWaterJump = false;
1069       mo.ReactionTime = 0.0;
1070       float hlen = mo.Velocity.xy.length;
1071       if (hlen < 50) {
1072         mo.Velocity += vforward*(60-hlen);
1073         #ifdef DEBUG_WATER_JUMPS
1074         printdebug("%C: TH0: vel=%s : %s", self, mo.Velocity, mo.Velocity.xy.length);
1075         #endif
1076       }
1077       #if 0
1078       else if (hlen > 70*7) {
1079         mo.Velocity -= vforward*(hlen-60*6);
1080         #ifdef DEBUG_WATER_JUMPS
1081         printdebug("%C: TH1: vel=%s : %s", self, mo.Velocity, mo.Velocity.xy.length);
1082         #endif
1083       }
1084       #endif
1085     }
1086     #ifdef DEBUG_WATER_JUMPS
1087     else {
1088       printdebug("%C: NO-TH: vel=%s : %s", self, mo.Velocity, mo.Velocity.xy.length);
1089     }
1090     #endif
1091   }
1095 //==========================================================================
1097 //  CheckWaterJump
1099 //  check for a jump-out-of-water
1101 //==========================================================================
1102 void CheckWaterJump (optional bool asStep) {
1103   EntityEx mo = EntityEx(MO);
1104   TVec vforward, start;
1106   if (mo.bWaterJump && mo.ReactionTime > 0) {
1107     CheckWaterJumpThrust();
1108     return;
1109   }
1111   // do not jump out if fully submerged
1112   if (MO.WaterLevel > 2) return;
1114   // not moving?
1115   if (ForwardMove < 100 && SideMove < 100) return;
1117   if (mo.Angles.pitch >= 0) return; // we're looking down
1118   //printdebug("p:%s", mo.Angles.pitch);
1119   //printdebug("wl:%s", mo.WaterLevel);
1121   vforward = (AngleYawVector(mo.Angles.yaw)*ForwardMove+YawVectorRight(mo.Angles.yaw)*SideMove).Normalise;
1122   start = mo.Origin + vforward*2;
1124   // if we cannot move forward...
1125   if (!CheckCanMoveTo(start)) {
1126     // ...but can do it when we're higher...
1127     start.z += (asStep ? mo.MaxStepHeight : mo.Height);
1128     if (CheckCanMoveTo(start)) {
1129       // ...then this is water jump
1130       #ifdef DEBUG_WATER_JUMPS
1131       printdebug("%C: EYEOPEN: vel=%s : %s -- %s", self, mo.Velocity, mo.Velocity.xy.length, vforward);
1132       #endif
1133       bWaterThrusted = false;
1134       mo.bWaterJump = true;
1135       mo.Velocity.z = 350.0;
1136       mo.ReactionTime = 2.0; // safety net
1137       mo.PlaySound('*jump', CHAN_VOICE); //FIXME
1138     } else {
1139       #ifdef DEBUG_WATER_JUMPS
1140       printdebug("%C: EYECLOSE: vel=%s : %s -- %s", self, mo.Velocity, mo.Velocity.xy.length, vforward);
1141       #endif
1142     }
1143   } else {
1144     #ifdef DEBUG_WATER_JUMPS
1145     printdebug("%C: CANMOVE: vel=%s : %s -- %s", self, mo.Velocity, mo.Velocity.xy.length, vforward);
1146     #endif
1147   }
1151 //==========================================================================
1153 //  WaterJump
1155 //==========================================================================
1156 void WaterJump () {
1157   EntityEx mo = EntityEx(MO);
1159   /*
1160   #ifdef DEBUG_WATER_JUMPS
1161   printdebug("%C: WJ: vel=%s : %s : rt=%s; wlev=%s; wjump=%B; wt=%B", self, mo.Velocity, mo.Velocity.xy.length,
1162              mo.ReactionTime, mo.WaterLevel, mo.bWaterJump, bWaterThrusted);
1163   #endif
1164   */
1166   if (mo.bWaterJump) {
1167     if (!mo.ReactionTime) {
1168       mo.bWaterJump = false;
1169       bWaterThrusted = false;
1170     } else if (mo.WaterLevel < 2) {
1171       //mo.bWaterJump = false;
1172       //mo.ReactionTime = 0.0;
1173       if (!bWaterThrusted) CheckWaterJumpThrust();
1174     }
1175   } else {
1176     bWaterThrusted = false;
1177   }
1181 //==========================================================================
1183 //  WaterMove
1185 //  this is called if WaterLevel is > 1
1187 //  water levels:
1188 //    0: not submerged at all (e.g. standing on solid ground or on shallow TERRAIN-based water)
1189 //    1: less than half submerged ("ankle deep")
1190 //    2: at least half submerged ("waist deep")
1191 //    3: entirely submerged (completely underwater)
1193 //==========================================================================
1194 void WaterMove (float deltaTime) {
1195   float forward;
1196   float side;
1197   TVec vforward;
1198   TVec vright;
1199   TVec vup;
1200   TVec wishvel;
1202   auto lastwtlevel = LastWaterLevel;
1203   LastWaterLevel = MO.WaterLevel;
1205   // if we just fell into water, and we have too big vertical velocity, clamp it
1206   if (lastwtlevel != MO.WaterLevel) {
1207     //print("***WATER: prev=%s; curr=%s; velz=%s", LastWaterLevel, MO.WaterLevel, MO.Velocity.z);
1208     if (!lastwtlevel && MO.Velocity.z < -20) {
1209       MO.Velocity.z = fmax(MO.Velocity.z, MO.Velocity.z+(MO.WaterLevel < 3 ? 100 : 100));
1210       if (MO.Velocity.z >= -20) LastWaterLevel = 0; // repeat it
1211     }
1212   }
1213   //else print("**WZ=%s", MO.Velocity.z);
1215   AngleVectors(MO.Angles, out vforward, out vright, out vup);
1217   forward = ForwardMove;
1218   side = SideMove;
1220   PlayerPawn(MO).AdjustSpeed(forward, side);
1222   wishvel = vforward*forward+vright*side;
1223   //print("wishvel=%s; waterlevel=%s", wishvel, LastWaterLevel);
1224   float wishvz = wishvel.z;
1225   if (!forward && !side /*&& !cmd.upmove*/ && !(Buttons&BT_JUMP)) {
1226     //wishvel.z -= 60.0; // drift towards bottom
1227     //wishvel.z -= (MO.WaterLevel < 2 ? 60 : 6);
1228     wishvel.z -= 6;
1229   }
1230   // prevent bunny-hopping while hovering on water
1231   if (LastWaterLevel < 2) {
1232     wishvz = 0;
1233     wishvel.z = 0;
1234     MO.Velocity.z = fmin(0, MO.Velocity.z);
1235   }
1236   //else wishvel.z += cmd.upmove;
1238   //print("wishvel=%s; forward=%s; prevel=%s", wishvel, forward, MO.Velocity);
1239   MO.Velocity += 3.5*deltaTime*wishvel;
1241   if (forward || side) SetPlayerRunState();
1243   bool doCrouchMove = true;
1244   bool doNormalJump = false;
1245   if (Buttons&BT_JUMP) {
1246     // jump if standing on a floor
1247     if (LastWaterLevel < 3 && !bFly && MO.Origin.z <= MO.FloorZ) {
1248       doNormalJump = true;
1249     } else if (LastWaterLevel >= 3) {
1250            if (MO.WaterType == CONTENTS_WATER) MO.Velocity.z = 100.0;
1251       else if (MO.WaterType == CONTENTS_NUKAGE || MO.WaterType == CONTENTS_SLIME || MO.WaterType == CONTENTS_SLUDGE) MO.Velocity.z = 80.0;
1252       else MO.Velocity.z = 50.0;
1253     }
1254   } else if (Buttons&BT_CROUCH) {
1255     // crouching means "sink" while in water
1256     if ((LastWaterLevel >= 2 || bFly) && MO.Origin.z > MO.FloorZ) {
1257            if (MO.WaterType == CONTENTS_WATER) MO.Velocity.z = -100.0/1.5;
1258       else if (MO.WaterType == CONTENTS_NUKAGE || MO.WaterType == CONTENTS_SLIME || MO.WaterType == CONTENTS_SLUDGE) MO.Velocity.z = -80.0/1.5;
1259       else MO.Velocity.z = -50.0/1.5;
1260     } else if (MO.Origin.z <= MO.FloorZ) {
1261       // normal crouch
1262       doCrouchMove = false;
1263       CrouchMove(deltaTime, (IsCrouchButtonCrouch() ? -1 : 1));
1264     }
1265   } else {
1266     if (MO.WaterLevel >= 2 && wishvel.z < 0 && wishvz == 0) {
1267       if (MO.Velocity.z > 0) MO.Velocity.z -= 10;
1268     }
1269     // if we are not moving vertically, do not sink too fast
1270     if (/*!forward*/!wishvz) {
1271       if (MO.Velocity.z < -20) MO.Velocity.z = fmin(-20, MO.Velocity.z+20);
1272     } else {
1273       float limit = ((Buttons&BT_SPEED) && IsRunEnabled() && IsWeaponRunEnabled() ? 140 : 140*2);
1274       MO.Velocity.z = fclamp(MO.Velocity.z, -limit, limit);
1275     }
1276   }
1278   // stand up
1279   if (doCrouchMove) CrouchMove(deltaTime, 1);
1281   CheckWaterJump();
1283   // do real jump here, but only if we aren't jumping out of water
1284   if (doNormalJump && !EntityEx(MO).bWaterJump) {
1285     MO.Velocity.z = (GetJumpVelZ()*(Cheats&CF_HIGHJUMP ? 2.0 : 1.0))*1.1;
1286     EntityEx(MO).bOnMobj = false;
1287     //JumpTime = 0.5;
1288   }
1292 //==========================================================================
1294 //  ProcessSectorScroll
1296 //==========================================================================
1297 void ProcessSectorScroll (float deltaTime, EntityEx ent, sector_t *sec) {
1298   float speed;
1299   float finean;
1301   switch (sec->special&SECSPEC_BASE_MASK) {
1302     case SECSPEC_ScrollCurrent:
1303       speed = float((sec->sectorTag-100)%10)/16.0*35.0;
1304       finean = float((sec->sectorTag-100)/10)*45.0;
1305       ent.Velocity.x += speed*cos(finean);
1306       ent.Velocity.y += speed*sin(finean);
1307       break;
1308     case SECSPEC_ScrollNorthSlow:
1309     case SECSPEC_ScrollNorthMedium:
1310     case SECSPEC_ScrollNorthFast:
1311       ThrustPlayer(90.0, LineSpecialGameInfo(Level.Game).pushTab[(sec->special&SECSPEC_BASE_MASK)-SECSPEC_ScrollNorthSlow], deltaTime);
1312       break;
1313     case SECSPEC_ScrollEastSlow:
1314     case SECSPEC_ScrollEastMedium:
1315     case SECSPEC_ScrollEastFast:
1316       ThrustPlayer(0.0, LineSpecialGameInfo(Level.Game).pushTab[(sec->special&SECSPEC_BASE_MASK)-SECSPEC_ScrollEastSlow], deltaTime);
1317       break;
1318     case SECSPEC_ScrollSouthSlow:
1319     case SECSPEC_ScrollSouthMedium:
1320     case SECSPEC_ScrollSouthFast:
1321       ThrustPlayer(270.0, LineSpecialGameInfo(Level.Game).pushTab[(sec->special&SECSPEC_BASE_MASK)-SECSPEC_ScrollSouthSlow], deltaTime);
1322       break;
1323     case SECSPEC_ScrollWestSlow:
1324     case SECSPEC_ScrollWestMedium:
1325     case SECSPEC_ScrollWestFast:
1326       ThrustPlayer(180.0, LineSpecialGameInfo(Level.Game).pushTab[(sec->special&SECSPEC_BASE_MASK)-SECSPEC_ScrollWestSlow], deltaTime);
1327       break;
1328     case SECSPEC_ScrollNorthWestSlow:
1329     case SECSPEC_ScrollNorthWestMedium:
1330     case SECSPEC_ScrollNorthWestFast:
1331       ThrustPlayer(135.0, LineSpecialGameInfo(Level.Game).pushTab[(sec->special&SECSPEC_BASE_MASK)-SECSPEC_ScrollNorthWestSlow], deltaTime);
1332       break;
1333     case SECSPEC_ScrollNorthEastSlow:
1334     case SECSPEC_ScrollNorthEastMedium:
1335     case SECSPEC_ScrollNorthEastFast:
1336       ThrustPlayer(45.0, LineSpecialGameInfo(Level.Game).pushTab[(sec->special&SECSPEC_BASE_MASK)-SECSPEC_ScrollNorthEastSlow], deltaTime);
1337       break;
1338     case SECSPEC_ScrollSouthEastSlow:
1339     case SECSPEC_ScrollSouthEastMedium:
1340     case SECSPEC_ScrollSouthEastFast:
1341       ThrustPlayer(315.0, LineSpecialGameInfo(Level.Game).pushTab[(sec->special&SECSPEC_BASE_MASK)-SECSPEC_ScrollSouthEastSlow], deltaTime);
1342       break;
1343     case SECSPEC_ScrollSouthWestSlow:
1344     case SECSPEC_ScrollSouthWestMedium:
1345     case SECSPEC_ScrollSouthWestFast:
1346       ThrustPlayer(225.0, LineSpecialGameInfo(Level.Game).pushTab[(sec->special&SECSPEC_BASE_MASK)-SECSPEC_ScrollSouthWestSlow], deltaTime);
1347       break;
1348     case SECSPEC_ScrollEast5:
1349     case SECSPEC_ScrollEast10:
1350     case SECSPEC_ScrollEast25:
1351     case SECSPEC_ScrollEast30:
1352     case SECSPEC_ScrollEast35:
1353       ThrustPlayer(0.0, LineSpecialGameInfo(Level.Game).pushTab[(sec->special&SECSPEC_BASE_MASK)-SECSPEC_ScrollEast5], deltaTime);
1354       break;
1355     case SECSPEC_ScrollNorth5:
1356     case SECSPEC_ScrollNorth10:
1357     case SECSPEC_ScrollNorth25:
1358     case SECSPEC_ScrollNorth30:
1359     case SECSPEC_ScrollNorth35:
1360       ThrustPlayer(90.0, LineSpecialGameInfo(Level.Game).pushTab[(sec->special&SECSPEC_BASE_MASK)-SECSPEC_ScrollNorth5], deltaTime);
1361       break;
1362     case SECSPEC_ScrollSouth5:
1363     case SECSPEC_ScrollSouth10:
1364     case SECSPEC_ScrollSouth25:
1365     case SECSPEC_ScrollSouth30:
1366     case SECSPEC_ScrollSouth35:
1367       ThrustPlayer(270.0, LineSpecialGameInfo(Level.Game).pushTab[(sec->special&SECSPEC_BASE_MASK)-SECSPEC_ScrollSouth5], deltaTime);
1368       break;
1369     case SECSPEC_ScrollWest5:
1370     case SECSPEC_ScrollWest10:
1371     case SECSPEC_ScrollWest25:
1372     case SECSPEC_ScrollWest30:
1373     case SECSPEC_ScrollWest35:
1374       ThrustPlayer(180.0, LineSpecialGameInfo(Level.Game).pushTab[(sec->special&SECSPEC_BASE_MASK)-SECSPEC_ScrollWest5], deltaTime);
1375       break;
1376   }
1380 //==========================================================================
1382 //  PlayerProcessScrollSectors
1384 //  Called every tic frame
1386 //==========================================================================
1387 void PlayerProcessScrollSectors (float deltaTime) {
1388   // process scroll sectors anyway (because we may slightly touch one of them)
1389   for (msecnode_t *mnode = EntityEx(MO).TouchingSectorList; mnode; mnode = mnode.TNext) {
1390     sector_t *sec = mnode.Sector;
1391     if (sec->special&SECSPEC_BASE_MASK) {
1392       if (MO.Origin.z == sec.floor.GetPointZClamped(MO.Origin)) {
1393         ProcessSectorScroll(deltaTime, EntityEx(MO), sec);
1394       }
1395     }
1396   }
1400 //==========================================================================
1402 //  FindProtectionSuit
1404 //==========================================================================
1405 Inventory FindProtectionSuit () {
1406   // search for iron feet power: any subclass will do
1407   Inventory IronFeet = EntityEx(MO).Inventory;
1408   while (IronFeet) {
1409     if (PowerIronFeet(IronFeet)) break;
1410     IronFeet = IronFeet.Inventory;
1411   }
1412   return IronFeet;
1416 //==========================================================================
1418 //  IsProtectionSuitActive
1420 //  `leakChance` is byte; 0 means "always active", >255 means "never active"
1422 //==========================================================================
1423 bool IsProtectionSuitActive (int leakChance) {
1424   if (leakChance > 255) return false; // never active
1425   if (leakChance > 0 && P_Random() < leakChance) {
1426     //if (FindProtectionSuit()) printdebug("SUIT LEAKS! chance=%s", leakChance);
1427     return false; // suit leaks
1428   }
1429   // check if we actually have a suit
1430   return !!FindProtectionSuit();
1434 //==========================================================================
1436 //  InitSectorDamageInfo
1438 //==========================================================================
1439 void InitSectorDamageInfo (ref SectorDamageInfo pdi) {
1440   pdi.flatDamageType = '';
1441   pdi.flatDamage = 0;
1442   pdi.flatDamageTimeout = 0;
1443   pdi.flatDamageLeaky = 0;
1444   pdi.flatDamageExit = false;
1445   pdi.flatDamageHitFloor = false;
1449 //==========================================================================
1451 //  SetupSectorUDMFDamage
1453 //==========================================================================
1454 void SetupSectorUDMFDamage (float deltaTime, EntityEx ent, sector_t *sec, ref SectorDamageInfo pdi) {
1455   if (sec.Damage) {
1456     pdi.flatDamage = sec.Damage;
1457     pdi.flatDamageType = sec.DamageType;
1458     pdi.flatDamageTimeout = max(1, (sec.DamageInterval ? sec.DamageInterval : 32)); // 0 is "default 32"
1459     pdi.flatDamageLeaky = max(0, (sec.DamageLeaky ? sec.DamageLeaky : 5)); // 0 is "default 5"
1460     if (pdi.flatDamageType.nameEquCI('Exit')) {
1461       // EXIT SUPER DAMAGE! (for E1M8 finale)
1462       pdi.flatDamageType = ''; //FIXME
1463       pdi.flatDamageExit = true;
1464     }
1465     //printdebug("sector #%d: damage=%s; interval=%s; leak=%s; type=%s", sec-&Level.XLevel.Sectors[0], sec.Damage, sec.DamageInterval, sec.DamageLeaky, sec.DamageType);
1466     //printdebug("  pdi: flatDamage=%s; flatDamageType=%s; flatDamageTimeout=%s; flatDamageLeaky=%s; flatDamageExit=%B", pdi.flatDamage, pdi.flatDamageType, pdi.flatDamageTimeout, pdi.flatDamageLeaky, pdi.flatDamageExit);
1467   }
1471 //==========================================================================
1473 //  SetupSectorFlatDamage
1475 //==========================================================================
1476 void SetupSectorFlatDamage (float deltaTime, EntityEx ent, sector_t *sec, ref SectorDamageInfo pdi) {
1477   if (sec->special >= SECSPEC_LightFlicker && sec->special <= 255) {
1478     switch (sec->special) {
1479       case SECSPEC_DamageHellslime:
1480         pdi.flatDamageType = 'Slime';
1481         pdi.flatDamage = 10;
1482         pdi.flatDamageTimeout = 32;
1483         pdi.flatDamageLeaky = 5; // default leakage value
1484         break;
1485       case SECSPEC_DamageSludge:
1486         pdi.flatDamageType = 'Slime';
1487         pdi.flatDamage = 4;
1488         pdi.flatDamageTimeout = 32;
1489         pdi.flatDamageLeaky = 5; // default leakage value
1490         break;
1491       case SECSPEC_DamageNukage:
1492         pdi.flatDamageType = 'Slime'; //FIXME
1493         pdi.flatDamage = 5;
1494         pdi.flatDamageTimeout = 32;
1495         pdi.flatDamageLeaky = 5; // default leakage value
1496         break;
1497       case SECSPEC_LightStrobeFastDamage:
1498       case SECSPEC_DamageSuperHellslime:
1499         pdi.flatDamageType = 'Slime'; //FIXME
1500         pdi.flatDamage = 20;
1501         pdi.flatDamageTimeout = 32;
1502         pdi.flatDamageLeaky = 5; // default leakage value
1503         break;
1504       case SECSPEC_DamageSuperHellslimeExit:
1505         // EXIT SUPER DAMAGE! (for E1M8 finale)
1506         pdi.flatDamageType = ''; //FIXME
1507         pdi.flatDamage = 20;
1508         pdi.flatDamageTimeout = 32;
1509         pdi.flatDamageExit = true;
1510         pdi.flatDamageLeaky = 5; // default leakage value
1511         break;
1512       case SECSPEC_DamageLavaWimpy:
1513         pdi.flatDamageType = 'Fire';
1514         pdi.flatDamage = 5;
1515         pdi.flatDamageTimeout = 16;
1516         pdi.flatDamageHitFloor = true;
1517         pdi.flatDamageLeaky = 5; // default leakage value
1518         break;
1519       case SECSPEC_DamageLavaHefty:
1520         pdi.flatDamageType = 'Fire';
1521         pdi.flatDamage = 8;
1522         pdi.flatDamageTimeout = 16;
1523         pdi.flatDamageHitFloor = true;
1524         pdi.flatDamageLeaky = 5; // default leakage value
1525         break;
1526       case SECSPEC_ScrollEastLavaDamage:
1527         ThrustPlayer(0.0, 1024.0, deltaTime);
1528         pdi.flatDamageType = 'Fire';
1529         pdi.flatDamage = 5;
1530         pdi.flatDamageTimeout = 16;
1531         pdi.flatDamageHitFloor = true;
1532         pdi.flatDamageLeaky = 5; // default leakage value
1533         break;
1534       case SECSPEC_DamageHazard:
1535         // hack: leaky suit
1536         if (!IsProtectionSuitActive(5)) HazardTime += 2.0*deltaTime;
1537         break;
1538       case SECSPEC_DamageInstantDeath:
1539         ent.Damage(none, none, 999, 'InstantDeath', spawnBlood:true);
1540         break;
1541       case SECSPEC_DamageSuperHazard:
1542         // hack: leaky suit
1543         if (!IsProtectionSuitActive(5)) HazardTime += 4.0*deltaTime;
1544         break;
1545     }
1546   } else {
1547     // extended sector damage type
1548     switch (sec->special&SECSPEC_DAMAGE_MASK) {
1549       case 0x0100:
1550         pdi.flatDamageType = 'Fire';
1551         pdi.flatDamage = 5;
1552         pdi.flatDamageTimeout = 32;
1553         pdi.flatDamageLeaky = 5; // default leakage value
1554         break;
1555       case 0x0200:
1556         pdi.flatDamageType = 'Slime';
1557         pdi.flatDamage = 10;
1558         pdi.flatDamageTimeout = 32;
1559         pdi.flatDamageLeaky = 5; // default leakage value
1560         break;
1561       case 0x0300:
1562         pdi.flatDamageType = 'Slime';
1563         pdi.flatDamage = 20;
1564         pdi.flatDamageTimeout = 32;
1565         pdi.flatDamageLeaky = 5; // default leakage value
1566         break;
1567     }
1568   }
1572 //==========================================================================
1574 //  ApplySectorDamageInfo
1576 //  performs no excessive checks
1577 //  i.e. `LastSectorDamageTic` or height should be checked by a caller
1579 //==========================================================================
1580 void ApplySectorDamageInfo (float deltaTime, EntityEx ent, sector_t *sec, ref SectorDamageInfo pdi) {
1581   // any damage at all?
1582   if (!pdi.flatDamage) return;
1583   // is damage timeout ok?
1584   if (pdi.flatDamageTimeout < 1) pdi.flatDamageTimeout = 1; // prevent divides by zero
1585   if (Level.XLevel.TicTime%pdi.flatDamageTimeout) return;
1586   // suit prevents damage, but not special effects
1587   if (!IsProtectionSuitActive(pdi.flatDamageLeaky)) {
1588     auto oldCheats = Cheats;
1589     scope(exit) Cheats = oldCheats;
1590     if (pdi.flatDamageExit) Cheats &= ~CF_GODMODE;
1591     if (pdi.flatDamageType) {
1592       ent.Damage(none, none, pdi.flatDamage, pdi.flatDamageType);
1593     } else {
1594       ent.Damage(none, none, pdi.flatDamage);
1595     }
1596   }
1597   if (pdi.flatDamageHitFloor) ent.HitFloorType();
1598   if (pdi.flatDamageExit && Health <= 10) Level.ExitLevel(0);
1602 //==========================================================================
1604 //  ApplySectorDamage
1606 //  caller should check if it is ok to call this (i.e. last damage tick)
1608 //==========================================================================
1609 void ApplySectorDamage (float deltaTime, EntityEx ent, sector_t *sec) {
1610   SectorDamageInfo pdi;
1612   // flat damage
1613   InitSectorDamageInfo(ref pdi);
1614   SetupSectorFlatDamage(deltaTime, ent, sec, ref pdi);
1615   if (pdi.flatDamage) ApplySectorDamageInfo(deltaTime, ent, sec, ref pdi);
1617   // UDMF sector damage
1618   if (sec->Damage) {
1619     //printdebug("sector #%d: damage=%s; interval=%s; leak=%s; type=%s", sec-&Level.XLevel.Sectors[0], sec.Damage, sec.DamageInterval, sec.DamageLeaky, sec.DamageType);
1620     InitSectorDamageInfo(ref pdi);
1621     SetupSectorUDMFDamage(deltaTime, ent, sec, ref pdi);
1622     if (pdi.flatDamage) ApplySectorDamageInfo(deltaTime, ent, sec, ref pdi);
1623   }
1627 //==========================================================================
1629 //  PlayerInSpecialSector
1631 //==========================================================================
1632 void PlayerInSpecialSector (float deltaTime) {
1633   // it doesn't matter if we're touching the floor or not for secret sectors
1634   if (MO.Sector->special&SECSPEC_SECRET_MASK) {
1635     // secret area
1636     ++SecretCount;
1637     ++Level.CurrentSecret;
1638     MO.Sector->special &= ~SECSPEC_SECRET_MASK;
1639     if (GetCvarB('show_secret_message')) centerprint("You found a secret area");
1640     if (GetCvarB('play_secret_sound')) MO.PlaySound('misc/secret', /*CHAN_VOICE*/CHAN_AUTO);
1641   }
1643   // do not apply damage continuously on the same tick
1644   if (LastSectorDamageTic < Level.XLevel.TicTime) {
1645     // this correctly processes 3d floors
1646     sector_t *swimmable;
1647     sector_t *tsec = EntityEx(MO).GetTouchedFloorSectorEx(out swimmable);
1648     if (tsec) ApplySectorDamage(deltaTime, EntityEx(MO), tsec);
1649     if (swimmable) ApplySectorDamage(deltaTime, EntityEx(MO), swimmable);
1651     /*
1652     if (MO.Origin.z != MO.Sector->floor.GetPointZClamped(MO.Origin) && !MO.WaterLevel) {
1653       // player is (possibly) not touching the floor
1654       tsec = EntityEx(MO).GetTouchedFloorSector();
1655       if (!tsec) return;
1656       printdebug("%C: orgz=%s; fz=%s; 3dfloor", self, MO.Origin.z, MO.FloorZ);
1657     }
1658     */
1659   }
1663 //============================================================================
1665 //  PlayerOnSpecialFlat
1667 //  Called every tic frame.
1669 //============================================================================
1670 void PlayerOnSpecialFlat (VTerrainInfo *floorType) {
1671   if (!floorType) return;
1672   if (MO.Origin.z != MO.FloorZ) return; // player is not touching the floor
1674   if (!floorType->DamageAmount) return;
1675   if (!(Level.XLevel.TicTime&floorType->DamageTimeMask)) return; // is this condition right?
1677   // apply protection suit (leaky)
1678   if (floorType->bAllowProtection && IsProtectionSuitActive(5)) return;
1680   EntityEx(MO).Damage(none, none, floorType->DamageAmount, floorType->DamageType/*, spawnBlood:true*/);
1681   //FIXME: use `DamageAmount` here?
1682   //EntityEx(MO).Damage(none, none, 10, 'Fire'/*, spawnBlood:true*/);
1683   //MO.PlaySound('world/lavasizzle', CHAN_BODY);
1687 //==========================================================================
1689 //  PlayerInContents
1691 //  Called every tic frame.
1693 //==========================================================================
1694 void PlayerInContents (float deltaTime) {
1695   if (!MO.WaterLevel) return;
1697   name flatDamageType = '';
1698   int flatDamage = 0;
1699   int flatDamageTimeout;
1701   switch (MO.WaterType) {
1702     case CONTENTS_LAVA:
1703       flatDamageType = 'Fire';
1704       flatDamage = 10;
1705       flatDamageTimeout = 32;
1706       break;
1707     case CONTENTS_NUKAGE:
1708       flatDamageType = 'Slime'; //FIXME
1709       flatDamage = 5;
1710       flatDamageTimeout = 32;
1711       break;
1712     case CONTENTS_SLIME:
1713       flatDamageType = 'Slime';
1714       flatDamage = 10;
1715       flatDamageTimeout = 32;
1716       break;
1717     case CONTENTS_HELLSLIME:
1718       flatDamageType = 'Slime';
1719       flatDamage = 20;
1720       flatDamageTimeout = 32;
1721       break;
1722     case CONTENTS_SLUDGE:
1723       flatDamageType = 'Slime'; //FIXME
1724       flatDamage = 4;
1725       flatDamageTimeout = 32;
1726       break;
1727     case CONTENTS_HAZARD:
1728       // apply protection suit (leaky)
1729       if (!IsProtectionSuitActive(5)) HazardTime += 2.0*deltaTime;
1730       return;
1731   }
1733   // apply flat damage?
1734   if (!flatDamage) return;
1735   // check timeout
1736   if (Level.XLevel.TicTime%flatDamageTimeout) return;
1737   // apply protection suit (leaky)
1738   if (IsProtectionSuitActive(5)) return;
1740   if (flatDamageType) {
1741     EntityEx(MO).Damage(none, none, flatDamage, flatDamageType);
1742   } else {
1743     EntityEx(MO).Damage(none, none, flatDamage);
1744   }
1748 //==========================================================================
1750 //  SetPlayerRunState
1752 //==========================================================================
1753 void SetPlayerRunState () {
1754   EntityEx mobj = EntityEx(MO);
1755   if (!mobj) return;
1756   if (mobj.SeeState && /*mobj.State == mobj.IdleState*/StateIsInSequence(mobj.State, mobj.IdleState)) {
1757     //printdebug("%C: going to see state %s from %s", mobj, mobj.IdleState, mobj.State);
1758     mobj.SetState(mobj.SeeState);
1759   }
1760   /*
1761   else {
1762     printdebug("%C: CANNOT go to see state %s", mobj, mobj.State);
1763   }
1764   */
1768 //***************************************************************************
1770 //  WEAPON UTILITES
1772 //***************************************************************************
1774 //==========================================================================
1776 //  ResetReadyWeaponBobbing
1778 //==========================================================================
1779 void ResetReadyWeaponBobbing () {
1780   Weapon wpn = ReadyWeapon;
1781   if (wpn) {
1782     wpn.bBobDisabled = true;
1783     wpn.bBobFrozen = false; // just in case
1784     BobbingTime = 0; // start from the base
1785   }
1789 //==========================================================================
1791 //  SetWeapon
1793 //==========================================================================
1794 void SetWeapon (Weapon NewWeapon) {
1795   ReadyWeapon = NewWeapon;
1796   PendingWeapon = none;
1797   if (NewWeapon) {
1798     PSpriteSY = NewWeapon.PSpriteSY;
1799     MO.ModelVersion = NewWeapon.PlayerModelVersion;
1800   } else {
1801     PSpriteSY = 0;
1802     MO.ModelVersion = 0;
1803     SetViewObject(none);
1804     SetViewState(PS_WEAPON, none);
1805   }
1809 //===========================================================================
1811 //  BringUpWeapon
1813 //  Starts bringing the pending weapon up from the bottom of the screen.
1815 //===========================================================================
1816 void BringUpWeapon (optional bool instant, optional bool skipSound) {
1817   //print("BringUpWeapon: %C (instant=%B: skipSound=%B)", ReadyWeapon, instant, skipSound);
1818   if (!skipSound && ReadyWeapon && ReadyWeapon.UpSound) {
1819     MO.PlaySound(ReadyWeapon.UpSound, CHAN_WEAPON);
1820   }
1822   if (PendingWeapon && PendingWeapon == ReadyWeapon) {
1823     printwarn("%C: RAISING ALREADY RAISED WEAPON! (0)", ReadyWeapon);
1824     //return;
1825   }
1827   PendingWeapon = none;
1828   SetViewStateOffsets(0, (instant || bInstantWeaponSwitch ? Weapon::WEAPONTOP : Weapon::WEAPONBOTTOM));
1829   ResetWeaponReloadRefire();
1830   // block firing for "no autofire" weapons
1831   bAttackDown = true;
1832   bAltAttackDown = true;
1833   if (ReadyWeapon) {
1834     ResetReadyWeaponBobbing();
1835     SetViewObject(ReadyWeapon);
1836     //dprint("MO=%C; RW=%C; upstate=%s", MO, ReadyWeapon, ReadyWeapon.GetUpState());
1837     //k8: old code did "up state" anyway; this seems to be wrong
1838     if (instant || bInstantWeaponSwitch) {
1839       if (instant) {
1840         SetViewState(PS_WEAPON, ReadyWeapon.GetInstaReadyState());
1841       } else {
1842         SetViewState(PS_WEAPON, ReadyWeapon.GetReadyState());
1843       }
1844     } else {
1845       //print("BRINGING %C: curr=%s; top=%s; bot=%s", ReadyWeapon, ViewStateSY, Weapon::WEAPONTOP, Weapon::WEAPONBOTTOM);
1846       SetViewState(PS_WEAPON, ReadyWeapon.GetUpState());
1847     }
1848     //dprint("MO=%C; RW=%C", MO, ReadyWeapon);
1849     if (!ReadyWeapon) {
1850       SetViewObject(none);
1851       print("RAISING NONE WEAPON! (0)");
1852       MO.ModelVersion = 0;
1853       PSpriteSY = 0;
1854       return;
1855     }
1856     MO.ModelVersion = ReadyWeapon.PlayerModelVersion;
1857   } else {
1858     SetViewObject(none);
1859     MO.ModelVersion = 0;
1860     PSpriteSY = 0;
1861   }
1865 //===========================================================================
1867 //  DropWeapon
1869 //  Player died, so put the weapon away.
1871 //===========================================================================
1872 void DropWeapon () {
1873   ResetWeaponReloadRefire();
1874   ResetWeaponActionFlags();
1875   if (ReadyWeapon) {
1876     printdebug("%C: bringing weapon %C down: %s", self, ReadyWeapon, ReadyWeapon.GetDownState());
1877     SetViewObject(ReadyWeapon);
1878     SetViewState(PS_WEAPON, ReadyWeapon.GetDownState());
1879   }
1883 //===========================================================================
1885 //  SetupPsprites
1887 //  Called at start of level for each player.
1889 //===========================================================================
1890 void SetupPsprites () {
1891   // remove all psprites
1892   foreach (auto i; 0..NUMPSPRITES) {
1893     SetViewObject(none);
1894     SetViewState(i, none);
1895   }
1897   // spawn the gun
1898   BringUpWeapon();
1902 //==========================================================================
1904 //  ProcessWeaponActions
1906 //==========================================================================
1907 void ProcessWeaponActions () {
1908   if (PendingWeapon == ReadyWeapon) PendingWeapon = none;
1910   if (!MO || MO.Health <= 0 || PlayerState != PST_LIVE) {
1911     ResetWeaponReloadRefire();
1912     ResetWeaponActionFlags();
1913     return;
1914   }
1916   #if 0
1917   printdebug("TICK: %s (bWeaponWasWeaponReady=%B)", MO.XLevel.TicTime, bWeaponWasWeaponReady);
1918   if (ReadyWeapon) {
1919     printdebug("%C: ReadyWeapon=%C; PendingWeapon=%C; rwstate=%s (%s)", self, ReadyWeapon, PendingWeapon,
1920                ViewStates[PS_WEAPON].State, ViewStates[PS_WEAPON].StateTime);
1921   }
1922   if (bWeaponAllowSwitch ||
1923       bWeaponAllowPrimaryFire ||
1924       bWeaponAllowAltFire ||
1925       bWeaponAllowReload ||
1926       bWeaponAllowZoom)
1927   {
1928     printdebug("%C: bWeaponAllowSwitch=%B", self, bWeaponAllowSwitch);
1929     printdebug("%C: bWeaponAllowPrimaryFire=%B", self, bWeaponAllowPrimaryFire);
1930     printdebug("%C: bWeaponAllowAltFire=%B", self, bWeaponAllowAltFire);
1931     printdebug("%C: bWeaponAllowReload=%B", self, bWeaponAllowReload);
1932     printdebug("%C: bWeaponAllowZoom=%B", self, bWeaponAllowZoom);
1933     printdebug("%C: ATTACK IS %B", self, !!(Buttons&BT_ATTACK));
1934   }
1935   #endif
1937   if (bWeaponAllowSwitch) {
1938     if (PendingWeapon && !bDisableWeaponSwitch) {
1939       ResetWeaponReloadRefire();
1940       ResetWeaponActionFlags();
1941       if (ReadyWeapon) {
1942         ResetReadyWeaponBobbing();
1943         SetViewObject(ReadyWeapon);
1944         SetViewState(PS_WEAPON, ReadyWeapon.GetDownState());
1945       } else if (PendingWeapon) {
1946         SetWeapon(PendingWeapon);
1947         BringUpWeapon();
1948       }
1949       return;
1950     }
1951   }
1953   Weapon Wpn = ReadyWeapon;
1954   if (!Wpn) {
1955     ResetWeaponReloadRefire();
1956     ResetWeaponActionFlags();
1957     bWeaponAllowSwitch = true;
1958     ResetPlayerFiringState();
1959     return;
1960   }
1962   // this is to emulate proper firing (once per tick)
1963   if (!bWeaponWasWeaponReady) return;
1964   if (!GetCvarB('wpn_gzdoom_firing')) bWeaponWasWeaponReady = false;
1965   Wpn.bBobFrozen = false; // just in case
1967   bool oldNoBob = Wpn.bBobDisabled;
1968   float oldBobTime = BobbingTime;
1970   if (bWeaponAllowPrimaryFire && (Buttons&BT_ATTACK) && (!bAttackDown || !Wpn.bNoAutoFire) && Wpn.FindState('Fire')) {
1971     bWeaponWasWeaponReady = false;
1972     bAttackDown = true;
1973     switch (GetCvarI('wp_fire_bobbing')) {
1974       case 0: // reset
1975         Wpn.bBobDisabled = true;
1976         Wpn.bBobFrozen = true;
1977         BobbingTime = 0;
1978         break;
1979       case 1: // freeze
1980         Wpn.bBobFrozen = true;
1981         break;
1982       //case 2: // bob: do nothing
1983     }
1984     if (FireWeapon()) return;
1985     Wpn.bBobDisabled = oldNoBob;
1986     Wpn.bBobFrozen = false;
1987     BobbingTime = oldBobTime;
1988   }
1990   if (bWeaponAllowAltFire && (Buttons&BT_ALT_ATTACK) && (!bAltAttackDown || !Wpn.bNoAutoFire) && Wpn.FindState('AltFire')) {
1991     bWeaponWasWeaponReady = false;
1992     bAltAttackDown = true;
1993     switch (GetCvarI('wp_fire_bobbing')) {
1994       case 0: // reset
1995         Wpn.bBobDisabled = true;
1996         Wpn.bBobFrozen = true;
1997         BobbingTime = 0;
1998         break;
1999       case 1: // freeze
2000         Wpn.bBobFrozen = true;
2001         break;
2002       //case 2: // bob: do nothing
2003     }
2004     if (AltFireWeapon()) return;
2005     Wpn.bBobDisabled = oldNoBob;
2006     Wpn.bBobFrozen = false;
2007     BobbingTime = oldBobTime;
2008   }
2010   // cannot fire
2011   ResetPlayerFiringState();
2012   Refire = 0;
2014   if (bWeaponAllowReload && ((Buttons&BT_RELOAD) || bReloadQueued)) {
2015     // `bReloadQueued` will be reset by `ReloadWeapon()`
2016     bWeaponWasWeaponReady = false;
2017     Wpn.bBobDisabled = true;
2018     BobbingTime = 0;
2019     if (ReloadWeapon()) return;
2020     Wpn.bBobDisabled = oldNoBob;
2021     BobbingTime = oldBobTime;
2022   }
2024   if (bWeaponAllowZoom && (Buttons&BT_ZOOM) && !bZoomDown && Wpn.GetZoomState()) {
2025     bWeaponWasWeaponReady = false;
2026     bZoomDown = true;
2027     Wpn.bBobDisabled = true;
2028     BobbingTime = 0;
2029     if (ZoomWeapon()) return;
2030     Wpn.bBobDisabled = oldNoBob;
2031     BobbingTime = oldBobTime;
2032   }
2034   if (bWeaponAllowUser1 && (Buttons&BT_BUTTON_5) && !bButton5Down && Wpn.FindState('User1')) {
2035     bWeaponWasWeaponReady = false;
2036     bButton5Down = true;
2037     Wpn.bBobDisabled = true;
2038     BobbingTime = 0;
2039     if (WeaponUserAction(1)) return;
2040     Wpn.bBobDisabled = oldNoBob;
2041     BobbingTime = oldBobTime;
2042   }
2044   if (bWeaponAllowUser2 && (Buttons&BT_BUTTON_6) && !bButton6Down && Wpn.FindState('User2')) {
2045     bWeaponWasWeaponReady = false;
2046     bButton6Down = true;
2047     Wpn.bBobDisabled = true;
2048     BobbingTime = 0;
2049     if (WeaponUserAction(2)) return;
2050     Wpn.bBobDisabled = oldNoBob;
2051     BobbingTime = oldBobTime;
2052   }
2054   if (bWeaponAllowUser3 && (Buttons&BT_BUTTON_7) && !bButton7Down && Wpn.FindState('User3')) {
2055     bWeaponWasWeaponReady = false;
2056     bButton7Down = true;
2057     Wpn.bBobDisabled = true;
2058     BobbingTime = 0;
2059     if (WeaponUserAction(3)) return;
2060     Wpn.bBobDisabled = oldNoBob;
2061     BobbingTime = oldBobTime;
2062   }
2064   if (bWeaponAllowUser4 && (Buttons&BT_BUTTON_8) && !bButton8Down && Wpn.FindState('User4')) {
2065     bWeaponWasWeaponReady = false;
2066     bButton8Down = true;
2067     Wpn.bBobDisabled = true;
2068     BobbingTime = 0;
2069     if (WeaponUserAction(4)) return;
2070     Wpn.bBobDisabled = oldNoBob;
2071     BobbingTime = oldBobTime;
2072   }
2076 //==========================================================================
2078 //  MovePsprites
2080 //  Called every tic by player thinking routine.
2082 //==========================================================================
2083 void MovePsprites (float deltaTime) {
2084   // moved here, so weapon can bob independently
2085   Weapon wpn = Weapon(ReadyWeapon);
2087   float newBobX = 0.0, newBobY = 0.0;
2089   //printdebug("%C: ReadyWeapon=%C; PendingWeapon=%C", self, ReadyWeapon, PendingWeapon);
2090   SetViewObject(ReadyWeapon);
2091   AdvanceViewStates(deltaTime);
2092   ProcessWeaponActions();
2094   wpn = Weapon(ReadyWeapon);
2096   if (PlayerState == PST_LIVE) {
2097     if (!ReadyWeapon) {
2098       if (wpn) printwarn("PLAYER: ReadyWeapon `%C` died, PendingWeapon is `%C`", wpn, PendingWeapon);
2099       bWeaponAllowSwitch = true;
2100     } else {
2101       if (!ViewStates[PS_WEAPON].State) {
2102         if (wpn) printwarn("PLAYER: ReadyWeapon %C (%C) removed itself, PendingWeapon is %C", ReadyWeapon, wpn, PendingWeapon);
2103         ReadyWeapon = none;
2104         SetViewStateOffsets(0, 0);
2105         bWeaponAllowSwitch = true;
2106       } else if (ViewStates[PS_WEAPON].StateTime < 0) {
2107         // this is totally wrong, because it cannot call `A_WeaponReady()`, and weapon switching is impossible
2108         //printwarn("PLAYER: ReadyWeapon %C (%C) put itself into endless state", ReadyWeapon, wpn, PendingWeapon);
2109         //ReadyWeapon = none;
2110         //SetViewStateOffsets(0, 0);
2111         bWeaponAllowSwitch = true;
2112       }
2113     }
2114   } else {
2115     // disable bobbing
2116     wpn = none;
2117   }
2119   if (MO && wpn && !wpn.bDontBob && !wpn.bBobDisabled) {
2120     if (!wpn.bBobFrozen) BobbingTime = fmod(BobbingTime+deltaTime, 360);
2121     // bob the weapon based on movement speed
2122     float mvbobbob = fmin(GetCvarF('weaponbob'), 1.0);
2123     if (mvbobbob > 0) {
2124       // regular movement bobbing (and don't allow too big bobbing too)
2125       float wbob = fclamp(MO.Velocity.x*MO.Velocity.x+MO.Velocity.y*MO.Velocity.y, -370492.0/2.5, +370492.0/2.5);
2126       //if (wbob) printdebug("wbob=%s", wbob);
2127       /*
2128       if (wbob < 300) {
2129         float dx = ViewStateBobOfsX;
2130         float dy = ViewStateBobOfsY;
2131         float move = 4.0*deltaTime;
2132         float mx = (dx < 0 ? fmin(dx+move, 0.0) : fmax(dx-move, 0.0));
2133         float my = (dy < 0 ? fmin(dy+move, 0.0) : fmax(dy-move, 0.0));
2134         if (dx || dy) printdebug("dx=%s; dy=%s; mx=%s; my=%s", dx, dy, mx, my);
2135         dx = (fabs(dx) <= 0.01 ? 0 : mx);
2136         dy = (fabs(dy) <= 0.01 ? 0 : my);
2137         ViewStateBobOfsX = dx;
2138         ViewStateBobOfsY = dy;
2139       } else
2140       */
2141       {
2142         wbob /= (3.0/mvbobbob)*35.0*35.0;
2143         if (wbob > MAXBOB) wbob = MAXBOB;
2144         float angle = AngleMod360(180.0*BobbingTime/*MO.XLevel.Time*/);
2145         newBobX = /*1.0+*/wbob*cos(angle); //k8: why it did `1.0+` here?
2146         if (angle >= 180.0) angle -= 180.0;
2147         newBobY = wbob*sin(angle);
2148       }
2149     }
2150     /*
2151     float wbob = Bob*fclamp(GetCvarF('weaponbob'), 0.0, 1.0);
2152     float angle = AngleMod360(180.0*MO.XLevel.Time);
2153     ViewStateSX = 1.0+wbob*cos(angle);
2154     if (angle >= 180.0) angle -= 180.0;
2155     ViewStateSY = Weapon::WEAPONTOP+wbob*sin(angle);
2156     */
2157   }
2159   ViewStateBobOfsX = newBobX;
2160   ViewStateBobOfsY = newBobY;
2164 //==========================================================================
2166 //  SetPlayerWeaponState
2168 //  changes player state (animation)
2169 //  if `newstate` is `none`, set idle state if we're in attacking one
2170 //  never use to set idle state!
2172 //==========================================================================
2173 void SetPlayerWeaponState (state newstate) {
2174   // get player out of attack state
2175   EntityEx ee = EntityEx(MO);
2176   if (!ee) return;
2177   if (newstate) {
2178     if (newstate != ee.IdleState) ee.SetState(newstate);
2179   } else {
2180     if (!ee.IdleState) return;
2181     if ((ee.MissileState && StateIsInSequence(ee.State, ee.MissileState)) ||
2182         (ee.MeleeState && StateIsInSequence(ee.State, ee.MeleeState)))
2183     {
2184       ee.SetState(ee.IdleState);
2185     }
2186   }
2190 //==========================================================================
2192 //  ResetPlayerFiringState
2194 //==========================================================================
2195 void ResetPlayerFiringState () {
2196   // get player out of attack state
2197   EntityEx ee = EntityEx(MO);
2198   if (!ee) return;
2199   if (!ee.IdleState) return;
2200   if ((ee.MissileState && StateIsInSequence(ee.State, ee.MissileState)) ||
2201       (ee.MeleeState && StateIsInSequence(ee.State, ee.MeleeState)))
2202   {
2203     //printdebug("%C: setting idle state", self);
2204     ee.SetState(ee.IdleState);
2205   } else {
2206     state st = ee.FindState('Melee', 'Crouch', Exact:true);
2207     if (st && StateIsInSequence(ee.State, st)) { ee.SetState(ee.IdleState); return; }
2208     st = ee.FindState('Missile', 'Crouch', Exact:true);
2209     if (st && StateIsInSequence(ee.State, st)) { ee.SetState(ee.IdleState); return; }
2210   }
2214 //===========================================================================
2216 //  CommonFireWeapon
2218 //  `firetype` is `Weapon::FIRE_Primary` or `Weapon::FIRE_Secondary`
2220 //  returns `true` if weapon state was changed
2222 //===========================================================================
2223 bool CommonFireWeapon (int firetype, state firestate) {
2224   bReloadQueued = false;
2225   if (!firestate) return false; // just in case
2226   assert(firetype == Weapon::FIRE_Primary || firetype == Weapon::FIRE_Secondary);
2227   if (!ReadyWeapon || !ReadyWeapon.CheckAmmo(firetype, AutoSwitch:true)) return false;
2228   ReadyWeapon.FireMode = firetype;
2229   EntityEx ee = EntityEx(MO);
2230   if (ee) {
2231     state st;
2232     if (ee.crouchfactor < 1) {
2233       // try crouch states
2234       if (ReadyWeapon.bBotMelee) st = ee.FindState('Melee', 'Crouch', Exact:true);
2235       if (!st) st = ee.FindState('Missile', 'Crouch', Exact:true);
2236     }
2237     if (!st) {
2238       if (ReadyWeapon.bBotMelee) st = ee.MeleeState;
2239       if (!st) st = ee.MissileState;
2240     }
2241     if (st && !StateIsInSequence(ee.State, st)) {
2242       //printdebug("%C: weapon=%C; ee=%C; setting attack state %s", self, ReadyWeapon, ee, st);
2243       ee.SetState(st);
2244     }
2245   }
2246   SetViewObject(ReadyWeapon);
2247   SetViewState(PS_WEAPON, firestate);
2248   if (ReadyWeapon && !ReadyWeapon.bNoAlert) {
2249     LineSpecialLevelInfo(Level).NoiseAlert(EntityEx(MO), EntityEx(MO));
2250   }
2251   return true;
2255 //===========================================================================
2257 //  FireWeapon
2259 //===========================================================================
2260 bool FireWeapon (optional state firestate) {
2261   if (!ReadyWeapon) return false;
2262   if (!firestate) firestate = ReadyWeapon.GetAttackState(Refire);
2263   return CommonFireWeapon(Weapon::FIRE_Primary, firestate);
2267 //===========================================================================
2269 //  AltFireWeapon
2271 //===========================================================================
2272 bool AltFireWeapon (optional state firestate) {
2273   if (!ReadyWeapon) return false;
2274   if (!firestate) firestate = ReadyWeapon.GetAltAttackState(Refire);
2275   return CommonFireWeapon((bSecondarySameAmmo ? Weapon::FIRE_Primary : Weapon::FIRE_Secondary), firestate);
2279 //===========================================================================
2281 //  ReloadWeapon
2283 //  returns `true` if weapon state was changed
2285 //===========================================================================
2286 bool ReloadWeapon () {
2287   //k8: reloading weapon resets refire state
2288   ResetWeaponReloadRefire();
2289   if (ReadyWeapon) {
2290     state rst = ReadyWeapon.GetReloadState();
2291     if (!rst) return false;
2292     //printdebug("reloading %C: state is %s", ReadyWeapon, rst);
2293     // set player pawn to reloading state, if there is any, or into idle one
2294     SetPlayerWeaponState(MO.FindState('Reload'));
2295     SetViewObject(ReadyWeapon);
2296     SetViewState(PS_WEAPON, rst);
2297     //printdebug("reloading %C: SET state is %s", ReadyWeapon, rst);
2298     return true;
2299   }
2300   return false;
2304 //===========================================================================
2306 //  ZoomWeapon
2308 //  returns `true` if weapon state was changed
2310 //===========================================================================
2311 bool ZoomWeapon () {
2312   //k8: zooming weapon resets refire state
2313   ResetWeaponReloadRefire();
2314   if (ReadyWeapon) {
2315     state zst = ReadyWeapon.GetZoomState();
2316     if (!zst) return false;
2317     SetPlayerWeaponState(MO.FindState('Zoom'));
2318     SetViewObject(ReadyWeapon);
2319     SetViewState(PS_WEAPON, zst);
2320     return true;
2321   }
2322   return false;
2326 //===========================================================================
2328 //  WeaponUserAction
2330 //  returns `true` if weapon state was changed
2332 //===========================================================================
2333 bool WeaponUserAction (int actnum) {
2334   if (actnum < 1 || actnum > 4) return false;
2335   ResetWeaponReloadRefire();
2336   if (ReadyWeapon) {
2337     name stname;
2338     switch (actnum) {
2339       case 1: stname = 'User1'; break;
2340       case 2: stname = 'User2'; break;
2341       case 3: stname = 'User3'; break;
2342       case 4: stname = 'User4'; break;
2343     }
2344     state zst = ReadyWeapon.FindState(stname);
2345     if (!zst) return false;
2346     SetPlayerWeaponState(MO.FindState(stname));
2347     SetViewObject(ReadyWeapon);
2348     SetViewState(PS_WEAPON, zst);
2349     return true;
2350   }
2351   return false;
2355 //==========================================================================
2357 //  SetPendingWeapon
2359 //==========================================================================
2360 void SetPendingWeapon (Weapon NewWpn) {
2361   if (!NewWpn) return;
2362   if (MorphTime) return;
2363   if (NewWpn == ReadyWeapon) return;
2364   // do not select morph weapon, do not change morph weapon
2365   if (NewWpn.bGivenAsMorphWeapon || (ReadyWeapon && ReadyWeapon.bGivenAsMorphWeapon)) return;
2366   bReloadQueued = false;
2367   bReloadDown = false;
2368   PendingWeapon = NewWpn;
2372 //==========================================================================
2374 //  ChangeWeapon
2376 //  The actual changing of the weapon is done when the weapon psprite can
2377 //  do it (read: not in the middle of an attack).
2379 //==========================================================================
2380 void ChangeWeapon (int slot) {
2381   SetPendingWeapon(GetSlotChangeWeapon(slot));
2385 //==========================================================================
2387 //  PrevWeapon
2389 //==========================================================================
2390 void PrevWeapon () {
2391   SetPendingWeapon(GetPrevWeapon(PendingWeapon ? PendingWeapon : ReadyWeapon));
2395 //==========================================================================
2397 //  NextWeapon
2399 //==========================================================================
2400 void NextWeapon () {
2401   SetPendingWeapon(GetNextWeapon(PendingWeapon ? PendingWeapon : ReadyWeapon));
2405 //==========================================================================
2407 //  BestWeapon
2409 //  Returns best weapon to use
2411 //==========================================================================
2412 Weapon BestWeapon (optional class!Ammo AmmoType) {
2413   if (!MO) return none;
2414   bool Powered = !!EntityEx(MO).FindInventory(PowerWeaponLevel2);
2415   Weapon Best = none;
2416   for (Inventory Item = EntityEx(MO).Inventory; Item; Item = Item.Inventory) {
2417     // must be a weapon
2418     Weapon Wpn = Weapon(Item);
2419     if (!Wpn) continue;
2420     // never ever select rocket lanucher
2421     if (Wpn.bBotProjectile) continue;
2422     // no selection order -- skip it
2423     if (Wpn.SelectionOrder < 1) continue;
2424     // check if best one is better that this one
2425     if (Best && Wpn.SelectionOrder > Best.SelectionOrder) continue;
2426     // possibly limit to specific ammo type
2427     if (AmmoType && Wpn.AmmoType1 != AmmoType) continue;
2428     // check if it's for the current tome of power state
2429     if (Powered && Wpn.SisterWeapon && Wpn.SisterWeapon.bPoweredUp) continue;
2430     if (!Powered && Wpn.bPoweredUp) continue;
2431     // make sure it has enough ammo
2432     if (!Wpn.CheckAmmo(Weapon::FIRE_Either, AutoSwitch:false)) continue;
2433     // good one
2434     Best = Wpn;
2435   }
2436   return Best;
2440 //==========================================================================
2442 //  ChoosePowered
2444 //==========================================================================
2445 Weapon ChoosePowered (Weapon Wpn) {
2446   if (!Wpn) return none;
2447   bool Powered = !!EntityEx(MO).FindInventory(PowerWeaponLevel2);
2448   if (Powered && Wpn.SisterWeapon && Wpn.SisterWeapon.bPoweredUp) {
2449     return Wpn.SisterWeapon;
2450   }
2451   return Wpn;
2455 //==========================================================================
2457 //  GetSlotChangeWeapon
2459 //==========================================================================
2460 Weapon GetSlotChangeWeapon (int slot) {
2461   auto pawn = PlayerPawn(MO);
2462   if (!pawn) return ReadyWeapon;
2464   if (slot < 1 || slot > pawn.GetNumberOfSlots()) return ReadyWeapon;
2465   --slot;
2467   // note that slot selection goes from the last weapon to the first weapon
2468   int slotsize = pawn.GetSlotSize(slot);
2469   if (!slotsize) return ReadyWeapon; // this slot is empty, nothing to do
2471   // find ready weapon position in slot (if any)
2472   int cwidx = slotsize;
2473   if (ReadyWeapon) {
2474     class!Weapon readyWpnClass = class!Weapon(ReadyWeapon.Class);
2475     if (readyWpnClass) {
2476       foreach (auto i; 0..slotsize; reverse) {
2477         class!Weapon slotwpn = pawn.GetWeaponInSlot(slot, i);
2478         if (!slotwpn) continue;
2479         if (slotwpn == readyWpnClass ||
2480             (ReadyWeapon.bPoweredUp && ReadyWeapon.SisterWeapon &&
2481              slotwpn == ReadyWeapon.SisterWeapon.Class))
2482         {
2483           // i found her!
2484           cwidx = i;
2485           break;
2486         }
2487       }
2488     }
2489   }
2491   // now cycle through the slot
2492   foreach (auto i; 0..slotsize) {
2493     // step back
2494     if ((--cwidx) < 0) cwidx = slotsize-1;
2495     // get weapon in slot position
2496     class!Weapon slotwpn = pawn.GetWeaponInSlot(slot, cwidx);
2497     if (!slotwpn) continue;
2498     // check if we have it in our inventory
2499     Weapon Wpn = Weapon(EntityEx(MO).FindInventory(slotwpn, disableReplacement:true));
2500     // check if it has ammo
2501     if (Wpn && Wpn.CheckAmmo(Weapon::FIRE_Either, AutoSwitch:false)) return ChoosePowered(Wpn);
2502   }
2504   return ReadyWeapon;
2508 //==========================================================================
2510 //  CycleWeaponWithDir
2512 //==========================================================================
2513 Weapon CycleWeaponWithDir (Weapon Current, bool forward) {
2514   auto pawn = PlayerPawn(MO);
2515   if (!pawn) return Current;
2516   if (!pawn.GetNumberOfSlots()) return Current; // just in case
2518   // find current weapon slot and index
2519   int currSlot, currIndex;
2520   if (!Current || !pawn.FindWeaponSlot(class!Weapon(Current.Class), out currSlot, out currIndex/*, debug:true*/)) {
2521     currSlot = 0;
2522     currIndex = (forward ? pawn.GetSlotSize(currSlot) : -1);
2523   }
2525   //printdebug("st(fwd=%B): s=%s; i=%s; current=%C", forward, currSlot, currIndex, Current);
2526   //if (Current) printdebug("  CW=%C; Wpn.SelectionOrder=%s", Current, Current.SelectionOrder);
2527   // now cycle
2528   int smax = pawn.GetNumberOfSlots();
2529   bool allowExtraSlot = (currSlot >= PlayerPawn::NUM_WEAPON_SLOTS || (smax > PlayerPawn::NUM_WEAPON_SLOTS && GetCvarB('wp_cycle_special_slot')));
2530   if (!allowExtraSlot) smax = PlayerPawn::NUM_WEAPON_SLOTS;
2531   foreach (auto sidx; 0..smax) {
2532     foreach (auto widx; 0..pawn.GetSlotSize(sidx)) {
2533       // move to the next weapon in slot
2534       if (forward) pawn.IncrementWeaponIndex(ref currSlot, ref currIndex, allowExtraSlot); else pawn.DecrementWeaponIndex(ref currSlot, ref currIndex, allowExtraSlot);
2535       class!Weapon swc = pawn.GetWeaponInSlot(currSlot, currIndex);
2536       //printdebug("  000: st(fwd=%B): s=%s; i=%s; sidx=%s; widx=%s; wpn=%C", forward, currSlot, currIndex, sidx, widx, swc);
2537       if (!swc) continue;
2538       if (Current && Current.Class == swc) continue;
2539       Weapon Wpn = Weapon(EntityEx(MO).FindInventory(swc, disableReplacement:true));
2540       if (!Wpn) continue;
2541       //printdebug("  001: st(fwd=%B): s=%s; i=%s; sidx=%s; widx=%s; wpn=%C", forward, currSlot, currIndex, sidx, widx, Wpn);
2542       if (Wpn.CheckAmmo(Weapon::FIRE_Either, AutoSwitch:false)) {
2543         //printdebug("  002: st(fwd=%B): s=%s; i=%s; sidx=%s; widx=%s; wpn=%C (%C)", forward, currSlot, currIndex, sidx, widx, Wpn, ChoosePowered(Wpn));
2544         return ChoosePowered(Wpn);
2545       }
2546     }
2547   }
2549   return Current;
2553 //==========================================================================
2555 //  GetPrevWeapon
2557 //==========================================================================
2558 Weapon GetPrevWeapon (Weapon Current) {
2559   return CycleWeaponWithDir(Current, forward:false);
2563 //==========================================================================
2565 //  GetNextWeapon
2567 //==========================================================================
2568 Weapon GetNextWeapon (Weapon Current) {
2569   return CycleWeaponWithDir(Current, forward:true);
2573 //==========================================================================
2575 //  UsePuzzleItem
2577 //  USING A PUZZLE ITEM
2579 //  Returns true if the puzzle item was used on a line or a thing.
2581 //==========================================================================
2582 bool UsePuzzleItem (int PuzzleItemType) {
2583   float ur, utr;
2584   GetUseRanges(out ur, out utr);
2586   TVec PuzzleUseDir;
2587   AngleVector(MO.Angles, out PuzzleUseDir);
2589   TVec start = MO.Origin;
2590   TVec end = start+ur*PuzzleUseDir.xy;
2591   /*
2592   float x1 = MO.Origin.x;
2593   float y1 = MO.Origin.y;
2594   float x2 = x1+/+DEFAULT_USERANGE+/ur*PuzzleUseDir.x;
2595   float y2 = y1+/+DEFAULT_USERANGE+/ur*PuzzleUseDir.y;
2596   */
2598   EntityEx mobj;
2599   //TVec hitPoint;
2600   //opening_t *open;
2601   line_t *ld;
2602   intercept_t in;
2604   foreach MO.PathTraverse(out in, start, end, PT_ADDLINES|PT_ADDTHINGS/*|PT_NOOPENS*//*|PT_AIMTHINGS*/, SPF_NOBLOCKING, ML_BLOCKEVERYTHING|ML_BLOCKUSE) {
2605     if (in.bIsAPlane) break; // plane hit, no usable lines/things found
2607     if (in.bIsALine) {
2608       // check line
2609       ld = in.line;
2611       // check 3d pobj line
2612       if (ld->pobject && ld->pobject->posector) {
2613         polyobj_t *po = ld->pobject;
2614         // 3d pobj, check height
2615         if (MO.Origin.z+fmax(0.0, MO.Height) <= po->pofloor.minz || MO.Origin.z >= po->poceiling.maxz) continue;
2616       }
2618       if (ld->special != LNSPEC_UsePuzzleItem) {
2619         if (in.bIsABlockingLine) break; // line hit, no actor found
2620         /*
2621         if (ld->flags&(ML_BLOCKEVERYTHING|ML_BLOCKUSE)) {
2622           // gozzo does this
2623           open = nullptr;
2624         } else {
2625           hitPoint = MO.Origin+(/+DEFAULT_USERANGE+/ur*in.frac)*PuzzleUseDir;
2626           open = MO.XLevel.LineOpenings(ld, hitPoint);
2627         }
2628         if (!open || open->range <= 0.0) {
2629           if (MO.bIsPlayer) MO.PlaySound('*puzzfail', CHAN_VOICE);
2630           break; // can't use through a wall
2631         }
2632         */
2633         continue; // continue searching
2634       }
2635       if (PointOnPlaneSide(start, *ld) == 1) {
2636         // don't use back sides
2637         break;
2638       }
2639       if (PuzzleItemType != ld->arg1) {
2640         // item type doesn't match
2641         break;
2642       }
2643       MO.XLevel.StartACS(ld->arg2, 0, ld->arg3, ld->arg4, ld->arg5, MO, ld, 0, false, false);
2644       ld->special = 0;
2645       return true; // stop searching
2646     }
2648     // check thing
2649     mobj = EntityEx(in.Thing);
2650     if (mobj.Special != LNSPEC_UsePuzzleItem) {
2651       // wrong special
2652       continue;
2653     }
2654     if (PuzzleItemType != mobj.Args[0]) {
2655       // item type doesn't match
2656       continue;
2657     }
2658     MO.XLevel.StartACS(mobj.Args[1], 0, mobj.Args[2], mobj.Args[3], mobj.Args[4], MO, nullptr, 0, false, false);
2659     mobj.Special = 0;
2660     return true; // stop searching
2661   }
2663   return false;
2667 //==========================================================================
2669 //  AddRevealedMap
2671 //==========================================================================
2672 bool AddRevealedMap () {
2673   bAutomapRevealed = true;
2674   foreach (auto i; 0..RevealedMaps.length) {
2675     if (RevealedMaps[i] == Level.XLevel.MapName) return false; // already revealed
2676   }
2677   RevealedMaps.length = RevealedMaps.length+1;
2678   RevealedMaps[RevealedMaps.length-1] = Level.XLevel.MapName;
2679   return true;
2683 //==========================================================================
2685 //  RemoveRevealedMap
2687 //==========================================================================
2688 void RemoveRevealedMap () {
2689   bAutomapRevealed = false;
2690   foreach (auto i; 0..RevealedMaps.length) {
2691     if (RevealedMaps[i] == Level.XLevel.MapName) {
2692       RevealedMaps.Remove(i);
2693       return;
2694     }
2695   }
2699 //==========================================================================
2701 //  UpdateRevealedMap
2703 //==========================================================================
2704 void UpdateRevealedMap () {
2705   bAutomapRevealed = false;
2706   foreach (auto i; 0..RevealedMaps.length) {
2707     if (RevealedMaps[i] == Level.XLevel.MapName) {
2708       bAutomapRevealed = true;
2709       return;
2710     }
2711   }
2715 //==========================================================================
2717 //  ParticleEffect
2719 //==========================================================================
2720 void ParticleEffect (int count, int type1, int type2, TVec origin, float ornd,
2721                      TVec velocity, float vrnd, float acceleration, float grav,
2722                      int clr, float duration, float ramp)
2724   Level.ParticleEffect(count, type1, type2, origin, ornd, velocity, vrnd, acceleration,
2725         grav, clr, duration, ramp);
2729 //==========================================================================
2731 //  DecalEffect
2733 //==========================================================================
2734 void DecalEffect (TVec org, name dectype, int side, int lineidx, optional int translation, optional int shadeclr,
2735                   optional float alpha, optional name animator, optional bool permanent,
2736                   optional float angle, optional bool forceFlipX)
2738   if (!dectype || lineidx < 0 || !Level.XLevel) return; // just in case
2739   if (lineidx >= Level.XLevel.Lines.length) return;
2740   Level.XLevel.AddDecal(org, dectype, side, cast([unsafe])(&Level.XLevel.Lines[lineidx]),
2741     translation:translation!optional, shadeclr:shadeclr!optional, alpha:alpha!optional,
2742     animator:animator!optional, permanent:permanent!optional, angle:angle!optional, forceFlipX:forceFlipX!optional);
2746 //==========================================================================
2748 //  FlatDecalEffect
2750 //==========================================================================
2751 void FlatDecalEffect (TVec org, name dectype, float range, optional int translation, optional int shadeclr,
2752                       optional float alpha, optional name animator, optional float angle, optional bool forceFlipX)
2754   if (!dectype || !Level.XLevel) return; // just in case
2755   Level.XLevel.AddFlatDecal(org, dectype, range,
2756     translation:translation!optional, shadeclr:shadeclr!optional, alpha:alpha!optional,
2757     animator:animator!optional, angle:angle!optional, forceFlipX:forceFlipX!optional);
2761 //==========================================================================
2763 //  ClientExplosion
2765 //==========================================================================
2766 void ClientExplosion (int clr, float rad, TVec org) {
2767   if (GetCvarI('r_sprlight_mode') > 0) return;
2768   dlight_t *dl = Level.AllocDlight(none, org, rad+150.0);
2769   if (dl) {
2770     //dl->origin = org;
2771     //dl->radius = rad+150.0;
2772     dl->color = clr;
2773     dl->die = Level.XLevel.Time+0.5;
2774     dl->decay = 300.0;
2775   }
2779 //==========================================================================
2781 //  ClientParticleExplosion
2783 //==========================================================================
2784 void ClientParticleExplosion (int clr, float rad, TVec org) {
2785   TVec porg;
2786   foreach (auto i; 0..512/*1024*/) {
2787     porg.x = org.x+((Random()*32.0)-16.0);
2788     porg.y = org.y+((Random()*32.0)-16.0);
2789     porg.z = org.z+((Random()*32.0)-16.0);
2790     particle_t *p = Level.NewParticle(porg);
2791     if (!p) break;
2792     p->die = Level.XLevel.Time+5.0;
2793     p->color = LineSpecialGameInfo.default.ramp1[0];
2794     p->Size = 1.0;
2795     p->ramp = Random()*4.0;
2796     if (i&1) {
2797       p->type = LineSpecialLevelInfo::pt_explode;
2798     } else {
2799       p->type = LineSpecialLevelInfo::pt_explode2;
2800     }
2801     p->vel.x = (Random()*512.0)-256.0;
2802     p->vel.y = (Random()*512.0)-256.0;
2803     p->vel.z = (Random()*512.0)-256.0;
2804     //p->accel = (Random()*512.0)-256.0;
2805     p->gravity = 40.0+(Random()*512.0)-256.0;
2806   }
2808   if (GetCvarI('r_sprlight_mode') <= 0) {
2809     dlight_t *dl = Level.AllocDlight(none, org, rad+150.0);
2810     if (dl) {
2811       //dl->origin = org;
2812       //dl->radius = rad + 150.0;
2813       dl->color = clr;
2814       dl->die = Level.XLevel.Time+0.5;
2815       dl->decay = 300.0;
2816     }
2817   }
2821 //==========================================================================
2823 //  ClientSparkParticles
2825 //==========================================================================
2826 void ClientSparkParticles (int Count, TVec Org, float Angle) {
2827   TVec porg;
2828   foreach (auto i; 0..Count) {
2829     float an = Angle+Random()*45.0;
2830     float s, c;
2831     sincos(an, out s, out c);
2832     porg.x = Org.x+(Random()*15.0)*c;
2833     porg.y = Org.y+(Random()*15.0)*s;
2834     porg.z = Org.z-Random()*4.0;
2836     particle_t *p = Level.NewParticle(porg);
2837     if (!p) break;
2839     p->type = LineSpecialLevelInfo::pt_spark;
2840     p->Size = 0.5;
2841     p->color = Random() < 0.5 ? RGBA(255, 120, 0, 255) : RGBA(255, 170, 0, 255);
2842     p->die = Level.XLevel.Time+10.0/35.0;
2844     p->vel.x = (Random()-0.5)*2.0;
2845     p->vel.y = (Random()-0.5)*2.0;
2846     p->vel.z = (Random()-0.5)*2.0-Random ()*70.0;
2848     p->accel.x = (Random()-0.5)*16.0+(Random ()-0.5)*35.0;
2849     p->accel.y = (Random()-0.5)*16.0+(Random ()-0.5)*35.0;
2850     p->accel.z = (Random()-0.5)*16.0-140.0;
2851   }
2855 //==========================================================================
2857 //  AddBlend
2859 //==========================================================================
2860 void AddBlend (ref float r, ref float g, ref float b, ref float a, int Col) {
2861   if (!(Col&0xff000000)) return; // no alpha
2862   float r1 = float((Col>>16)&0xff)/255.0;
2863   float g1 = float((Col>>8)&0xff)/255.0;
2864   float b1 = float(Col&0xff)/255.0;
2865   float a1 = float((Col>>24)&0xff)/255.0;
2866   float TmpA = fclamp(1.0-(1.0-a)*(1.0-a1), 0.0, 1.0);
2867   if (!TmpA) return;
2868   r = (r*a*(1.0-a1)+r1*a1)/TmpA;
2869   g = (g*a*(1.0-a1)+g1*a1)/TmpA;
2870   b = (b*a*(1.0-a1)+b1*a1)/TmpA;
2871   a = TmpA;
2875 //==========================================================================
2877 //  AddBlendWithAlpha
2879 //  overrides blend color alpha
2881 //==========================================================================
2882 void AddBlendWithAlpha (ref float r, ref float g, ref float b, ref float a, int Col, float alpha) {
2883   if (alpha <= 0) return; // no alpha
2884   float r1 = float((Col>>16)&0xff)/255.0;
2885   float g1 = float((Col>>8)&0xff)/255.0;
2886   float b1 = float(Col&0xff)/255.0;
2887   float TmpA = fclamp(1.0-(1.0-a)*(1.0-alpha), 0.0, 1.0);
2888   if (!TmpA) return;
2889   r = (r*a*(1.0-alpha)+r1*alpha)/TmpA;
2890   g = (g*a*(1.0-alpha)+g1*alpha)/TmpA;
2891   b = (b*a*(1.0-alpha)+b1*alpha)/TmpA;
2892   a = TmpA;
2896 //==========================================================================
2898 //  PaletteFlash
2900 //  Sets the new palette color shift based upon the current values of
2901 //  Player.DamageFlash and Player.BonusFlash, contents and other inventory
2902 //  items.
2904 //==========================================================================
2905 void PaletteFlash () {
2906   float r = 0.0;
2907   float g = 0.0;
2908   float b = 0.0;
2909   float a = 0.0;
2911   // done in main renderer, sorry
2912   #if 0
2913   if (MO.WaterLevel == 3) {
2914     switch (MO.WaterType) {
2915       case CONTENTS_WATER: AddBlend(r, g, b, a, RGBA(130, 80, 50, 128)); break;
2916       case CONTENTS_LAVA: AddBlend(r, g, b, a, RGBA(255, 80, 0, 150)); break;
2917       case CONTENTS_NUKAGE: AddBlend(r, g, b, a, RGBA(50, 255, 50, 150)); break;
2918       case CONTENTS_SLIME: AddBlend(r, g, b, a, RGBA(0, 25, 5, 150)); break;
2919       case CONTENTS_HELLSLIME: AddBlend(r, g, b, a, RGBA(255, 80, 0, 150)); break;
2920       case CONTENTS_BLOOD: AddBlend(r, g, b, a, RGBA(160, 16, 16, 150)); break;
2921       case CONTENTS_SLUDGE: AddBlend(r, g, b, a, RGBA(128, 160, 128, 150)); break;
2922       case CONTENTS_HAZARD: AddBlend(r, g, b, a, RGBA(128, 160, 128, 128)); break;
2923     }
2924   }
2926   DamageFlashBlend = 0;
2927   if (nameicmp(EntityEx(MO).DamageType, 'Ice') == 0) {
2928     // Frozen player
2929     AddBlend(r, g, b, a, RGBA(2, 2, 255, 113));
2930   } else
2931   #endif
2932   {
2933     if (DamageFlash < 0) DamageFlash = 0;
2934     if (DamageFlash) {
2935       auto pawn = PlayerPawn(MO);
2936       int dmgColor = pawn.DamageScreenColor;
2937       float dmgIntensity = 1.0;
2939       name dmgType = DamageFlashType;
2940       if (dmgType && !nameEquCI(dmgType, 'None')) {
2941         foreach (auto ref dmg; pawn.DamageColors) {
2942           if (nameEquCI(dmgType, dmg.Type)) {
2943             //printdebug("%C: found damage record! Type=%s", self, dmg.Type);
2944             dmgColor = dmg.Color;
2945             dmgIntensity = fclamp(dmg.Intensity, 0.0, 1.0)*2.0;
2946             break;
2947           }
2948           //printdebug("%C: skipped damage record with type=%s, int=%s, clr=0x%08x (%s)", self, dmg.Type, dmg.Intensity, dmg.Color, dmgType);
2949         }
2950       }
2951       /*
2952       else {
2953         printdebug("%C: damage without a type", self);
2954       }
2955       */
2957       float maxent = fmin(GetCvarF('k8DamageFlashMaxIntensity'), 1.0)*255.0;
2958       if (maxent < 0.0) maxent = 255.0; // cheaters must be punished! ;-)
2959       int Amount = int(fclamp(114.0*DamageFlash*dmgIntensity, 0, maxent));
2960       //printdebug("%C: maxent=%s; DamageFlash=%s; dmgIntensity=%s; amount=%s (%s)", self, maxent, DamageFlash, dmgIntensity, int(114.0*DamageFlash*dmgIntensity), Amount);
2961       //if (Amount >= 228) Amount = 228;
2962       if (Amount > 0) AddBlend(r, g, b, a, (Amount<<24)|(dmgColor&0x00ffffff));
2963       DamageFlashBlend = Amount;
2964     }
2966     if (BonusFlash) {
2967       float maxBonusFlashTime = GetCvarF('k8BonusFlashMaxTime');
2968       if (maxBonusFlashTime >= 0) BonusFlash = fmin(BonusFlash, maxBonusFlashTime);
2969       if (BonusFlash > 0) {
2970         int Amount = int(256.0*BonusFlash);
2971         if (Amount >= 128) Amount = 128;
2972         AddBlend(r, g, b, a, RGBA(215, 186, 68, Amount));
2973       } else {
2974         BonusFlash = 0;
2975       }
2976     }
2978     if (PoisonCount) {
2979       int Amount = PoisonCount*160/32;
2980       if (Amount >= 160) Amount = 160;
2981       AddBlend(r, g, b, a, RGBA(56, 118, 46, Amount));
2982     }
2984     //FIXME add hazard flash.
2985     if (HazardTime > 16.0 || (int(4.0*HazardTime)&1)) {
2986       AddBlend(r, g, b, a, RGBA(0, 255, 0, 32));
2987     }
2988   }
2990   // Health Accumulation Device effect, and palette flash
2991   if (MO && MO.XLevel) {
2992     // item effects
2993     for (Inventory Item = EntityEx(MO).Inventory; Item; Item = Item.Inventory) {
2994       int Blend = Item.GetBlend();
2995       if (Blend) AddBlend(r, g, b, a, Blend);
2996     }
2998     float lvtime = MO.XLevel.Time;
2999     //print("lvtime=%f; k8HealthAccum_LastBoostTime=%f; k8HealthAccum_LastRegenTime=%f", lvtime, k8HealthAccum_LastBoostTime, k8HealthAccum_LastRegenTime);
3000     bool doRegen = true;
3001     if (k8HealthAccum_LastBoostTime > 0) {
3002       float diff = lvtime-k8HealthAccum_LastBoostTime;
3003       if (diff > 0 && diff < 1) {
3004         AddBlendWithAlpha(r, g, b, a, RGBA(255, 127, 0, 0), 0.5-diff/2.0);
3005         doRegen = false;
3006       }
3007     }
3008     if (doRegen && k8HealthAccum_LastRegenTime > 0) {
3009       float diff = lvtime-k8HealthAccum_LastRegenTime;
3010       if (diff > 0 && diff < 0.1) {
3011         AddBlendWithAlpha(r, g, b, a, RGBA(0, 128, 0, 0), 0.1-diff*1);
3012       }
3013     }
3014   }
3016   if (BlendA) {
3017     AddBlend(r, g, b, a, RGBA(int(BlendR*255.0), int(BlendG*255.0), int(BlendB*255.0), int(BlendA*255.0)));
3018   }
3020   r = fclamp(r, 0.0, 1.0);
3021   g = fclamp(g, 0.0, 1.0);
3022   b = fclamp(b, 0.0, 1.0);
3023   CShift = RGBA(int(r*255.0), int(g*255.0), int(b*255.0), int(a*255.0));
3027 //==========================================================================
3029 //  PreTravel
3031 //==========================================================================
3032 override void PreTravel () {
3033   ::PreTravel();
3034   ClearSubSeenInfo();
3035   if (MO) {
3036     // remove all powerups that cannot survive map teleports
3037     /* nope, this is done in `PlayerExitMap()`
3038     auto inv = EntityEx(MO).Inventory;
3039     while (inv) {
3040       Powerup pw = Powerup(inv);
3041       inv = inv.Inventory;
3042       if (pw && !pw.bSurvivesMapTeleport) {
3043         printdebug("*** removed powerup '%C'", pw);
3044         pw.EndEffect();
3045         delete pw;
3046       }
3047     }
3048     */
3049     //WARNING! don't set `Owner` to none here, because `SV_MapTeleport()` checks it!
3050     SavedInventory = EntityEx(MO).Inventory;
3051     EntityEx(MO).Inventory = none;
3052     /*
3053     printdebug("%C: PreTravel inventory:", self);
3054     for (auto inv = SavedInventory; inv; inv = inv.Inventory) {
3055       printdebug("  %C: owner=%C(%s); MO=%C(%s)", inv, inv.Owner, (inv.Owner ? inv.Owner.UniqueId : 0), MO, MO.UniqueId);
3056       //inv.Owner = none; // crash if somebody will try to remove something
3057     }
3058     */
3059   } else {
3060     //printdebug("%C: PreTravel inventory: CLEARED!", self);
3061     SavedInventory = none; // just in case
3062   }
3063   ResetACSButtons();
3064   ResetBootPrints();
3068 //==========================================================================
3070 //  UseInventory
3072 //==========================================================================
3073 override void UseInventory (string Inv) {
3074   if (!Inv) return;
3076   if (bTotallyFrozen || (Level.bFrozen && !(Cheats&CF_TIMEFREEZE))) {
3077     // you can't use items if you're totally frozen
3078     return;
3079   }
3081   class!Inventory invCls = class!Inventory(FindClassNoCaseStr(Inv));
3082   if (!invCls) return;
3084   Inventory item = EntityEx(MO).FindInventory(invCls, disableReplacement:true);
3085   if (!item) item = EntityEx(MO).FindInventory(class!Inventory(GetClassReplacement(invCls)), disableReplacement:true);
3087   if (item) {
3088     // use Inventory item
3089     EntityEx(MO).UseInventory(item);
3090   }
3094 //==========================================================================
3096 //  CheckDoubleFiringSpeed
3098 //==========================================================================
3099 override bool CheckDoubleFiringSpeed () {
3100   return !!(Cheats&CF_DOUBLEFIRINGSPEED);
3104 //==========================================================================
3106 //  ClientSpeech
3108 //==========================================================================
3109 void ClientSpeech (EntityEx Speaker, int SpeechNum) {
3110   LineSpecialClientGame(ClGame).StartSpeech(Speaker, SpeechNum);
3114 //==========================================================================
3116 //  ClientSlideshow1
3118 //==========================================================================
3119 void ClientSlideshow1 () {
3120   LineSpecialClientGame(ClGame).StartConSlideshow1();
3124 //==========================================================================
3126 //  ClientSlideshow2
3128 //==========================================================================
3129 void ClientSlideshow2 () {
3130   LineSpecialClientGame(ClGame).StartConSlideshow2();
3134 //==========================================================================
3136 //  ClientFinaleType
3138 //==========================================================================
3139 void ClientFinaleType (int Type) {
3140   LineSpecialClientGame(ClGame).SetFinaleType(Type);
3144 //==========================================================================
3146 //  SetObjectives
3148 //==========================================================================
3149 void SetObjectives (int NewObjectives) {
3150   if (!NewObjectives) return;
3151   // check if log text lump exists in wad file
3152   if (!WadLumpPresent(name(va("log%d", NewObjectives)))) return;
3153   Objectives = NewObjectives;
3157 //==========================================================================
3159 //  StrReplaceSubstitutes
3161 //  replaces '%g', '%h', '%p', '%o', '%k'
3163 //==========================================================================
3164 string StrReplaceSubstitutes (string str, EntityEx source) {
3165   auto ppos = str.strIndexOf("%");
3166   if (ppos < 0) return str;
3167   // we have something to process...
3168   //FIXME: replace with correct gender when we'll implement player genders
3169   bool isFemale = (MO ? (string(MO.SoundGender).strIndexOf("female", caseSensitive:false) >= 0) : false);
3170   //print("<>::: isFemale=%B (%s : %d)", isFemale, (MO ? MO.SoundGender : '???'), (string(MO.SoundGender).strIndexOf("female", caseSensitive:false)));
3171   string res;
3172   while (ppos >= 0 && ppos+1 < str.length) {
3173     res ~= str[0..ppos];
3174     auto nch = str[ppos+1];
3175     bool validMod = true;
3176     switch (nch) {
3177       // double percent, insert one
3178       case '%': res ~= "%"; break;
3179       case 'o': case 'O': res ~= PlayerName; break;
3180       case 'g': res ~= (isFemale ? "she" : "he"); break;
3181       case 'h': res ~= (isFemale ? "her" : "him"); break;
3182       case 'p': res ~= (isFemale ? "hers" : "his"); break;
3183       case 'G': res ~= (isFemale ? "She" : "He"); break;
3184       case 'H': res ~= (isFemale ? "Her" : "Him"); break;
3185       case 'P': res ~= (isFemale ? "Hers" : "His"); break;
3186       case 'k': case 'K':
3187         if (source && source.bIsPlayer && source.Player) {
3188           res ~= source.Player.PlayerName;
3189         } else if (Actor(source) && Actor(source).StrifeName) {
3190           res ~= Actor(source).StrifeName;
3191         } else if (source) {
3192           res ~= string(GetClassName(source.Class));
3193         } else {
3194           res ~= "unknown";
3195         }
3196         break;
3197       default:
3198         validMod = false;
3199     }
3200     if (validMod) {
3201       str = str[ppos+2..$];
3202     } else {
3203       res ~= "%";
3204       str = str[ppos+1..$];
3205     }
3206     ppos = str.strIndexOf("%");
3207   }
3208   if (str) res ~= str;
3209   return res;
3213 //==========================================================================
3215 //  DisplayObituary
3217 //==========================================================================
3218 void DisplayObituary (EntityEx inflictor, EntityEx source, name DmgType) {
3219   string Msg;
3221        if (nameicmp(DmgType, 'Suicide') == 0) Msg = "$ob_suicide"; // commited a suicide
3222   else if (nameicmp(DmgType, 'Falling') == 0) Msg = "$ob_falling"; // fell down
3223   else if (nameicmp(DmgType, 'Crush') == 0) Msg = "$ob_crush"; // crushed by the environment
3224   else if (nameicmp(DmgType, 'Exit') == 0) Msg = "$ob_exit"; // tried to exit when it's not allowed
3225   else if (nameicmp(DmgType, 'Drowning') == 0) Msg = "$ob_water"; // drowned int the water
3226   else if (nameicmp(DmgType, 'Slime') == 0) Msg = "$ob_slime"; // was standing in the slime
3227   else if (nameicmp(DmgType, 'Fire') == 0 && !source) Msg = "$ob_lava"; // was standing in the lava
3228   else if (source) {
3229     if (source.Player == self) {
3230       // suicide
3231       Msg = "$ob_killedself";
3232     } else if (!source.bIsPlayer) {
3233       // killed by monster
3234            if (nameicmp(DmgType, 'Telefrag') == 0) Msg = "$ob_montelefrag"; // monster telefrag
3235       else if (nameicmp(DmgType, 'Melee') == 0 && source.HitObituary) Msg = source.HitObituary;
3236       else Msg = source.Obituary;
3237     }
3238   }
3240   if (!Msg && source && source.bIsPlayer) {
3241     if (Level.Game.netgame && !Level.Game.deathmatch) {
3242       // killed another player in cooperative net game
3243       Msg = va("$ob_friendly%d", (P_Random()&3)+1); // it goes from 1 to 4, not from 0 to 3
3244     } else if (nameicmp(DmgType, 'Telefrag') == 0) {
3245       // telefrag
3246       Msg = "$ob_mptelefrag";
3247     } else if (inflictor && inflictor.Obituary) {
3248       // missile with its own obituary
3249       Msg = inflictor.Obituary;
3250     } else {
3251       PlayerEx pex = (inflictor && inflictor.Player ? PlayerEx(inflictor.Player) : none);
3252       Weapon Wpn = (pex ? pex.ReadyWeapon : none);
3253            if (Wpn && Wpn.Obituary) Msg = Wpn.Obituary; // weapon obituary message
3254       else if (nameicmp(DmgType, 'BFGSplash') == 0) Msg = "$ob_mpbfg_splash"; // BFG splash damage
3255       else if (nameicmp(DmgType, 'Railgun') == 0) Msg = "$ob_railgun"; // railgun
3256       else Msg = "$ob_mpdefault"; // default multiplayer kill message
3257     }
3258   } else {
3259     source = EntityEx(MO);
3260   }
3262   if (!Msg) Msg = "$ob_default"; // generic death
3264   // look up string in language lump if necesary
3265   Msg = TranslateString(Msg);
3267   // do replacements
3268   Msg = StrReplaceSubstitutes(Msg, source);
3270   Level.bprint("%s", Msg);
3274 //==========================================================================
3276 //  ClientRailTrail
3278 //==========================================================================
3279 void ClientRailTrail (TVec From, TVec To, optional int Col1, optional int Col2, float MaxDiff) {
3280   if (!Col1!specified) Col1 = RGBA(255, 255, 255, 255);
3281   if (!Col2!specified) Col2 = RGBA(0, 0, 255, 255);
3283   if (!Col1 && !Col2) return;
3285   float Len = Length(To-From);
3286   TVec Dir = Normalise(To-From);
3287   TAVec Ang;
3288   VectorAngles(Dir, out Ang);
3289   Ang.roll = 270.0;
3291   TVec Diff;
3292   for (float Offs = 0.0; Offs < Len; Offs += 3.0) {
3293     if (MaxDiff > 0.0) {
3294       int Rnd = P_Random();
3295       if (Rnd&1) {
3296         Diff.x += (Rnd&8 ? 1.0 : -1.0);
3297         Diff.x = fclamp(Diff.x, -MaxDiff, MaxDiff);
3298       }
3299       if (Rnd&2) {
3300         Diff.y += (Rnd&16 ? 1.0 : -1.0);
3301         Diff.y = fclamp(Diff.y, -MaxDiff, MaxDiff);
3302       }
3303       if (Rnd&4) {
3304         Diff.z += (Rnd&32 ? 1.0 : -1.0);
3305         Diff.z = fclamp(Diff.z , -MaxDiff, MaxDiff);
3306       }
3307     }
3309     if (Col1) {
3310       TVec porg = From+Dir*Offs+Diff;
3311       particle_t *p = Level.NewParticle(porg);
3312       if (!p) break; // cannot spawn any more particles, no sense in further tries
3313       p->type = LineSpecialLevelInfo::pt_rail;
3314       p->Size = 0.5;
3315       p->color = Col1;
3316       p->die = Level.XLevel.Time+1.0;
3317       p->vel.x = (Random()-0.5)*2.0;
3318       p->vel.y = (Random()-0.5)*2.0;
3319       p->vel.z = (Random()-0.5)*2.0;
3320       p->accel = vector(0.0, 0.0, 0.0);
3321     }
3323     Ang.roll += 14.0;
3325     if (Col2) {
3326       TVec Forward;
3327       TVec Right;
3328       TVec Up;
3329       AngleVectors(Ang, out Forward, out Right, out Up);
3331       TVec porg = From+Dir*Offs+Up*3.0;
3332       particle_t *p = Level.NewParticle(porg);
3333       if (!p) break; // cannot spawn any more particles, no sense in further tries
3334       p->type = LineSpecialLevelInfo::pt_rail;
3335       p->Size = 0.5;
3336       p->color = Col2;
3337       p->die = Level.XLevel.Time+1.0;
3338       p->vel = Up;
3339       p->accel = vector(0.0, 0.0, 0.0);
3340     }
3341   }
3345 //==========================================================================
3347 //  ClientVoice
3349 //==========================================================================
3350 void ClientVoice (int VoiceNum) {
3351   LocalSound(name(va("svox/voc%d", VoiceNum)));
3355 //==========================================================================
3357 //  GiveAmmo
3359 //  Returns false if the ammo can't be picked up at all
3361 //==========================================================================
3362 bool GiveAmmo (class!Ammo ammo, int count) {
3363   int oldammo;
3365   if (!ammo) return false;
3367   Ammo AmmoItem = Ammo(EntityEx(MO).FindInventory(ammo));
3368   //if (!AmmoItem) AmmoItem = Ammo(EntityEx(MO).FindInventory(class!Ammo(GetClassReplacement(ammo))));
3370   if (!AmmoItem) {
3371     class!Ammo repl = class!Ammo(GetClassReplacement(ammo));
3372     if (!repl) repl = ammo;
3373     AmmoItem = Level.SpawnEntityChecked(class!Ammo, repl, default, default, default, AllowReplace:false);
3374     if (!AmmoItem) return false;
3375     AmmoItem.AttachToOwner(EntityEx(MO));
3376     AmmoItem.Amount = 0;
3377   }
3379   if (!count) return false;
3381   int maxam = AmmoItem./*MaxAmount*/k8GetAmmoKingMax();
3382   if (AmmoItem.Amount == maxam) return false;
3384   // extra ammo in baby mode and nightmare mode
3385   count = max(0, int(float(count)*Level.World.GetAmmoFactor()));
3387   oldammo = AmmoItem.Amount;
3388   //AmmoItem.Amount += count;
3389   //if (AmmoItem.Amount > AmmoItem.MaxAmount) AmmoItem.Amount = AmmoItem.MaxAmount;
3390   AmmoItem.Amount = min(AmmoItem.Amount+count, maxam);
3392   if (oldammo <= 0) GotAmmo(AmmoItem);
3394   return true;
3398 //==========================================================================
3400 //  PutClientIntoServer
3402 //==========================================================================
3403 override void PutClientIntoServer () {
3404   ClearSubSeenInfo();
3405   if (bIsBot) CreateBot();
3409 //==========================================================================
3411 //  PerformRebornSpawn
3413 //  returns non-null if no players was spawned
3415 //==========================================================================
3416 mthing_t *PerformRebornSpawn () {
3417   bool spawned = false;
3418   mthing_t *best = nullptr;
3419   float bestDist = float.max;
3420   mthing_t *bestOther = nullptr;
3421   float bestOtherDist = float.max;
3422   foreach (auto i; 0..Level.PlayerStarts.length) {
3423     auto sp = cast([unsafe])(&Level.PlayerStarts[i]);
3424     if (sp.type == GetPlayerNum()+1) {
3425       if (sp.args[0] == Level.Game.RebornPosition) {
3426         // i found her!
3427         SpawnPlayer(sp, spawned);
3428         spawned = true;
3429         continue;
3430       }
3431       if (spawned) continue;
3432       if (CheckSpot(sp, onlyCheck:true)) {
3433         float spdist = (MO ? (MO.Origin-vector(sp.x, sp.y)).length2DSquared() : bestDist);
3434         if (!best || spdist < bestDist) {
3435           best = sp;
3436           bestDist = spdist;
3437         }
3438       }
3439     } else if (!spawned) {
3440       // not ours, but still remember it
3441       if (!best && CheckSpot(sp, onlyCheck:true)) {
3442         float spdist = (MO ? (MO.Origin-vector(sp.x, sp.y)).length2DSquared() : bestOtherDist);
3443         if (!bestOther || spdist < bestOtherDist) {
3444           bestOther = sp;
3445           bestOtherDist = spdist;
3446         }
3447       }
3448     }
3449   }
3451   if (spawned) return nullptr;
3453   if (!best && !bestOther) Error("Player %d has no start spots", GetPlayerNum()+1);
3455   if (!best) {
3456     print("Player %d has no start spot for position %d, using other spot position", GetPlayerNum()+1, Level.Game.RebornPosition);
3457     return bestOther;
3458   } else {
3459     print("Player %d has no start spot for position %d", GetPlayerNum()+1, Level.Game.RebornPosition);
3460   }
3462   return best;
3466 //==========================================================================
3468 //  InitWeaponSlots
3470 //  used by dumb client to setup weapon slots
3472 //==========================================================================
3473 override void InitWeaponSlots () {
3474   if (PlayerPawn(MO)) PlayerPawn(MO).InitializeWeaponSlots(/*LineSpecialGameInfo(Level.Game)*/);
3478 //==========================================================================
3480 //  SpawnClient
3482 //==========================================================================
3483 override void SpawnClient () {
3484   bool playerWasReborn;
3485   EntityEx OldMO = EntityEx(MO);
3486   ClearSubSeenInfo();
3488   //printdebug("*** SPAWN CLIENT");
3490   Attacker = none;
3491   Poisoner = none;
3493   AddVisitedMap(Level.XLevel.MapName);
3495   if (Level.Game.netgame && !Level.Game.deathmatch) {
3496     // cooperative net-play, retain keys and weapons
3497     playerWasReborn = (PlayerState == PST_REBORN || PlayerState == PST_CHEAT_REBORN);
3498   } else {
3499     playerWasReborn = (PlayerState == PST_CHEAT_REBORN);
3500   }
3502   // spawn player
3503   if (Level.Game.deathmatch) {
3504     DeathMatchSpawnPlayer();
3505   } else if (Level.Game.netgame || Level.bClusterHub) {
3506     SpawnPlayer(Level.GetPlayerStart(GetPlayerNum(), Level.Game.RebornPosition), false);
3507   } else if (PlayerState == PST_CHEAT_REBORN) {
3508     SpawnPlayer(nullptr, false);
3509   } else {
3510     mthing_t *best = PerformRebornSpawn();
3511     // if returned non-nullptr, it means that no good spawn position was found
3512     // spawn player anyway, we don't want to totally ruin the game for them
3513     if (best) SpawnPlayer(best, false);
3514   }
3516   /* ResetPlayerOnSpawn() should take care of this
3517   if (!playerWasReborn && LocalQuakeHappening) {
3518     print("*** RESETTING QUAKING!");
3519     LocalQuakeHappening = 0;
3520   }
3521   */
3523   // setup weapon slots
3524   //printdebug("*** SPAWN CLIENT: MO=%C (%C)", MO, PlayerPawn(MO));
3525   if (PlayerPawn(MO)) PlayerPawn(MO).InitializeWeaponSlots(/*LineSpecialGameInfo(Level.Game)*/);
3527   // coop
3528   if (Level.Game.netgame && !Level.Game.deathmatch) {
3529     if (playerWasReborn) OnNetReborn(OldMO); else OnNetSpawn(OldMO);
3530   }
3532   // destroy all things touching players
3533   Actor(MO).TeleportMove(MO.Origin);
3535   k8BossesDetected = (GetCvarB('k8ElvenDetect') ? -0.5 : 666);
3537   ResetBootPrints();
3538   HealthBarInit();
3542 //==========================================================================
3544 //  NetGameReborn
3546 //  Respawn at the start
3548 //==========================================================================
3549 override void NetGameReborn () {
3550   EntityEx OldMO = EntityEx(MO);
3551   ClearSubSeenInfo();
3553   // remove pitch and roll angles from corpse
3554   MO.Angles.pitch = 0.0;
3555   MO.Angles.roll = 0.0;
3557   // first dissasociate the corpse
3558   MO.Player = none;
3559   MO.bIsPlayer = false;
3561   LastRegenTicTime = 0;
3562   ResetBootPrints();
3564   // spawn at random spot if in death match
3565   if (Level.Game.deathmatch) {
3566     k8HealthAccum_Amount = 0;
3567     OldMO.DestroyAllInventory();
3568     DeathMatchSpawnPlayer();
3569     return;
3570   }
3572   if (PlayerState == PST_CHEAT_REBORN) {
3573     PreTravel(); // this stores all required inventory in `SavedInventory`
3574     auto oldAccum = k8HealthAccum_Amount;
3575     auto oldTID = OldMO.TID;
3576     if (oldTID) OldMO.SetTID(0); // remove TID from the dead body (lol)
3577     SpawnPlayer(nullptr, false);
3578     k8HealthAccum_Amount = oldAccum;
3579     if (MO && oldTID) MO.SetTID(oldTID); // restore TID
3580   } else {
3581     k8HealthAccum_Amount = 0;
3582     bool foundSpot = false;
3583     auto sp = Level.GetPlayerStart(GetPlayerNum(), Level.Game.RebornPosition, failIfNotFound:false);
3584     if (sp && CheckSpot(sp)) {
3585       // appropriate player start spot is open
3586       SpawnPlayer(sp, false);
3587       foundSpot = true;
3588     } else {
3589       // try to spawn at one of the other player start spots
3590       // this should not fail hard if player start spot is not found, because
3591       // there can be more players allowed that we have spawn spots
3592       foreach (auto i; 0..MAXPLAYERS) {
3593         sp = Level.GetPlayerStart(i, Level.Game.RebornPosition, failIfNotFound:false);
3594         if (sp && CheckSpot(sp)) {
3595           print("*** FOUND OTHER PLAYER (%s) STARTING SPOT FOR PLAYER %s", i+1, GetPlayerNum()+1);
3596           // found an open start spot
3597           SpawnPlayer(sp, false);
3598           foundSpot = true;
3599           break;
3600         }
3601       }
3602     }
3604     if (!foundSpot) {
3605       // player's going to be inside something. too bad.
3606       print("*** NOT FOUND OTHER PLAYER STARTING SPOTS FOR PLAYER %s", GetPlayerNum()+1);
3607       sp = Level.GetPlayerStart(GetPlayerNum(), Level.Game.RebornPosition);
3608       CheckSpot(sp); // spawn teleport fog
3609       SpawnPlayer(sp, false);
3610     }
3611   }
3613   if (PlayerPawn(MO)) PlayerPawn(MO).InitializeWeaponSlots(/*LineSpecialGameInfo(Level.Game)*/);
3615   OnNetReborn(OldMO);
3619 //==========================================================================
3621 //  DisconnectClient
3623 //==========================================================================
3624 override void DisconnectClient () {
3625   ClearSubSeenInfo();
3626   DestroyBot();
3627   if (MO) {
3628     MO.Player = none;
3629     MO.bIsPlayer = false;
3630     Actor(MO).Damage(none, none, 10000, forced:true, spawnBlood:true);
3631   }
3632   Level.bprint("%s left the game", PlayerName);
3633   if (MO) MO.PlaySound('misc/chat', CHAN_AUTO, 1.0, ATTN_NONE);
3634   HealthBarDeinit();
3638 //==========================================================================
3640 //  DeathMatchSpawnPlayer
3642 //  Spawns a player at one of the random death match spots called at level
3643 //  load and each death
3645 //==========================================================================
3646 void DeathMatchSpawnPlayer () {
3647   if (!Level.DeathmatchStarts.length) Error("oops! no deathmatch starts where they should be!");
3649   // shuffle them
3650   array!int dms;
3651   dms.length = Level.DeathmatchStarts.length;
3652   foreach (int didx; 0..dms.length) dms[didx] = didx;
3654   // the famous Fisher-Yates shuffle
3655   foreach (int didx; 0..dms.length-1) {
3656     int swapidx = didx+roundi(FRandomFull()*(dms.length-didx-1));
3657     if (swapidx != didx) {
3658       int tmp = dms[swapidx];
3659       dms[swapidx] = dms[didx];
3660       dms[didx] = tmp;
3661     }
3662   }
3664   foreach (int i; dms) {
3665     auto sp = cast([unsafe])(&Level.DeathmatchStarts[i]);
3666     if (CheckSpot(sp)) {
3667       SpawnPlayer(sp, false);
3668       return;
3669     }
3670   }
3672   // no good spot, so the player will probably get stuck
3673   {
3674     auto sp = Level.GetPlayerStart(GetPlayerNum(), 0);
3675     CheckSpot(sp); // spawn teleport fog
3676     SpawnPlayer(sp, false);
3677   }
3681 //==========================================================================
3683 //  eventAfterUnarchiveThinkers
3685 //==========================================================================
3686 override void eventAfterUnarchiveThinkers () {
3687   //printwarn("player `%C`: after unarchive...", self);
3688   ::eventAfterUnarchiveThinkers();
3689   // setup weapon slots
3690   if (PlayerPawn(MO)) PlayerPawn(MO).InitializeWeaponSlots(/*LineSpecialGameInfo(Level.Game)*/);
3694 //==========================================================================
3696 //  CheckSpot
3698 //  Returns false if the player cannot be respawned at the given mthing_t
3699 //  spot because something is occupying it
3701 //==========================================================================
3702 bool CheckSpot (mthing_t *mthing, optional bool onlyCheck) {
3703   float x;
3704   float y;
3705   sector_t *sec;
3706   float an;
3708   if (!MO) {
3709     // first spawn of level, before corpses
3710     foreach (auto i; 0..MAXPLAYERS) {
3711       if (Level.Game.Players[i] && Level.Game.Players[i].MO &&
3712           Level.Game.Players[i].MO.Origin.x == mthing->x &&
3713           Level.Game.Players[i].MO.Origin.y == mthing->y)
3714       {
3715         return false;
3716       }
3717     }
3718     return true;
3719   }
3721   x = mthing->x;
3722   y = mthing->y;
3724   {
3725     auto oldPassMObj = MO.bPassMobj;
3726     scope(exit) MO.bPassMobj = oldPassMObj;
3727     MO.bPassMobj = false;
3728     if (!MO.CheckPosition(vector(x, y, Actor::ONFLOORZ))) return false;
3729   }
3731   if (!onlyCheck) {
3732     if (!PlayerChunk(MO)) {
3733       LineSpecialLevelInfo(Level).AddPlayerCorpse(EntityEx(MO));
3734     }
3736     // spawn a teleport fog
3737     sec = Level.XLevel.PointInSector(vector(x, y, 0.0));
3738     an = float(45*(mthing->angle/45));
3740     Level.Spawn(TeleportFog, vector(x+20.0*cos(an), y+20.0*sin(an),
3741       sec->floor.TexZ+LineSpecialGameInfo(Level.Game).TeleFogHeight));
3742   }
3744   return true;
3748 //==========================================================================
3750 //  SetupPlayerClass
3752 //==========================================================================
3753 void SetupPlayerClass () {
3754   if (LineSpecialGameInfo(Level.Game).bRandomClass &&
3755       Level.Game.deathmatch && Level.Game.PlayerClasses.length > 1)
3756   {
3757     PClass = P_Random()%Level.Game.PlayerClasses.length;
3758     if (PClass == BaseClass) PClass = (PClass+1)%Level.Game.PlayerClasses.length;
3759     BaseClass = PClass;
3760     SB_Start();
3761   } else {
3762     PClass = BaseClass;
3763   }
3767 //==========================================================================
3769 //  ResetACSButtons
3771 //  reset ACS button update timers and values
3773 //==========================================================================
3774 void ResetACSButtons () {
3775   AcsCurrButtonsPressed = 0;
3776   AcsCurrButtons = 0;
3777   AcsButtons = 0;
3778   OldButtons = 0;
3779   AcsNextButtonUpdate = 0;
3780   AcsPrevMouseX = 0;
3781   AcsPrevMouseY = 0;
3782   AcsMouseX = 0;
3783   AcsMouseY = 0;
3787 //==========================================================================
3789 //  ResetRenderStyles
3791 //==========================================================================
3792 void ResetRenderStyles () {
3793   FixedColormap = 0;
3794   ExtraLight = 0;
3795   CShift = 0;
3799 //==========================================================================
3801 //  ResetBootPrints
3803 //==========================================================================
3804 void ResetBootPrints () {
3805   bootprintTimeLeft = 0.0f;
3809 //==========================================================================
3811 //  ResetPlayerOnSpawn
3813 //==========================================================================
3814 void ResetPlayerOnSpawn (optional bool keepPlayerState) {
3815   if (!keepPlayerState) PlayerState = PST_LIVE;
3816   ClearSubSeenInfo();
3817   Refire = 0;
3818   DamageFlash = 0.0;
3819   DamageFlashType = '';
3820   BonusFlash = 0.0;
3821   PoisonCount = 0;
3822   MorphTime = 0.0;
3823   ExtraLight = 0;
3824   FixedColormap = 0;
3825   CShift = 0;
3826   LastSectorDamageTic = 0;
3827   LastHazardTime = 0.0;
3828   Rain1 = none;
3829   Rain2 = none;
3831   JumpTime = 0;
3832   LocalQuakeHappening = vector(0, 0, 0);
3833   //MoveDir = vector(0, 0, 0);
3834   HazardTime = 0;
3835   //Weapon ReadyWeapon;
3836   //Weapon PendingWeapon;  // Is none if not changing.
3837   //FlyHeight = 0;
3838   /*
3839   bFrozen = false; // just in case
3840   bTotallyFrozen = false; // just in case
3841   */
3842   bFly = false; // just in case
3843   PoisonCount = 0; // screen flash for poison damage
3844   LastPoisonTime = 0;
3845   Poisoner = none; // none for non-player mobjs
3846   PoisonerPlayer = none; // for KArena
3848   // reset mouselook flags
3849   bInverseMouseX = false;
3850   bInverseMouseY = false;
3851   bBlockMouseX = false;
3852   bBlockMouseY = false;
3854   MorphTime = 0;  // player is morphed into something if > 0
3855   MorphStyle = 0;
3856   BlendR = 0;
3857   BlendG = 0;
3858   BlendB = 0;
3859   BlendA = 0;
3860   ChickenPeck = 0; // chicken peck countdown
3862   //k8: just in case
3863   ResetWeaponReloadRefire();
3864   ResetWeaponActionFlags();
3865   bDisableWeaponSwitch = false;
3866   bForceCrouchingDown = 0;
3868   if (GetCvarB('k8HealthAccum_Reset')) k8HealthAccum_Amount = 0;
3869   k8HealthAccum_LastRegenTime = -10000;
3870   k8HealthAccum_LastBoostTime = -10000;
3872   if (GetCvarB('r_allow_ambient')) bFlashlightOn = false;
3873   bFlashlightButtonDown = false;
3875   k8NextSuperBulletTime = 0;
3876   LastRegenTicTime = 0;
3878   ResetAttackers();
3880   ResetACSButtons();
3882   k8BossesDetected = 666;
3883   k8ElvenGiftMessageTime = 666;
3885   Cheats &= ~(CF_NOCLIP|CF_TIMEFREEZE);
3887   // reset zoom
3888   FOV = DesiredFOV;
3889   ClientFOV(0);
3891   ResetRenderStyles();
3893   ResetBootPrints();
3897 //==========================================================================
3899 //  GiveDefaultDeathMatchInventory
3901 //  FIXME: replacements?
3903 //==========================================================================
3904 void GiveDefaultDeathMatchInventory () {
3905   class!Inventory Cls;
3906   // give all keys in death match mode
3907   foreach AllClasses(Key, Cls) {
3908     if (!FindClassState(Cls, 'Spawn')) continue; // abstract class
3909     if (!AreStateSpritesPresent(FindClassState(Cls, 'Spawn'))) continue; // from a different game
3910     EntityEx(MO).GiveInventoryType(Cls);
3911   }
3915 //==========================================================================
3917 //  ResetInventory
3919 //==========================================================================
3920 override void ResetInventory () {
3921   EntityEx PP = EntityEx(MO);
3922   if (!PP) return;
3923   while (PP.Inventory) PP.Inventory.Destroy();
3925   ResetRenderStyles();
3927   SetWeapon(none);
3928   BringUpWeapon(instant:true, skipSound:true);
3930   AddDefaultInventory();
3932   k8ElvenGifted = false;
3933   GiveElvenGifts(nullptr);
3937 //==========================================================================
3939 //  RemoveKeysVerbose
3941 //==========================================================================
3942 override void RemoveKeysVerbose (optional bool verbose) {
3943   EntityEx PP = EntityEx(MO);
3944   if (!PP) return;
3945   if (!verbose!specified) verbose = true;
3947   //bool again = true;
3948   int count = 0;
3949   class!Inventory Cls;
3950   foreach AllClasses(Key, Cls) {
3951     class!Key repl = class!Key(GetClassReplacement(Cls));
3952     if (repl && repl != Cls) continue;
3953     if (!EntityEx.IsSpawnableClass(Cls)) continue;
3954     auto inv = PP.FindInventory(Cls, disableReplacement:true);
3955     if (inv) {
3956       //again = true;
3957       if (verbose) print("removed key '%C'", inv);
3958       PP.RemoveInventory(inv);
3959       delete inv;
3960       ++count;
3961     }
3962   }
3963   if (verbose && count > 0) cprint("Removed %s key%s!", count, (count != 1 ? "s" : ""));
3967 //==========================================================================
3969 //  ResetHealth
3971 //==========================================================================
3972 override void ResetHealth () {
3973   EntityEx PP = EntityEx(MO);
3974   if (!PP) return;
3975   Health = GetRebornHealth();
3976   PP.Health = Health;
3980 //==========================================================================
3982 //  PreraiseWeapon
3984 //==========================================================================
3985 override void PreraiseWeapon () {
3986   EntityEx PP = EntityEx(MO);
3987   if (!PP) return;
3988   if (!ReadyWeapon) return;
3989   print("PRERAISE!");
3990   BringUpWeapon(instant:true, skipSound:true);
3994 //==========================================================================
3996 //  SpawnPlayer
3998 //  Called when a player is spawned on the level. Most of the player
3999 //  structure stays unchanged between levels.
4001 //==========================================================================
4002 PlayerPawn SpawnPlayer (mthing_t *mthing, bool Voodoo) {
4003   PlayerPawn PP;
4004   bool ResetInventory = false;
4005   bool reborned = false;
4006   Inventory Item;
4008   TVec spawnOrigin = (mthing ? vector(mthing->x, mthing->y, Actor::ONFLOORZ) : MO.Origin);
4009   float spawnYaw = (mthing ? float(45*(mthing->angle/45)) : MO.Angles.yaw);
4011   //print("*** SPAWN PLAYER");
4013   bool cheatReborn = false;
4015   //printdebug("************SpawnPlayer:%C: 000; Voodoo=%B; PST=%s (SVI=%C)", self, Voodoo, PlayerState, SavedInventory);
4016   if (PlayerState == PST_REBORN) {
4017     //printdebug("  normal reborn (SVI=%C)", SavedInventory);
4018     PlayerReborn();
4019     ResetInventory = true;
4020     reborned = true;
4021   } else if (PlayerState == PST_CHEAT_REBORN) {
4022     //print("*** CHEAT REBORN ***");
4023     PlayerReborn();
4024     ResetInventory = false;
4025     reborned = true;
4026     cheatReborn = true;
4027   } else {
4028     if (Level.bResetInventory && !GetCvarB('sv_ignore_reset_inventory')) ResetInventory = true;
4029   }
4031   if (!cheatReborn && GetCvarB('sv_force_pistol_start')) ResetInventory = true;
4033   SetupPlayerClass();
4035   float x = spawnOrigin.x;
4036   float y = spawnOrigin.y;
4037   float z = spawnOrigin.z;
4038   if (PClass >= Level.Game.PlayerClasses.length) Error("Player::SpawnPlayer: Unknown class type (class=%s; max=%s)", PClass, Level.Game.PlayerClasses.length-1);
4040   PP = Level.SpawnEntityChecked(class!PlayerPawn, class!PlayerPawn(Level.Game.PlayerClasses[PClass]), vector(x, y, z), default, default, AllowReplace:false);
4041   if (!PP) Error("Player::SpawnPlayer: cannot spawn `%C` (it is prolly not a player pawn)", Level.Game.PlayerClasses[PClass]);
4042   printdebug("spawned player with class <%C>", PP);
4043   PP.InitializeWeaponSlots();
4044   //print("falling damage: normal=%B; old=%B; strife=%B", Level.bFallingDamage, Level.bOldFallingDamage, Level.bStrifeFallingDamage);
4045   if (GetCvarS('player_default_gender')) PP.SoundGender = name(GetCvarS('player_default_gender'));
4046   if (mthing) PP.Origin.z += mthing->height;
4047   PP.LinkToWorld(properFloorCheck:true);
4048   PP.FriendPlayer = GetPlayerNum()+1; // players are their own friends
4049   ClearSubSeenInfo();
4051   // set color translations for player sprites
4052   TranslStart = PP.TranslStart;
4053   TranslEnd = PP.TranslEnd;
4054   PP.Translation = (Entity::TRANSL_Player<<Entity::TRANSL_TYPE_SHIFT)+GetPlayerNum();
4056   // if a voodoo doll has been spawned, take its inventory
4057   if (Voodoo) {
4058     //printdebug("  moving inventory to the Voodoo Doll from MO (SVI=%C)", SavedInventory);
4059     PP.ObtainInventory(EntityEx(MO));
4060   }
4062   DesiredFOV = 90.0; //float(GetCvar('FOV'));
4063   FOV = DesiredFOV;
4064   PP.Angles.yaw = spawnYaw;
4065   PP.Player = self;
4066   PP.bIsPlayer = true;
4067   PP.Health = Health;
4068   MO = PP;
4069   Camera = PP;
4070   ViewHeight = PP.GetPawnViewHeight;
4071   ViewOrg = MO.Origin;
4072   ViewOrg.z += ViewHeight;
4073   SaveViewOrgFixInfo();
4074   ViewAngles = PP.Angles;
4075   bFixAngle = true;
4076   ResetRenderStyles();
4078   ResetPlayerOnSpawn();
4080   if (!Voodoo) {
4081     PP.Inventory = SavedInventory;
4082     for (Item = PP.Inventory; Item; Item = Item.Inventory) Item.Owner = MO;
4083     //printdebug("%C: PostTravel inventory (%C:%s):", self, PP, PP.UniqueId);
4084     //for (auto inv = PP.Inventory; inv; inv = inv.Inventory) printdebug("  %C: owner=%C(%s); MO=%C(%s)", inv, inv.Owner, (inv.Owner ? inv.Owner.UniqueId : 0), MO, MO.UniqueId);
4085     if (ResetInventory) {
4086       //printdebug("%C: ResetInventory!", self);
4087       //while (PP.Inventory) PP.Inventory.Destroy();
4088       //FIXME!!!
4089       // for some reason, bot inventory sometimes refusing to go; wtf?!
4090       auto pinv = PP.Inventory;
4091       while (pinv) {
4092         auto pnext = pinv.Inventory;
4093         //printdebug("%C: going to destroy inventory item '%C' (owner=%C %B) (inv=%C; next=%C)", PP, pinv, pinv.Owner, (pinv.Owner == PP), PP.Inventory, pnext);
4094         delete pinv;
4095         if (PP.Inventory != pnext) printdebug("%C: inventory item '%C' refused to go (owner=%C %B)", PP, PP.Inventory, PP.Inventory.Owner, (PP.Inventory.Owner == PP));
4096         pinv = pnext;
4097       }
4098       PP.Inventory = none;
4099       AddDefaultInventory();
4100     } else if (Level.bResetItems && !GetCvarB('sv_ignore_reset_items')) {
4101       //printdebug("*** %C: cleaning up inventory...", self);
4102       Inventory inv = PP.Inventory;
4103       while (inv) {
4104         Inventory c = inv;
4105         inv = inv.Inventory;
4106         //if (c.bInvBar && !c.bUndroppable) c.Destroy();
4107         //printdebug("*** %C:   %C(power=%s): bInvBar=%B; bPersistentPower=%B; bUndroppable=%B", self, c, (Powerup(c) ? "tan" : "ona"), c.bPersistentPower, c.bUndroppable);
4108         if (!c.bInvBar) continue;
4109         if (Powerup(c)) {
4110           if (c.bPersistentPower) continue;
4111         } else {
4112           if (c.bUndroppable) continue;
4113         }
4114         c.Destroy();
4115       }
4116     }
4117   }
4119   // set up gun psprite
4120   SetupPsprites();
4122   if (Level.Game.deathmatch && !IsCheckpointSpawn) GiveDefaultDeathMatchInventory();
4124   // wake up the status bar
4125   SB_Start();
4127   if (bIsBot) BotOnSpawn();
4129   SetClientModel();
4131   UpdateRevealedMap();
4133   if (ResetInventory) {
4134     if (reborned) Level.XLevel.StartTypedACScripts(VLevel::SCRIPT_Respawn, 0, 0, 0, Activator:MO, Always:true, RunNow:false);
4135     k8ElvenGifted = false;
4136   }
4138   //print("*** CHECKPOINT SPAWN: %B", IsCheckpointSpawn);
4139   if (!IsCheckpointSpawn) {
4140     GiveElvenGifts(mthing, Voodoo);
4141   }
4143   if (!reborned && !IsCheckpointSpawn) {
4144     if ((Level.bResetHealth && !GetCvarB('sv_ignore_reset_health')) || GetCvarB('sv_force_health_reset')) {
4145       Health = GetRebornHealth();
4146       EntityEx(MO).Health = Health;
4147     } else {
4148       int rh = clamp(GetCvarI('sv_min_startmap_health'), 0, 200);
4149       if (rh && Health < rh) {
4150         Health = rh;
4151         EntityEx(MO).Health = rh;
4152       }
4153     }
4154   }
4156   k8BossesDetected = (GetCvarB('k8ElvenDetect') ? -0.5 : 666);
4158   return PP;
4162 //===========================================================================
4164 //  PerformBossDetection
4166 //  this is virtual, so mods can override it
4168 //===========================================================================
4169 void PerformBossDetection () {
4170   if (!MO) return;
4171   // detect bosses
4172   Actor act;
4173   int cybbieCount = 0, mindCount = 0;
4174   foreach MO.AllThinkers(Actor, out act) {
4175     if (!act.bMonster || act.bCorpse || act.Health <= 0) continue;
4176     class!Actor ec = class!Actor(act.Class);
4177     while (ec) {
4178       switch (GetClassName(ec)) {
4179         case 'Cyberdemon': ++cybbieCount; break;
4180         case 'SpiderMastermind': ++mindCount; break;
4181       }
4182       ec = class!Actor(GetClassParent(ec));
4183     }
4184     //if (hasCybbie && hasMind) break; // just in case
4185   }
4186   string msg;
4187   if (cybbieCount && mindCount) {
4188     if (cybbieCount > 1 && mindCount > 1) msg = va("%d Cyberdemons and %d Masterminds detected!", cybbieCount, mindCount);
4189     else if (cybbieCount > 1) msg = va("%d Cyberdemons and Mastermind detected!", cybbieCount);
4190     else if (mindCount > 1) msg = va("Cyberdemon and %d Masterminds detected!", mindCount);
4191     else msg = "Cyberdemon and Mastermind detected!";
4192   }
4193   else if (cybbieCount > 1) msg = va("%d Cyberdemons detected!", cybbieCount);
4194   else if (cybbieCount) msg = "Cyberdemon detected!";
4195   else if (mindCount > 1) msg = va("%d Masterminds detected!", mindCount);
4196   else if (mindCount) msg = "Mastermind detected!";
4197   if (msg) {
4198     print("\c[Red]*** %s ***", msg);
4199     ClientHudMessage(msg, 'smallfont', HUDMsgType.FadeInOut|HUDMsgFlag.ColorString/*|HUDMsgFlag.Log*/, 123669, CR_UNDEFINED, "Green",
4200       0.5, 0.56, 0, 0,
4201       2.5, 0.5, 1.0,
4202       1.0);
4203   }
4207 //===========================================================================
4209 //  ShowElvenGiftMessage
4211 //  this is virtual, so mods can override it
4213 //===========================================================================
4214 void ShowElvenGiftMessage () {
4215   if (!MO) return;
4216   ClientHudMessage("ELVEN GIFT", 'smallfont', HUDMsgType.FadeInOut, 123666, CR_ORANGE, "",
4217     0.5, 0.45, 0, 0,
4218     2.0, 0.4, 0.8,
4219     1.0);
4223 //===========================================================================
4225 //  GiveElvenGifts
4227 //  this is virtual, so mods can override it
4229 //===========================================================================
4230 void GiveElvenGifts (optional mthing_t *mthing, optional bool Voodoo, optional bool forced) {
4231   // do not do this on a titlemap
4232   if (nameicmp(Level.XLevel.MapName, 'titlemap') == 0) return;
4233   if (Level.XLevel.bIsBadApple) return;
4235   //print("************SpawnPlayer: gifted=%s", (k8ElvenGifted ? "tan" : "ona"));
4236   k8ElvenGiftMessageTime = 666;
4237   if (forced || GetCvarB('k8ElvenGift')) {
4238     if (forced) k8ElvenGifted = false;
4239     if (!Voodoo && !k8ElvenGifted) {
4240       k8ElvenGifted = true; // gifts should be given only once
4241       bool showMessage = false;
4242       if (AddElvenGift('Shotgun')) { showMessage = true; AddElvenGift('ShellBox'); }
4243       if (AddElvenGift('BDW_Rifle')) { showMessage = true; foreach (; 0..9) AddElvenGift('Clip'); if (ShouldRemovePistol()) RemovePistol(); }
4244       print("\c[Green]%s", "Elven Gifts!");
4245       if (showMessage) k8ElvenGiftMessageTime = -0.3;
4246     }
4247     // oops; change weapon, as voodoo doll was spawned
4248     if (Voodoo && k8ElvenGifted) {
4249       if (FindClass('BDW_Rifle') && ReadyWeapon && ReadyWeapon.bWimpyWeapon && (!PendingWeapon || PendingWeapon.bWimpyWeapon)) {
4250         auto rfc = FindClass('BDW_Rifle');
4251         for (Inventory Item = EntityEx(MO).Inventory; Item; Item = Item.Inventory) {
4252           if (Item.Class == rfc) {
4253             if (ReadyWeapon != Weapon(Item)) {
4254               //PendingWeapon = Weapon(Item);
4255               SetWeapon(Weapon(Item));
4256               BringUpWeapon();
4257             }
4258             break;
4259           }
4260         }
4261       }
4262     }
4263   }
4267 //===========================================================================
4269 //  AddElvenGift
4271 //===========================================================================
4272 bool AddElvenGift (name Klass) {
4273   class!Actor th = class!Actor(FindClass(Klass));
4274   if (th) {
4275     Actor act = Actor(Level.Spawn(th));
4276     if (!act) return false;
4277     Weapon wpn = Weapon(act);
4278     int fixAmmoGive = -1;
4279     if (Klass == 'BDW_Rifle') {
4280       wpn.AmmoGive2 = 31;
4281       if (ReadyWeapon != wpn) {
4282         //PendingWeapon = wpn;
4283         SetWeapon(wpn);
4284         BringUpWeapon();
4285       }
4286     } else if (Klass == 'BDW_Shotgun' || (Klass == 'Shotgun' && FindClass('BDW_Shotgun'))) {
4287       wpn.AmmoGive2 = 8;
4288     } else if (Klass == 'FragfireGun' || (Klass == 'Shotgun' && FindClass('FragfireGun'))) {
4289       //TODO: check ammo type? let's hope nobody else has FragfireGun like that
4290       if (wpn.AmmoGive1 == 0 && wpn.AmmoGive2 == 40) {
4291         fixAmmoGive = 80;
4292       }
4293     }
4294     int oldAmmoGive = (wpn ? wpn.AmmoGive1 : 0);
4295     if (wpn && fixAmmoGive >= 0) wpn.AmmoGive1 = fixAmmoGive;
4296     act.TouchSpecial(EntityEx(MO), true);
4297     if (wpn) wpn.AmmoGive1 = oldAmmoGive;
4298     if (wpn && Klass == 'BDW_Rifle' && ReadyWeapon && ReadyWeapon.bWimpyWeapon && (!PendingWeapon || PendingWeapon.bWimpyWeapon)) {
4299       if (ReadyWeapon != wpn) {
4300         //PendingWeapon = wpn;
4301         SetWeapon(wpn);
4302         BringUpWeapon();
4303       }
4304     }
4305     return true;
4306   } else {
4307     return false;
4308   }
4312 //===========================================================================
4314 //  ShouldRemovePistol
4316 //  this is virtual, so mods can override it
4318 //===========================================================================
4319 bool ShouldRemovePistol () {
4320   Inventory inv = EntityEx(MO).Inventory;
4321   while (inv) {
4322     Weapon Wpn = Weapon(inv);
4323     inv = inv.Inventory;
4324     if (Wpn) {
4325       class!Weapon wc = class!Weapon(Wpn.Class);
4326       while (wc) {
4327         if (GetClassName(wc) == 'BDW_Rifle') return true;
4328         wc = class!Weapon(GetClassParent(wc));
4329       }
4330     }
4331   }
4332   return false;
4336 //===========================================================================
4338 //  RemovePistol
4340 //  this is virtual, so mods can override it
4342 //===========================================================================
4343 void RemovePistol () {
4344   Inventory inv = EntityEx(MO).Inventory;
4345   while (inv) {
4346     Weapon Wpn = Weapon(inv);
4347     inv = inv.Inventory;
4348     if (Wpn) {
4349       //print("WPN: %C", Wpn);
4350       switch (GetClassName(Wpn.Class)) {
4351         case 'Pistol':
4352         case 'PerkPistol':
4353           EntityEx(MO).RemoveInventory(Wpn);
4354           delete Wpn;
4355           break;
4356       }
4357     }
4358   }
4362 //===========================================================================
4364 //  AddDefaultInventory
4366 //===========================================================================
4367 void AddDefaultInventory () {
4368   HexenArmor HArmor = Level.SpawnEntityChecked(class!HexenArmor, HexenArmor, default, default, default, AllowReplace:false);
4369   if (HArmor) {
4370     HArmor.AttachToOwner(EntityEx(MO));
4371     HArmor.Slots[4] = PlayerPawn(MO).HexenArmor[0];
4372     HArmor.SlotsIncrement[0] = PlayerPawn(MO).HexenArmor[1];
4373     HArmor.SlotsIncrement[1] = PlayerPawn(MO).HexenArmor[2];
4374     HArmor.SlotsIncrement[2] = PlayerPawn(MO).HexenArmor[3];
4375     HArmor.SlotsIncrement[3] = PlayerPawn(MO).HexenArmor[4];
4376   }
4378   BasicArmor BArmor = Level.SpawnEntityChecked(class!BasicArmor, BasicArmor, default, default, default, AllowReplace:false);
4379   if (BArmor) {
4380     BArmor.Amount = 0;
4381     BArmor.AttachToOwner(EntityEx(MO));
4382   }
4384   //print("AddDefaultInventory: MO=`%C`", MO);
4385   foreach (auto i; 0..EntityEx(MO).DropItemList.length) {
4386     class!EntityEx tp = class!EntityEx(EntityEx(MO).DropItemList[i].Type);
4387     class!EntityEx tprepl = none;
4388     if (tp) tprepl = class!Inventory(GetClassReplacement(tp));
4389     //printdebug("  i=%d; type=`%C`(%C); name=`%s`; amount=%s; chance=%s", i, tp, tprepl, EntityEx(MO).DropItemList[i].TypeName, EntityEx(MO).DropItemList[i].Amount, EntityEx(MO).DropItemList[i].Chance);
4390     if (!tprepl) continue;
4391     //printdebug("  i=%d; type=`%C`(%C); name=`%s`; amount=%s; chance=%s", i, tp, tprepl, EntityEx(MO).DropItemList[i].TypeName, EntityEx(MO).DropItemList[i].Amount, EntityEx(MO).DropItemList[i].Chance);
4392     Inventory Item = Inventory(Level.Spawn(tprepl, default, default, default, AllowReplace:false));
4393     if (Item) {
4394       Item.bIgnoreSkill = true;
4395       int amount = EntityEx(MO).DropItemList[i].Amount;
4396       if (amount > 0) Item.Amount = amount; else if (amount == -666) Item.Amount = 0;
4397       if (Weapon(Item)) {
4398         // for better control empty weapon's ammo
4399         Weapon(Item).AmmoGive1 = 0;
4400         Weapon(Item).AmmoGive2 = 0;
4401       }
4402       if (!Item.TryPickup(EntityEx(MO))) {
4403         Item.Destroy();
4404       } else if (Weapon(Item) && Weapon(Item).CheckAmmo(Weapon::FIRE_Either, AutoSwitch:false)) {
4405         SetWeapon(Weapon(Item));
4406         Weapon(Item).bBobDisabled = true;
4407         Weapon(Item).bBobFrozen = false; // just in case
4408       }
4409     }
4410   }
4414 //==========================================================================
4416 //  PlayerReborn
4418 //  Called after a player dies almost everything is cleared and initialised
4420 //==========================================================================
4421 void PlayerReborn () {
4422   // clear player struct
4423   DoClearPlayer();
4425   PClass = BaseClass;
4427   // set initial data
4428   ResetPlayerOnSpawn(keepPlayerState:true);
4429   bUseDown = true; // don't do anything immediately
4430   bAttackDown = true;
4431   bAltAttackDown = false;
4432   bReloadQueued = false;
4433   bReloadDown = false;
4434   bZoomDown = false;
4435   bButton5Down = false;
4436   bButton6Down = false;
4437   bButton7Down = false;
4438   bButton8Down = false;
4439   ResetWeaponReloadRefire();
4440   ResetWeaponActionFlags();
4441   PlayerState = PST_LIVE;
4442   Health = GetRebornHealth();
4446 //==========================================================================
4448 //  DoClearPlayer
4450 //==========================================================================
4451 void DoClearPlayer () {
4452   ClearPlayer();
4456 //==========================================================================
4458 //  PlayerBeforeExitMap
4460 //  Called when a player completes a level, but before going to imis.
4461 //  Not called for intermissions.
4463 //==========================================================================
4464 override void PlayerBeforeExitMap () {
4465   ClearSubSeenInfo();
4466   if (GetCvarB('k8HealthAccum_NewMapHeal')) {
4467     int healthmax = GetMaxHealth();
4468     int oldhealth = Health;
4469     if (healthmax > 0 && oldhealth < healthmax && k8HealthAccum_Amount > 0) {
4470       Health = min(oldhealth+k8HealthAccum_Amount, healthmax);
4471       if (Health > oldhealth) {
4472         if (MO) MO.Health = Health;
4473         k8HealthAccum_Amount -= Health-oldhealth; // just in case
4474       }
4475     }
4476   }
4480 //==========================================================================
4482 //  PlayerExitMap
4484 //  Called when a player completes a level.
4486 //==========================================================================
4487 override void PlayerExitMap (bool clusterChange) {
4488   ClearSubSeenInfo();
4490   //printdebug("%C: *** exiting map *** (clusterChange=%B)", self, clusterChange);
4492   // moved between hubs, clamp inventory amount
4493   if (clusterChange && !Level.bClusterHub && !Level.bKeepFullInventory) {
4494     for (Inventory Item = EntityEx(MO).Inventory; Item; Item = Item.Inventory) {
4495       if (Item.bInvBar && Item.Amount > Item.InterHubAmount) {
4496         //printdebug("%C: clamping inventory item `%C` from %s to %s", self, Item, Item.Amount, Item.InterHubAmount);
4497         Item.Amount = Item.InterHubAmount;
4498       }
4499     }
4500   }
4502   // strip all non-persistent powers
4503   for (Inventory Item = EntityEx(MO).Inventory; Item; ) {
4504     Inventory Next = Item.Inventory;
4505     if (Powerup(Item)) {
4506       if (Level.Game.deathmatch || (!Item.bPersistentPower && (clusterChange || !Item.bHubPower))) {
4507         //printdebug("%C: removing powerup `%C`", self, Item);
4508         Item.Destroy();
4509       }
4510     }
4511     Item = Next;
4512   }
4514   if (clusterChange) {
4515     // entering new cluster
4516     // some items are stripped
4517     for (Inventory Item = EntityEx(MO).Inventory; Item; ) {
4518       Inventory Next = Item.Inventory;
4519       if (Item.bInterHubStrip /*&& Item.InterHubAmount < 1*/) {
4520         //printdebug("%C: removing item `%C` due to `bInterHubStrip`", self, Item);
4521         Item.Destroy();
4522       }
4523       Item = Next;
4524     }
4525   }
4527   if (MorphTime) {
4528     Weapon wpn = Weapon(Actor(MO).Tracer);
4529     if (wpn) {
4530       wpn.bBobDisabled = true;
4531       wpn.bBobFrozen = false; // just in case
4532     }
4533     SetWeapon(wpn); // restore weapon
4534     MorphTime = 0.0;
4535   }
4537   MO.Angles.pitch = 0.0;
4538   MO.RenderStyle = Entity::STYLE_Normal;
4539   MO.Alpha = 1.0;
4540   EntityEx(MO).bShadow = false; // cancel invisibility
4541   ExtraLight = 0; // cancel gun flashes
4542   FixedColormap = 0; // cancel ir gogles
4543   CShift = 0;
4544   DamageFlash = 0.0; // no palette changes
4545   DamageFlashType = '';
4546   BonusFlash = 0.0;
4547   PoisonCount = 0;
4548   BlendR = 0.0;
4549   BlendG = 0.0;
4550   BlendB = 0.0;
4551   BlendA = 0.0;
4552   Rain1 = none;
4553   Rain2 = none;
4554   //k8: just in case
4555   ResetWeaponReloadRefire();
4556   ResetWeaponActionFlags();
4557   //bDisableWeaponSwitch = false;
4558   bForceCrouchingDown = 0;
4559   LocalQuakeHappening = vector(0, 0, 0);
4563 //==========================================================================
4565 //  InventoryLeft
4567 //==========================================================================
4568 void InventoryLeft () {
4569   if (!bInventoryAlwaysOpen) {
4570     if (!InventoryTime) {
4571       InventoryTime = 5.0;
4572       return;
4573     }
4574     InventoryTime = 5.0;
4575   }
4577   if (InvPtr) {
4578     Inventory Prev = InvPtr.PrevInv();
4579     if (Prev) {
4580       InvPtr = Prev;
4581       AdjustInvFirst();
4582     }
4583   }
4587 //==========================================================================
4589 //  InventoryRight
4591 //==========================================================================
4592 void InventoryRight () {
4593   if (!bInventoryAlwaysOpen) {
4594     if (!InventoryTime) {
4595       InventoryTime = 5.0;
4596       return;
4597     }
4598     InventoryTime = 5.0;
4599   }
4601   if (InvPtr) {
4602     Inventory Next = InvPtr.NextInv();
4603     if (Next) {
4604       InvPtr = Next;
4605       AdjustInvFirst();
4606     }
4607   }
4611 //==========================================================================
4613 //  InventoryUse
4615 //==========================================================================
4616 void InventoryUse () {
4617   // flag to denote that it's okay to use an artifact
4618        if (InventoryTime) InventoryTime = 0.0;
4619   else if (InvPtr) EntityEx(MO).UseInventory(InvPtr);
4623 //==========================================================================
4625 //  InventoryTick
4627 //==========================================================================
4628 void InventoryTick (float deltaTime) {
4629   // turn inventory off after a certain amount of time
4630   if (InventoryTime) {
4631     InventoryTime -= deltaTime;
4632     if (InventoryTime <= 0.0) InventoryTime = 0.0;
4633   }
4634   if (ArtifactFlash) --ArtifactFlash;
4638 //==========================================================================
4640 //  AdjustInvFirst
4642 //==========================================================================
4643 void AdjustInvFirst () {
4644   Inventory Item;
4646   if (!InvPtr) {
4647     InvFirst = none;
4648     return;
4649   }
4651   // count how many items are following the current one, also make sure
4652   // that first is not after this one
4653   int NumFollowing = 0;
4654   for (Item = InvPtr.NextInv(); Item; Item = Item.NextInv()) {
4655     ++NumFollowing;
4656     if (InvFirst == Item) InvFirst = InvPtr;
4657   }
4659   int FirstOffs = 0;
4660   for (Item = InvPtr; Item && Item != InvFirst; Item = Item.PrevInv()) ++FirstOffs;
4662   while (FirstOffs > InvSize) {
4663     InvFirst = InvFirst.NextInv();
4664     --FirstOffs;
4665   }
4667   while (NumFollowing+FirstOffs < InvSize && InvFirst.PrevInv()) {
4668     InvFirst = InvFirst.PrevInv();
4669     ++FirstOffs;
4670   }
4674 //==========================================================================
4676 //  InventoryThrow
4678 //==========================================================================
4679 EntityEx InventoryThrow () {
4680   if (!InvPtr) return none;
4681   return EntityEx(MO).DropInventory(InvPtr);
4685 //==========================================================================
4687 //  UseFlyPower
4689 //==========================================================================
4690 void UseFlyPower () {
4691   PlayerUseArtifactType(ArtiFly);
4695 //==========================================================================
4697 //  PlayerUseArtifactType
4699 //==========================================================================
4700 void PlayerUseArtifactType (class!Inventory arti) {
4701   Inventory Item = EntityEx(MO).FindInventory(arti);
4702   if (Item) EntityEx(MO).UseInventory(Item);
4706 //==========================================================================
4708 //  PlayerNextArtifact
4710 //==========================================================================
4711 void PlayerNextArtifact () {
4712   if (!InvPtr) return;
4713   Inventory NewPtr = InvPtr.PrevInv();
4714   if (!NewPtr) {
4715     NewPtr = InvPtr;
4716     while (NewPtr.NextInv()) NewPtr = NewPtr.NextInv();
4717   }
4718   InvPtr = NewPtr;
4719   AdjustInvFirst();
4723 //==========================================================================
4725 //  DeathPlayerTick
4727 //  Fall on your face when dying. Decrease POV height to floor height.
4729 //==========================================================================
4730 void DeathPlayerTick (float deltaTime) {
4731   scope(exit) LastWaterLevel = (MO ? MO.WaterLevel : 0);
4733   // just in case
4734   k8HealthAccum_LastRegenTime = 0;
4735   k8HealthAccum_LastBoostTime = 0;
4736   ResetWeaponReloadRefire();
4737   ResetWeaponActionFlags();
4738   bDisableWeaponSwitch = false;
4739   bForceCrouchingDown = 0;
4741   ClearSubSeenInfo();
4742   MovePsprites(deltaTime);
4744   if (MO.WaterLevel > 1) MO.Velocity.z = -60.0; // drift towards bottom
4746   onground = (MO.Origin.z <= MO.FloorZ);
4748   if (PlayerChunk(MO)) {
4749     // flying bloody skull or flying ice chunk
4750     ViewHeight = 6.0;
4751     DeltaViewHeight = 0.0;
4752     //damagecount = 20;
4753     if (onground) {
4754 #ifdef FIXME
4755       if (lookdir < 60) {
4756         int lookDelta = (60-lookdir)/8;
4757              if (lookDelta < 1 && (level->tictime&1)) lookDelta = 1;
4758         else if (lookDelta > 6) lookDelta = 6;
4759         lookdir += lookDelta;
4760       }
4761 #endif
4762     }
4763   } else if (nameicmp(Actor(MO).DamageType, 'Ice') != 0) {
4764     // fall to ground (if not frozen)
4765     DeltaViewHeight = 0.0;
4766     if (ViewHeight > 6.0) ViewHeight -= 35.0*deltaTime;
4767     if (ViewHeight < 6.0) ViewHeight = 6.0;
4768 #ifdef FIXME
4769          if (lookdir > 0) lookdir -= 6;
4770     else if (lookdir < 0) lookdir += 6;
4771     if (abs(lookdir) < 6) lookdir = 0;
4772 #endif
4773   }
4774   CalcHeight(deltaTime);
4776   if (Attacker && Attacker != MO) {
4777     // watch killer
4778     float delta;
4779     int dir = EntityEx(MO).FaceActor(EntityEx(Attacker), delta);
4780     if (delta < 10.0) {
4781       // looking at killer, so fade damage and poison counters
4782       if (DamageFlash) {
4783         DamageFlash -= deltaTime;
4784         if (DamageFlash <= 0.0) DamageFlash = 0.0;
4785       }
4786       if (PoisonCount) --PoisonCount;
4787     }
4788     delta = fmin(5.0, delta/8.0);
4789     if (dir) {
4790       // turn clockwise
4791       MO.Angles.yaw += delta;
4792     } else {
4793       // turn counter clockwise
4794       MO.Angles.yaw -= delta;
4795     }
4796   } else if (DamageFlash) {
4797     DamageFlash -= deltaTime;
4798     if (DamageFlash <= 0.0) DamageFlash = 0.0;
4799   } else if (PoisonCount) {
4800     --PoisonCount;
4801   }
4803   if (CheckForRespawn(deltaTime)) {
4804     InvPtr = none;
4805     InvFirst = none;
4806     PlayerState = PST_REBORN;
4807   }
4811 //==========================================================================
4813 //  CheckForRespawn
4815 //==========================================================================
4816 bool CheckForRespawn (float deltaTime) {
4817   return !!(Buttons&BT_USE);
4821 //==========================================================================
4823 //  checkDoHealthAccumBoost
4825 //  this should be called after health < maxhealth checked
4826 //  k8HealthAccum_Amount should be checked and set too
4828 //==========================================================================
4829 bool checkDoHealthAccumBoost () {
4830   if (k8HealthAccum_Amount < 1 || Health < 1) return false;
4831   // can boost?
4832   int boostPoints = GetCvarI('k8HealthAccum_BoostPoints');
4833   if (!boostPoints) return false;
4834   // hit boost limit?
4835   int boostLow = GetCvarI('k8HealthAccum_BoostLow');
4836   if (Health >= boostLow) return false;
4837   // boost cooldowned?
4838   float boostCooldownTime = fmax(0, GetCvarF('k8HealthAccum_BoostCooldown'));
4839   if (k8HealthAccum_LastBoostTime+boostCooldownTime > MO.XLevel.Time) return false;
4840   // calculate boost amount
4841   int newh = (boostPoints < 0 ? -boostPoints : Health+boostPoints);
4842   if (newh <= Health) return false;
4843   int relh = min(k8HealthAccum_Amount, newh-Health);
4844   if (relh < 1) return false; // just in case
4845   // perform health boost
4846   if (GetCvarB('k8HealthAccum_MessagesBoost')) {
4847     if (boostPoints < 0) cprint("Health boosted to %d", Health+relh); else cprint("Health boosted by %d", relh);
4848   }
4849   k8HealthAccum_LastBoostTime = MO.XLevel.Time;
4850   k8HealthAccum_Amount -= relh;
4851   Health += relh;
4852   MO.Health = Health; //k8: we need this for UI
4853   return true;
4857 //==========================================================================
4859 //  checkDoHealthAccumRegen
4861 //  this should be called after health < maxhealth checked
4862 //  k8HealthAccum_Amount should be checked and set too
4864 //==========================================================================
4865 bool checkDoHealthAccumRegen () {
4866   if (k8HealthAccum_Amount < 1 || Health < 1) return false;
4867   // can regen?
4868   int regenPoints = GetCvarI('k8HealthAccum_RegenPoints');
4869   if (regenPoints < 1) return false;
4870   // hit regen limit?
4871   int healthmax = GetMaxHealth();
4872   if (Health >= healthmax) return false;
4873   int regenLow = GetCvarI('k8HealthAccum_RegenLow');
4874   if (regenLow > 0 && Health >= regenLow) return false; // player is not hurt enough
4875   // regen cooldowned?
4876   float regenRateTime = fmax(0, GetCvarF('k8HealthAccum_RegenRate'));
4877   /*
4878   print("k8HealthAccum_Amount=%s; Health=%s; rp=%s; max=%s; rate=%s; last=%s; time=%s",
4879     k8HealthAccum_Amount, Health, regenPoints, healthmax, regenRateTime, k8HealthAccum_LastRegenTime, MO.XLevel.Time);
4880   */
4881   if (k8HealthAccum_LastRegenTime+regenRateTime > MO.XLevel.Time) return false;
4882   // check for boost cooldown too
4883   float boostCooldownTime = fmax(0, GetCvarF('k8HealthAccum_BoostCooldown'));
4884   if (k8HealthAccum_LastBoostTime+boostCooldownTime > MO.XLevel.Time) return false;
4885   // calculate regen amount
4886   int newh = Health+regenPoints;
4887   int relh = min(k8HealthAccum_Amount, newh-Health);
4888   if (relh < 1) return false; // just in case
4889   // perform health regen
4890   if (GetCvarB('k8HealthAccum_MessagesRegen')) cprint("Health regenerated by %d", relh);
4891   k8HealthAccum_LastRegenTime = MO.XLevel.Time;
4892   k8HealthAccum_Amount -= relh;
4893   Health += relh;
4894   MO.Health = Health; //k8: we need this for UI
4895   return true;
4899 //==========================================================================
4901 //  doHealthAccumHealthPickup
4903 //  returns `true` if Health Accumulator (or player) consumed the item
4904 //  must increase player's health if necessary
4905 //  should never be called with `none`
4907 //==========================================================================
4908 bool doHealthAccumHealthPickup (Health item, EntityEx toucher) {
4909   if (!item) return false; // meh, just in case
4910   int amount = int(float(item.Amount)*Level.World.GetHealthFactor());
4911   int itemhealthlimit = item.MaxAmount;
4912   // is this small health bonus (like bottle)?
4913   if (itemhealthlimit) {
4914     // you should be able to pick up health bonuses even at full health
4915     // note that health bonuses cannot be accumulated
4916     // also, don't do anything if current health is higher than the limit
4917     //printdebug("%C: Health=%s; itemhealthlimit=%s; always=%B", item, Health, itemhealthlimit, item.bAlwaysPickup);
4918     if (Health >= itemhealthlimit) return item.bAlwaysPickup; // eat if it should be always eaten
4919     // increase health
4920     Health = min(Health+amount, itemhealthlimit);
4921     if (toucher) toucher.Health = Health;
4922     return true; // eaten
4923   }
4924   // normal health pickup
4925   int plrmaxhealth = GetMaxHealth();
4926   //print("Health=%s; healthmax=%s; amount=%s; accum=%s; acclimit=%s; accumon=%s", Health, plrmaxhealth, amount, k8HealthAccum_Amount, GetCvarI('k8HealthAccum_Max'), GetCvarB('k8HealthAccum_Enabled'));
4927   // too healthy?
4928   if (Health >= plrmaxhealth) {
4929     // accumulate everything
4930     if (!GetCvarB('k8HealthAccum_Enabled')) return false; // Health Accumulator is not active
4931     int accMax = max(0, GetCvarI('k8HealthAccum_Max'));
4932     if (!accMax) return false; // cannot pickup
4933     if (k8HealthAccum_Amount >= accMax) return false; // cannot pickup
4934     // do full accumulation
4935     // (don't bother checking, player tick will check and clamp HA amount for us)
4936     k8HealthAccum_Amount += amount;
4937     if (GetCvarB('k8HealthAccum_MessagesAccumed')) cprint("Accumulated %d health.", amount);
4938   } else {
4939     // we should heal the player, and accumulate the rest (if anything)
4940     int newh = min(plrmaxhealth, Health+amount);
4941     int relh = newh-Health;
4942     // accumulate leftovers
4943     if (relh < amount && GetCvarB('k8HealthAccum_Enabled')) {
4944       int accMax = max(0, GetCvarI('k8HealthAccum_Max'));
4945       if (accMax) {
4946         // (don't bother checking, player tick will check and clamp HA amount for us)
4947         k8HealthAccum_Amount += amount-relh;
4948         if (GetCvarB('k8HealthAccum_MessagesAccumed')) cprint("Accumulated %d health.", amount-relh);
4949       }
4950     }
4951     // heal player
4952     Health = newh;
4953     if (toucher) toucher.Health = Health;
4954   }
4955   // processed
4956   return true;
4960 //==========================================================================
4962 //  SimulatedPlayerTick
4964 //==========================================================================
4965 void SimulatedPlayerTick (float deltaTime) {
4966   assert(bAutonomousProxy); // just in case
4967   assert(!bIsBot); // the thing that should not be
4969   if (PlayerState == PST_DEAD) return;
4971   // copy view angles
4972   if (MO == Camera) MO.Angles = ViewAngles;
4974   // you can only press use while totally frozen
4975   if (bTotallyFrozen || (Level.bFrozen && !(Cheats&CF_TIMEFREEZE))) return;
4977   // move around
4978   // ReactionTime is used to prevent movement for a bit after a teleport (or water jump)
4979   if (Actor(MO).ReactionTime) {
4980     Actor(MO).ReactionTime = fmax(0.0, Actor(MO).ReactionTime-deltaTime);
4981     if (Actor(MO).bWaterJump) WaterJump();
4982   } else {
4983     if (MO.WaterLevel > 1) {
4984       WaterMove(deltaTime);
4985     } else {
4986       LastWaterLevel = 0; // yes, zero
4987       MovePlayer(deltaTime);
4988       // allow to jump out of the water if we aren't submerged too deep
4989       if (MO.WaterLevel) CheckWaterJump(asStep:true);
4990     }
4991   }
4993   //printdebug("%C:000: vh=%s; movh=%s", self, ViewHeight, PlayerPawn(MO).ViewHeight);
4994   CalcHeight(deltaTime);
4995   //printdebug("%C:001: vh=%s; movh=%s", self, ViewHeight, PlayerPawn(MO).ViewHeight);
4997   if (MO.Sector) {
4998     PlayerProcessScrollSectors(deltaTime);
4999     //if (MO.Sector->special || MO.Sector->Damage) PlayerInSpecialSector(deltaTime);
5000   }
5002   /*
5003   if (MO.Velocity.z <= -35.0*35.0 && MO.Velocity.z >= -40.0*35.0 &&
5004       !MorphTime && MO.WaterLevel == 0 &&
5005       !GetSoundPlayingInfo(MO, GetSoundID('*falling')))
5006   {
5007     MO.PlaySound('*falling', CHAN_VOICE);
5008   }
5009   */
5013 //==========================================================================
5015 //  PlayerTick
5017 //==========================================================================
5018 override void PlayerTick (float deltaTime) {
5019   if (!MO) return;
5021   if (bIsClient) { SimulatedPlayerTick(deltaTime); return; }
5023   #if 0
5024   if (ViewMouseDeltaX || ViewMouseDeltaY || RawMouseDeltaX || RawMouseDeltaY) {
5025     printdebug("VMS: x=%s; y=%s  --  RMS: x=%s; y=%s", ViewMouseDeltaX, ViewMouseDeltaY, RawMouseDeltaX, RawMouseDeltaY);
5026   }
5027   bInverseMouseX = false;
5028   bInverseMouseY = false;
5029   bBlockMouseX = true;
5030   bBlockMouseY = true;
5031   ViewAngles.yaw = AngleMod360(ViewAngles.yaw-ViewMouseDeltaX);
5032   ViewAngles.pitch = fclamp(ViewAngles.pitch-ViewMouseDeltaY, -80, 80);
5033   #endif
5035   LastDeltaTime = deltaTime;
5037   // update current world tick for server
5038   // advance it, because it will be like that when sent
5039   GameTime = MO.XLevel.Time+deltaTime;
5041   bool isBot = bIsBot;
5043   if (isBot) {
5044     if (!Level.bFrozen && !(Cheats&CF_TIMEFREEZE)) BotTick(deltaTime);
5045   }
5047   //printdebug("PlayerTick(%C): Health=%s; MO.Health=%s", self, Health, MO.Health);
5049   if (!isBot && Level.Game.netgame && !Level.Game.deathmatch) {
5050     if (lastSubSector != MO.SubSector) AddSeenSubsector();
5051   }
5053   // health regeneration powerup
5054   if (MO.XLevel.TicTime%(3*35) == 0 && Health > 0 && LastRegenTicTime != MO.XLevel.TicTime) {
5055     LastRegenTicTime = MO.XLevel.TicTime;
5056     PowerRegeneration rgp = EntityEx(MO).FindRegenerationPowerup();
5057     if (rgp) {
5058       rgp.Regenerate(EntityEx(MO));
5059     } else if (Cheats&CF_REGENERATION) {
5060       if (EntityEx(MO).GiveBody(5) && !GetSoundPlayingInfo(MO, GetSoundID('*regenerate'))) {
5061         MO.PlaySound('*regenerate', /*CHAN_VOICE*/CHAN_BODY);
5062       }
5063     }
5064   }
5066   // chainsaw (and other "pull-in") attacks set this flag
5067   // this code does "pulling in"
5068   if (Actor(MO).bJustAttacked) {
5069     ForwardMove = 100.0;
5070     SideMove = 0.0;
5071     Actor(MO).bJustAttacked = false;
5072   }
5074   // you can only press use while totally frozen
5075   if (bTotallyFrozen || (Level.bFrozen && !(Cheats&CF_TIMEFREEZE))) {
5076     Buttons &= BT_USE;
5077     Impulse = 0;
5078     ViewAngles = MO.Angles;
5079     ForwardMove = 0.0;
5080     SideMove = 0.0;
5081     FlyMove = 0.0;
5082     ResetWeaponReloadRefire(); // no reload too
5083     ResetWeaponActionFlags();
5084   } else if (bFrozen) {
5085     ForwardMove = 0.0;
5086     SideMove = 0.0;
5087     FlyMove = 0.0;
5088   }
5090   // update map timer
5091   WorldTimer += deltaTime;
5093   // process inventory
5094   InventoryTick(deltaTime);
5096   // dead player doesn't have to perform a lot of actions ;-)
5097   if (PlayerState == PST_DEAD) {
5098     DeathPlayerTick(deltaTime);
5099     return;
5100   }
5102   // Health Accumulator
5103   if (GetCvarB('k8HealthAccum_Enabled')) {
5104     // do not reset accumulator when player is dead
5105     if (PlayerState == PST_LIVE) {
5106       int accMax = GetCvarI('k8HealthAccum_Max');
5107       //int accAmount = 0;
5108       if (accMax > 0) {
5109         //k8HealthAccum_LastRegenTime = 0;
5110         //k8HealthAccum_LastBoostTime = 0;
5111         k8HealthAccum_Amount = max(0, min(k8HealthAccum_Amount, accMax)); // sanitize
5112         //print("k8HealthAccum_Amount=%s; bt=%s; rt=%s; lt=%s; tb=%s; tr=%s", k8HealthAccum_Amount, MO.XLevel.Time-k8HealthAccum_LastBoostTime, MO.XLevel.Time-k8HealthAccum_LastRegenTime, MO.XLevel.Time, k8HealthAccum_LastBoostTime, k8HealthAccum_LastRegenTime);
5113         if (!checkDoHealthAccumBoost()) checkDoHealthAccumRegen();
5114       } else {
5115         k8HealthAccum_Amount = 0;
5116       }
5117     }
5118   } else {
5119     k8HealthAccum_Amount = 0;
5120   }
5122   // "elven gift" message
5123   if (k8ElvenGiftMessageTime <= 0) {
5124     k8ElvenGiftMessageTime += deltaTime;
5125     if (k8ElvenGiftMessageTime >= 0) {
5126       k8ElvenGiftMessageTime = 666;
5127       ShowElvenGiftMessage();
5128     }
5129   }
5131   // "elven senses" message
5132   if (k8BossesDetected <= 0) {
5133     k8BossesDetected += deltaTime;
5134     if (k8BossesDetected >= 0) {
5135       k8BossesDetected = 666;
5136       PerformBossDetection();
5137     }
5138   }
5140   // process buttons (reset "down" flags)
5141   if (Buttons&BT_RELOAD) {
5142     // set "reload queued" only if the button was released and pressed again
5143     if (!bReloadDown) bReloadQueued = true;
5144     bReloadDown = true;
5145   } else {
5146     bReloadDown = false;
5147   }
5149   if (!(Buttons&BT_ATTACK)) bAttackDown = false;
5150   if (!(Buttons&BT_ALT_ATTACK)) bAltAttackDown = false;
5151   if (!(Buttons&BT_ZOOM)) bZoomDown = false;
5152   if (!(Buttons&BT_BUTTON_5)) bButton5Down = false;
5153   if (!(Buttons&BT_BUTTON_6)) bButton6Down = false;
5154   if (!(Buttons&BT_BUTTON_7)) bButton7Down = false;
5155   if (!(Buttons&BT_BUTTON_8)) bButton8Down = false;
5157   // copy view angles
5158   if (MO == Camera) MO.Angles = ViewAngles;
5160   // jump time cooldown
5161   if (JumpTime) JumpTime = fmax(0.0, JumpTime-deltaTime);
5163   // call specialized ticker for morped player
5164   if (MorphTime) PlayerPawn(MO).MorphPlayerThink();
5167   // remember 3d pobj we are standing on
5168   {
5169     auto pawn = PlayerPawn(MO);
5170     if (pawn) {
5171       // fix current polyobject
5172       //polyobj_t *oldpo = pawn.lastStand3DPObj;
5173       pawn.lastStand3DPObj = nullptr;
5174       pawn.lastStand3DPObjVel = vector(0, 0);
5175       if (MO.Sector && MO.Sector.ownpobj) {
5176         polyobj_t *po = MO.Sector.ownpobj;
5177         if (po.posector) {
5178           // are we standing on it?
5179           float dz = MO.Origin.z-po.poceiling.minz;
5180           if (dz >= 0.0 && dz < 0.1) {
5181             pawn.lastStand3DPObjVel = PolyobjThinker.CalcSpeedVector(po);
5182             while (po.polinkprev) po = po.polinkprev;
5183             pawn.lastStand3DPObj = po;
5184           }
5185         }
5186       }
5188       #ifdef DEBUG_3DPOBJ_JUMPS
5189       if (oldpo != pawn.lastStand3DPObj) {
5190         printdebug("%C: changed current pobj from %s to %s (poz=%s; z=%s)", pawn, (oldpo ? oldpo.tag : -1), (pawn.lastStand3DPObj ? pawn.lastStand3DPObj.tag : -1),
5191           (pawn.lastStand3DPObj ? pawn.lastStand3DPObj.poceiling.minz : float.nan), MO.Origin.z);
5192       }
5193       #endif
5195       // check for landing
5196       if (pawn.lastJump3DPObj) {
5197         // on ground?
5198         if (MO.Origin.z <= MO.FloorZ || EntityEx(MO).bOnMobj) {
5199           // yes, subtract velocity (if landed on the same pobj)
5200           #ifdef DEBUG_3DPOBJ_JUMPS
5201           if (pawn.lastStand3DPObj != pawn.lastJump3DPObj) {
5202             printdebug("%C: landed from pobj %s to %s (FloorZ=%s; z=%s, onmobj=%B)", pawn, pawn.lastJump3DPObj.tag, (pawn.lastStand3DPObj ? pawn.lastStand3DPObj.tag : -1),
5203               MO.FloorZ, MO.Origin.z, EntityEx(MO).bOnMobj);
5204           } else {
5205             printdebug("%C: landed on the same pobj %s; stvel=%s", pawn, pawn.lastJump3DPObj.tag, PolyobjThinker.CalcSpeedVector(pawn.lastJump3DPObj));
5206           }
5207           #endif
5208           if (pawn.lastStand3DPObj == pawn.lastJump3DPObj) {
5209             pawn.Velocity -= PolyobjThinker.CalcSpeedVector(pawn.lastJump3DPObj);
5210           }
5211           pawn.lastJump3DPObj = nullptr;
5212         }
5213       }
5214     }
5215   }
5218   // move around
5219   // ReactionTime is used to prevent movement for a bit after a teleport (or water jump)
5220   if (Actor(MO).ReactionTime) {
5221     Actor(MO).ReactionTime = fmax(0.0, Actor(MO).ReactionTime-deltaTime);
5222     if (Actor(MO).bWaterJump) WaterJump();
5223   } else {
5224     if (MO.WaterLevel > 1) {
5225       WaterMove(deltaTime);
5226     } else {
5227       LastWaterLevel = 0; // yes, zero
5228       MovePlayer(deltaTime);
5229       // allow to jump out of the water if we aren't submerged too deep
5230       if (MO.WaterLevel) CheckWaterJump(asStep:true);
5231     }
5232     if (EntityEx(MO).FindInventory(PowerSpeed) &&
5233         !(Level.XLevel.TicTime&1) && Length(MO.Velocity) > 12.0*35.0)
5234     {
5235       SpawnSpeedEffect();
5236     }
5237   }
5239   // bobbing, and view height calculation
5240   CalcHeight(deltaTime);
5242   // scrollers and such
5243   if (MO.Sector) {
5244     PlayerProcessScrollSectors(deltaTime);
5245     if (MO.Sector->special || MO.Sector->Damage || SectorHas3DFloors(MO.Sector)) {
5246       PlayerInSpecialSector(deltaTime);
5247     }
5248   }
5250   // do not apply effects continuously on the same tick
5251   if (LastSectorDamageTic != Level.XLevel.TicTime) {
5252     PlayerOnSpecialFlat(Actor(MO).GetActorTerrain());
5253     PlayerInContents(deltaTime);
5254     LastSectorDamageTic = Level.XLevel.TicTime;
5255   }
5257   // falling sound
5258   if (MO.Velocity.z <= -35.0*35.0 && MO.Velocity.z >= -40.0*35.0 &&
5259       !MorphTime && MO.WaterLevel == 0 &&
5260       !GetSoundPlayingInfo(MO, GetSoundID('*falling')))
5261   {
5262     MO.PlaySound('*falling', CHAN_VOICE);
5263   }
5265   // check for weapon change (and other impulse commands)
5266   if (Impulse) PlayerImpulse();
5268   // cheater "superbullet" shots
5269   if (Buttons&BT_SUPERBULLET) {
5270     float lt = MO.XLevel.Time;
5271     if (lt >= k8NextSuperBulletTime) {
5272       k8NextSuperBulletTime = lt+GetCvarF('k8SuperBulletCooldown');
5273       Actor(MO).FireSuperBullet();
5274     }
5275   }
5277   // use action
5278   if (Buttons&BT_USE) {
5279     if (!bUseDown) {
5280       float ur, utr;
5281       GetUseRanges(out ur, out utr);
5282       EntityEx(MO).UseLines(/*DEFAULT_USERANGE*/ur, /*DEFAULT_USETHINGRANGE*/utr, '*usefail');
5283       bUseDown = true;
5284     }
5285   } else {
5286     //if (bUseDown) print("***USE GOES UP!");
5287     bUseDown = false;
5288   }
5290   /* done in `MovePsprites()`
5291   if (!ReadyWeapon && PendingWeapon && !bDisableWeaponSwitch && bWeaponAllowSwitch) {
5292     SetWeapon(PendingWeapon);
5293     BringUpWeapon();
5294   }
5295   */
5297   // morph counter
5298   if (MorphTime) {
5299     if (ChickenPeck) {
5300       // chicken attack counter
5301       ChickenPeck -= 3;
5302     }
5303     MorphTime -= deltaTime;
5304     if (MorphTime <= 0.0) {
5305       // attempt to undo the chicken/pig
5306       MorphTime = 0.0;
5307       UndoPlayerMorph(false, self);
5308     }
5309   }
5311   // cycle psprites
5312   MovePsprites(deltaTime);
5314   // poison/damage/etc. counters
5315   if (PoisonCount && Level.XLevel.Time-LastPoisonTime >= 0.5) {
5316     PoisonCount -= 5;
5317     if (PoisonCount < 0) PoisonCount = 0;
5318     LastPoisonTime = Level.XLevel.Time;
5319     Actor(MO).PoisonDamage(Poisoner, Poisoner, 1, true);
5320   }
5322   if (DamageFlash) {
5323     DamageFlash -= deltaTime;
5324     if (DamageFlash <= 0.0) DamageFlash = 0.0;
5325   }
5327   if (BonusFlash) {
5328     BonusFlash -= deltaTime;
5329     if (BonusFlash <= 0.0) BonusFlash = 0.0;
5330   }
5332   if (HazardTime) {
5333     HazardTime -= deltaTime;
5334     if (HazardTime <= 0.0) HazardTime = 0.0;
5335     if (Level.XLevel.Time-LastHazardTime >= 32.0/35.0 && HazardTime > 16.0) {
5336       LastHazardTime = Level.XLevel.Time;
5337       Actor(MO).Damage(none, none, 5/*, spawnBlood:true*/);
5338     }
5339   }
5341   // zoom the player's FOV
5342   float desired = DesiredFOV; // default player FOV
5343   // adjust FOV using on the currently held weapon
5344   if (PlayerState != PST_DEAD && // no adjustment while dead
5345       ReadyWeapon && // no adjustment if no weapon
5346       ReadyWeapon.FOVScale != 0.0) // no adjustment if the adjustment is zero
5347   {
5348     // a negative scale is used to prevent G_AddViewAngle/G_AddViewPitch
5349     // from scaling with the FOV scale
5350     desired *= fabs(ReadyWeapon.FOVScale);
5351   }
5353   if (FOV != desired) {
5354     if (fabs(FOV-desired) < 7.0) {
5355       FOV = desired;
5356     } else {
5357       float zoom = FMax(7.0, fabs(FOV-desired)*0.025);
5358       if (FOV > desired) FOV = FOV-zoom; else FOV = FOV+zoom;
5359     }
5360     if (int(FOV) == int(DesiredFOV)) {
5361       ClientFOV(0);
5362     } else {
5363       ClientFOV(FOV);
5364     }
5365   }
5367   if (!isBot) HealthBarProcessor();
5369   // flashlight code
5370   if (Buttons&BT_FLASHLIGHT) {
5371     if (!bFlashlightButtonDown) {
5372       bFlashlightButtonDown = true;
5373       bFlashlightOn = !bFlashlightOn;
5374     }
5375   } else {
5376     bFlashlightButtonDown = false;
5377   }
5379   //ProcessFlashligh(); // nope, it is done in `ClientTick()`
5381   if (bForceCrouchingDown && bForceCrouchingDown <= Level.XLevel.TicTime) {
5382     //printdebug("%C: crouch reset!", self);
5383     bForceCrouchingDown = 0;
5384   }
5386   // do it here, not in physics code, becase physics code is already overloaded with shit
5387   // this is purely cosmetic feature, so i think it's ok
5388   LeaveBootPrints(deltaTime);
5390   ProduceFootSteps(deltaTime);
5392   #if 0
5393   // debug
5394   if (MO.Sector && MO.Sector.ownpobj) {
5395     polyobj_t *po = MO.Sector.ownpobj;
5396     if (po.posector) {
5397       // this is 3d pobj
5398       TVec vel = PolyobjThinker.CalcSpeedVector(po);
5399       printdebug("PLAYER: 3d pobj #%s; velocity=%s", po.tag, vel);
5400     }
5401   }
5402   #endif
5406 //==========================================================================
5408 //  ProcessFlashligh
5410 //==========================================================================
5411 void ProcessFlashligh () {
5412   if (!bFlashlightOn || !MO) return;
5413   float flradius = GetCvarF('k8_flashlight_distance');
5414   int flcolor = GetCvarI('k8_flashlight_color')&0xff_ff_ff;
5415   if (flcolor && flradius > 0) {
5416     TVec dir;
5417     TVec florg;
5418     if (bIsClient) {
5419       // network client
5420       florg = ViewOrg;
5421       AngleVector(ViewAngles, out dir);
5422     } else {
5423       AngleVector(MO.Angles, out dir);
5424       florg = MO.Origin;
5425       //florg.z += MO.Height*0.5-MO.FloorClip;
5426       //florg.z += GetAttackZOfs();
5427       florg.z += ViewHeight;
5428     }
5429     dlight_t *fl = MO.AllocDlight(MO, florg, flradius, FlashlightLightId);
5430     if (fl) {
5431       fl.coneDirection = dir;
5432       fl.coneAngle = GetCvarF('k8_flashlight_angle');
5433       fl.color = flcolor|0xff_00_00_00;
5434       fl.die = MO.XLevel.Time+(2.0/35.0);
5435       fl.type = DynamicLight::DLTYPE_Spot;
5436       //fl.minlight = 64;
5437       fl.bNoShadow = !GetCvarB('k8_flashlight_shadows');
5438       fl.bPlayerLight = true;
5439     }
5440   }
5444 //==========================================================================
5446 //  SetViewPos
5448 //  called from main world thinker after all thinkers were called
5450 //==========================================================================
5451 override void SetViewPos () {
5452   if (!MO) {
5453     //printdebug("%C: fuck", self);
5454     if (!Camera || Camera.isDestroyed) {
5455       Error("this mod's player pawn code is totally fucked (dead player pawn)");
5456     }
5457   }
5459   // just in a case camera entity has been destroyed
5460   if (!Camera) Camera = MO;
5462   if (MO != Camera) {
5463     // don't do any height fixes if our camera is not a player pawn
5464     ViewOrg = Camera.Origin;
5465     ViewOrg.z += EntityEx(Camera).CameraHeight;
5466     ViewAngles = Camera.Angles;
5467   } else {
5468     // fix camera height to avoid "lift sinking" effect
5469     PostfixViewHeight();
5471     ViewOrg.x = MO.Origin.x;
5472     ViewOrg.y = MO.Origin.y;
5474     if (LocalQuakeHappening && !(GetCvarI('r_screen_shake_mode')&1)) {
5475       //TODO: implement A_QuakeEx flags!
5476       //float intensity = float(LocalQuakeHappening);
5477       if (LocalQuakeHappening.x) ViewOrg.x += (Random()-0.5)*(LocalQuakeHappening.x*4.0);
5478       if (LocalQuakeHappening.y) ViewOrg.y += (Random()-0.5)*(LocalQuakeHappening.y*4.0);
5479       if (LocalQuakeHappening.z) ViewOrg.z += (Random()-0.5)*(LocalQuakeHappening.z*4.0);
5480     }
5482     if (PlayerState != PST_DEAD) {
5483       ViewAngles = MO.Angles;
5484     } else {
5485       ViewAngles.yaw = MO.Angles.yaw;
5486       ViewAngles.pitch = MO.Angles.pitch;
5487     }
5489     if (MorphTime && ChickenPeck) {
5490       // set chicken attack view position
5491       float s, c;
5492       sincos(MO.Angles.yaw, out s, out c);
5493       ViewOrg.x += float(ChickenPeck)*c;
5494       ViewOrg.y += float(ChickenPeck)*s;
5495     }
5496   }
5498   PaletteFlash();
5500   ClientSetViewOrg(ViewOrg);
5502   if (Level.XLevel.Zones.length && Camera.Sector) {
5503     SoundEnvironment = Level.XLevel.Zones[Camera.Sector->Zone];
5504   } else {
5505     SoundEnvironment = 0;
5506   }
5508   if (!SoundEnvironment) {
5509     if (Camera.WaterLevel >= 3) {
5510       // under water
5511       SoundEnvironment = 0x1600;
5512     } else {
5513       // generic
5514       SoundEnvironment = 1;
5515     }
5516   }
5520 //==========================================================================
5522 //  DebugPutRotatingSpotlight
5524 //==========================================================================
5525 final void DebugPutRotatingSpotlight (TVec florg, float speed, float flradius, int clr, int id) {
5526   TAVec ag;
5527   ag.yaw = AngleMod360(MO.XLevel.Time*speed);
5528   TVec dir;
5529   AngleVector(ag, out dir);
5530   dlight_t *fl = MO.AllocDlight(MO, florg, flradius, 669+id);
5531   if (!fl) return;
5532   fl.coneDirection = dir;
5533   fl.coneAngle = 42.0f;
5534   fl.color = clr|0xff_00_00_00;
5535   fl.die = MO.XLevel.Time+(2.0/35.0);
5536   fl.type = DynamicLight::DLTYPE_Spot;
5537   //fl.minlight = 64;
5538   fl.bNoShadow = false;
5539   fl.bPlayerLight = true; // so it won't be rejected
5543 //==========================================================================
5545 //  DebugPutRotatingSpotlightHead
5547 //==========================================================================
5548 final void DebugPutRotatingSpotlightHead (TVec florg, float speed, float flradius, int clr, int id) {
5549   TAVec ag;
5550   ag.yaw = AngleMod360(MO.XLevel.Time*speed+id*120);
5551   TVec dir;
5552   AngleVector(ag, out dir);
5553   florg += dir*8;
5554   dlight_t *fl = MO.AllocDlight(MO, florg, flradius, 669+id);
5555   if (!fl) return;
5556   fl.coneDirection = dir;
5557   fl.coneAngle = 42.0f;
5558   fl.color = clr|0xff_00_00_00;
5559   fl.die = MO.XLevel.Time+(2.0/35.0);
5560   fl.type = DynamicLight::DLTYPE_Spot;
5561   //fl.minlight = 64;
5562   fl.bNoShadow = false;
5563   fl.bPlayerLight = true; // so it won't be rejected
5567 //==========================================================================
5569 //  ClientTick
5571 //==========================================================================
5572 override void ClientTick (float deltaTime) {
5573   bAutoAim = !!GetCvar('autoaim');
5574   //printdebug("%C: flashlight=%B", self, bFlashlightOn);
5575   //if (MO) printdebug("%C: va=%s; %C: a=%s", self, ViewAngles, MO, MO.Angles);
5576   ProcessFlashligh();
5577 #ifdef SPOTLIGHT_DISCO_TEST
5578   DebugPutRotatingSpotlight(vector(1100, -3270, 110), 100, 512, 0xff_ff_00_00, 0);
5579   DebugPutRotatingSpotlight(vector(990, -2990, 68), 100, 512, 0xff_00_ff_00, 1);
5580   DebugPutRotatingSpotlight(vector(588, -3226, 100), 100, 512, 0xff_ff_00_00, 2);
5581 #endif
5582 #ifdef SPOTLIGHT_DISCO_CROWN_TEST
5583   if (MO) {
5584     DebugPutRotatingSpotlightHead(MO.Origin+vector(0, 0, MO.Height+32), 100, 512, 0xff_ff_00_00, 0);
5585     DebugPutRotatingSpotlightHead(MO.Origin+vector(0, 0, MO.Height+32), 100, 512, 0xff_00_ff_00, 1);
5586     DebugPutRotatingSpotlightHead(MO.Origin+vector(0, 0, MO.Height+32), 100, 512, 0xff_ff_00_00, 2);
5587   }
5588 #endif
5589 #ifdef CALCLIGHT_TEST
5590   if (MO) {
5591     int ll = MO.CalcLight();
5592     if (ll != lastCalcLight) {
5593       lastCalcLight = ll;
5594       printdebug("PLAYER LIGHT: intensity=%3d; color=%02x_%02x_%02x", (ll>>>24), RGBGetR(ll), RGBGetG(ll), RGBGetB(ll));
5595       ll = MO.CalcLight(Entity::ELFlag_IgnoreDynLights|Entity::ELFlag_IgnoreStaticLights);
5596       printdebug("PLAYER AMB LIGHT: intensity=%3d; color=%02x_%02x_%02x", (ll>>>24), RGBGetR(ll), RGBGetG(ll), RGBGetB(ll));
5597       ll = MO.CalcLight(Entity::ELFlag_IgnoreAmbLights|Entity::ELFlag_IgnoreDynLights);
5598       printdebug("PLAYER STT LIGHT: intensity=%3d; color=%02x_%02x_%02x", (ll>>>24), RGBGetR(ll), RGBGetG(ll), RGBGetB(ll));
5599       ll = MO.CalcLight(Entity::ELFlag_IgnoreAmbLights|Entity::ELFlag_IgnoreStaticLights);
5600       printdebug("PLAYER DYN LIGHT: intensity=%3d; color=%02x_%02x_%02x", (ll>>>24), RGBGetR(ll), RGBGetG(ll), RGBGetB(ll));
5601     }
5602   }
5603 #endif
5604   ::ClientTick(deltaTime);
5608 //==========================================================================
5610 //  AdjustPlayerAngle
5612 //==========================================================================
5613 void AdjustPlayerAngle (EntityEx AimTarget) {
5614   float angle;
5615   float difference;
5617   angle = atan2(AimTarget.Origin.y-MO.Origin.y, AimTarget.Origin.x-MO.Origin.x);
5618   difference = AngleMod180(angle-MO.Angles.yaw);
5619   if (fabs(difference) > 5.0) {
5620     MO.Angles.yaw += difference > 0.0 ? 5.0 : -5.0;
5621   } else {
5622     MO.Angles.yaw = angle;
5623   }
5624   bFixAngle = true;
5628 //==========================================================================
5630 //  UndoPlayerMorph
5632 //==========================================================================
5633 bool UndoPlayerMorph (bool Force, PlayerEx Activator) {
5634   Actor A;
5636   if (EntityEx(MO).bInvulnerable && (self != Activator || !(MorphStyle&EntityEx::MORPH_WHENINVULNERABLE))) {
5637     // immune when invulnerable unless this is something we initiated.
5638     // if the WORLD is the initiator, the same player should be given
5639     // as the activator; WORLD initiated actions should always succeed.
5640     return false;
5641   }
5643   int CorrectWeapon = MorphStyle&EntityEx::MORPH_LOSEACTUALWEAPON;
5645   MO.UnlinkFromWorld();
5646   if (BaseClass >= Level.Game.PlayerClasses.length) Error("UndoPlayerMorph: Unknown class type");
5648   A = Level.SpawnEntityChecked(class!PlayerPawn, class!PlayerPawn(Level.Game.PlayerClasses[BaseClass]), MO.Origin, default, default, AllowReplace:false);
5649   if (!A) Error("UndoPlayerMorph: class `%C` is prolly not a player pawn", Level.Game.PlayerClasses[BaseClass]);
5650   if (!Force && !A.TestLocation()) {
5651     // didn't fit
5652     A.Destroy();
5653     MO.LinkToWorld(properFloorCheck:true);
5654     MorphTime = 2.0;
5655     return false;
5656   }
5657   if (GetCvarS('player_default_gender')) A.SoundGender = name(GetCvarS('player_default_gender'));
5659   MO.LinkToWorld(properFloorCheck:true);
5660   // set color translation
5661   A.Translation = (Entity::TRANSL_Player<<Entity::TRANSL_TYPE_SHIFT)+GetPlayerNum();
5662   A.Angles = MO.Angles;
5663   ViewHeight = PlayerPawn(A).GetPawnViewHeight;
5664   DeltaViewHeight = 0.0;
5665   A.Player = self;
5666   A.bIsPlayer = true;
5667   A.ReactionTime = 0.5;
5668   if (MO.bFly) {
5669     A.bFly = true;
5670     A.bNoGravity = true;
5671   }
5672   A.bShadow = EntityEx(MO).bShadow;
5673   A.bGhost = EntityEx(MO).bGhost;
5674   A.ObtainInventory(EntityEx(MO));
5675   if (MO.TID && (MorphStyle&EntityEx::MORPH_NEWTIDBEHAVIOUR)) A.SetTID(MO.TID);
5676   MorphTime = 0.0;
5677   MorphStyle = 0;
5678   Inventory Pw = EntityEx(MO).FindInventory(PowerWeaponLevel2);
5679   if (Pw) Pw.Destroy();
5680   A.Health = GetRebornHealth();
5681   Health = A.Health;
5682   PClass = BaseClass;
5683   Weapon OldWeapon = ReadyWeapon;
5684   PostMorphWeapon(Weapon(Actor(MO).Tracer));
5685   if (CorrectWeapon) {
5686     // better "lose morphed weapon" semantics
5687     class!Weapon MorphWeapon = PlayerPawn(MO).MorphWeapon;
5688     if (MorphWeapon) {
5689       Weapon OrigWpn = Weapon(EntityEx(MO).FindInventory(MorphWeapon));
5690       if (OrigWpn && OrigWpn.bGivenAsMorphWeapon) {
5691         // you don't get to keep your morphed weapon
5692         OrigWpn.Destroy();
5693       }
5694     }
5695   } else {
5696     if (OldWeapon) OldWeapon.Destroy();
5697   }
5699   Level.Spawn(UnmorphFlash ? UnmorphFlash : class!Actor(TeleportFog),
5700     MO.Origin+vector(20.0*cos(MO.Angles.yaw),
5701     20.0*sin(MO.Angles.yaw),
5702     LineSpecialGameInfo(Level.Game).TeleFogHeight));
5704   MO.SetState(MO.FindState('FreeTargMobj'));
5705   MO = A;
5706   Camera = A;
5707   PlayerUnmorphed();
5708   return true;
5712 //===========================================================================
5714 //  ActivateMorphWeapon
5716 //===========================================================================
5717 void ActivateMorphWeapon () {
5718   class!Weapon WpnClass = PlayerPawn(MO).MorphWeapon;
5719   Weapon Wpn;
5720   if (WpnClass) {
5721     Wpn = Level.SpawnEntityChecked(class!Weapon, WpnClass);
5722     if (Wpn) Wpn.bGivenAsMorphWeapon = true;
5723   } else {
5724     // couldn't find any weapons, use the default weapon for this player (from initial inventory)
5725     foreach (int i; 0..EntityEx(MO).DropItemList.length) {
5726       class!EntityEx itc = class!EntityEx(EntityEx(MO).DropItemList[i].Type);
5727       if (!itc) continue;
5728       class!EntityEx itcrepl = class!EntityEx(GetClassReplacement(itc));
5729       if (itcrepl) itc = itcrepl;
5730       Inventory Item = Level.SpawnEntityChecked(class!Inventory, itc, default, default, default, AllowReplace:false);
5731       if (Item) {
5732         if (EntityEx(MO).DropItemList[i].Amount > 0) {
5733           Item.Amount = EntityEx(MO).DropItemList[i].Amount;
5734         }
5735         if (Weapon(Item)) {
5736           Wpn = Weapon(Item);
5737           WpnClass = class!Weapon(Weapon(Item).Class);
5738           break;
5739         }
5740         delete Item;
5741       }
5742     }
5743   }
5744   if (Wpn && !Wpn.TryPickup(EntityEx(MO))) delete Wpn;
5746   if (WpnClass) {
5747     SetWeapon(Weapon(EntityEx(MO).FindInventory(WpnClass)));
5748   } else {
5749     SetWeapon(none);
5750   }
5751   SetViewObject(ReadyWeapon);
5752   if (ReadyWeapon) {
5753     ReadyWeapon.bBobDisabled = true;
5754     ReadyWeapon.bBobFrozen = false; // just in case
5755     SetViewState(PS_WEAPON, ReadyWeapon.GetReadyState());
5756   } else {
5757     SetViewState(PS_WEAPON, none);
5758   }
5759   SetViewStateOffsets(0, Weapon::WEAPONTOP);
5763 //===========================================================================
5765 //  PostMorphWeapon
5767 //===========================================================================
5768 void PostMorphWeapon (Weapon weapon) {
5769   SetWeapon(weapon);
5770   SetViewStateOffsets(0, Weapon::WEAPONBOTTOM);
5771   SetViewObject(ReadyWeapon);
5772   if (ReadyWeapon) {
5773     ReadyWeapon.bBobDisabled = true;
5774     ReadyWeapon.bBobFrozen = false; // just in case
5775     SetViewState(PS_WEAPON, ReadyWeapon.GetUpState());
5776   } else {
5777     SetViewState(PS_WEAPON, none);
5778   }
5782 //==========================================================================
5784 //  AddVisitedMap
5786 //==========================================================================
5787 void AddVisitedMap (name Map) {
5788   foreach (auto i; 0..MAX_MAPS_VISITED) {
5789     if (MapsVisited[i] == Map) return;
5790     if (!MapsVisited[i]) { MapsVisited[i] = Map; return; }
5791   }
5795 //==========================================================================
5797 //  GetMaxHealth
5799 //==========================================================================
5800 int GetMaxHealth () {
5801   int Max = (PlayerPawn(MO) && PlayerPawn(MO).MaxHealth > 0 ? PlayerPawn(MO).MaxHealth :
5802     Level.CompatDehHealth ? MAXHEALTH :
5803     HealthBonus.default.MaxAmount/2)+(MO ? MO.Stamina : 0);
5804   if (MorphTime) {
5805     if (MorphStyle&EntityEx::MORPH_FULLHEALTH) {
5806       if (!(MorphStyle&EntityEx::MORPH_ADDSTAMINA)) Max -= (MO ? MO.Stamina : 0);
5807     } else {
5808       Max = MAXMORPHHEALTH;
5809       if (MorphStyle&EntityEx::MORPH_ADDSTAMINA) Max += (MO ? MO.Stamina : 0);
5810     }
5811   }
5812   return Max;
5816 //==========================================================================
5818 //  CheckFriendlyFire
5820 //  TODO
5822 //==========================================================================
5823 bool CheckFriendlyFire (EntityEx source, int damage) {
5824   return false;
5828 //==========================================================================
5830 //  IsWeaponAlwaysExtremeDeath
5832 //  TODO
5834 //==========================================================================
5835 bool IsWeaponAlwaysExtremeDeath () {
5836   return false;
5840 //==========================================================================
5842 //  StartDeathSlideShow
5844 //==========================================================================
5845 void StartDeathSlideShow () {
5849 //==========================================================================
5851 //  GotAmmo
5853 //  default action for all games
5855 //==========================================================================
5856 void GotAmmo (Ammo NewAmmo) {
5857   if (!NewAmmo) return;
5858   // we were down to zero, so select a new weapon
5859   // preferences are not user selectable
5860   if (!ReadyWeapon || (ReadyWeapon.bWimpyWeapon && !ReadyWeapon.bNoAutoSwitch)) {
5861     Weapon Best = BestWeapon(class!Ammo(NewAmmo.Class));
5862     if (Best && (!ReadyWeapon || Best.SelectionOrder < ReadyWeapon.SelectionOrder)) {
5863       PendingWeapon = Best;
5864     }
5865   }
5869 //==========================================================================
5871 //  Damaged
5873 //==========================================================================
5874 void Damaged (EntityEx inflictor) {
5878 //==========================================================================
5880 //  KilledActor
5882 //==========================================================================
5883 void KilledActor (EntityEx Victim) {
5887 //==========================================================================
5889 //  Killed
5891 //==========================================================================
5892 void Killed (EntityEx source, EntityEx inflictor) {
5896 //==========================================================================
5898 //  GetSigilPieces
5900 //==========================================================================
5901 int GetSigilPieces () {
5902   return 0;
5906 //==========================================================================
5908 //  PlayerMorphed
5910 //==========================================================================
5911 void PlayerMorphed (EntityEx OldMO) {
5912   // so we can select weapons
5913   if (PlayerPawn(MO)) PlayerPawn(MO).InitializeWeaponSlots();
5917 //==========================================================================
5919 //  PlayerUnmorphed
5921 //==========================================================================
5922 void PlayerUnmorphed () {
5923   // so we can select weapons
5924   if (PlayerPawn(MO)) PlayerPawn(MO).InitializeWeaponSlots();
5928 //==========================================================================
5930 //  CreateBot
5932 //==========================================================================
5933 void CreateBot () {
5937 //==========================================================================
5939 //  CoopGetAllSharedKeys
5941 //==========================================================================
5942 void CoopGetAllSharedKeys () {
5943   if (!MO) return;
5944   if (!GetCvarB('sv_coop_share_keys')) return;
5945   // share all all keys
5946   foreach (auto playnum; 0..MAXPLAYERS) {
5947     auto pl = PlayerEx(Level.Game.Players[playnum]);
5948     if (!pl || !pl.bSpawned || !pl.MO || pl.MO == MO || pl == self) continue;
5949     for (auto inv = EntityEx(pl.MO).Inventory; inv; inv = inv.Inventory) {
5950       if (!class!Key(inv.Class)) continue;
5951       bool found = false;
5952       for (auto myinv = EntityEx(MO).Inventory; myinv; myinv = myinv.Inventory) {
5953         if (myinv.Class == inv.Class) {
5954           found = true;
5955           break;
5956         }
5957       }
5958       if (found) continue;
5959       printdebug("%C: autoadding to the player #%d", inv, playnum);
5960       EntityEx pmo = EntityEx(MO);
5961       // must create a copy
5962       Inventory Copy = pmo.SpawnEntityChecked(class!Inventory, class!Inventory(inv.Class), pmo.Origin, pmo.Angles, default, AllowReplace:false);
5963       assert(Copy);
5964       Copy.Amount = inv.Amount;
5965       Copy.MaxAmount = inv.MaxAmount;
5966       Copy.bTransferInventory = inv.bTransferInventory;
5967       Copy.AttachToOwner(pmo);
5968       printdebug("%C: autoadded to the player #%d", inv, playnum);
5969     }
5970   }
5974 //==========================================================================
5976 //  OnNetSpawn
5978 //==========================================================================
5979 void OnNetSpawn (EntityEx OldMO) {
5980   // give all keys other players have to this one when in coop
5981   if (!Level.Game.netgame || Level.Game.deathmatch) return;
5982   CoopGetAllSharedKeys();
5986 //==========================================================================
5988 //  OnNetReborn
5990 //==========================================================================
5991 void OnNetReborn (EntityEx OldMO) {
5992   // restore keys and weapons when reborn in co-op
5993   if (!Level.Game.netgame || Level.Game.deathmatch) return;
5994   if (!OldMO) {
5995     CoopGetAllSharedKeys();
5996     return;
5997   }
5998   //printdebug("%C: transfer weapons...", self);
5999   for (auto inv = OldMO.Inventory; inv; ) {
6000     //printdebug("%C:  inv=%C", self, inv);
6001     Inventory next = inv.Inventory;
6002     if (Weapon(inv)) {
6003       if (GetCvarB('sv_coop_keep_weapons')) inv.AttachToOwner(EntityEx(MO));
6004       //printdebug("%C:  inv=%C: WEAPON TRANSFER", self, inv);
6005     } else if (FourthWeaponHolder(inv) || Key(inv)) {
6006       //printdebug("%C:  inv=%C: KEY TRANSFER", self, inv);
6007       inv.AttachToOwner(EntityEx(MO));
6008     }
6009     inv = next;
6010   }
6011   OldMO.DestroyAllInventory();
6015 //==========================================================================
6017 //  DestroyBot
6019 //==========================================================================
6020 void DestroyBot () {
6024 //==========================================================================
6026 //  BotOnSpawn
6028 //==========================================================================
6029 void BotOnSpawn () {
6033 //==========================================================================
6035 //  BotSendSubSectorChange
6037 //==========================================================================
6038 void BotSendSubSectorChange (subsector_t *ss) {
6042 //==========================================================================
6044 //  SetClientModel
6046 //==========================================================================
6047 void SetClientModel () {
6051 //==========================================================================
6053 //  GetRebornHealth
6055 //==========================================================================
6056 int GetRebornHealth () {
6057   return 0;
6061 //==========================================================================
6063 //  BotTick
6065 //==========================================================================
6066 void BotTick (float deltaTime) {
6070 //==========================================================================
6072 //  SpawnSpeedEffect
6074 //==========================================================================
6075 void SpawnSpeedEffect () {
6079 //==========================================================================
6081 //  PlayerImpulse
6083 //  return `true` if impulse was eaten
6085 //==========================================================================
6086 void PlayerImpulse () {
6087   if (!Impulse) return;
6089   // weapon slot 11 is used for weapons without explicit slot
6090   if (Impulse <= PlayerPawn::NUM_WEAPON_SLOTS) { ChangeWeapon(Impulse); Impulse = 0; return; }
6092   //printdebug("%C: impulse=%s", self, Impulse);
6094   switch (Impulse) {
6095     //^C test:case 222: for (;;) { /*print("boo");*/ } return;
6096     case 13: InventoryLeft(); Impulse = 0; return;
6097     case 14: InventoryRight(); Impulse = 0; return;
6098     case 15: InventoryUse(); Impulse = 0; return;
6099     case 16: Actor(MO).Damage(none, none, 10000); Impulse = 0; return;
6100     case 17: PrevWeapon(); Impulse = 0; return;
6101     case 18: NextWeapon(); Impulse = 0; return;
6102     case 43: Actor(MO).FireSuperBullet(); Impulse = 0; return;
6103     case 48: ChangeWeapon(11); Impulse = 0; return; // weapons without explicit slot
6104     case 56: bReloadQueued = true; Impulse = 0; return; // so you can use this in aliases and such
6105     case 142: Actor(MO).DebugCheckMidTex(); Impulse = 0; return;
6106     case 149: bFlashlightOn = !bFlashlightOn; Impulse = 0; return; // so you can use this in aliases and such
6107     case 155:
6108       foreach (auto pidx; 0..MAXPLAYERS) {
6109         PlayerEx plr = PlayerEx(Level.Game.Players[pidx]);
6110         if (!plr || !plr.bIsBot) continue;
6111         plr.BotDumpNodes();
6112       }
6113       Impulse = 0;
6114       return;
6115     case 156:
6116       if (MO) {
6117         foreach (auto pidx; 0..MAXPLAYERS) {
6118           PlayerEx plr = PlayerEx(Level.Game.Players[pidx]);
6119           if (!plr || !plr.bIsBot) continue;
6120           plr.BotTestFindPathTo(MO.Origin);
6121         }
6122       }
6123       Impulse = 0;
6124       return;
6125   }
6127   if (Impulse >= 200 && Impulse <= 205) {
6128     LineSpecialLevelInfo(Level).ConChoiceImpulse(Impulse-200); // strife does additional processing
6129     Impulse = 0;
6130     return;
6131   }
6135 //==========================================================================
6137 //  eventGetReadyWeapon
6139 //==========================================================================
6140 override Entity eventGetReadyWeapon () {
6141   return ReadyWeapon;
6145 //==========================================================================
6147 //  GetCurrentArmor
6149 //==========================================================================
6150 BasicArmor GetCurrentArmor () {
6151   if (!MO) return none;
6152   return BasicArmor(EntityEx(MO).FindInventory(BasicArmor, disableReplacement:true));
6156 //==========================================================================
6158 //  GetCurrentArmorClassName
6160 //==========================================================================
6161 override string GetCurrentArmorClassName () {
6162   auto armor = GetCurrentArmor();
6163   return (armor ? string(armor.GetArmorName()) : "None");
6167 //k8: basically, this is wrong, because
6169 //==========================================================================
6171 //  GetCurrentArmorSaveAmount
6173 //==========================================================================
6174 override int GetCurrentArmorSaveAmount () {
6175   auto armor = GetCurrentArmor();
6176   return (armor ? armor.Amount : 0);
6180 //==========================================================================
6182 //  GetCurrentArmorSavePercent
6184 //==========================================================================
6185 override float GetCurrentArmorSavePercent () {
6186   auto armor = GetCurrentArmor();
6187   return (armor ? armor.SavePercent : 0);
6191 //==========================================================================
6193 //  GetCurrentArmorMaxAbsorb
6195 //==========================================================================
6196 override int GetCurrentArmorMaxAbsorb () {
6197   // this is for HexenArmor, and it is not implemented yet
6198   return 0;
6202 //==========================================================================
6204 //  GetCurrentArmorFullAbsorb
6206 //==========================================================================
6207 override int GetCurrentArmorFullAbsorb () {
6208   // this is for HexenArmor, and it is not implemented yet
6209   return 0;
6213 //==========================================================================
6215 //  GetCurrentArmorActualSaveAmount
6217 //==========================================================================
6218 override int GetCurrentArmorActualSaveAmount () {
6219   auto armor = GetCurrentArmor();
6220   if (!armor) return 0;
6221   auto bonus = BasicArmorBonus(armor);
6222   if (bonus) return bonus.SaveAmount;
6223   auto pickup = BasicArmorPickup(armor);
6224   if (pickup) return pickup.SaveAmount;
6225   // i don't know what is it
6226   return 0;
6230 //==========================================================================
6232 //  GetCurrentArmorActualSaveAmount
6234 //==========================================================================
6235 bool IsWeaponFromSlot11 (class!Weapon wpn) {
6236   if (!wpn) return true; // why not?
6238   auto pawn = PlayerPawn(MO);
6239   if (!pawn) return true;
6241   //printdebug("checking weapon '%C' for slot 11...", wpn);
6243   // slots are numbered from zero
6244   int slotsize = pawn.GetSlotSize(10);
6245   if (!slotsize) return false; // this slot is empty, nothing to do
6246   //printdebug("slot 11 has %s weapons...", slotsize);
6248   foreach (auto i; 0..slotsize) {
6249     class!Weapon slotwpn = pawn.GetWeaponInSlot(10, i);
6250     //printdebug("  weapon #%d: '%C'", i, slotwpn);
6251     if (!slotwpn) continue;
6252     if (slotwpn == wpn) return true;
6253   }
6255   return false;
6259 //==========================================================================
6261 //  CheatHelper_AllWeapons
6263 //==========================================================================
6264 void CheatHelper_AllWeapons (bool full) {
6265   bool gotit = false;
6266   class!Weapon WpnCls;
6267   foreach AllClasses(Weapon, out WpnCls) {
6268     if (WpnCls.default.bCheatNotWeapon) continue;
6269     class!Weapon repl = class!Weapon(GetClassReplacement(WpnCls));
6270     if (repl && repl != WpnCls) continue;
6271     // check if it is valid
6272     if (!EntityEx.CheckSpawnGameFilter(WpnCls, Level.Game)) continue;
6273     if (!Weapon.IsUsableWeaponClass(WpnCls)) continue;
6274     repl = class!Weapon(WpnCls.Replacee);
6275     if (repl && repl != WpnCls) {
6276       if (!EntityEx.CheckSpawnGameFilter(repl, Level.Game)) {
6277         //printdebug("REJECTED '%C' (replacee: %C)", WpnCls, WpnCls.Replacee);
6278         continue;
6279       }
6280       if (repl.default.SisterWeapon && !EntityEx.CheckSpawnGameFilter(repl.default.SisterWeapon.Class, Level.Game)) {
6281         //printdebug("REJECTED '%C' (replacee: %C, sister: %C)", WpnCls, WpnCls.Replacee, repl.default.SisterWeapon.Class);
6282         continue;
6283       }
6284     }
6285     //printdebug("giving '%C' (replacee: %C)", WpnCls, WpnCls.Replacee);
6286     // check if already have it
6287     if (EntityEx(MO).FindInventory(WpnCls, disableReplacement:true)) continue;
6288     // check for slot 11
6289     if (!full) {
6290       if (IsWeaponFromSlot11(WpnCls)) continue;
6291       /*
6292       auto pawn = PlayerPawn(MO);
6293       if (!pawn) continue;
6294       int slot, widx;
6295       if (!pawn.FindWeaponSlot(WpnCls, out slot, out widx)) continue;
6296       */
6297     }
6298     //Weapon Wpn = Weapon(Level.Spawn(WpnCls, default, default, default, AllowReplace:false));
6299     Weapon Wpn = EntityEx.SpawnWeaponType(Level, WpnCls, disableReplace:true);
6300     Wpn.AmmoGive1 = 0;
6301     Wpn.AmmoGive2 = 0;
6302     if (!Wpn.TryPickup(EntityEx(MO))) {
6303       //printdebug("%C: rejected weapon '%C'!", MO, Wpn);
6304       Wpn.Destroy();
6305     } else {
6306       if (WpnCls.Replacee && WpnCls.Replacee != WpnCls) print("CHEAT: got '%C' (replacee: '%C')!", WpnCls, WpnCls.Replacee);
6307       else print("CHEAT: got '%C'!", WpnCls);
6308       //printdebug("     : GameFilter=%08x : %08x", WpnCls.default.GameFilter, class!Weapon(WpnCls.Replacee).default.GameFilter);
6309       gotit = true;
6310     }
6311   }
6312   if (gotit) cprint("You got weapons!");
6316 //==========================================================================
6318 //  CheatHelper_GiveAmmoClass
6320 //==========================================================================
6321 bool CheatHelper_GiveAmmoClass (class!Ammo acls, optional bool verbose) {
6322   if (!acls) return false;
6324   Ammo AmmoItem = EntityEx.SpawnAmmoType(Level, acls);
6325   if (!AmmoItem) {
6326     if (verbose) printwarn("failed to spawn ammo '%C'", acls);
6327     return false;
6328   }
6329   if (verbose) printdebug("spawned ammo '%C' for ammo class '%C'", AmmoItem, acls);
6331   bool res = false;
6332   Inventory invAmmo = EntityEx(MO).FindInventory(acls);
6333   if (invAmmo && Ammo(invAmmo)) {
6334     delete AmmoItem;
6335     AmmoItem = Ammo(invAmmo);
6336     assert(AmmoItem);
6337   } else {
6338     assert(Ammo(AmmoItem));
6339     if (!AmmoItem.TryPickup(EntityEx(MO))) {
6340       AmmoItem.Destroy();
6341       AmmoItem = none;
6342     } else {
6343       res = true;
6344     }
6345   }
6347   if (Ammo(AmmoItem)) {
6348     auto spwspate = FindClassState(AmmoItem.Class, 'Spawn');
6349     if (spwspate && AreStateSpritesPresent(spwspate)) {
6350       AmmoItem.Amount = Ammo(AmmoItem).k8GetAmmoKingMax();
6351     } else {
6352       bool hasBackpack = false;
6353       if (AmmoItem.Owner && AmmoItem.Owner.bIsPlayer) {
6354         PlayerEx plr = PlayerEx(AmmoItem.Owner.Player);
6355         hasBackpack = (plr && plr.bHasBackpack);
6356       }
6357       if (verbose) printdebug("unpickable ammo '%C', AmmoKing ignored", AmmoItem);
6358       int am = Ammo(AmmoItem).MaxAmount;
6359       if (hasBackpack) am = max(am, Ammo(AmmoItem).BackpackMaxAmount);
6360       AmmoItem.Amount = am;
6361     }
6362     res = true;
6363   }
6365   return res;
6369 //==========================================================================
6371 //  CheatHelper_AllAmmo
6373 //  if `full` is `false`, give ammo only for weapons in hands
6375 //==========================================================================
6376 void CheatHelper_AllAmmo (bool full, optional bool current) {
6377   bool gotit = false;
6379   if (current) {
6380     Weapon wpn = ReadyWeapon;
6381     if (!wpn) return;
6382     if (wpn.AmmoType1) {
6383       if (CheatHelper_GiveAmmoClass(wpn.AmmoType1, verbose:true)) gotit = true;
6384     }
6385     if (wpn.AmmoType2 && wpn.AmmoType2 != wpn.AmmoType1) {
6386       if (CheatHelper_GiveAmmoClass(wpn.AmmoType2, verbose:true)) gotit = true;
6387     }
6388   } else if (full) {
6389     // all ammo
6390     class!Ammo Cls;
6391     foreach AllClasses(Ammo, out Cls) {
6392       // only direct descendants
6393       if (GetClassParent(Cls) != Ammo) continue;
6394       if (!EntityEx.CheckSpawnGameFilter(Cls, Level.Game)) continue;
6395       class!Ammo repl = class!Ammo(Cls.Replacee);
6396       if (repl && repl != Cls && !EntityEx.CheckSpawnGameFilter(repl, Level.Game)) continue;
6397       if (CheatHelper_GiveAmmoClass(class!Ammo(Cls))) gotit = true;
6398     }
6399   } else {
6400     // only for weapons in hands
6401     auto pawn = PlayerPawn(MO);
6402     if (!pawn) return;
6404     array!(class!Ammo) ammoList;
6406     // check all weapon slots
6407     /*
6408     foreach (auto slot; 0..pawn.GetNumberOfSlots()) {
6409       int slotsize = pawn.GetSlotSize(slot);
6410       if (!slotsize) continue;
6411       foreach (auto i; 0..slotsize; reverse) {
6412         class!Weapon slotwpn = pawn.GetWeaponInSlot(slot, i);
6413         if (!slotwpn) continue;
6414       }
6415     }
6416     */
6418     // collect possible ammo
6419     for (Inventory inv = pawn.Inventory; inv; inv = inv.Inventory) {
6420       auto wpn = Weapon(inv);
6421       if (!wpn) continue;
6422       foreach (int atidx; 0..2) {
6423         class!Ammo acls = (atidx ? wpn.AmmoType2 : wpn.AmmoType1);
6424         if (!acls) continue;
6425         int xidx = 0;
6426         for (; xidx < ammoList.length; ++xidx) if (ammoList[xidx] == acls) break;
6427         if (xidx >= ammoList.length) ammoList[$] = acls;
6428       }
6429     }
6431     // now give collected ammo
6432     foreach (class!Ammo acls; ammoList) {
6433       //if (!EntityEx.CheckSpawnGameFilter(acls, Level.Game)) continue; // just in case?
6434       if (CheatHelper_GiveAmmoClass(acls, verbose:true)) gotit = true;
6435     }
6436   }
6438   if (gotit) cprint("You got ammo!");
6442 //==========================================================================
6444 //  CheatHelper_AllKeys
6446 //==========================================================================
6447 void CheatHelper_AllKeys () {
6448   bool gotit = false;
6449   class!Key Cls;
6450   foreach AllClasses(Key, out Cls) {
6451     class!Key repl = class!Key(GetClassReplacement(Cls));
6452     if (repl && repl != Cls) continue;
6453     if (!EntityEx.CheckSpawnGameFilter(Cls, Level.Game)) continue;
6454     if (!EntityEx.IsSpawnableClass(Cls)) continue;
6455     if (EntityEx(MO).FindInventory(Cls, disableReplacement:true)) continue;
6456     EntityEx(MO).GiveInventoryType(Cls, disableReplace:true);
6457     gotit = true;
6458   }
6459   if (gotit) cprint("You got keys!");
6463 //==========================================================================
6465 //  IsWeaponRunEnabled
6467 //  can be overriden in user mods
6469 //==========================================================================
6470 bool IsWeaponRunEnabled () {
6471   Weapon rwp = ReadyWeapon;
6472   return (rwp ? !rwp.bDisablePlayerRun : true);
6476 //==========================================================================
6478 //  ApplyWeaponSpeedRestrictions
6480 //  can be overriden in user mods
6482 //==========================================================================
6483 void ApplyWeaponSpeedRestrictions (ref float speed, ref float moveWalk, ref float moveRun) {
6484   Weapon rwp = ReadyWeapon;
6485   if (rwp) {
6486     if (rwp.bDisablePlayerRun) {
6487       speed = fclamp(speed, -PlayerPawn::WALKING_SPEED, PlayerPawn::WALKING_SPEED);
6488       moveWalk *= rwp.PlayerSpeedScaleWalk;
6489       moveRun *= rwp.PlayerSpeedScaleWalk;
6490     } else if (fabs(speed) <= PlayerPawn::WALKING_SPEED) {
6491       moveWalk *= rwp.PlayerSpeedScaleWalk;
6492       moveRun *= rwp.PlayerSpeedScaleWalk;
6493     } else {
6494       moveWalk *= rwp.PlayerSpeedScaleRun;
6495       moveRun *= rwp.PlayerSpeedScaleRun;
6496     }
6497   }
6501 //==========================================================================
6503 //  AdjustForwardMove
6505 //  can be overriden in user mods
6507 //  `speed` the original speed in abstract units
6508 //    |speed| <= 1000 is walking, otherwise running
6510 //==========================================================================
6511 void AdjustForwardMove (ref float speed, ref float moveWalk, ref float moveRun) {
6515 //==========================================================================
6517 //  AdjustSideMove
6519 //  can be overriden in user mods
6521 //  `speed` the original speed in abstract units
6522 //    |speed| <= 1000 is walking, otherwise running
6524 //==========================================================================
6525 void AdjustSideMove (ref float speed, ref float moveWalk, ref float moveRun) {
6529 //==========================================================================
6531 //  GetJumpVelZ
6533 //  can be overriden in user mods
6535 //==========================================================================
6536 float GetJumpVelZ () {
6537   float res = PlayerPawn(MO).JumpVelZ;
6538   if (ReadyWeapon) res *= ReadyWeapon.PlayerJumpScale;
6539   return res;
6543 //==========================================================================
6545 //  LeaveBootPrints
6547 //==========================================================================
6548 void LeaveBootPrints (float deltaTime) {
6549   if (!MO || MO.Health <= 0) return; // just in case
6551   PlayerPawn pawn = PlayerPawn(MO);
6552   if (!pawn.BootPrintEnabled) return;
6554   if (!GetCvarB('r_bootprints')) return;
6556   name dcname = pawn.BaseBootPrintDecal;
6557   if (!dcname || nameEquCI(dcname, 'none')) return;
6559   float mintm = pawn.BootPrintDelayTime0;
6560   float maxtm = pawn.BootPrintDelayTime1;
6562   if (mintm <= 0.0f && maxtm <= 0.0f) return;
6564   bool onFloor = (fabs(pawn.Origin.z-pawn.FloorZ) <= 0.1);
6566   VLevel::VBootPrintDecalParams newpar;
6568   if (onFloor && pawn.XLevel.CheckBootPrints(pawn.Origin, pawn.SubSector, out newpar)) {
6569     if (newpar.MarkTime > 0.0) {
6570       //printdebug("%C: new mark time: %s", pawn, newpar.MarkTime);
6571       bootprintParams = newpar;
6572       bootprintTimeLeft = newpar.MarkTime+deltaTime;
6573       bootprintTimeTotal = newpar.MarkTime;
6574       bootprintLastOrg = pawn.Origin;
6575       bootprintNextPutTime = FRandomBetween(mintm, maxtm);
6576     } else {
6577       ResetBootPrints();
6578       return;
6579     }
6580   }
6582   //printdebug("%C: dcname=%s; tm=(%s,%s); bootprintTimeLeft=%s; bootprintNextPutTime=%s", pawn, dcname, mintm, maxtm, bootprintTimeLeft, bootprintNextPutTime);
6584   bootprintTimeLeft -= deltaTime;
6585   if (bootprintTimeLeft <= 0.0f) {
6586     bootprintTimeLeft = 0.0f;
6587     return;
6588   }
6590   bootprintNextPutTime -= deltaTime;
6591   if (bootprintNextPutTime > 0.0f) return;
6593   bootprintNextPutTime = FRandomBetween(mintm, maxtm);
6595   if (!onFloor) return; // not on the floor, oops
6597   float dist2d = (bootprintLastOrg-pawn.Origin).length2D();
6598   if (dist2d < pawn.BootPrintMinDist) return;
6600   // calculate alpha
6601   // total time
6602   float alpha = fclamp(bootprintTimeLeft/bootprintTimeTotal*bootprintParams.Alpha, 0.0f, 1.0f);
6603   //printdebug("...alpha=%s (%s)", alpha, bootprintParams.Alpha);
6605   if (alpha <= 0.004) {
6606     ResetBootPrints();
6607     return;
6608   }
6610   TVec org = pawn.Origin;
6611   bootprintLastOrg = org;
6613   // bootprintFlip==true: right
6614   float xofs = pawn.Radius*pawn.BootPrintRadiusMult+FRandomBetween(pawn.BootPrintRandomOfs0, pawn.BootPrintRandomOfs1);
6615   if (pawn.BootPrintFlipOffset && bootprintFlip) xofs = -xofs;
6616   //printdebug("org=%s; xofs=%s; neworg=%s", org, xofs, xofs+YawVectorRight(pawn.Angles.yaw));
6617   org += YawVectorRight(pawn.Angles.yaw)*xofs;
6619   bool doflip = (pawn.BootPrintFlip ? bootprintFlip : false);
6620   pawn.SpawnFlatDecal(org, dcname, range:0,
6621     atranslation:bootprintParams.Translation, ashadeclr:bootprintParams.Shade, alpha:alpha/*+1000.0f*/,
6622     animator:bootprintParams.Animator, angle:pawn.Angles.yaw,
6623     forceFlipX:doflip);
6625   bootprintFlip = !bootprintFlip;
6629 //==========================================================================
6631 //  ProduceFootSteps
6633 //==========================================================================
6634 void ProduceFootSteps (float deltaTime) {
6635   if (!MO || MO.Health <= 0) return; // just in case
6637   if (MO.WaterLevel > 1 || fabs(MO.Origin.z-MO.FloorZ) > 0.1) {
6638     // not on a floor, reset footstep time
6639     lastFootstepSoundTime = GameTime;
6640     return;
6641   }
6643   PlayerPawn pwn = PlayerPawn(MO);
6644   if (!pwn) return;
6646   //printdebug("VEL=%s", MO.Velocity);
6648   VTerrainInfo *TInfo = MO.GetActorTerrain();
6650   int speed = pwn.GetMoveSpeedType();
6651   //if (speed) printdebug("%C: speedtype=%s", MO, speed);
6652   if (speed == PlayerPawn::PMT_Standing) return;
6654   float tm, vol;
6655   switch (speed) {
6656     case PlayerPawn::PMT_Crouching: tm = TInfo.CrouchingStepTime; vol = TInfo.CrouchingStepVolume; break;
6657     case PlayerPawn::PMT_Walking: tm = TInfo.WalkingStepTime; vol = TInfo.WalkingStepVolume; break;
6658     case PlayerPawn::PMT_Running: tm = TInfo.RunningStepTime; vol = TInfo.RunningStepVolume; break;
6659     default: return; // just in case
6660   }
6661   if (tm <= 0.0f || vol <= 0.0f) {
6662     // reset footstep time
6663     lastFootstepSoundTime = GameTime;
6664     return;
6665   }
6667   float gt = GameTime;
6668   if (gt-lastFootstepSoundTime < tm) return; // not yet
6669   lastFootstepSoundTime = gt;
6671   name snd = (lastFootstepIsLeft ? TInfo.LeftStepSound : TInfo.RightStepSound);
6672   lastFootstepIsLeft = !lastFootstepIsLeft;
6673   if (GetCvarB('gm_footsteps')) {
6674     if (snd) MO.PlaySound(snd, CHAN_FOOTSTEP, fmin(1.0f, vol));
6675   }
6679 defaultproperties {
6680   InvSize = 6;
6681   PrevViewHeight = -666;