1 //**************************************************************************
3 //** ## ## ## ## ## #### #### ### ###
4 //** ## ## ## ## ## ## ## ## ## ## #### ####
5 //** ## ## ## ## ## ## ## ## ## ## ## ## ## ##
6 //** ## ## ######## ## ## ## ## ## ## ## ### ##
7 //** ### ## ## ### ## ## ## ## ## ##
8 //** # ## ## # #### #### ## ##
10 //** Copyright (C) 1999-2006 Jānis Legzdiņš
11 //** Copyright (C) 2018-2023 Ketmar Dark
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.
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.
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/>.
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
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;
53 const float MAXBOB = 16.0;
55 const int MAXHEALTH = 100;
56 const int MAXMORPHHEALTH = 30;
58 // for screen flashing (red or bright)
62 transient int DamageFlashBlend; // for advdamage indicator
64 // base height above floor for viewz
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)
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;
84 // bit flags, for cheats and debug
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)
96 array!name RevealedMaps;
104 Inventory SavedInventory;
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
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
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
144 float MorphTime; // player is morphed into something if > 0
146 class!Actor UnmorphFlash;
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;
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 {
209 int flatDamageTimeout;
210 int flatDamageLeaky; // byte, probability
212 bool flatDamageHitFloor;
216 #include "PlayerEx.cheats.vc"
218 #ifdef CALCLIGHT_TEST
219 transient int lastCalcLight = 0;
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 //==========================================================================
252 //==========================================================================
253 final void ClearSubSeenInfo () {
254 lastSubSector = nullptr;
259 //==========================================================================
263 //==========================================================================
264 final void AddSeenSubsector () {
266 if (!MO.SubSector) return;
267 if (lastSubSector == MO.SubSector) return;
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;
274 if (subSeen[ssnum]) return;
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);
286 //==========================================================================
290 //==========================================================================
291 void BotDumpNodes () {
295 //==========================================================================
299 //==========================================================================
300 void BotTestFindPathTo (TVec dest) {
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,
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,
324 // from client to server
325 reliable if (bIsClient)
330 //==========================================================================
334 //==========================================================================
335 void GetUseRanges (out float ur, out float utr) {
336 PlayerPawn pwn = PlayerPawn(MO);
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);
342 ur = PlayerPawn::DEFAULT_USERANGE;
343 utr = PlayerPawn::DEFAULT_USETHINGRANGE;
348 #include "PlayerEx.healthbar.vc"
351 //==========================================================================
355 // fix various cheat flags
357 //==========================================================================
358 void FixCheatFlags () {
359 Level.bFrozen = !!(Cheats&CF_TIMEFREEZE);
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;
367 if (Cheats&CF_NOCLIP) {
368 MO.bColideWithThings = !(Cheats&CF_NOCLIP);
369 MO.bColideWithWorld = !(Cheats&CF_NOCLIP);
375 //==========================================================================
379 // this is called on save loading, etc.
380 // reset every important field to default
382 //==========================================================================
383 override void ResetToDefaults () {
385 ResetPlayerOnSpawn(keepPlayerState:true);
386 k8HealthAccum_Amount = 0;
387 //print("*** RESET ***");
388 //print("k8ElvenGiftMessageTime=%s", k8ElvenGiftMessageTime);
392 //==========================================================================
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
405 k8BossesDetected = (GetCvarB('k8ElvenDetect') ? -0.5 : 666);
409 //==========================================================================
413 //==========================================================================
414 override void eventOnBeforeSave (bool isAutosave, bool isCheckpoint) {
415 ::eventOnBeforeSave(isAutosave, isCheckpoint);
416 //print("*** BEFORE SAVING (auto:%B; checkpoint:%B) ***", isAutosave, isCheckpoint);
420 //==========================================================================
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 //==========================================================================
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;
462 //==========================================================================
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));
476 foreach (auto midx; 0..MAX_MAPS_VISITED; reverse) {
477 if (MapsVisited[midx]) {
483 QS_PutInt("MAX_MAPS_VISITED", viscount);
484 foreach (auto midx; 0..viscount) QS_PutName(va("MapVisited.%d", midx), MapsVisited[midx]);
488 //==========================================================================
492 //==========================================================================
493 override void QS_Load () {
494 Health = QS_GetInt("Health");
496 Cheats = QS_GetInt("Cheats");
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;
513 if (mname) Error("invalid number of visited maps in quicksave");
517 foreach (auto midx; viscount..MAX_MAPS_VISITED) MapsVisited[midx] = '';
519 k8ElvenGifted = true;
523 //==========================================================================
525 // ClearEntityInventoryQS
527 //==========================================================================
529 override void ClearEntityInventoryQS () {
531 PendingWeapon = none;
532 ::ClearEntityInventoryQS();
537 //==========================================================================
539 // ResetWeaponReloadRefire
541 //==========================================================================
542 void ResetWeaponReloadRefire () {
543 bReloadQueued = false;
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) {
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) {
578 Weapon wpn = Weapon(ent);
579 for (Inventory inv = EntityEx(MO).Inventory; inv; inv = inv.Inventory) {
585 BringUpWeapon(instant:instant, skipSound:instant);
589 //==========================================================================
591 // eventIsReadyWeaponByName
593 //==========================================================================
594 override bool eventIsReadyWeaponByName (string classname, bool allowReplace) {
595 if (!classname || !ReadyWeapon) return false;
597 auto wpnClass = class!Weapon(FindClassNoCaseStr(classname));
598 if (!wpnClass) return false;
599 if (wpnClass == ReadyWeapon.Class) return true;
603 auto wpnRepl = class!Weapon(GetClassReplacement(wpnClass));
604 if (wpnRepl && wpnRepl != wpnClass && wpnRepl == ReadyWeapon.Class) return true;
606 auto wpnSrc = class!Weapon(GetClassReplacee(ReadyWeapon.Class));
607 if (wpnSrc && stricmp(string(GetClassName(wpnSrc)), classname) == 0) return true;
609 wpnClass = class!Weapon(ReadyWeapon.Class);
611 if (stricmp(string(GetClassName(wpnClass)), classname) == 0) return true;
612 wpnClass = class!Weapon(GetClassParent(wpnClass));
620 //==========================================================================
622 // eventFindInventoryWeapon
624 //==========================================================================
625 override Entity eventFindInventoryWeapon (string classname, bool allowReplace) {
626 if (!classname) return none;
627 if (!MO) return none;
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);
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);
649 //==========================================================================
653 //==========================================================================
654 float GetAttackZOfs () {
655 auto plrmo = PlayerPawn(MO);
656 if (!plrmo) return 8.0;
657 return plrmo.AttackZOffset*plrmo.crouchfactor;
661 //==========================================================================
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))
674 move *= LineSpecialGameInfo(Level.Game).IceMoveFactor;
677 sincos(angle, out s, out c);
678 MO.Velocity.x += move*c*deltaTime;
679 MO.Velocity.y += move*s*deltaTime;
683 //==========================================================================
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 () {
703 lastViewOrgZForPfx = MO.Origin.z;
704 lastViewOrgZSector = MO.Sector;
705 lastViewOrgVH = ViewHeight;
707 lastViewOrgZSector = nullptr;
712 //==========================================================================
716 //==========================================================================
717 override void ClientSetViewOrg (TVec neworg) {
718 ::ClientSetViewOrg(neworg);
719 SaveViewOrgFixInfo();
723 //==========================================================================
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);
749 //SanitizeViewOrgZ();
753 //==========================================================================
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');
765 // regular movement bobbing
766 // (needs to be calculated for gun swing even if not on ground)
767 if (MO.bFly && !onground) {
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;
777 // when crouching, bobbing have to be reduced
778 if (PlayerPawn(MO)) {
779 auto plrmo = PlayerPawn(MO);
780 Bob *= plrmo.crouchfactor;
783 float angle = 180.0*35.0/10.0*Level.XLevel.Time;
784 mvbobbob = Bob/2.0*sin(angle);
787 if (PlayerState == PST_LIVE) {
788 ViewHeight += DeltaViewHeight*deltaTime;
790 float plrVH = PlayerPawn(MO).GetPawnViewHeight*PlayerPawn(MO).crouchfactor;
792 if (ViewHeight > plrVH) {
794 DeltaViewHeight = 0.0;
797 if (ViewHeight < plrVH/2.0) {
798 ViewHeight = plrVH/2.0;
799 if (DeltaViewHeight <= 0.0) DeltaViewHeight = 0.00001;
802 if (DeltaViewHeight) {
803 DeltaViewHeight += 256.0*deltaTime;
804 if (!DeltaViewHeight) DeltaViewHeight = 0.00001;
808 if (Level.XLevel.bIsBadApple) {
809 ViewOrg.z = MO.Origin.z+41;
811 ViewOrg.z = MO.Origin.z+ViewHeight+mvbobbob;
813 SaveViewOrgFixInfo();
819 //==========================================================================
821 // SetNewCrouchFactor
823 //==========================================================================
824 void SetNewCrouchFactor (float newcrf) {
825 auto plrmo = PlayerPawn(MO);
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;
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
853 //CheckFakeFloorTriggers(pos.Z + oldheight, true);
857 //==========================================================================
861 //==========================================================================
862 void CrouchMove (float deltaTime, int direction) {
863 auto plrmo = PlayerPawn(MO);
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 //==========================================================================
880 //==========================================================================
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);
901 //==========================================================================
905 //==========================================================================
906 void MovePlayer (float deltaTime) {
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);
923 if (spGetNormalZ(MO.EFloor) != 1.0) printdebug("%C: slope onground=%s", self, onground);
926 if (!onground && spGetNormalZ(MO.EFloor) != 1.0 && MO.Origin.z <= spGetPointZ(MO.EFloor, MO.Origin)) {
927 printdebug("%C: on a slope!", self);
932 forward = ForwardMove*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;
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) {
950 bRevertCamera = false;
955 if (fly && (bFly || EntityEx(MO).FindInventory(PowerFlight))) {
956 if (FlyMove != TOCENTRE) {
960 MO.bNoGravity = true;
961 if (MO.Velocity.z <= -39.0*35.0) {
962 // stop falling scream
963 MO.StopSound(CHAN_VOICE);
968 MO.bNoGravity = false;
970 } else if (fly > 0.0) {
976 MO.Velocity.z = FlyHeight*35.0;
977 if (FlyHeight) FlyHeight /= 2.0;
979 if (fabs(FlyHeight) > 0.1) {
980 MO.Velocity.z = FlyHeight*35.0;
982 if (fabs(FlyHeight) <= 0.1) FlyHeight = 0;
984 // directional flight
987 AngleVector(MO.Angles, out vfdir);
988 vfdir.z *= forward*deltaTime;
989 MO.Velocity.z += vfdir.z;
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);
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;
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);
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;
1010 MO.PlaySound('*jump', CHAN_VOICE);
1011 // add last polyobject speed, if there is any
1012 auto pawn = PlayerPawn(MO);
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);
1019 if (pawn.lastStand3DPObj) {
1020 MO.Velocity += pawn.lastStand3DPObjVel;
1021 pawn.lastJump3DPObj = pawn.lastStand3DPObj;
1022 pawn.lastStand3DPObj = nullptr;
1024 pawn.lastJump3DPObj = nullptr;
1031 //==========================================================================
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)) {
1044 return (org.z >= tmt.FloorZ && org.z+mo.Height < tmt.CeilingZ);
1049 //==========================================================================
1051 // CheckWaterJumpThrust
1053 //==========================================================================
1054 void CheckWaterJumpThrust () {
1055 EntityEx mo = EntityEx(MO);
1056 TVec vforward, start;
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);
1063 if (!bWaterThrusted && mo.WaterLevel < 2) {
1064 vforward = (AngleYawVector(mo.Angles.yaw)*ForwardMove+YawVectorRight(mo.Angles.yaw)*SideMove).Normalise;
1066 if (CheckCanMoveTo(start)) {
1067 bWaterThrusted = true;
1068 mo.bWaterJump = false;
1069 mo.ReactionTime = 0.0;
1070 float hlen = mo.Velocity.xy.length;
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);
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);
1086 #ifdef DEBUG_WATER_JUMPS
1088 printdebug("%C: NO-TH: vel=%s : %s", self, mo.Velocity, mo.Velocity.xy.length);
1095 //==========================================================================
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();
1111 // do not jump out if fully submerged
1112 if (MO.WaterLevel > 2) return;
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);
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
1139 #ifdef DEBUG_WATER_JUMPS
1140 printdebug("%C: EYECLOSE: vel=%s : %s -- %s", self, mo.Velocity, mo.Velocity.xy.length, vforward);
1144 #ifdef DEBUG_WATER_JUMPS
1145 printdebug("%C: CANMOVE: vel=%s : %s -- %s", self, mo.Velocity, mo.Velocity.xy.length, vforward);
1151 //==========================================================================
1155 //==========================================================================
1157 EntityEx mo = EntityEx(MO);
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);
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();
1176 bWaterThrusted = false;
1181 //==========================================================================
1185 // this is called if WaterLevel is > 1
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) {
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
1213 //else print("**WZ=%s", MO.Velocity.z);
1215 AngleVectors(MO.Angles, out vforward, out vright, out vup);
1217 forward = ForwardMove;
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);
1230 // prevent bunny-hopping while hovering on water
1231 if (LastWaterLevel < 2) {
1234 MO.Velocity.z = fmin(0, MO.Velocity.z);
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;
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) {
1262 doCrouchMove = false;
1263 CrouchMove(deltaTime, (IsCrouchButtonCrouch() ? -1 : 1));
1266 if (MO.WaterLevel >= 2 && wishvel.z < 0 && wishvz == 0) {
1267 if (MO.Velocity.z > 0) MO.Velocity.z -= 10;
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);
1273 float limit = ((Buttons&BT_SPEED) && IsRunEnabled() && IsWeaponRunEnabled() ? 140 : 140*2);
1274 MO.Velocity.z = fclamp(MO.Velocity.z, -limit, limit);
1279 if (doCrouchMove) CrouchMove(deltaTime, 1);
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;
1292 //==========================================================================
1294 // ProcessSectorScroll
1296 //==========================================================================
1297 void ProcessSectorScroll (float deltaTime, EntityEx ent, sector_t *sec) {
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
1400 //==========================================================================
1402 // FindProtectionSuit
1404 //==========================================================================
1405 Inventory FindProtectionSuit () {
1406 // search for iron feet power: any subclass will do
1407 Inventory IronFeet = EntityEx(MO).Inventory;
1409 if (PowerIronFeet(IronFeet)) break;
1410 IronFeet = IronFeet.Inventory;
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
1429 // check if we actually have a suit
1430 return !!FindProtectionSuit();
1434 //==========================================================================
1436 // InitSectorDamageInfo
1438 //==========================================================================
1439 void InitSectorDamageInfo (ref SectorDamageInfo pdi) {
1440 pdi.flatDamageType = '';
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) {
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;
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);
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
1485 case SECSPEC_DamageSludge:
1486 pdi.flatDamageType = 'Slime';
1488 pdi.flatDamageTimeout = 32;
1489 pdi.flatDamageLeaky = 5; // default leakage value
1491 case SECSPEC_DamageNukage:
1492 pdi.flatDamageType = 'Slime'; //FIXME
1494 pdi.flatDamageTimeout = 32;
1495 pdi.flatDamageLeaky = 5; // default leakage value
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
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
1512 case SECSPEC_DamageLavaWimpy:
1513 pdi.flatDamageType = 'Fire';
1515 pdi.flatDamageTimeout = 16;
1516 pdi.flatDamageHitFloor = true;
1517 pdi.flatDamageLeaky = 5; // default leakage value
1519 case SECSPEC_DamageLavaHefty:
1520 pdi.flatDamageType = 'Fire';
1522 pdi.flatDamageTimeout = 16;
1523 pdi.flatDamageHitFloor = true;
1524 pdi.flatDamageLeaky = 5; // default leakage value
1526 case SECSPEC_ScrollEastLavaDamage:
1527 ThrustPlayer(0.0, 1024.0, deltaTime);
1528 pdi.flatDamageType = 'Fire';
1530 pdi.flatDamageTimeout = 16;
1531 pdi.flatDamageHitFloor = true;
1532 pdi.flatDamageLeaky = 5; // default leakage value
1534 case SECSPEC_DamageHazard:
1536 if (!IsProtectionSuitActive(5)) HazardTime += 2.0*deltaTime;
1538 case SECSPEC_DamageInstantDeath:
1539 ent.Damage(none, none, 999, 'InstantDeath', spawnBlood:true);
1541 case SECSPEC_DamageSuperHazard:
1543 if (!IsProtectionSuitActive(5)) HazardTime += 4.0*deltaTime;
1547 // extended sector damage type
1548 switch (sec->special&SECSPEC_DAMAGE_MASK) {
1550 pdi.flatDamageType = 'Fire';
1552 pdi.flatDamageTimeout = 32;
1553 pdi.flatDamageLeaky = 5; // default leakage value
1556 pdi.flatDamageType = 'Slime';
1557 pdi.flatDamage = 10;
1558 pdi.flatDamageTimeout = 32;
1559 pdi.flatDamageLeaky = 5; // default leakage value
1562 pdi.flatDamageType = 'Slime';
1563 pdi.flatDamage = 20;
1564 pdi.flatDamageTimeout = 32;
1565 pdi.flatDamageLeaky = 5; // default leakage value
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);
1594 ent.Damage(none, none, pdi.flatDamage);
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;
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
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);
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) {
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);
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);
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();
1656 printdebug("%C: orgz=%s; fz=%s; 3dfloor", self, MO.Origin.z, MO.FloorZ);
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 //==========================================================================
1691 // Called every tic frame.
1693 //==========================================================================
1694 void PlayerInContents (float deltaTime) {
1695 if (!MO.WaterLevel) return;
1697 name flatDamageType = '';
1699 int flatDamageTimeout;
1701 switch (MO.WaterType) {
1703 flatDamageType = 'Fire';
1705 flatDamageTimeout = 32;
1707 case CONTENTS_NUKAGE:
1708 flatDamageType = 'Slime'; //FIXME
1710 flatDamageTimeout = 32;
1712 case CONTENTS_SLIME:
1713 flatDamageType = 'Slime';
1715 flatDamageTimeout = 32;
1717 case CONTENTS_HELLSLIME:
1718 flatDamageType = 'Slime';
1720 flatDamageTimeout = 32;
1722 case CONTENTS_SLUDGE:
1723 flatDamageType = 'Slime'; //FIXME
1725 flatDamageTimeout = 32;
1727 case CONTENTS_HAZARD:
1728 // apply protection suit (leaky)
1729 if (!IsProtectionSuitActive(5)) HazardTime += 2.0*deltaTime;
1733 // apply flat damage?
1734 if (!flatDamage) return;
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);
1743 EntityEx(MO).Damage(none, none, flatDamage);
1748 //==========================================================================
1750 // SetPlayerRunState
1752 //==========================================================================
1753 void SetPlayerRunState () {
1754 EntityEx mobj = EntityEx(MO);
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);
1762 printdebug("%C: CANNOT go to see state %s", mobj, mobj.State);
1768 //***************************************************************************
1772 //***************************************************************************
1774 //==========================================================================
1776 // ResetReadyWeaponBobbing
1778 //==========================================================================
1779 void ResetReadyWeaponBobbing () {
1780 Weapon wpn = ReadyWeapon;
1782 wpn.bBobDisabled = true;
1783 wpn.bBobFrozen = false; // just in case
1784 BobbingTime = 0; // start from the base
1789 //==========================================================================
1793 //==========================================================================
1794 void SetWeapon (Weapon NewWeapon) {
1795 ReadyWeapon = NewWeapon;
1796 PendingWeapon = none;
1798 PSpriteSY = NewWeapon.PSpriteSY;
1799 MO.ModelVersion = NewWeapon.PlayerModelVersion;
1802 MO.ModelVersion = 0;
1803 SetViewObject(none);
1804 SetViewState(PS_WEAPON, none);
1809 //===========================================================================
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);
1822 if (PendingWeapon && PendingWeapon == ReadyWeapon) {
1823 printwarn("%C: RAISING ALREADY RAISED WEAPON! (0)", ReadyWeapon);
1827 PendingWeapon = none;
1828 SetViewStateOffsets(0, (instant || bInstantWeaponSwitch ? Weapon::WEAPONTOP : Weapon::WEAPONBOTTOM));
1829 ResetWeaponReloadRefire();
1830 // block firing for "no autofire" weapons
1832 bAltAttackDown = true;
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) {
1840 SetViewState(PS_WEAPON, ReadyWeapon.GetInstaReadyState());
1842 SetViewState(PS_WEAPON, ReadyWeapon.GetReadyState());
1845 //print("BRINGING %C: curr=%s; top=%s; bot=%s", ReadyWeapon, ViewStateSY, Weapon::WEAPONTOP, Weapon::WEAPONBOTTOM);
1846 SetViewState(PS_WEAPON, ReadyWeapon.GetUpState());
1848 //dprint("MO=%C; RW=%C", MO, ReadyWeapon);
1850 SetViewObject(none);
1851 print("RAISING NONE WEAPON! (0)");
1852 MO.ModelVersion = 0;
1856 MO.ModelVersion = ReadyWeapon.PlayerModelVersion;
1858 SetViewObject(none);
1859 MO.ModelVersion = 0;
1865 //===========================================================================
1869 // Player died, so put the weapon away.
1871 //===========================================================================
1872 void DropWeapon () {
1873 ResetWeaponReloadRefire();
1874 ResetWeaponActionFlags();
1876 printdebug("%C: bringing weapon %C down: %s", self, ReadyWeapon, ReadyWeapon.GetDownState());
1877 SetViewObject(ReadyWeapon);
1878 SetViewState(PS_WEAPON, ReadyWeapon.GetDownState());
1883 //===========================================================================
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);
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();
1917 printdebug("TICK: %s (bWeaponWasWeaponReady=%B)", MO.XLevel.TicTime, bWeaponWasWeaponReady);
1919 printdebug("%C: ReadyWeapon=%C; PendingWeapon=%C; rwstate=%s (%s)", self, ReadyWeapon, PendingWeapon,
1920 ViewStates[PS_WEAPON].State, ViewStates[PS_WEAPON].StateTime);
1922 if (bWeaponAllowSwitch ||
1923 bWeaponAllowPrimaryFire ||
1924 bWeaponAllowAltFire ||
1925 bWeaponAllowReload ||
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));
1937 if (bWeaponAllowSwitch) {
1938 if (PendingWeapon && !bDisableWeaponSwitch) {
1939 ResetWeaponReloadRefire();
1940 ResetWeaponActionFlags();
1942 ResetReadyWeaponBobbing();
1943 SetViewObject(ReadyWeapon);
1944 SetViewState(PS_WEAPON, ReadyWeapon.GetDownState());
1945 } else if (PendingWeapon) {
1946 SetWeapon(PendingWeapon);
1953 Weapon Wpn = ReadyWeapon;
1955 ResetWeaponReloadRefire();
1956 ResetWeaponActionFlags();
1957 bWeaponAllowSwitch = true;
1958 ResetPlayerFiringState();
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;
1973 switch (GetCvarI('wp_fire_bobbing')) {
1975 Wpn.bBobDisabled = true;
1976 Wpn.bBobFrozen = true;
1980 Wpn.bBobFrozen = true;
1982 //case 2: // bob: do nothing
1984 if (FireWeapon()) return;
1985 Wpn.bBobDisabled = oldNoBob;
1986 Wpn.bBobFrozen = false;
1987 BobbingTime = oldBobTime;
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')) {
1995 Wpn.bBobDisabled = true;
1996 Wpn.bBobFrozen = true;
2000 Wpn.bBobFrozen = true;
2002 //case 2: // bob: do nothing
2004 if (AltFireWeapon()) return;
2005 Wpn.bBobDisabled = oldNoBob;
2006 Wpn.bBobFrozen = false;
2007 BobbingTime = oldBobTime;
2011 ResetPlayerFiringState();
2014 if (bWeaponAllowReload && ((Buttons&BT_RELOAD) || bReloadQueued)) {
2015 // `bReloadQueued` will be reset by `ReloadWeapon()`
2016 bWeaponWasWeaponReady = false;
2017 Wpn.bBobDisabled = true;
2019 if (ReloadWeapon()) return;
2020 Wpn.bBobDisabled = oldNoBob;
2021 BobbingTime = oldBobTime;
2024 if (bWeaponAllowZoom && (Buttons&BT_ZOOM) && !bZoomDown && Wpn.GetZoomState()) {
2025 bWeaponWasWeaponReady = false;
2027 Wpn.bBobDisabled = true;
2029 if (ZoomWeapon()) return;
2030 Wpn.bBobDisabled = oldNoBob;
2031 BobbingTime = oldBobTime;
2034 if (bWeaponAllowUser1 && (Buttons&BT_BUTTON_5) && !bButton5Down && Wpn.FindState('User1')) {
2035 bWeaponWasWeaponReady = false;
2036 bButton5Down = true;
2037 Wpn.bBobDisabled = true;
2039 if (WeaponUserAction(1)) return;
2040 Wpn.bBobDisabled = oldNoBob;
2041 BobbingTime = oldBobTime;
2044 if (bWeaponAllowUser2 && (Buttons&BT_BUTTON_6) && !bButton6Down && Wpn.FindState('User2')) {
2045 bWeaponWasWeaponReady = false;
2046 bButton6Down = true;
2047 Wpn.bBobDisabled = true;
2049 if (WeaponUserAction(2)) return;
2050 Wpn.bBobDisabled = oldNoBob;
2051 BobbingTime = oldBobTime;
2054 if (bWeaponAllowUser3 && (Buttons&BT_BUTTON_7) && !bButton7Down && Wpn.FindState('User3')) {
2055 bWeaponWasWeaponReady = false;
2056 bButton7Down = true;
2057 Wpn.bBobDisabled = true;
2059 if (WeaponUserAction(3)) return;
2060 Wpn.bBobDisabled = oldNoBob;
2061 BobbingTime = oldBobTime;
2064 if (bWeaponAllowUser4 && (Buttons&BT_BUTTON_8) && !bButton8Down && Wpn.FindState('User4')) {
2065 bWeaponWasWeaponReady = false;
2066 bButton8Down = true;
2067 Wpn.bBobDisabled = true;
2069 if (WeaponUserAction(4)) return;
2070 Wpn.bBobDisabled = oldNoBob;
2071 BobbingTime = oldBobTime;
2076 //==========================================================================
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) {
2098 if (wpn) printwarn("PLAYER: ReadyWeapon `%C` died, PendingWeapon is `%C`", wpn, PendingWeapon);
2099 bWeaponAllowSwitch = true;
2101 if (!ViewStates[PS_WEAPON].State) {
2102 if (wpn) printwarn("PLAYER: ReadyWeapon %C (%C) removed itself, PendingWeapon is %C", ReadyWeapon, wpn, PendingWeapon);
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;
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);
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);
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;
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);
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);
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);
2178 if (newstate != ee.IdleState) ee.SetState(newstate);
2180 if (!ee.IdleState) return;
2181 if ((ee.MissileState && StateIsInSequence(ee.State, ee.MissileState)) ||
2182 (ee.MeleeState && StateIsInSequence(ee.State, ee.MeleeState)))
2184 ee.SetState(ee.IdleState);
2190 //==========================================================================
2192 // ResetPlayerFiringState
2194 //==========================================================================
2195 void ResetPlayerFiringState () {
2196 // get player out of attack state
2197 EntityEx ee = EntityEx(MO);
2199 if (!ee.IdleState) return;
2200 if ((ee.MissileState && StateIsInSequence(ee.State, ee.MissileState)) ||
2201 (ee.MeleeState && StateIsInSequence(ee.State, ee.MeleeState)))
2203 //printdebug("%C: setting idle state", self);
2204 ee.SetState(ee.IdleState);
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; }
2214 //===========================================================================
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);
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);
2238 if (ReadyWeapon.bBotMelee) st = ee.MeleeState;
2239 if (!st) st = ee.MissileState;
2241 if (st && !StateIsInSequence(ee.State, st)) {
2242 //printdebug("%C: weapon=%C; ee=%C; setting attack state %s", self, ReadyWeapon, ee, st);
2246 SetViewObject(ReadyWeapon);
2247 SetViewState(PS_WEAPON, firestate);
2248 if (ReadyWeapon && !ReadyWeapon.bNoAlert) {
2249 LineSpecialLevelInfo(Level).NoiseAlert(EntityEx(MO), EntityEx(MO));
2255 //===========================================================================
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 //===========================================================================
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 //===========================================================================
2283 // returns `true` if weapon state was changed
2285 //===========================================================================
2286 bool ReloadWeapon () {
2287 //k8: reloading weapon resets refire state
2288 ResetWeaponReloadRefire();
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);
2304 //===========================================================================
2308 // returns `true` if weapon state was changed
2310 //===========================================================================
2311 bool ZoomWeapon () {
2312 //k8: zooming weapon resets refire state
2313 ResetWeaponReloadRefire();
2315 state zst = ReadyWeapon.GetZoomState();
2316 if (!zst) return false;
2317 SetPlayerWeaponState(MO.FindState('Zoom'));
2318 SetViewObject(ReadyWeapon);
2319 SetViewState(PS_WEAPON, zst);
2326 //===========================================================================
2330 // returns `true` if weapon state was changed
2332 //===========================================================================
2333 bool WeaponUserAction (int actnum) {
2334 if (actnum < 1 || actnum > 4) return false;
2335 ResetWeaponReloadRefire();
2339 case 1: stname = 'User1'; break;
2340 case 2: stname = 'User2'; break;
2341 case 3: stname = 'User3'; break;
2342 case 4: stname = 'User4'; break;
2344 state zst = ReadyWeapon.FindState(stname);
2345 if (!zst) return false;
2346 SetPlayerWeaponState(MO.FindState(stname));
2347 SetViewObject(ReadyWeapon);
2348 SetViewState(PS_WEAPON, zst);
2355 //==========================================================================
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 //==========================================================================
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 //==========================================================================
2389 //==========================================================================
2390 void PrevWeapon () {
2391 SetPendingWeapon(GetPrevWeapon(PendingWeapon ? PendingWeapon : ReadyWeapon));
2395 //==========================================================================
2399 //==========================================================================
2400 void NextWeapon () {
2401 SetPendingWeapon(GetNextWeapon(PendingWeapon ? PendingWeapon : ReadyWeapon));
2405 //==========================================================================
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);
2416 for (Inventory Item = EntityEx(MO).Inventory; Item; Item = Item.Inventory) {
2418 Weapon Wpn = Weapon(Item);
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;
2440 //==========================================================================
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;
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;
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;
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))
2491 // now cycle through the slot
2492 foreach (auto i; 0..slotsize) {
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);
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*/)) {
2522 currIndex = (forward ? pawn.GetSlotSize(currSlot) : -1);
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);
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);
2538 if (Current && Current.Class == swc) continue;
2539 Weapon Wpn = Weapon(EntityEx(MO).FindInventory(swc, disableReplacement:true));
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);
2553 //==========================================================================
2557 //==========================================================================
2558 Weapon GetPrevWeapon (Weapon Current) {
2559 return CycleWeaponWithDir(Current, forward:false);
2563 //==========================================================================
2567 //==========================================================================
2568 Weapon GetNextWeapon (Weapon Current) {
2569 return CycleWeaponWithDir(Current, forward:true);
2573 //==========================================================================
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) {
2584 GetUseRanges(out ur, out utr);
2587 AngleVector(MO.Angles, out PuzzleUseDir);
2589 TVec start = MO.Origin;
2590 TVec end = start+ur*PuzzleUseDir.xy;
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;
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
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;
2618 if (ld->special != LNSPEC_UsePuzzleItem) {
2619 if (in.bIsABlockingLine) break; // line hit, no actor found
2621 if (ld->flags&(ML_BLOCKEVERYTHING|ML_BLOCKUSE)) {
2625 hitPoint = MO.Origin+(/+DEFAULT_USERANGE+/ur*in.frac)*PuzzleUseDir;
2626 open = MO.XLevel.LineOpenings(ld, hitPoint);
2628 if (!open || open->range <= 0.0) {
2629 if (MO.bIsPlayer) MO.PlaySound('*puzzfail', CHAN_VOICE);
2630 break; // can't use through a wall
2633 continue; // continue searching
2635 if (PointOnPlaneSide(start, *ld) == 1) {
2636 // don't use back sides
2639 if (PuzzleItemType != ld->arg1) {
2640 // item type doesn't match
2643 MO.XLevel.StartACS(ld->arg2, 0, ld->arg3, ld->arg4, ld->arg5, MO, ld, 0, false, false);
2645 return true; // stop searching
2649 mobj = EntityEx(in.Thing);
2650 if (mobj.Special != LNSPEC_UsePuzzleItem) {
2654 if (PuzzleItemType != mobj.Args[0]) {
2655 // item type doesn't match
2658 MO.XLevel.StartACS(mobj.Args[1], 0, mobj.Args[2], mobj.Args[3], mobj.Args[4], MO, nullptr, 0, false, false);
2660 return true; // stop searching
2667 //==========================================================================
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
2677 RevealedMaps.length = RevealedMaps.length+1;
2678 RevealedMaps[RevealedMaps.length-1] = Level.XLevel.MapName;
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);
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;
2715 //==========================================================================
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 //==========================================================================
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 //==========================================================================
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 //==========================================================================
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);
2771 //dl->radius = rad+150.0;
2773 dl->die = Level.XLevel.Time+0.5;
2779 //==========================================================================
2781 // ClientParticleExplosion
2783 //==========================================================================
2784 void ClientParticleExplosion (int clr, float rad, TVec org) {
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);
2792 p->die = Level.XLevel.Time+5.0;
2793 p->color = LineSpecialGameInfo.default.ramp1[0];
2795 p->ramp = Random()*4.0;
2797 p->type = LineSpecialLevelInfo::pt_explode;
2799 p->type = LineSpecialLevelInfo::pt_explode2;
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;
2808 if (GetCvarI('r_sprlight_mode') <= 0) {
2809 dlight_t *dl = Level.AllocDlight(none, org, rad+150.0);
2812 //dl->radius = rad + 150.0;
2814 dl->die = Level.XLevel.Time+0.5;
2821 //==========================================================================
2823 // ClientSparkParticles
2825 //==========================================================================
2826 void ClientSparkParticles (int Count, TVec Org, float Angle) {
2828 foreach (auto i; 0..Count) {
2829 float an = Angle+Random()*45.0;
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);
2839 p->type = LineSpecialLevelInfo::pt_spark;
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;
2855 //==========================================================================
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);
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;
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);
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;
2896 //==========================================================================
2900 // Sets the new palette color shift based upon the current values of
2901 // Player.DamageFlash and Player.BonusFlash, contents and other inventory
2904 //==========================================================================
2905 void PaletteFlash () {
2911 // done in main renderer, sorry
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;
2926 DamageFlashBlend = 0;
2927 if (nameicmp(EntityEx(MO).DamageType, 'Ice') == 0) {
2929 AddBlend(r, g, b, a, RGBA(2, 2, 255, 113));
2933 if (DamageFlash < 0) DamageFlash = 0;
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;
2948 //printdebug("%C: skipped damage record with type=%s, int=%s, clr=0x%08x (%s)", self, dmg.Type, dmg.Intensity, dmg.Color, dmgType);
2953 printdebug("%C: damage without a type", self);
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;
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));
2979 int Amount = PoisonCount*160/32;
2980 if (Amount >= 160) Amount = 160;
2981 AddBlend(r, g, b, a, RGBA(56, 118, 46, Amount));
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));
2990 // Health Accumulation Device effect, and palette flash
2991 if (MO && MO.XLevel) {
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);
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);
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);
3017 AddBlend(r, g, b, a, RGBA(int(BlendR*255.0), int(BlendG*255.0), int(BlendB*255.0), int(BlendA*255.0)));
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 //==========================================================================
3031 //==========================================================================
3032 override void PreTravel () {
3036 // remove all powerups that cannot survive map teleports
3037 /* nope, this is done in `PlayerExitMap()`
3038 auto inv = EntityEx(MO).Inventory;
3040 Powerup pw = Powerup(inv);
3041 inv = inv.Inventory;
3042 if (pw && !pw.bSurvivesMapTeleport) {
3043 printdebug("*** removed powerup '%C'", pw);
3049 //WARNING! don't set `Owner` to none here, because `SV_MapTeleport()` checks it!
3050 SavedInventory = EntityEx(MO).Inventory;
3051 EntityEx(MO).Inventory = none;
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
3060 //printdebug("%C: PreTravel inventory: CLEARED!", self);
3061 SavedInventory = none; // just in case
3068 //==========================================================================
3072 //==========================================================================
3073 override void UseInventory (string Inv) {
3076 if (bTotallyFrozen || (Level.bFrozen && !(Cheats&CF_TIMEFREEZE))) {
3077 // you can't use items if you're totally frozen
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);
3088 // use Inventory item
3089 EntityEx(MO).UseInventory(item);
3094 //==========================================================================
3096 // CheckDoubleFiringSpeed
3098 //==========================================================================
3099 override bool CheckDoubleFiringSpeed () {
3100 return !!(Cheats&CF_DOUBLEFIRINGSPEED);
3104 //==========================================================================
3108 //==========================================================================
3109 void ClientSpeech (EntityEx Speaker, int SpeechNum) {
3110 LineSpecialClientGame(ClGame).StartSpeech(Speaker, SpeechNum);
3114 //==========================================================================
3118 //==========================================================================
3119 void ClientSlideshow1 () {
3120 LineSpecialClientGame(ClGame).StartConSlideshow1();
3124 //==========================================================================
3128 //==========================================================================
3129 void ClientSlideshow2 () {
3130 LineSpecialClientGame(ClGame).StartConSlideshow2();
3134 //==========================================================================
3138 //==========================================================================
3139 void ClientFinaleType (int Type) {
3140 LineSpecialClientGame(ClGame).SetFinaleType(Type);
3144 //==========================================================================
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)));
3172 while (ppos >= 0 && ppos+1 < str.length) {
3173 res ~= str[0..ppos];
3174 auto nch = str[ppos+1];
3175 bool validMod = true;
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;
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));
3201 str = str[ppos+2..$];
3204 str = str[ppos+1..$];
3206 ppos = str.strIndexOf("%");
3208 if (str) res ~= str;
3213 //==========================================================================
3217 //==========================================================================
3218 void DisplayObituary (EntityEx inflictor, EntityEx source, name DmgType) {
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
3229 if (source.Player == self) {
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;
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) {
3246 Msg = "$ob_mptelefrag";
3247 } else if (inflictor && inflictor.Obituary) {
3248 // missile with its own obituary
3249 Msg = inflictor.Obituary;
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
3259 source = EntityEx(MO);
3262 if (!Msg) Msg = "$ob_default"; // generic death
3264 // look up string in language lump if necesary
3265 Msg = TranslateString(Msg);
3268 Msg = StrReplaceSubstitutes(Msg, source);
3270 Level.bprint("%s", Msg);
3274 //==========================================================================
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);
3288 VectorAngles(Dir, out Ang);
3292 for (float Offs = 0.0; Offs < Len; Offs += 3.0) {
3293 if (MaxDiff > 0.0) {
3294 int Rnd = P_Random();
3296 Diff.x += (Rnd&8 ? 1.0 : -1.0);
3297 Diff.x = fclamp(Diff.x, -MaxDiff, MaxDiff);
3300 Diff.y += (Rnd&16 ? 1.0 : -1.0);
3301 Diff.y = fclamp(Diff.y, -MaxDiff, MaxDiff);
3304 Diff.z += (Rnd&32 ? 1.0 : -1.0);
3305 Diff.z = fclamp(Diff.z , -MaxDiff, MaxDiff);
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;
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);
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;
3337 p->die = Level.XLevel.Time+1.0;
3339 p->accel = vector(0.0, 0.0, 0.0);
3345 //==========================================================================
3349 //==========================================================================
3350 void ClientVoice (int VoiceNum) {
3351 LocalSound(name(va("svox/voc%d", VoiceNum)));
3355 //==========================================================================
3359 // Returns false if the ammo can't be picked up at all
3361 //==========================================================================
3362 bool GiveAmmo (class!Ammo ammo, int count) {
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))));
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;
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);
3398 //==========================================================================
3400 // PutClientIntoServer
3402 //==========================================================================
3403 override void PutClientIntoServer () {
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) {
3427 SpawnPlayer(sp, spawned);
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) {
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) {
3445 bestOtherDist = spdist;
3451 if (spawned) return nullptr;
3453 if (!best && !bestOther) Error("Player %d has no start spots", GetPlayerNum()+1);
3456 print("Player %d has no start spot for position %d, using other spot position", GetPlayerNum()+1, Level.Game.RebornPosition);
3459 print("Player %d has no start spot for position %d", GetPlayerNum()+1, Level.Game.RebornPosition);
3466 //==========================================================================
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 //==========================================================================
3482 //==========================================================================
3483 override void SpawnClient () {
3484 bool playerWasReborn;
3485 EntityEx OldMO = EntityEx(MO);
3488 //printdebug("*** SPAWN CLIENT");
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);
3499 playerWasReborn = (PlayerState == PST_CHEAT_REBORN);
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);
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);
3516 /* ResetPlayerOnSpawn() should take care of this
3517 if (!playerWasReborn && LocalQuakeHappening) {
3518 print("*** RESETTING QUAKING!");
3519 LocalQuakeHappening = 0;
3523 // setup weapon slots
3524 //printdebug("*** SPAWN CLIENT: MO=%C (%C)", MO, PlayerPawn(MO));
3525 if (PlayerPawn(MO)) PlayerPawn(MO).InitializeWeaponSlots(/*LineSpecialGameInfo(Level.Game)*/);
3528 if (Level.Game.netgame && !Level.Game.deathmatch) {
3529 if (playerWasReborn) OnNetReborn(OldMO); else OnNetSpawn(OldMO);
3532 // destroy all things touching players
3533 Actor(MO).TeleportMove(MO.Origin);
3535 k8BossesDetected = (GetCvarB('k8ElvenDetect') ? -0.5 : 666);
3542 //==========================================================================
3546 // Respawn at the start
3548 //==========================================================================
3549 override void NetGameReborn () {
3550 EntityEx OldMO = EntityEx(MO);
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
3559 MO.bIsPlayer = false;
3561 LastRegenTicTime = 0;
3564 // spawn at random spot if in death match
3565 if (Level.Game.deathmatch) {
3566 k8HealthAccum_Amount = 0;
3567 OldMO.DestroyAllInventory();
3568 DeathMatchSpawnPlayer();
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
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);
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);
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);
3613 if (PlayerPawn(MO)) PlayerPawn(MO).InitializeWeaponSlots(/*LineSpecialGameInfo(Level.Game)*/);
3619 //==========================================================================
3623 //==========================================================================
3624 override void DisconnectClient () {
3629 MO.bIsPlayer = false;
3630 Actor(MO).Damage(none, none, 10000, forced:true, spawnBlood:true);
3632 Level.bprint("%s left the game", PlayerName);
3633 if (MO) MO.PlaySound('misc/chat', CHAN_AUTO, 1.0, ATTN_NONE);
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!");
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];
3664 foreach (int i; dms) {
3665 auto sp = cast([unsafe])(&Level.DeathmatchStarts[i]);
3666 if (CheckSpot(sp)) {
3667 SpawnPlayer(sp, false);
3672 // no good spot, so the player will probably get stuck
3674 auto sp = Level.GetPlayerStart(GetPlayerNum(), 0);
3675 CheckSpot(sp); // spawn teleport fog
3676 SpawnPlayer(sp, false);
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 //==========================================================================
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) {
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)
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;
3732 if (!PlayerChunk(MO)) {
3733 LineSpecialLevelInfo(Level).AddPlayerCorpse(EntityEx(MO));
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));
3748 //==========================================================================
3752 //==========================================================================
3753 void SetupPlayerClass () {
3754 if (LineSpecialGameInfo(Level.Game).bRandomClass &&
3755 Level.Game.deathmatch && Level.Game.PlayerClasses.length > 1)
3757 PClass = P_Random()%Level.Game.PlayerClasses.length;
3758 if (PClass == BaseClass) PClass = (PClass+1)%Level.Game.PlayerClasses.length;
3767 //==========================================================================
3771 // reset ACS button update timers and values
3773 //==========================================================================
3774 void ResetACSButtons () {
3775 AcsCurrButtonsPressed = 0;
3779 AcsNextButtonUpdate = 0;
3787 //==========================================================================
3789 // ResetRenderStyles
3791 //==========================================================================
3792 void ResetRenderStyles () {
3799 //==========================================================================
3803 //==========================================================================
3804 void ResetBootPrints () {
3805 bootprintTimeLeft = 0.0f;
3809 //==========================================================================
3811 // ResetPlayerOnSpawn
3813 //==========================================================================
3814 void ResetPlayerOnSpawn (optional bool keepPlayerState) {
3815 if (!keepPlayerState) PlayerState = PST_LIVE;
3819 DamageFlashType = '';
3826 LastSectorDamageTic = 0;
3827 LastHazardTime = 0.0;
3832 LocalQuakeHappening = vector(0, 0, 0);
3833 //MoveDir = vector(0, 0, 0);
3835 //Weapon ReadyWeapon;
3836 //Weapon PendingWeapon; // Is none if not changing.
3839 bFrozen = false; // just in case
3840 bTotallyFrozen = false; // just in case
3842 bFly = false; // just in case
3843 PoisonCount = 0; // screen flash for poison damage
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
3860 ChickenPeck = 0; // chicken peck countdown
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;
3882 k8BossesDetected = 666;
3883 k8ElvenGiftMessageTime = 666;
3885 Cheats &= ~(CF_NOCLIP|CF_TIMEFREEZE);
3891 ResetRenderStyles();
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);
3915 //==========================================================================
3919 //==========================================================================
3920 override void ResetInventory () {
3921 EntityEx PP = EntityEx(MO);
3923 while (PP.Inventory) PP.Inventory.Destroy();
3925 ResetRenderStyles();
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);
3945 if (!verbose!specified) verbose = true;
3947 //bool again = true;
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);
3957 if (verbose) print("removed key '%C'", inv);
3958 PP.RemoveInventory(inv);
3963 if (verbose && count > 0) cprint("Removed %s key%s!", count, (count != 1 ? "s" : ""));
3967 //==========================================================================
3971 //==========================================================================
3972 override void ResetHealth () {
3973 EntityEx PP = EntityEx(MO);
3975 Health = GetRebornHealth();
3980 //==========================================================================
3984 //==========================================================================
3985 override void PreraiseWeapon () {
3986 EntityEx PP = EntityEx(MO);
3988 if (!ReadyWeapon) return;
3990 BringUpWeapon(instant:true, skipSound:true);
3994 //==========================================================================
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) {
4004 bool ResetInventory = false;
4005 bool reborned = false;
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);
4019 ResetInventory = true;
4021 } else if (PlayerState == PST_CHEAT_REBORN) {
4022 //print("*** CHEAT REBORN ***");
4024 ResetInventory = false;
4028 if (Level.bResetInventory && !GetCvarB('sv_ignore_reset_inventory')) ResetInventory = true;
4031 if (!cheatReborn && GetCvarB('sv_force_pistol_start')) ResetInventory = true;
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
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
4058 //printdebug(" moving inventory to the Voodoo Doll from MO (SVI=%C)", SavedInventory);
4059 PP.ObtainInventory(EntityEx(MO));
4062 DesiredFOV = 90.0; //float(GetCvar('FOV'));
4064 PP.Angles.yaw = spawnYaw;
4066 PP.bIsPlayer = true;
4070 ViewHeight = PP.GetPawnViewHeight;
4071 ViewOrg = MO.Origin;
4072 ViewOrg.z += ViewHeight;
4073 SaveViewOrgFixInfo();
4074 ViewAngles = PP.Angles;
4076 ResetRenderStyles();
4078 ResetPlayerOnSpawn();
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();
4089 // for some reason, bot inventory sometimes refusing to go; wtf?!
4090 auto pinv = PP.Inventory;
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);
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));
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;
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;
4110 if (c.bPersistentPower) continue;
4112 if (c.bUndroppable) continue;
4119 // set up gun psprite
4122 if (Level.Game.deathmatch && !IsCheckpointSpawn) GiveDefaultDeathMatchInventory();
4124 // wake up the status bar
4127 if (bIsBot) BotOnSpawn();
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;
4138 //print("*** CHECKPOINT SPAWN: %B", IsCheckpointSpawn);
4139 if (!IsCheckpointSpawn) {
4140 GiveElvenGifts(mthing, Voodoo);
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;
4148 int rh = clamp(GetCvarI('sv_min_startmap_health'), 0, 200);
4149 if (rh && Health < rh) {
4151 EntityEx(MO).Health = rh;
4156 k8BossesDetected = (GetCvarB('k8ElvenDetect') ? -0.5 : 666);
4162 //===========================================================================
4164 // PerformBossDetection
4166 // this is virtual, so mods can override it
4168 //===========================================================================
4169 void PerformBossDetection () {
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);
4178 switch (GetClassName(ec)) {
4179 case 'Cyberdemon': ++cybbieCount; break;
4180 case 'SpiderMastermind': ++mindCount; break;
4182 ec = class!Actor(GetClassParent(ec));
4184 //if (hasCybbie && hasMind) break; // just in case
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!";
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!";
4198 print("\c[Red]*** %s ***", msg);
4199 ClientHudMessage(msg, 'smallfont', HUDMsgType.FadeInOut|HUDMsgFlag.ColorString/*|HUDMsgFlag.Log*/, 123669, CR_UNDEFINED, "Green",
4207 //===========================================================================
4209 // ShowElvenGiftMessage
4211 // this is virtual, so mods can override it
4213 //===========================================================================
4214 void ShowElvenGiftMessage () {
4216 ClientHudMessage("ELVEN GIFT", 'smallfont', HUDMsgType.FadeInOut, 123666, CR_ORANGE, "",
4223 //===========================================================================
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;
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));
4267 //===========================================================================
4271 //===========================================================================
4272 bool AddElvenGift (name Klass) {
4273 class!Actor th = class!Actor(FindClass(Klass));
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') {
4281 if (ReadyWeapon != wpn) {
4282 //PendingWeapon = wpn;
4286 } else if (Klass == 'BDW_Shotgun' || (Klass == 'Shotgun' && FindClass('BDW_Shotgun'))) {
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) {
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;
4312 //===========================================================================
4314 // ShouldRemovePistol
4316 // this is virtual, so mods can override it
4318 //===========================================================================
4319 bool ShouldRemovePistol () {
4320 Inventory inv = EntityEx(MO).Inventory;
4322 Weapon Wpn = Weapon(inv);
4323 inv = inv.Inventory;
4325 class!Weapon wc = class!Weapon(Wpn.Class);
4327 if (GetClassName(wc) == 'BDW_Rifle') return true;
4328 wc = class!Weapon(GetClassParent(wc));
4336 //===========================================================================
4340 // this is virtual, so mods can override it
4342 //===========================================================================
4343 void RemovePistol () {
4344 Inventory inv = EntityEx(MO).Inventory;
4346 Weapon Wpn = Weapon(inv);
4347 inv = inv.Inventory;
4349 //print("WPN: %C", Wpn);
4350 switch (GetClassName(Wpn.Class)) {
4353 EntityEx(MO).RemoveInventory(Wpn);
4362 //===========================================================================
4364 // AddDefaultInventory
4366 //===========================================================================
4367 void AddDefaultInventory () {
4368 HexenArmor HArmor = Level.SpawnEntityChecked(class!HexenArmor, HexenArmor, default, default, default, AllowReplace:false);
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];
4378 BasicArmor BArmor = Level.SpawnEntityChecked(class!BasicArmor, BasicArmor, default, default, default, AllowReplace:false);
4381 BArmor.AttachToOwner(EntityEx(MO));
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));
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;
4398 // for better control empty weapon's ammo
4399 Weapon(Item).AmmoGive1 = 0;
4400 Weapon(Item).AmmoGive2 = 0;
4402 if (!Item.TryPickup(EntityEx(MO))) {
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
4414 //==========================================================================
4418 // Called after a player dies almost everything is cleared and initialised
4420 //==========================================================================
4421 void PlayerReborn () {
4422 // clear player struct
4428 ResetPlayerOnSpawn(keepPlayerState:true);
4429 bUseDown = true; // don't do anything immediately
4431 bAltAttackDown = false;
4432 bReloadQueued = false;
4433 bReloadDown = 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 //==========================================================================
4450 //==========================================================================
4451 void DoClearPlayer () {
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 () {
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
4480 //==========================================================================
4484 // Called when a player completes a level.
4486 //==========================================================================
4487 override void PlayerExitMap (bool clusterChange) {
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;
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);
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);
4528 Weapon wpn = Weapon(Actor(MO).Tracer);
4530 wpn.bBobDisabled = true;
4531 wpn.bBobFrozen = false; // just in case
4533 SetWeapon(wpn); // restore weapon
4537 MO.Angles.pitch = 0.0;
4538 MO.RenderStyle = Entity::STYLE_Normal;
4540 EntityEx(MO).bShadow = false; // cancel invisibility
4541 ExtraLight = 0; // cancel gun flashes
4542 FixedColormap = 0; // cancel ir gogles
4544 DamageFlash = 0.0; // no palette changes
4545 DamageFlashType = '';
4555 ResetWeaponReloadRefire();
4556 ResetWeaponActionFlags();
4557 //bDisableWeaponSwitch = false;
4558 bForceCrouchingDown = 0;
4559 LocalQuakeHappening = vector(0, 0, 0);
4563 //==========================================================================
4567 //==========================================================================
4568 void InventoryLeft () {
4569 if (!bInventoryAlwaysOpen) {
4570 if (!InventoryTime) {
4571 InventoryTime = 5.0;
4574 InventoryTime = 5.0;
4578 Inventory Prev = InvPtr.PrevInv();
4587 //==========================================================================
4591 //==========================================================================
4592 void InventoryRight () {
4593 if (!bInventoryAlwaysOpen) {
4594 if (!InventoryTime) {
4595 InventoryTime = 5.0;
4598 InventoryTime = 5.0;
4602 Inventory Next = InvPtr.NextInv();
4611 //==========================================================================
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 //==========================================================================
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;
4634 if (ArtifactFlash) --ArtifactFlash;
4638 //==========================================================================
4642 //==========================================================================
4643 void AdjustInvFirst () {
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()) {
4656 if (InvFirst == Item) InvFirst = InvPtr;
4660 for (Item = InvPtr; Item && Item != InvFirst; Item = Item.PrevInv()) ++FirstOffs;
4662 while (FirstOffs > InvSize) {
4663 InvFirst = InvFirst.NextInv();
4667 while (NumFollowing+FirstOffs < InvSize && InvFirst.PrevInv()) {
4668 InvFirst = InvFirst.PrevInv();
4674 //==========================================================================
4678 //==========================================================================
4679 EntityEx InventoryThrow () {
4680 if (!InvPtr) return none;
4681 return EntityEx(MO).DropInventory(InvPtr);
4685 //==========================================================================
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();
4716 while (NewPtr.NextInv()) NewPtr = NewPtr.NextInv();
4723 //==========================================================================
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);
4734 k8HealthAccum_LastRegenTime = 0;
4735 k8HealthAccum_LastBoostTime = 0;
4736 ResetWeaponReloadRefire();
4737 ResetWeaponActionFlags();
4738 bDisableWeaponSwitch = false;
4739 bForceCrouchingDown = 0;
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
4751 DeltaViewHeight = 0.0;
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;
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;
4769 if (lookdir > 0) lookdir -= 6;
4770 else if (lookdir < 0) lookdir += 6;
4771 if (abs(lookdir) < 6) lookdir = 0;
4774 CalcHeight(deltaTime);
4776 if (Attacker && Attacker != MO) {
4779 int dir = EntityEx(MO).FaceActor(EntityEx(Attacker), delta);
4781 // looking at killer, so fade damage and poison counters
4783 DamageFlash -= deltaTime;
4784 if (DamageFlash <= 0.0) DamageFlash = 0.0;
4786 if (PoisonCount) --PoisonCount;
4788 delta = fmin(5.0, delta/8.0);
4791 MO.Angles.yaw += delta;
4793 // turn counter clockwise
4794 MO.Angles.yaw -= delta;
4796 } else if (DamageFlash) {
4797 DamageFlash -= deltaTime;
4798 if (DamageFlash <= 0.0) DamageFlash = 0.0;
4799 } else if (PoisonCount) {
4803 if (CheckForRespawn(deltaTime)) {
4806 PlayerState = PST_REBORN;
4811 //==========================================================================
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;
4832 int boostPoints = GetCvarI('k8HealthAccum_BoostPoints');
4833 if (!boostPoints) return false;
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);
4849 k8HealthAccum_LastBoostTime = MO.XLevel.Time;
4850 k8HealthAccum_Amount -= relh;
4852 MO.Health = Health; //k8: we need this for UI
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;
4868 int regenPoints = GetCvarI('k8HealthAccum_RegenPoints');
4869 if (regenPoints < 1) return false;
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'));
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);
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;
4894 MO.Health = Health; //k8: we need this for UI
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
4920 Health = min(Health+amount, itemhealthlimit);
4921 if (toucher) toucher.Health = Health;
4922 return true; // eaten
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'));
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);
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'));
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);
4953 if (toucher) toucher.Health = Health;
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;
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;
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();
4983 if (MO.WaterLevel > 1) {
4984 WaterMove(deltaTime);
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);
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);
4998 PlayerProcessScrollSectors(deltaTime);
4999 //if (MO.Sector->special || MO.Sector->Damage) PlayerInSpecialSector(deltaTime);
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')))
5007 MO.PlaySound('*falling', CHAN_VOICE);
5013 //==========================================================================
5017 //==========================================================================
5018 override void PlayerTick (float deltaTime) {
5021 if (bIsClient) { SimulatedPlayerTick(deltaTime); return; }
5024 if (ViewMouseDeltaX || ViewMouseDeltaY || RawMouseDeltaX || RawMouseDeltaY) {
5025 printdebug("VMS: x=%s; y=%s -- RMS: x=%s; y=%s", ViewMouseDeltaX, ViewMouseDeltaY, RawMouseDeltaX, RawMouseDeltaY);
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);
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;
5044 if (!Level.bFrozen && !(Cheats&CF_TIMEFREEZE)) BotTick(deltaTime);
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();
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();
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);
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;
5071 Actor(MO).bJustAttacked = false;
5074 // you can only press use while totally frozen
5075 if (bTotallyFrozen || (Level.bFrozen && !(Cheats&CF_TIMEFREEZE))) {
5078 ViewAngles = MO.Angles;
5082 ResetWeaponReloadRefire(); // no reload too
5083 ResetWeaponActionFlags();
5084 } else if (bFrozen) {
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);
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;
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();
5115 k8HealthAccum_Amount = 0;
5119 k8HealthAccum_Amount = 0;
5122 // "elven gift" message
5123 if (k8ElvenGiftMessageTime <= 0) {
5124 k8ElvenGiftMessageTime += deltaTime;
5125 if (k8ElvenGiftMessageTime >= 0) {
5126 k8ElvenGiftMessageTime = 666;
5127 ShowElvenGiftMessage();
5131 // "elven senses" message
5132 if (k8BossesDetected <= 0) {
5133 k8BossesDetected += deltaTime;
5134 if (k8BossesDetected >= 0) {
5135 k8BossesDetected = 666;
5136 PerformBossDetection();
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;
5146 bReloadDown = false;
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;
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
5169 auto pawn = PlayerPawn(MO);
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;
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;
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);
5195 // check for landing
5196 if (pawn.lastJump3DPObj) {
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);
5205 printdebug("%C: landed on the same pobj %s; stvel=%s", pawn, pawn.lastJump3DPObj.tag, PolyobjThinker.CalcSpeedVector(pawn.lastJump3DPObj));
5208 if (pawn.lastStand3DPObj == pawn.lastJump3DPObj) {
5209 pawn.Velocity -= PolyobjThinker.CalcSpeedVector(pawn.lastJump3DPObj);
5211 pawn.lastJump3DPObj = nullptr;
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();
5224 if (MO.WaterLevel > 1) {
5225 WaterMove(deltaTime);
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);
5232 if (EntityEx(MO).FindInventory(PowerSpeed) &&
5233 !(Level.XLevel.TicTime&1) && Length(MO.Velocity) > 12.0*35.0)
5239 // bobbing, and view height calculation
5240 CalcHeight(deltaTime);
5242 // scrollers and such
5244 PlayerProcessScrollSectors(deltaTime);
5245 if (MO.Sector->special || MO.Sector->Damage || SectorHas3DFloors(MO.Sector)) {
5246 PlayerInSpecialSector(deltaTime);
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;
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')))
5262 MO.PlaySound('*falling', CHAN_VOICE);
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();
5278 if (Buttons&BT_USE) {
5281 GetUseRanges(out ur, out utr);
5282 EntityEx(MO).UseLines(/*DEFAULT_USERANGE*/ur, /*DEFAULT_USETHINGRANGE*/utr, '*usefail');
5286 //if (bUseDown) print("***USE GOES UP!");
5290 /* done in `MovePsprites()`
5291 if (!ReadyWeapon && PendingWeapon && !bDisableWeaponSwitch && bWeaponAllowSwitch) {
5292 SetWeapon(PendingWeapon);
5300 // chicken attack counter
5303 MorphTime -= deltaTime;
5304 if (MorphTime <= 0.0) {
5305 // attempt to undo the chicken/pig
5307 UndoPlayerMorph(false, self);
5312 MovePsprites(deltaTime);
5314 // poison/damage/etc. counters
5315 if (PoisonCount && Level.XLevel.Time-LastPoisonTime >= 0.5) {
5317 if (PoisonCount < 0) PoisonCount = 0;
5318 LastPoisonTime = Level.XLevel.Time;
5319 Actor(MO).PoisonDamage(Poisoner, Poisoner, 1, true);
5323 DamageFlash -= deltaTime;
5324 if (DamageFlash <= 0.0) DamageFlash = 0.0;
5328 BonusFlash -= deltaTime;
5329 if (BonusFlash <= 0.0) BonusFlash = 0.0;
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*/);
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
5348 // a negative scale is used to prevent G_AddViewAngle/G_AddViewPitch
5349 // from scaling with the FOV scale
5350 desired *= fabs(ReadyWeapon.FOVScale);
5353 if (FOV != desired) {
5354 if (fabs(FOV-desired) < 7.0) {
5357 float zoom = FMax(7.0, fabs(FOV-desired)*0.025);
5358 if (FOV > desired) FOV = FOV-zoom; else FOV = FOV+zoom;
5360 if (int(FOV) == int(DesiredFOV)) {
5367 if (!isBot) HealthBarProcessor();
5370 if (Buttons&BT_FLASHLIGHT) {
5371 if (!bFlashlightButtonDown) {
5372 bFlashlightButtonDown = true;
5373 bFlashlightOn = !bFlashlightOn;
5376 bFlashlightButtonDown = false;
5379 //ProcessFlashligh(); // nope, it is done in `ClientTick()`
5381 if (bForceCrouchingDown && bForceCrouchingDown <= Level.XLevel.TicTime) {
5382 //printdebug("%C: crouch reset!", self);
5383 bForceCrouchingDown = 0;
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);
5394 if (MO.Sector && MO.Sector.ownpobj) {
5395 polyobj_t *po = MO.Sector.ownpobj;
5398 TVec vel = PolyobjThinker.CalcSpeedVector(po);
5399 printdebug("PLAYER: 3d pobj #%s; velocity=%s", po.tag, vel);
5406 //==========================================================================
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) {
5421 AngleVector(ViewAngles, out dir);
5423 AngleVector(MO.Angles, out dir);
5425 //florg.z += MO.Height*0.5-MO.FloorClip;
5426 //florg.z += GetAttackZOfs();
5427 florg.z += ViewHeight;
5429 dlight_t *fl = MO.AllocDlight(MO, florg, flradius, FlashlightLightId);
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;
5437 fl.bNoShadow = !GetCvarB('k8_flashlight_shadows');
5438 fl.bPlayerLight = true;
5444 //==========================================================================
5448 // called from main world thinker after all thinkers were called
5450 //==========================================================================
5451 override void SetViewPos () {
5453 //printdebug("%C: fuck", self);
5454 if (!Camera || Camera.isDestroyed) {
5455 Error("this mod's player pawn code is totally fucked (dead player pawn)");
5459 // just in a case camera entity has been destroyed
5460 if (!Camera) Camera = MO;
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;
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);
5482 if (PlayerState != PST_DEAD) {
5483 ViewAngles = MO.Angles;
5485 ViewAngles.yaw = MO.Angles.yaw;
5486 ViewAngles.pitch = MO.Angles.pitch;
5489 if (MorphTime && ChickenPeck) {
5490 // set chicken attack view position
5492 sincos(MO.Angles.yaw, out s, out c);
5493 ViewOrg.x += float(ChickenPeck)*c;
5494 ViewOrg.y += float(ChickenPeck)*s;
5500 ClientSetViewOrg(ViewOrg);
5502 if (Level.XLevel.Zones.length && Camera.Sector) {
5503 SoundEnvironment = Level.XLevel.Zones[Camera.Sector->Zone];
5505 SoundEnvironment = 0;
5508 if (!SoundEnvironment) {
5509 if (Camera.WaterLevel >= 3) {
5511 SoundEnvironment = 0x1600;
5514 SoundEnvironment = 1;
5520 //==========================================================================
5522 // DebugPutRotatingSpotlight
5524 //==========================================================================
5525 final void DebugPutRotatingSpotlight (TVec florg, float speed, float flradius, int clr, int id) {
5527 ag.yaw = AngleMod360(MO.XLevel.Time*speed);
5529 AngleVector(ag, out dir);
5530 dlight_t *fl = MO.AllocDlight(MO, florg, flradius, 669+id);
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;
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) {
5550 ag.yaw = AngleMod360(MO.XLevel.Time*speed+id*120);
5552 AngleVector(ag, out dir);
5554 dlight_t *fl = MO.AllocDlight(MO, florg, flradius, 669+id);
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;
5562 fl.bNoShadow = false;
5563 fl.bPlayerLight = true; // so it won't be rejected
5567 //==========================================================================
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);
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);
5582 #ifdef SPOTLIGHT_DISCO_CROWN_TEST
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);
5589 #ifdef CALCLIGHT_TEST
5591 int ll = MO.CalcLight();
5592 if (ll != lastCalcLight) {
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));
5604 ::ClientTick(deltaTime);
5608 //==========================================================================
5610 // AdjustPlayerAngle
5612 //==========================================================================
5613 void AdjustPlayerAngle (EntityEx AimTarget) {
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;
5622 MO.Angles.yaw = angle;
5628 //==========================================================================
5632 //==========================================================================
5633 bool UndoPlayerMorph (bool Force, PlayerEx Activator) {
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.
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()) {
5653 MO.LinkToWorld(properFloorCheck:true);
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;
5667 A.ReactionTime = 0.5;
5670 A.bNoGravity = true;
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);
5678 Inventory Pw = EntityEx(MO).FindInventory(PowerWeaponLevel2);
5679 if (Pw) Pw.Destroy();
5680 A.Health = GetRebornHealth();
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;
5689 Weapon OrigWpn = Weapon(EntityEx(MO).FindInventory(MorphWeapon));
5690 if (OrigWpn && OrigWpn.bGivenAsMorphWeapon) {
5691 // you don't get to keep your morphed weapon
5696 if (OldWeapon) OldWeapon.Destroy();
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'));
5712 //===========================================================================
5714 // ActivateMorphWeapon
5716 //===========================================================================
5717 void ActivateMorphWeapon () {
5718 class!Weapon WpnClass = PlayerPawn(MO).MorphWeapon;
5721 Wpn = Level.SpawnEntityChecked(class!Weapon, WpnClass);
5722 if (Wpn) Wpn.bGivenAsMorphWeapon = true;
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);
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);
5732 if (EntityEx(MO).DropItemList[i].Amount > 0) {
5733 Item.Amount = EntityEx(MO).DropItemList[i].Amount;
5737 WpnClass = class!Weapon(Weapon(Item).Class);
5744 if (Wpn && !Wpn.TryPickup(EntityEx(MO))) delete Wpn;
5747 SetWeapon(Weapon(EntityEx(MO).FindInventory(WpnClass)));
5751 SetViewObject(ReadyWeapon);
5753 ReadyWeapon.bBobDisabled = true;
5754 ReadyWeapon.bBobFrozen = false; // just in case
5755 SetViewState(PS_WEAPON, ReadyWeapon.GetReadyState());
5757 SetViewState(PS_WEAPON, none);
5759 SetViewStateOffsets(0, Weapon::WEAPONTOP);
5763 //===========================================================================
5767 //===========================================================================
5768 void PostMorphWeapon (Weapon weapon) {
5770 SetViewStateOffsets(0, Weapon::WEAPONBOTTOM);
5771 SetViewObject(ReadyWeapon);
5773 ReadyWeapon.bBobDisabled = true;
5774 ReadyWeapon.bBobFrozen = false; // just in case
5775 SetViewState(PS_WEAPON, ReadyWeapon.GetUpState());
5777 SetViewState(PS_WEAPON, none);
5782 //==========================================================================
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; }
5795 //==========================================================================
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);
5805 if (MorphStyle&EntityEx::MORPH_FULLHEALTH) {
5806 if (!(MorphStyle&EntityEx::MORPH_ADDSTAMINA)) Max -= (MO ? MO.Stamina : 0);
5808 Max = MAXMORPHHEALTH;
5809 if (MorphStyle&EntityEx::MORPH_ADDSTAMINA) Max += (MO ? MO.Stamina : 0);
5816 //==========================================================================
5818 // CheckFriendlyFire
5822 //==========================================================================
5823 bool CheckFriendlyFire (EntityEx source, int damage) {
5828 //==========================================================================
5830 // IsWeaponAlwaysExtremeDeath
5834 //==========================================================================
5835 bool IsWeaponAlwaysExtremeDeath () {
5840 //==========================================================================
5842 // StartDeathSlideShow
5844 //==========================================================================
5845 void StartDeathSlideShow () {
5849 //==========================================================================
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;
5869 //==========================================================================
5873 //==========================================================================
5874 void Damaged (EntityEx inflictor) {
5878 //==========================================================================
5882 //==========================================================================
5883 void KilledActor (EntityEx Victim) {
5887 //==========================================================================
5891 //==========================================================================
5892 void Killed (EntityEx source, EntityEx inflictor) {
5896 //==========================================================================
5900 //==========================================================================
5901 int GetSigilPieces () {
5906 //==========================================================================
5910 //==========================================================================
5911 void PlayerMorphed (EntityEx OldMO) {
5912 // so we can select weapons
5913 if (PlayerPawn(MO)) PlayerPawn(MO).InitializeWeaponSlots();
5917 //==========================================================================
5921 //==========================================================================
5922 void PlayerUnmorphed () {
5923 // so we can select weapons
5924 if (PlayerPawn(MO)) PlayerPawn(MO).InitializeWeaponSlots();
5928 //==========================================================================
5932 //==========================================================================
5937 //==========================================================================
5939 // CoopGetAllSharedKeys
5941 //==========================================================================
5942 void CoopGetAllSharedKeys () {
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;
5952 for (auto myinv = EntityEx(MO).Inventory; myinv; myinv = myinv.Inventory) {
5953 if (myinv.Class == inv.Class) {
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);
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);
5974 //==========================================================================
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 //==========================================================================
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;
5995 CoopGetAllSharedKeys();
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;
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));
6011 OldMO.DestroyAllInventory();
6015 //==========================================================================
6019 //==========================================================================
6020 void DestroyBot () {
6024 //==========================================================================
6028 //==========================================================================
6029 void BotOnSpawn () {
6033 //==========================================================================
6035 // BotSendSubSectorChange
6037 //==========================================================================
6038 void BotSendSubSectorChange (subsector_t *ss) {
6042 //==========================================================================
6046 //==========================================================================
6047 void SetClientModel () {
6051 //==========================================================================
6055 //==========================================================================
6056 int GetRebornHealth () {
6061 //==========================================================================
6065 //==========================================================================
6066 void BotTick (float deltaTime) {
6070 //==========================================================================
6074 //==========================================================================
6075 void SpawnSpeedEffect () {
6079 //==========================================================================
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);
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
6108 foreach (auto pidx; 0..MAXPLAYERS) {
6109 PlayerEx plr = PlayerEx(Level.Game.Players[pidx]);
6110 if (!plr || !plr.bIsBot) continue;
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);
6127 if (Impulse >= 200 && Impulse <= 205) {
6128 LineSpecialLevelInfo(Level).ConChoiceImpulse(Impulse-200); // strife does additional processing
6135 //==========================================================================
6137 // eventGetReadyWeapon
6139 //==========================================================================
6140 override Entity eventGetReadyWeapon () {
6145 //==========================================================================
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
6202 //==========================================================================
6204 // GetCurrentArmorFullAbsorb
6206 //==========================================================================
6207 override int GetCurrentArmorFullAbsorb () {
6208 // this is for HexenArmor, and it is not implemented yet
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
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;
6259 //==========================================================================
6261 // CheatHelper_AllWeapons
6263 //==========================================================================
6264 void CheatHelper_AllWeapons (bool full) {
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);
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);
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
6290 if (IsWeaponFromSlot11(WpnCls)) continue;
6292 auto pawn = PlayerPawn(MO);
6293 if (!pawn) continue;
6295 if (!pawn.FindWeaponSlot(WpnCls, out slot, out widx)) continue;
6298 //Weapon Wpn = Weapon(Level.Spawn(WpnCls, default, default, default, AllowReplace:false));
6299 Weapon Wpn = EntityEx.SpawnWeaponType(Level, WpnCls, disableReplace:true);
6302 if (!Wpn.TryPickup(EntityEx(MO))) {
6303 //printdebug("%C: rejected weapon '%C'!", MO, Wpn);
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);
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);
6326 if (verbose) printwarn("failed to spawn ammo '%C'", acls);
6329 if (verbose) printdebug("spawned ammo '%C' for ammo class '%C'", AmmoItem, acls);
6332 Inventory invAmmo = EntityEx(MO).FindInventory(acls);
6333 if (invAmmo && Ammo(invAmmo)) {
6335 AmmoItem = Ammo(invAmmo);
6338 assert(Ammo(AmmoItem));
6339 if (!AmmoItem.TryPickup(EntityEx(MO))) {
6347 if (Ammo(AmmoItem)) {
6348 auto spwspate = FindClassState(AmmoItem.Class, 'Spawn');
6349 if (spwspate && AreStateSpritesPresent(spwspate)) {
6350 AmmoItem.Amount = Ammo(AmmoItem).k8GetAmmoKingMax();
6352 bool hasBackpack = false;
6353 if (AmmoItem.Owner && AmmoItem.Owner.bIsPlayer) {
6354 PlayerEx plr = PlayerEx(AmmoItem.Owner.Player);
6355 hasBackpack = (plr && plr.bHasBackpack);
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;
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) {
6380 Weapon wpn = ReadyWeapon;
6382 if (wpn.AmmoType1) {
6383 if (CheatHelper_GiveAmmoClass(wpn.AmmoType1, verbose:true)) gotit = true;
6385 if (wpn.AmmoType2 && wpn.AmmoType2 != wpn.AmmoType1) {
6386 if (CheatHelper_GiveAmmoClass(wpn.AmmoType2, verbose:true)) gotit = true;
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;
6400 // only for weapons in hands
6401 auto pawn = PlayerPawn(MO);
6404 array!(class!Ammo) ammoList;
6406 // check all weapon slots
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;
6418 // collect possible ammo
6419 for (Inventory inv = pawn.Inventory; inv; inv = inv.Inventory) {
6420 auto wpn = Weapon(inv);
6422 foreach (int atidx; 0..2) {
6423 class!Ammo acls = (atidx ? wpn.AmmoType2 : wpn.AmmoType1);
6424 if (!acls) continue;
6426 for (; xidx < ammoList.length; ++xidx) if (ammoList[xidx] == acls) break;
6427 if (xidx >= ammoList.length) ammoList[$] = acls;
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;
6438 if (gotit) cprint("You got ammo!");
6442 //==========================================================================
6444 // CheatHelper_AllKeys
6446 //==========================================================================
6447 void CheatHelper_AllKeys () {
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);
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;
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;
6494 moveWalk *= rwp.PlayerSpeedScaleRun;
6495 moveRun *= rwp.PlayerSpeedScaleRun;
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 //==========================================================================
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 //==========================================================================
6533 // can be overriden in user mods
6535 //==========================================================================
6536 float GetJumpVelZ () {
6537 float res = PlayerPawn(MO).JumpVelZ;
6538 if (ReadyWeapon) res *= ReadyWeapon.PlayerJumpScale;
6543 //==========================================================================
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);
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;
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;
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) {
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,
6625 bootprintFlip = !bootprintFlip;
6629 //==========================================================================
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;
6643 PlayerPawn pwn = PlayerPawn(MO);
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;
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
6661 if (tm <= 0.0f || vol <= 0.0f) {
6662 // reset footstep time
6663 lastFootstepSoundTime = GameTime;
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));
6681 PrevViewHeight = -666;