Ammo is not always spent by A_FireProjectile with useammo

Fri Jan 14, 2022 12:10 am

I am making a standalone (i.e. supposed to be usable with or without other weapon mods) grenades mod, which I already mentioned in a couple of other topics here. It is mostly ready, but I can't iron out a couple of quirks. One of them is that ammo (i.e. grenades) are not always spent even though the function responsible for firing is the same. The second quirk is that the weapon can be selected even with 0 ammo despite the property Weapon.AmmoUse 1. I'm asking for help, please! I'm gonna post the code, and below it describe the idea, what works, and what doesn't.

Code:
// Grenade weapon
class KZ_GrenadeWeapon : DoomWeapon
{
   Default
   {
      Weapon.AmmoUse 1;
      Weapon.AmmoGive 3;
      Weapon.AmmoType "KZ_GrenadeAmmo";
      Weapon.SelectionOrder 0xffffffff;
      Weapon.MinSelectionAmmo1 21;
      +Weapon.NOALERT;
      Tag "$TAG_KZ_GRENADEWEAPON";
      Weapon.SlotNumber 8;
   }
   States
   {
   Ready:
      TNT1 A 0 A_JumpIfInventory("KZ_GrenadeAmmo", 1, "ReadyLoop");
      TNT1 A 0 A_SelectWeapon("", SWF_SELECTPRIORITY);
   ReadyLoop:
      TNT1 A 0 A_JumpIfInventory("KZ_DoQuickThrow", 1, "Fire");
      GRTH Z 1 A_WeaponReady;
      Loop;
   AltReady:
      TNT1 A 0 A_JumpIfInventory("KZ_DoQuickThrow", 1, "ThrowDo");
      GRTH Y 1 A_WeaponReady(WRF_NOSECONDARY);
      Loop;
   Deselect:
      TNT1 A 0 {if (invoker.isPinRemoved) return ResolveState("ThrowDo"); return ResolveState(null);}
      GRTH Z 1 A_Lower(12);
      Loop;
   Select:
      GRTH Z 1 A_Raise(12);
      Loop;
   Fire:
      TNT1 A 0 {invoker.ExtraForce = 0;}
      TNT1 A 0 {if (invoker.isPinRemoved) return ResolveState("UnpinnedThrow"); return ResolveState("PinnedThrow");}
   UnpinnedThrow:
      GRHO B 2; // to prevent immediately starting to hold and thus keep the possibility to use the lowest throw force
      TNT1 A 0 A_JumpIfInventory("KZ_DoQuickThrow", 1, "ThrowDo"); // skip holding and pin removal animation if using a quick throw
   UnpinnedHold:
      GRHO B 1 IncreaseForce();
      TNT1 A 1 A_ReFire("UnpinnedHold");
      Goto ThrowDo;
   PinnedThrow:
      GRTH AB 1;
      GRTH CD 2;
      TNT1 A 0 A_JumpIfInventory("KZ_DoQuickThrow", 1, "PullPin"); // skip holding if using a quick throw
   Hold:
      GRTH E 1 IncreaseForce();
      TNT1 A 1 A_ReFire("Hold");
   PullPin:
      GRTH EE 2;
      TNT1 A 0 A_StartSound("Grenade_Pin");
      GRTH FG 2;
      Goto ThrowDo;
   ThrowDo:
      TNT1 A 0 A_StartSound("Grenade_Toss");
      GRTH HI 1;
      TNT1 A 0 ThrowGrenade();
      GRTH JKLM 1;
      TNT1 A 0 A_TakeInventory("KZ_DoQuickThrow", 1);
      TNT1 A 1 A_ReFire("Fire");
      Goto Ready;
// alt fire
   AltFire:
      GRTH EE 2;
      TNT1 A 0 A_StartSound("Grenade_Pin");
      GRTH FG 2;
      TNT1 A 0 RemovePin();
      Goto AltReady;
   }
   
   // weapon variables
   int ExtraForce;
   bool isPinRemoved;
   int CurGrenadeTimer;
   int MaxGrenadeTimer;
   
   // weapon functions
   override void PostBeginPlay()
   {
      super.PostBeginPlay();
      
      MaxGrenadeTimer = 105;
      CurGrenadeTimer = MaxGrenadeTimer;
   }
   
   override void DoEffect()
   {
      super.DoEffect();
      
      if (!owner) Destroy();
      
      if (isPinRemoved == false) return;
      // otherwise
      CurGrenadeTimer--;
      if (CurGrenadeTimer <= 0)
      {
         let proj = Spawn("KZ_GrenadeThrown", (owner.pos.x, owner.pos.y, owner.pos.z + 32) );
         if (proj)
         {
            proj.Speed = 0;
            proj.Vel3DFromAngle(0, owner.angle, owner.pitch);
            proj.ReactionTime = 0;
            proj.target = owner;
         }
         owner.TakeInventory("KZ_GrenadeAmmo", 1);
         isPinRemoved = false;
         CurGrenadeTimer = MaxGrenadeTimer;
         owner.Player.SetPSprite(PSP_WEAPON, FindState("Ready") );
      }
   }
   
