//----------------------------------------------------------------------------- // // Copyright 1994-1996 Raven Software // Copyright 1999-2016 Randy Heit // Copyright 2002-2018 Christoph Oelckers // Copyright 2005-2008 Martin Howe // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see http://www.gnu.org/licenses/ // //----------------------------------------------------------------------------- // extend class Actor { virtual Actor, int, int MorphedDeath() { return null, 0, 0; } //=========================================================================== // // Main entry point // //=========================================================================== bool Morph(Actor activator, class playerclass, class monsterclass, int duration = 0, int style = 0, class morphflash = null, classunmorphflash = null) { if (player != null && player.mo != null && playerclass != null) { return player.mo.MorphPlayer(activator? activator.player : null, playerclass, duration, style, morphflash, unmorphflash); } else { return MorphMonster(monsterclass, duration, style, morphflash, unmorphflash); } } //=========================================================================== // // Action function variant whose arguments differ from the generic one. // //=========================================================================== bool A_Morph(class type, int duration = 0, int style = 0, class morphflash = null, classunmorphflash = null) { if (self.player != null) { let playerclass = (class)(type); if (playerclass && self.player.mo != null) return player.mo.MorphPlayer(self.player, playerclass, duration, style, morphflash, unmorphflash); } else { return MorphMonster(type, duration, style, morphflash, unmorphflash); } return false; } //=========================================================================== // // Main entry point // //=========================================================================== bool UnMorph(Actor activator, int flags, bool force) { if (player) { return player.mo.UndoPlayerMorph(activator? activator.player : null, flags, force); } else { let morphed = MorphedMonster(self); if (morphed) return morphed.UndoMonsterMorph(force); } return false; } //--------------------------------------------------------------------------- // // FUNC P_MorphMonster // // Returns true if the monster gets turned into a chicken/pig. // //--------------------------------------------------------------------------- virtual bool MorphMonster (Class spawntype, int duration, int style, Class enter_flash, Class exit_flash) { if (player || spawntype == NULL || bDontMorph || !bIsMonster || !(spawntype is 'MorphedMonster')) { return false; } let morphed = MorphedMonster(Spawn (spawntype, Pos, NO_REPLACE)); Substitute (morphed); if ((style & MRF_TRANSFERTRANSLATION) && !morphed.bDontTranslate) { morphed.Translation = Translation; } morphed.ChangeTid(tid); ChangeTid(0); morphed.Angle = Angle; morphed.UnmorphedMe = self; morphed.Alpha = Alpha; morphed.RenderStyle = RenderStyle; morphed.Score = Score; morphed.UnmorphTime = level.time + ((duration) ? duration : DEFMORPHTICS) + random[morphmonst](); morphed.MorphStyle = style; morphed.MorphExitFlash = (exit_flash) ? exit_flash : (class)("TeleportFog"); morphed.FlagsSave = bSolid * 2 + bShootable * 4 + bInvisible * 0x40; // The factors are for savegame compatibility morphed.special = special; morphed.args[0] = args[0]; morphed.args[1] = args[1]; morphed.args[2] = args[2]; morphed.args[3] = args[3]; morphed.args[4] = args[4]; morphed.CopyFriendliness (self, true); morphed.bShadow |= bShadow; morphed.bGhost |= bGhost; special = 0; bSolid = false; bShootable = false; bUnmorphed = true; bInvisible = true; let eflash = Spawn(enter_flash ? enter_flash : (class)("TeleportFog"), Pos + (0, 0, gameinfo.TELEFOGHEIGHT), ALLOW_REPLACE); if (eflash) eflash.target = morphed; return true; } } extend class PlayerPawn { //=========================================================================== // // EndAllPowerupEffects // // Calls EndEffect() on every Powerup in the inventory list. // //=========================================================================== void InitAllPowerupEffects() { let item = Inv; while (item != null) { let power = Powerup(item); if (power != null) { power.InitEffect(); } item = item.Inv; } } //=========================================================================== // // EndAllPowerupEffects // // Calls EndEffect() on every Powerup in the inventory list. // //=========================================================================== void EndAllPowerupEffects() { let item = Inv; while (item != null) { let power = Powerup(item); if (power != null) { power.EndEffect(); } item = item.Inv; } } //=========================================================================== // // // //=========================================================================== virtual void ActivateMorphWeapon () { class morphweaponcls = MorphWeapon; player.PendingWeapon = WP_NOCHANGE; if (player.ReadyWeapon != null) { player.GetPSprite(PSP_WEAPON).y = WEAPONTOP; } if (morphweaponcls == null || !(morphweaponcls is 'Weapon')) { // No weapon at all while morphed! player.ReadyWeapon = null; } else { player.ReadyWeapon = Weapon(FindInventory (morphweaponcls)); if (player.ReadyWeapon == null) { player.ReadyWeapon = Weapon(GiveInventoryType (morphweaponcls)); if (player.ReadyWeapon != null) { player.ReadyWeapon.GivenAsMorphWeapon = true; // flag is used only by new beastweap semantics in UndoPlayerMorph } } if (player.ReadyWeapon != null) { player.SetPsprite(PSP_WEAPON, player.ReadyWeapon.GetReadyState()); } } if (player.ReadyWeapon != null) { player.SetPsprite(PSP_FLASH, null); } player.PendingWeapon = WP_NOCHANGE; } //--------------------------------------------------------------------------- // // MorphPlayer // // Returns true if the player gets turned into a chicken/pig. // // TODO: Allow morphed players to receive weapon sets (not just one weapon), // since they have their own weapon slots now. // //--------------------------------------------------------------------------- virtual bool MorphPlayer(playerinfo activator, Class spawntype, int duration, int style, Class enter_flash = null, Class exit_flash = null) { if (bDontMorph) { return false; } if (bInvulnerable && (player != activator || !(style & MRF_WHENINVULNERABLE))) { // Immune when invulnerable unless this is a power we activated return false; } if (player.morphTics) { // Player is already a beast if ((GetClass() == spawntype) && bCanSuperMorph && (player.morphTics < (((duration) ? duration : DEFMORPHTICS) - TICRATE)) && FindInventory('PowerWeaponLevel2', true) == null) { // Make a super chicken GiveInventoryType ('PowerWeaponLevel2'); } return false; } if (health <= 0) { // Dead players cannot morph return false; } if (spawntype == null) { return false; } if (!(spawntype is 'PlayerPawn')) { return false; } if (spawntype == GetClass()) { return false; } let morphed = PlayerPawn(Spawn (spawntype, Pos, NO_REPLACE)); EndAllPowerupEffects(); Substitute(morphed); if ((style & MRF_TRANSFERTRANSLATION) && !morphed.bDontTranslate) { morphed.Translation = Translation; } if (tid != 0 && (style & MRF_NEWTIDBEHAVIOUR)) { morphed.ChangeTid(tid); ChangeTid(0); } morphed.Angle = Angle; morphed.target = target; morphed.tracer = tracer; morphed.alternative = self; morphed.FriendPlayer = FriendPlayer; morphed.DesignatedTeam = DesignatedTeam; morphed.Score = Score; player.PremorphWeapon = player.ReadyWeapon; morphed.special2 = bSolid * 2 + bShootable * 4 + bInvisible * 0x40; // The factors are for savegame compatibility morphed.player = player; if (morphed.ViewHeight > player.viewheight && player.deltaviewheight == 0) { // If the new view height is higher than the old one, start moving toward it. player.deltaviewheight = player.GetDeltaViewHeight(); } morphed.bShadow |= bShadow; morphed.bNoGravity |= bNoGravity; morphed.bFly |= bFly; morphed.bGhost |= bGhost; if (enter_flash == null) enter_flash = 'TeleportFog'; let eflash = Spawn(enter_flash, Pos + (0, 0, gameinfo.telefogheight), ALLOW_REPLACE); let p = player; player = null; alternative = morphed; bSolid = false; bShootable = false; bUnmorphed = true; bInvisible = true; p.morphTics = (duration) ? duration : DEFMORPHTICS; // [MH] Used by SBARINFO to speed up face drawing p.MorphedPlayerClass = spawntype; p.MorphStyle = style; if (exit_flash == null) exit_flash = 'TeleportFog'; p.MorphExitFlash = exit_flash; p.health = morphed.health; p.mo = morphed; p.vel = (0, 0); morphed.ObtainInventory (self); // Remove all armor for (Inventory item = morphed.Inv; item != null; ) { let next = item.Inv; if (item is 'Armor') { item.DepleteOrDestroy(); } item = next; } morphed.InitAllPowerupEffects(); morphed.ActivateMorphWeapon (); if (p.camera == self) // can this happen? { p.camera = morphed; } morphed.ScoreIcon = ScoreIcon; // [GRB] if (eflash) eflash.target = morphed; return true; } //---------------------------------------------------------------------------- // // FUNC UndoPlayerMorph // //---------------------------------------------------------------------------- virtual bool UndoPlayerMorph(playerinfo activator, int unmorphflag = 0, bool force = false) { if (alternative == null) { return false; } let player = self.player; bool DeliberateUnmorphIsOkay = !!(MRF_STANDARDUNDOING & unmorphflag); if ((bInvulnerable) // If the player is invulnerable && ((player != activator) // and either did not decide to unmorph, || (!((player.MorphStyle & MRF_WHENINVULNERABLE) // or the morph style does not allow it || (DeliberateUnmorphIsOkay))))) // (but standard morph styles always allow it), { // Then the player is immune to the unmorph. return false; } let altmo = PlayerPawn(alternative); altmo.SetOrigin (Pos, false); altmo.bSolid = true; bSolid = false; if (!force && !altmo.TestMobjLocation()) { // Didn't fit altmo.bSolid = false; bSolid = true; player.morphTics = 2*TICRATE; return false; } // No longer using tracer as morph storage. That is what 'alternative' is for. // If the tracer has changed on the morph, change the original too. altmo.target = target; altmo.tracer = tracer; self.player = null; altmo.alternative = alternative = null; // Remove the morph power if the morph is being undone prematurely. for (Inventory item = Inv; item != null;) { let next = item.Inv; if (item is "PowerMorph") { item.Destroy(); } item = next; } EndAllPowerupEffects(); altmo.ObtainInventory (self); Substitute(altmo); if ((tid != 0) && (player.MorphStyle & MRF_NEWTIDBEHAVIOUR)) { altmo.ChangeTid(tid); } altmo.Angle = Angle; altmo.player = player; altmo.reactiontime = 18; altmo.bSolid = !!(special2 & 2); altmo.bShootable = !!(special2 & 4); altmo.bInvisible = !!(special2 & 0x40); altmo.Vel = (0, 0, Vel.Z); player.Vel = (0, 0); altmo.floorz = floorz; altmo.bShadow = bShadow; altmo.bNoGravity = bNoGravity; altmo.bGhost = bGhost; altmo.bUnmorphed = false; altmo.Score = Score; altmo.InitAllPowerupEffects(); let exit_flash = player.MorphExitFlash; bool correctweapon = !!(player.MorphStyle & MRF_LOSEACTUALWEAPON); bool undobydeathsaves = !!(player.MorphStyle & MRF_UNDOBYDEATHSAVES); player.morphTics = 0; player.MorphedPlayerClass = null; player.MorphStyle = 0; player.MorphExitFlash = null; player.viewheight = altmo.ViewHeight; Inventory level2 = altmo.FindInventory("PowerWeaponLevel2", true); if (level2 != null) { level2.Destroy (); } if ((player.health > 0) || undobydeathsaves) { player.health = altmo.health = altmo.SpawnHealth(); } else // killed when morphed so stay dead { altmo.health = player.health; } player.mo = altmo; if (player.camera == self) { player.camera = altmo; } // [MH] // If the player that was morphed is the one // taking events, reset up the face, if any; // this is only needed for old-skool skins // and for the original DOOM status bar. if (player == players[consoleplayer]) { if (face != 'None') { // Assume root-level base skin to begin with let skinindex = 0; let skin = player.GetSkin(); // If a custom skin was in use, then reload it // or else the base skin for the player class. if (skin >= PlayerClasses.Size () && skin < PlayerSkins.Size()) { skinindex = skin; } else if (PlayerClasses.Size () > 1) { let whatami = altmo.GetClass(); for (int i = 0; i < PlayerClasses.Size (); ++i) { if (PlayerClasses[i].Type == whatami) { skinindex = i; break; } } } } } Actor eflash = null; if (exit_flash != null) { eflash = Spawn(exit_flash, Vec3Angle(20., altmo.Angle, gameinfo.telefogheight), ALLOW_REPLACE); if (eflash) eflash.target = altmo; } WeaponSlots.SetupWeaponSlots(altmo); // Use original class's weapon slots. let beastweap = player.ReadyWeapon; if (player.PremorphWeapon != null) { player.PremorphWeapon.PostMorphWeapon (); } else { player.ReadyWeapon = player.PendingWeapon = null; } if (correctweapon) { // Better "lose morphed weapon" semantics class morphweaponcls = MorphWeapon; if (morphweaponcls != null && morphweaponcls is 'Weapon') { let OriginalMorphWeapon = Weapon(altmo.FindInventory (morphweapon)); if ((OriginalMorphWeapon != null) && (OriginalMorphWeapon.GivenAsMorphWeapon)) { // You don't get to keep your morphed weapon. if (OriginalMorphWeapon.SisterWeapon != null) { OriginalMorphWeapon.SisterWeapon.Destroy (); } OriginalMorphWeapon.Destroy (); } } } else // old behaviour (not really useful now) { // Assumptions made here are no longer valid if (beastweap != null) { // You don't get to keep your morphed weapon. if (beastweap.SisterWeapon != null) { beastweap.SisterWeapon.Destroy (); } beastweap.Destroy (); } } Destroy (); // Restore playerclass armor to its normal amount. let hxarmor = HexenArmor(altmo.FindInventory('HexenArmor')); if (hxarmor != null) { hxarmor.Slots[4] = altmo.HexenArmor[0]; } return true; } //=========================================================================== // // // //=========================================================================== override Actor, int, int MorphedDeath() { // Voodoo dolls should not unmorph the real player here. if ((player.mo == self) && (player.morphTics) && (player.MorphStyle & MRF_UNDOBYDEATH) && (alternative)) { Actor realme = alternative; int realstyle = player.MorphStyle; int realhealth = health; if (UndoPlayerMorph(player, 0, !!(player.MorphStyle & MRF_UNDOBYDEATHFORCED))) { return realme, realstyle, realhealth; } } return null, 0, 0; } } //=========================================================================== // // // //=========================================================================== class MorphProjectile : Actor { Class PlayerClass; Class MonsterClass, MorphFlash, UnMorphFlash; int Duration, MorphStyle; Default { Damage 1; Projectile; -ACTIVATEIMPACT -ACTIVATEPCROSS } override int DoSpecialDamage (Actor target, int damage, Name damagetype) { if (target.player) { // Voodoo dolls forward this to the real player target.player.mo.MorphPlayer (NULL, PlayerClass, Duration, MorphStyle, MorphFlash, UnMorphFlash); } else { target.MorphMonster (MonsterClass, Duration, MorphStyle, MorphFlash, UnMorphFlash); } return -1; } } //=========================================================================== // // // //=========================================================================== class MorphedMonster : Actor { Actor UnmorphedMe; int UnmorphTime, MorphStyle; Class MorphExitFlash; int FlagsSave; Default { Monster; -COUNTKILL +FLOORCLIP } override void OnDestroy () { if (UnmorphedMe != NULL) { UnmorphedMe.Destroy (); } Super.OnDestroy(); } override void Die (Actor source, Actor inflictor, int dmgflags, Name MeansOfDeath) { Super.Die (source, inflictor, dmgflags, MeansOfDeath); if (UnmorphedMe != NULL && UnmorphedMe.bUnmorphed) { UnmorphedMe.health = health; UnmorphedMe.Die (source, inflictor, dmgflags, MeansOfDeath); } } override void Tick () { if (UnmorphTime > level.time || !UndoMonsterMorph()) { Super.Tick(); } } //---------------------------------------------------------------------------- // // FUNC P_UndoMonsterMorph // // Returns true if the monster unmorphs. // //---------------------------------------------------------------------------- virtual bool UndoMonsterMorph(bool force = false) { if (UnmorphTime == 0 || UnmorphedMe == NULL || bStayMorphed || UnmorphedMe.bStayMorphed) { return false; } let unmorphed = UnmorphedMe; unmorphed.SetOrigin (Pos, false); unmorphed.bSolid = true; bSolid = false; bool save = bTouchy; bTouchy = false; if (!force && !unmorphed.TestMobjLocation ()) { // Didn't fit unmorphed.bSolid = false; bSolid = true; bTouchy = save; UnmorphTime = level.time + 5*TICRATE; // Next try in 5 seconds return false; } unmorphed.Angle = Angle; unmorphed.target = target; unmorphed.bShadow = bShadow; unmorphed.bGhost = bGhost; unmorphed.bSolid = !!(flagssave & 2); unmorphed.bShootable = !!(flagssave & 4); unmorphed.bInvisible = !!(flagssave & 0x40); unmorphed.health = unmorphed.SpawnHealth(); unmorphed.Vel = Vel; unmorphed.ChangeTid(tid); unmorphed.special = special; unmorphed.Score = Score; unmorphed.args[0] = args[0]; unmorphed.args[1] = args[1]; unmorphed.args[2] = args[2]; unmorphed.args[3] = args[3]; unmorphed.args[4] = args[4]; unmorphed.CopyFriendliness (self, true); UnmorphedMe = NULL; Substitute(unmorphed); Destroy (); let eflash = Spawn(MorphExitFlash, Pos + (0, 0, gameinfo.TELEFOGHEIGHT), ALLOW_REPLACE); if (eflash) eflash.target = unmorphed; return true; } //=========================================================================== // // // //=========================================================================== override Actor, int, int MorphedDeath() { let realme = UnmorphedMe; if (realme != NULL) { if ((UnmorphTime) && (MorphStyle & MRF_UNDOBYDEATH)) { int realstyle = MorphStyle; int realhealth = health; if (UndoMonsterMorph(!!(MorphStyle & MRF_UNDOBYDEATHFORCED))) { return realme, realstyle, realhealth; } } if (realme.bBossDeath) { realme.health = 0; // make sure that A_BossDeath considers it dead. realme.A_BossDeath(); } } return null, 0, 0; } }