[ZScript] Permanent Player Morph

Mon Sep 13, 2021 5:40 pm

In my project I have a couple of episodes, the main episode uses modern GZDoom features and a custom player class with new weapons and player sounds. The second episode is a bonus episode which uses a vanilla weapon and item set. Since I replaced the default player with a modified one for the main episode, I needed a way to make the player a vanilla compatible class if they were playing the bonus episode. I could of course instead replacing the vanilla Doom Player, allow the player to choose the their class, but that would mean they could potentially choose the vanilla class for the main episode and the vice versa for the bonus episode which wont do it all.

So my solution was to use an Event Handler to check if the player was starting a map in the bonus episode and if so, morph the player into the vanilla compatible class. I did a bunch of searching prior to trying this and it seems that morphing the player in a permanent way hasn't had too much success in the past. Nevertheless, I looked up how player morphing was handled in the game code, (which was somewhat hard to follow as I haven't learned C++), and tried to copy that back into ZScript form.

The results are below, from my testing it seems to work properly, the player changes player pawns to the vanilla compatible class, and I'm able to pick up and use items/weapons restricted to the vanilla class. The class and inventory even persists through normal level changes. I haven't tested multiplayer so I don't know if it will break there but from my testing so far, it seems to work properly. So my question is, looking at my code below, is there anything I missed that may cause issues? I think I read somewhere about global pointer substitution which I don't really understand what that means, so I probably didn't do it properly. In fact there are several parts of the C++ that I didn't understand so I didn't bother porting over, so if anyone can point out something I missed it would be appreciated.

Code:
   Override Void PlayerEntered(PlayerEvent E)
   {
      PlayerInfo TempPlayerNumber = Players[E.PlayerNumber];
      Let TempPlayerID = Players[E.PlayerNumber].mo;
      Actor PlayerID = TempPlayerID;
      Let ActualPlayerID = AlphaPlayerBaseClass(PlayerID);
      String MapName = Level.MapName;

      If (MapName == "BONUS01" || MapName == "BONUS02" || MapName == "BONUS03" || MapName == "BONUS04" || MapName == "BONUS05" || MapName == "BONUS06" || MapName == "BONUS07" || MapName == "BONUS08")
      {
         ACS_NamedExecuteWithResult('Bonus_Episode_Sector_Colors',0,0,0,0);

         If (!CVar.FindCVar('Player_Setup').GetBool())
         {
            Let NewPlayer = ActualPlayerID.Spawn("AlphaOldPlayer",ActualPlayerID.Pos,NO_REPLACE);

            NewPlayer.Angle = ActualPlayerID.Angle;
            NewPlayer.Pitch = ActualPlayerID.Pitch;
            NewPlayer.Translation = ActualPlayerID.Translation;

            If (ActualPlayerID.TID != 0)
            {
               NewPlayer.ChangeTID(ActualPlayerID.TID);
               ActualPlayerID.ChangeTID(0);
            }

            NewPlayer.Target = ActualPlayerID.Target;
            NewPlayer.Tracer = ActualPlayerID.Tracer;
            NewPlayer.FriendPlayer = ActualPlayerID.FriendPlayer;
            NewPlayer.DesignatedTeam = ActualPlayerID.DesignatedTeam;
            NewPlayer.Score = ActualPlayerID.Score;
            PlayerPawn(NewPlayer).Player = PlayerPawn(ActualPlayerID).Player;
            PlayerPawn(ActualPlayerID).Player.Mo = PlayerPawn(NewPlayer);
            PlayerPawn(ActualPlayerID).Player.Camera = PlayerPawn(NewPlayer);
            ActualPlayerID.Player = Null;
            ActualPlayerID.Destroy();

            NewPlayer.A_GiveInventory("AlphaOldFist",1,AAPTR_DEFAULT);
            NewPlayer.A_GiveInventory("AlphaOldPistol",1,AAPTR_DEFAULT);
            NewPlayer.A_GiveInventory("Clip",30,AAPTR_DEFAULT);

            CVar.FindCVar('Player_Setup').SetBool(True);
         }
      }
   }

   Override Void PlayerRespawned(PlayerEvent E)
   {
      PlayerInfo TempPlayerNumber = Players[E.PlayerNumber];
      Let TempPlayerID = Players[E.PlayerNumber].mo;
      Actor PlayerID = TempPlayerID;
      Let ActualPlayerID = AlphaPlayerBaseClass(PlayerID);
      String MapName = Level.MapName;

      If (MapName == "BONUS01" || MapName == "BONUS02" || MapName == "BONUS03" || MapName == "BONUS04" || MapName == "BONUS05" || MapName == "BONUS06" || MapName == "BONUS07" || MapName == "BONUS08")
      {
         If (ActualPlayerID)
         {
            Let NewPlayer = ActualPlayerID.Spawn("AlphaOldPlayer",ActualPlayerID.Pos,NO_REPLACE);

            NewPlayer.Angle = ActualPlayerID.Angle;
            NewPlayer.Pitch = ActualPlayerID.Pitch;
            NewPlayer.Translation = ActualPlayerID.Translation;

            If (ActualPlayerID.TID != 0)
            {
               NewPlayer.ChangeTID(ActualPlayerID.TID);
               ActualPlayerID.ChangeTID(0);
            }

            NewPlayer.Target = ActualPlayerID.Target;
            NewPlayer.Tracer = ActualPlayerID.Tracer;
            NewPlayer.FriendPlayer = ActualPlayerID.FriendPlayer;
            NewPlayer.DesignatedTeam = ActualPlayerID.DesignatedTeam;
            NewPlayer.Score = ActualPlayerID.Score;
            PlayerPawn(NewPlayer).Player = PlayerPawn(ActualPlayerID).Player;
            PlayerPawn(ActualPlayerID).Player.Mo = PlayerPawn(NewPlayer);
            PlayerPawn(ActualPlayerID).Player.Camera = PlayerPawn(NewPlayer);
            ActualPlayerID.Player = Null;
            ActualPlayerID.Destroy();

            NewPlayer.A_GiveInventory("AlphaOldFist",1,AAPTR_DEFAULT);
            NewPlayer.A_GiveInventory("AlphaOldPistol",1,AAPTR_DEFAULT);
            NewPlayer.A_GiveInventory("Clip",30,AAPTR_DEFAULT);
         }

         CVar.FindCVar('Player_Setup').SetBool(True);
      }
   }