   action void IncreaseForce()
   {
      invoker.ExtraForce++;
      if (invoker.ExtraForce > 40)
      {
         invoker.ExtraForce = 40;
         Console.Printf("Max throw force");
      }
   }
   
   action void ThrowGrenade()
   {
      let proj = A_FireProjectile("KZ_GrenadeThrown", useammo: true);
      if (proj)
      {
         if (FindInventory("KZ_DoQuickThrow") == null)
         {
            proj.Speed = 3 + invoker.ExtraForce;
            proj.Vel3DFromAngle(proj.Speed, angle, pitch);
         }
         if (invoker.CurGrenadeTimer != invoker.MaxGrenadeTimer)
         {
            proj.ReactionTime = invoker.CurGrenadeTimer;
         }
         invoker.isPinRemoved = false;
         invoker.CurGrenadeTimer = invoker.MaxGrenadeTimer;
      }
   }
   
   action void RemovePin()
   {
      invoker.isPinRemoved = true;
      invoker.CurGrenadeTimer = invoker.MaxGrenadeTimer;
   }
}


The idea:

So, the weapon is supposed to be used both by selecting it and using primary or alt fire, and by a key for a quick throw.

Primary fire can be held for increasing the force of the throw, so that tapping the attack key just drops a grenade near you, while holding it for some time lets you throw it rather far.

Alt fire pulls the pin from the grenade, so you can wait for some time before throwing it. After removing the pin you can still use the primary fire the same way, i.e. the longer you press the primary fire key, the farther you throw this already unpinned grenade.

The quick throw key gives the player an item KZ_DoQuickThrow. When the player receives it, he switches to the grenade weapon (if it is not already selected) and goes to the Fire state. If the grenade weapon is already selected, he still goes to the fire and skips the Hold state, as the quick throw always throws the grenade with a medium force (i.e. the speed of the spawned grenade projectile is not adjusted and its native speed (defined in the projectile's properties) is used).

What works:
Almost everything. The player can adjust the force of the throw, can unpin a grenade and wait until it detonates in his hands or throw it after some time, he can use the quick throw key, and if some other weapon is selected, it is switched to the grenades and a grenade is thrown.

What doesn't work:

1. Normally quick throw consumes ammo like it should. However, if the pin is removed (alt fire is pressed) and then a quick throw is used, the ammo is not spent. After that you can switch to some other weapon and continue using the quick throw key, and the ammo will not be spent.

2. As you can see, Deselect state has a check for whether the pin is removed, and if yes, drops the grenade before switching the weapon. In that case again the ammo is not spent, and the quick throw can be used for free.

Please help! I don't understand what is going on here! After all, all these cases call the same function ThrowGrenade, which unconditionally spawns the projectile and only after that checks whether the player has KZ_DoQuickThrow to decide whether or not to adjust the projectile speed. And how the use of ammo can be affected by going to the ThrowDo state from the Deselect state?!

3. As you can see I put a condition into the Ready state to switch to some other weapon when there is no ammo. However, the weapon can still be selected, even though it is immediately deselected. Why the heck, when I has clearly stated Weapon.AmmoUse 1? And how do I prevent it? I know that I could put into the Select state the same condition as into the Ready state. However, this would would just lead to the fact that trying to switch to grenades would switch you to the weapon with the highest priority. So, any way to fix it, please?

Re: Ammo is not always spent by A_FireProjectile with useamm

Fri Jan 14, 2022 7:04 am

It's odd that the weapon is selectable despite having no ammo. Unsure what could be causing that.
An idea on how to fix the ammo consumption: Add those lines to the default block:
Code:
      Weapon.AmmoUse2 1
      Weapon
.AmmoType2 "KZ_GrenadeAmmo";

From what I could see in the code, the non-consumption only happens if the altfire is used.
Even if it jumps to the same firing state, it still counts as an altfire, so you need to also specify the alternate ammo. (Which will be the same as the primary ammo)

If that doesn't work, then I guess you will have to replace AmmoUse with A_TakeInventory in the throwing states.

Re: Ammo is not always spent by A_FireProjectile with useamm

Fri Jan 14, 2022 8:01 am

Thanks, that works perfectly! It also fixed the weapon being selectable with 0 ammo. I guess the engine assumes that if alt fire doesn't require ammo, then the weapon can be switched to when primary ammo is 0 (and it's a correct assumption, I just never thought that with both ammo being the same class I need to explicitly define AmmoType2). Also after adding these lines I was able to get rid of that additional condition in the Ready state, since now the weapon switches to something other after running out of ammo even without that condition.