by MartinHowe » Sat Aug 16, 2025 12:14 am
Discussion redirected from
the Doomworld thread, so as not to spam NightDive's thread with ZDoom-specific stuff.
Regarding the matter of implementing class changing as morphing; experiments seem to suggest that it can be done in ZScript. Indeed, most of the morph code nowadays seems to be in ZScript. However, while a script is possible that saves weapons across morphs, one cannot wield more than one weapon when morphed. You can give a morphed player another weapon, it goes in their inventory, they get the mana in it (if applicable), and they can can drop it; but they can't wield it if they already have another weapon.
Sadly, the weapon code is so convoluted, that it's nowhere near obvious why one cannot use, say, a cleric staff while morphed as a cleric. The item is given, in the inventory, but cannot be switched to. This it really bizarre. What on earth, deep in the code, is stopping it?
So it seems that maybe there should be a clean method of switching player classes that isn't based on morphing but is a straight-up thing in itself; maybe start with the morph code and strip out everything not essential to it. Of course, to support it might require changes under the hood. It'd be interesting to know what the devs think.
I have always considered it kinda sad that morphing wasn't in from the start; theoretically one should have always been able to attach a player avatar to any actor, even a barrel, on the understanding that actions are limited to those of the actor and that, for monsters, PSprites would need adding. The standard attacks and actions common to most monsters would be tagged as such so that player control can be automatically mapped to them by the engine. For example, a barrel would ignore movement commands and only have one attack state: Melee, which would be to self-destruct, essentially making it a suicide bomber. However, this would need to have been set up from the start, so I can understand why it was never a thing. Of course it could now be done in the form of some huge mod; but as always, there'd never be the time to do it
Experimental morph code:
Code: Select all
class MorphCycler : ArtiEgg
{
bool giveFWeapFist;
bool giveCWeapMace;
bool giveMWeapWand;
bool morphed;
bool hasFWeapFist;
bool hasFWeapAxe;
bool hasFWeapHammer;
bool hasFWeapQuietus;
bool hasCWeapMace;
bool hasCWeapStaff;
bool hasCWeapFlame;
bool hasCWeapWraithverge;
bool hasMWeapWand;
bool hasMWeapFrost;
bool hasMWeapLightning;
bool hasMWeapBloodscourge;
class<Weapon> fighterWielding;
class<Weapon> clericWielding;
class<Weapon> mageWielding;
Default
{
Inventory.PickupMessage "Morph Cycler";
Tag "Morph Cycler";
}
override bool TryPickup (in out Actor toucher)
{
bool result = super.TryPickup(toucher);
if (result)
{
console.printf("PICKUP!");
// When first morphed we must
// give them the starting weapon;
// this is pickup so not morphed,
// at least not by us.
giveFWeapFist = !(toucher is "FighterPlayer");
giveCWeapMace = !(toucher is "ClericPlayer");
giveMWeapWand = !(toucher is "MagePlayer");
}
return result;
}
override bool Use(bool pickup)
{
if (!owner)
{
console.printf("No owner!");
return false;
}
let currentClass = owner.GetClass();
if (!(currentClass is "PlayerPawn"))
{
console.printf("Not player class!");
return false;
}
if (currentClass is "FighterPlayer")
{
MorphToNewClass("ClericPlayer");
}
else if (currentClass is "ClericPlayer")
{
MorphToNewClass("MagePlayer");
}
else if (currentClass is "MagePlayer")
{
MorphToNewClass("FighterPlayer");
}
return false;
}
void MorphToNewClass(class <Actor> newClass)
{
string oldClassName = owner.GetClassName();
string newClassName = newClass.GetClassName();
int flags = MRF_ADDSTAMINA|
MRF_FULLHEALTH|
MRF_WHENINVULNERABLE|
MRF_LOSEACTUALWEAPON|
MRF_NEWTIDBEHAVIOUR|
MRF_UNDOBYDEATHFORCED|
MRF_UNDOALWAYS|
MRF_TRANSFERTRANSLATION;
console.printf("Currently a %s, requesting %s.", oldClassName, newClassName);
if (owner is newClass)
{
console.printf("Already a %s, no action required.", newClassName);
return;
}
bool wasMorphed = false;
if (morphed)
{
console.printf("Attempting unmorph.");
SaveWeaponState(owner.GetClass());
owner.Unmorph(owner, true);
wasMorphed = true;
morphed = false;
}
if (owner is newClass)
{
RestoreWeaponState(newClass);
return;
}
// If execution reaches here and we were previously morphed then:
// * The state of the old class has already been saved.
// * Because we were not already the current class, its state was not in
// use and thus has not been modified since it was last morphed away from.
// * Because we are going to immediately morph away from it again, we
// we do not need to restore its state.
// If execution reaches here and we were NOT previously morphed then:
// * Because we are the current class, its state is in
// use and thus must be saved before the morph.
// * Because we are not already the new class, its state was not in
// use and thus must be restored after the morph.
console.printf("Attempting morph to %s.", newClassName);
if (!wasMorphed) SaveWeaponState(owner.GetClass());
if (owner.A_Morph(newClass, 10500, flags))
{
console.printf("Now a %s\n", newClass.GetClassName());
RestoreWeaponState(newClass);
morphed = true;
}
else
{
console.printf("Morph to %s failed!", newClassName);
console.printf("Remaining at %s\n", owner.GetClassName());
}
}
void SaveWeaponState(class <Actor> currentClass)
{
let readyWeapon = owner.Player.ReadyWeapon;
if (currentClass is "FighterPlayer")
{
FighterSaveWeapons();
fighterWielding = null;
if (readyWeapon) fighterWielding = readyWeapon.GetClass();
}
else if (currentClass is "ClericPlayer")
{
ClericSaveWeapons();
clericWielding = null;
if (readyWeapon) clericWielding = readyWeapon.GetClass();
}
else if (currentClass is "MagePlayer")
{
MageSaveWeapons();
mageWielding = null;
if (readyWeapon) mageWielding = readyWeapon.GetClass();
}
}
void RestoreWeaponState(class <Actor> newClass)
{
if (newClass is "FighterPlayer")
{
if (giveFWeapFist) // first time as a fighter
{
if (!(owner.FindInventory("FWeapFist")))
{
owner.GiveInventory("FWeapFist", 1);
}
owner.A_SelectWeapon("FWeapFist");
giveFWeapFist = false;
}
else
{
FighterRestoreWeapons();
if (fighterWielding) owner.A_SelectWeapon(fighterWielding, SWF_SELECTPRIORITY);
}
}
else if (newClass is "ClericPlayer")
{
if (giveCWeapMace) // first time as a cleric
{
if (!(owner.FindInventory("CWeapMace")))
{
owner.GiveInventory("CWeapMace", 1);
}
owner.A_SelectWeapon("CWeapMace");
giveCWeapMace = false;
}
else
{
ClericRestoreWeapons();
if (clericWielding) owner.A_SelectWeapon(clericWielding, SWF_SELECTPRIORITY);
}
}
else if (newClass is "MagePlayer")
{
if (giveMWeapWand) // first time as a mage
{
if (!(owner.FindInventory("MWeapWand")))
{
owner.GiveInventory("MWeapWand", 1);
}
owner.A_SelectWeapon("MWeapWand");
giveMWeapWand = false;
}
else
{
MageRestoreWeapons();
if (mageWielding) owner.A_SelectWeapon(mageWielding, SWF_SELECTPRIORITY);
}
}
}
void FighterSaveWeapons()
{
hasFWeapFist = !!(FindInventory("FWeapFist"));
hasFWeapAxe = !!(FindInventory("FWeapAxe"));
hasFWeapHammer = !!(FindInventory("FWeapHammer"));
hasFWeapQuietus = !!(FindInventory("FWeapQuietus"));
}
void FighterRestoreWeapons()
{
if (hasFWeapFist) owner.GiveInventory("FWeapFist", 1);
if (hasFWeapAxe) owner.GiveInventory("FWeapAxe", 1);
if (hasFWeapHammer) owner.GiveInventory("FWeapHammer", 1);
if (hasFWeapQuietus) owner.GiveInventory("FWeapQuietus", 1);
}
void ClericSaveWeapons()
{
hasCWeapMace = !!(FindInventory("CWeapMace"));
hasCWeapStaff = !!(FindInventory("CWeapStaff"));
hasCWeapFlame = !!(FindInventory("CWeapFlame"));
hasCWeapWraithverge = !!(FindInventory("CWeapWraithverge"));
}
void ClericRestoreWeapons()
{
if (hasCWeapMace) owner.GiveInventory("CWeapMace", 1);
if (hasCWeapStaff) owner.GiveInventory("CWeapStaff", 1);
if (hasCWeapFlame) owner.GiveInventory("CWeapFlame", 1);
if (hasCWeapWraithverge) owner.GiveInventory("CWeapWraithverge", 1);
}
void MageSaveWeapons()
{
hasMWeapWand = !!(FindInventory("MWeapWand"));
hasMWeapFrost = !!(FindInventory("MWeapFrost"));
hasMWeapLightning = !!(FindInventory("MWeapLightning"));
hasMWeapBloodscourge = !!(FindInventory("MWeapBloodscourge"));
}
void MageRestoreWeapons()
{
if (hasMWeapWand) owner.GiveInventory("MWeapWand", 1);
if (hasMWeapFrost) owner.GiveInventory("MWeapFrost", 1);
if (hasMWeapLightning) owner.GiveInventory("MWeapLightning", 1);
if (hasMWeapBloodscourge) owner.GiveInventory("MWeapBloodscourge", 1);
}
}
Discussion redirected from [url=https://www.doomworld.com/forum/topic/154611-hexen-heretic-nightdive/]the Doomworld thread[/url], so as not to spam NightDive's thread with ZDoom-specific stuff.
Regarding the matter of implementing class changing as morphing; experiments seem to suggest that it can be done in ZScript. Indeed, most of the morph code nowadays seems to be in ZScript. However, while a script is possible that saves weapons across morphs, one cannot wield more than one weapon when morphed. You can give a morphed player another weapon, it goes in their inventory, they get the mana in it (if applicable), and they can can drop it; but they can't wield it if they already have another weapon.
Sadly, the weapon code is so convoluted, that it's nowhere near obvious why one cannot use, say, a cleric staff while morphed as a cleric. The item is given, in the inventory, but cannot be switched to. This it really bizarre. What on earth, deep in the code, is stopping it? :shrug:
So it seems that maybe there should be a clean method of switching player classes that isn't based on morphing but is a straight-up thing in itself; maybe start with the morph code and strip out everything not essential to it. Of course, to support it might require changes under the hood. It'd be interesting to know what the devs think.
[i]I have always considered it kinda sad that morphing wasn't in from the start; theoretically one should have always been able to attach a player avatar to any actor, even a barrel, on the understanding that actions are limited to those of the actor and that, for monsters, PSprites would need adding. The standard attacks and actions common to most monsters would be tagged as such so that player control can be automatically mapped to them by the engine. For example, a barrel would ignore movement commands and only have one attack state: Melee, which would be to self-destruct, essentially making it a suicide bomber. However, this would need to have been set up from the start, so I can understand why it was never a thing. Of course it could now be done in the form of some huge mod; but as always, there'd never be the time to do it :( [/i]
[b]Experimental morph code:[/b]
[code]class MorphCycler : ArtiEgg
{
bool giveFWeapFist;
bool giveCWeapMace;
bool giveMWeapWand;
bool morphed;
bool hasFWeapFist;
bool hasFWeapAxe;
bool hasFWeapHammer;
bool hasFWeapQuietus;
bool hasCWeapMace;
bool hasCWeapStaff;
bool hasCWeapFlame;
bool hasCWeapWraithverge;
bool hasMWeapWand;
bool hasMWeapFrost;
bool hasMWeapLightning;
bool hasMWeapBloodscourge;
class<Weapon> fighterWielding;
class<Weapon> clericWielding;
class<Weapon> mageWielding;
Default
{
Inventory.PickupMessage "Morph Cycler";
Tag "Morph Cycler";
}
override bool TryPickup (in out Actor toucher)
{
bool result = super.TryPickup(toucher);
if (result)
{
console.printf("PICKUP!");
// When first morphed we must
// give them the starting weapon;
// this is pickup so not morphed,
// at least not by us.
giveFWeapFist = !(toucher is "FighterPlayer");
giveCWeapMace = !(toucher is "ClericPlayer");
giveMWeapWand = !(toucher is "MagePlayer");
}
return result;
}
override bool Use(bool pickup)
{
if (!owner)
{
console.printf("No owner!");
return false;
}
let currentClass = owner.GetClass();
if (!(currentClass is "PlayerPawn"))
{
console.printf("Not player class!");
return false;
}
if (currentClass is "FighterPlayer")
{
MorphToNewClass("ClericPlayer");
}
else if (currentClass is "ClericPlayer")
{
MorphToNewClass("MagePlayer");
}
else if (currentClass is "MagePlayer")
{
MorphToNewClass("FighterPlayer");
}
return false;
}
void MorphToNewClass(class <Actor> newClass)
{
string oldClassName = owner.GetClassName();
string newClassName = newClass.GetClassName();
int flags = MRF_ADDSTAMINA|
MRF_FULLHEALTH|
MRF_WHENINVULNERABLE|
MRF_LOSEACTUALWEAPON|
MRF_NEWTIDBEHAVIOUR|
MRF_UNDOBYDEATHFORCED|
MRF_UNDOALWAYS|
MRF_TRANSFERTRANSLATION;
console.printf("Currently a %s, requesting %s.", oldClassName, newClassName);
if (owner is newClass)
{
console.printf("Already a %s, no action required.", newClassName);
return;
}
bool wasMorphed = false;
if (morphed)
{
console.printf("Attempting unmorph.");
SaveWeaponState(owner.GetClass());
owner.Unmorph(owner, true);
wasMorphed = true;
morphed = false;
}
if (owner is newClass)
{
RestoreWeaponState(newClass);
return;
}
// If execution reaches here and we were previously morphed then:
// * The state of the old class has already been saved.
// * Because we were not already the current class, its state was not in
// use and thus has not been modified since it was last morphed away from.
// * Because we are going to immediately morph away from it again, we
// we do not need to restore its state.
// If execution reaches here and we were NOT previously morphed then:
// * Because we are the current class, its state is in
// use and thus must be saved before the morph.
// * Because we are not already the new class, its state was not in
// use and thus must be restored after the morph.
console.printf("Attempting morph to %s.", newClassName);
if (!wasMorphed) SaveWeaponState(owner.GetClass());
if (owner.A_Morph(newClass, 10500, flags))
{
console.printf("Now a %s\n", newClass.GetClassName());
RestoreWeaponState(newClass);
morphed = true;
}
else
{
console.printf("Morph to %s failed!", newClassName);
console.printf("Remaining at %s\n", owner.GetClassName());
}
}
void SaveWeaponState(class <Actor> currentClass)
{
let readyWeapon = owner.Player.ReadyWeapon;
if (currentClass is "FighterPlayer")
{
FighterSaveWeapons();
fighterWielding = null;
if (readyWeapon) fighterWielding = readyWeapon.GetClass();
}
else if (currentClass is "ClericPlayer")
{
ClericSaveWeapons();
clericWielding = null;
if (readyWeapon) clericWielding = readyWeapon.GetClass();
}
else if (currentClass is "MagePlayer")
{
MageSaveWeapons();
mageWielding = null;
if (readyWeapon) mageWielding = readyWeapon.GetClass();
}
}
void RestoreWeaponState(class <Actor> newClass)
{
if (newClass is "FighterPlayer")
{
if (giveFWeapFist) // first time as a fighter
{
if (!(owner.FindInventory("FWeapFist")))
{
owner.GiveInventory("FWeapFist", 1);
}
owner.A_SelectWeapon("FWeapFist");
giveFWeapFist = false;
}
else
{
FighterRestoreWeapons();
if (fighterWielding) owner.A_SelectWeapon(fighterWielding, SWF_SELECTPRIORITY);
}
}
else if (newClass is "ClericPlayer")
{
if (giveCWeapMace) // first time as a cleric
{
if (!(owner.FindInventory("CWeapMace")))
{
owner.GiveInventory("CWeapMace", 1);
}
owner.A_SelectWeapon("CWeapMace");
giveCWeapMace = false;
}
else
{
ClericRestoreWeapons();
if (clericWielding) owner.A_SelectWeapon(clericWielding, SWF_SELECTPRIORITY);
}
}
else if (newClass is "MagePlayer")
{
if (giveMWeapWand) // first time as a mage
{
if (!(owner.FindInventory("MWeapWand")))
{
owner.GiveInventory("MWeapWand", 1);
}
owner.A_SelectWeapon("MWeapWand");
giveMWeapWand = false;
}
else
{
MageRestoreWeapons();
if (mageWielding) owner.A_SelectWeapon(mageWielding, SWF_SELECTPRIORITY);
}
}
}
void FighterSaveWeapons()
{
hasFWeapFist = !!(FindInventory("FWeapFist"));
hasFWeapAxe = !!(FindInventory("FWeapAxe"));
hasFWeapHammer = !!(FindInventory("FWeapHammer"));
hasFWeapQuietus = !!(FindInventory("FWeapQuietus"));
}
void FighterRestoreWeapons()
{
if (hasFWeapFist) owner.GiveInventory("FWeapFist", 1);
if (hasFWeapAxe) owner.GiveInventory("FWeapAxe", 1);
if (hasFWeapHammer) owner.GiveInventory("FWeapHammer", 1);
if (hasFWeapQuietus) owner.GiveInventory("FWeapQuietus", 1);
}
void ClericSaveWeapons()
{
hasCWeapMace = !!(FindInventory("CWeapMace"));
hasCWeapStaff = !!(FindInventory("CWeapStaff"));
hasCWeapFlame = !!(FindInventory("CWeapFlame"));
hasCWeapWraithverge = !!(FindInventory("CWeapWraithverge"));
}
void ClericRestoreWeapons()
{
if (hasCWeapMace) owner.GiveInventory("CWeapMace", 1);
if (hasCWeapStaff) owner.GiveInventory("CWeapStaff", 1);
if (hasCWeapFlame) owner.GiveInventory("CWeapFlame", 1);
if (hasCWeapWraithverge) owner.GiveInventory("CWeapWraithverge", 1);
}
void MageSaveWeapons()
{
hasMWeapWand = !!(FindInventory("MWeapWand"));
hasMWeapFrost = !!(FindInventory("MWeapFrost"));
hasMWeapLightning = !!(FindInventory("MWeapLightning"));
hasMWeapBloodscourge = !!(FindInventory("MWeapBloodscourge"));
}
void MageRestoreWeapons()
{
if (hasMWeapWand) owner.GiveInventory("MWeapWand", 1);
if (hasMWeapFrost) owner.GiveInventory("MWeapFrost", 1);
if (hasMWeapLightning) owner.GiveInventory("MWeapLightning", 1);
if (hasMWeapBloodscourge) owner.GiveInventory("MWeapBloodscourge", 1);
}
}[/code]