[ZScript] Permanent Player Morph

Ask about ACS, DECORATE, ZScript, or any other scripting questions here!

Moderator: GZDoom Developers

Forum rules
Before asking on how to use a ZDoom feature, read the ZDoom wiki first. If you still don't understand how to use a feature, then ask here.

[ZScript] Permanent Player Morph

Postby 22alpha22 » 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: Select allExpand view
   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);
      }
   }
User avatar
22alpha22
So lonely...
 
Joined: 21 Feb 2014
Location: Montana, USA
Operating System: Windows Vista/7/2008 64-bit
Graphics Processor: nVidia (Modern GZDoom)

Re: [ZScript] Permanent Player Morph

Postby Player701 » 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: Select allExpand view
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: Select allExpand view
if (MapName.Left(5) == "BONUS")
instead.
User avatar
Player701
 
 
 
Joined: 13 May 2009
Location: Russia
Discord: Player701#8214
Operating System: Windows 10/8.1/8/201x 64-bit
OS Test Version: No (Using Stable Public Version)
Graphics Processor: nVidia with Vulkan support

Re: [ZScript] Permanent Player Morph

Postby 22alpha22 » 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.
User avatar
22alpha22
So lonely...
 
Joined: 21 Feb 2014
Location: Montana, USA
Operating System: Windows Vista/7/2008 64-bit
Graphics Processor: nVidia (Modern GZDoom)


Return to Scripting

Who is online

Users browsing this forum: Hey Doomer and 0 guests