Re: [ZScript] Permanent Player Morph

Tue Sep 14, 2021 1:54 am

It can be easy to overlook something when there's a lot of stuff going on. Rather than trying to account for everything at once, it is usually much more productive to subject the code to actual playtesting. If you discover that something does not work as expected, further action should be taken from there.

As for now I can tell you that you should probably not use a CVAR as a flag to check if the required action has already been done. The main reason for that is that CVAR values are never under your (the modder's) full control, and can be changed arbitrarily by the player(s), potentially resulting in unexpected behavior. If you want to check if the player has already been morphed, then use the is operator on their player pawn to check for its current class.

A few other nitpicks I have:

1) You appear to have two nearly-identical parts of code, so you should probably refactor them to a separate method and call it from both PlayerEntered and PlayerRespawned. This reduces the possibility of introducing bugs in your code when you need to change something in it, since you won't have to make the changes in both places at the same time.

2) Spawn is a static method of Actor so it can be called like this:
Code:
let NewPlayer = Actor.Spawn(...)

Note that there is no functional change, it just improves the readability of the code a bit.

3) You could also forgo all the == comparisons and do something like:
Code:
if (MapName.Left(5) == "BONUS")
instead.

Re: [ZScript] Permanent Player Morph

Tue Sep 14, 2021 10:01 am

Thanks for the feedback, it is much appreciated. You were right about the CVar causing problems. It turns out if I start a new game in the bonus episode after already starting a new game, the CVar doesn't update and the player fails to morph into the vanilla class. I solved this by simply checking the player's class as suggested and now it works perfectly. I also used your other suggestions and everything on the surface seems to be working now aside from the fact the class doesn't update in the player setup menu and it keeps trying to use the sprites from the custom player class rather than the vanilla PLAY sprites. I solved this by forcing the vanilla sprites by overriding the Tick virtual function and using GetSpriteIndex. This seems to be a brute force method and I wish I knew why it wasn't just using the PLAY sprites as defined for the class states.