/* Emacs style mode select   -*- C++ -*-
 *-----------------------------------------------------------------------------
 *
 *
 *  PrBoom a Doom port merged with LxDoom and LSDLDoom
 *  based on BOOM, a modified and improved DOOM engine
 *  Copyright (C) 1999 by
 *  id Software, Chi Hoang, Lee Killough, Jim Flynn, Rand Phares, Ty Halderman
 *  Copyright (C) 1999-2000 by
 *  Jess Haas, Nicolas Kalkhof, Colin Phipps, Florian Schulze
 *
 *  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 2
 *  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, write to the Free Software
 *  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
 *  02111-1307, USA.
 *
 * DESCRIPTION:
 *      Weapon sprite animation, weapon objects.
 *      Action functions for weapons.
 *
 *-----------------------------------------------------------------------------*/

#include "doomstat.h"
#include "r_main.h"
#include "p_map.h"
#include "p_inter.h"
#include "p_pspr.h"
#include "p_enemy.h"
#include "m_random.h"
#include "s_sound.h"
#include "sounds.h"
#include "d_event.h"
#include "rockmacros.h"
#define LOWERSPEED   (FRACUNIT*6)
#define RAISESPEED   (FRACUNIT*6)
#define WEAPONBOTTOM (FRACUNIT*128)
#define WEAPONTOP    (FRACUNIT*32)

// plasma cells for a bfg attack
#define BFGCELLS  40
//#define BFGCELLS bfgcells        /* Ty 03/09/98 externalized in p_inter.c */

extern void P_Thrust(player_t *, angle_t, fixed_t);

// The following array holds the recoil values         // phares

static const int recoil_values[] = {    // phares
                                      10, // wp_fist
                                      10, // wp_pistol
                                      30, // wp_shotgun
                                      10, // wp_chaingun
                                      100,// wp_missile
                                      20, // wp_plasma
                                      100,// wp_bfg
                                      0,  // wp_chainsaw
                                      80  // wp_supershotgun
                                   };

//
// P_SetPsprite
//

static void P_SetPsprite(player_t *player, int position, statenum_t stnum)
{
   pspdef_t *psp = &player->psprites[position];

   do
   {
      state_t *state;

      if (!stnum)
      {
         // object removed itself
         psp->state = NULL;
         break;
      }

      state = &states[stnum];
      psp->state = state;
      psp->tics = state->tics;        // could be 0

      if (state->misc1)
      {
         // coordinate set
         psp->sx = state->misc1 << FRACBITS;
         psp->sy = state->misc2 << FRACBITS;
      }

      // Call action routine.
      // Modified handling.
      if (state->action)
      {
         state->action(player, psp);
         if (!psp->state)
            break;
      }
      stnum = psp->state->nextstate;
   }
   while (!psp->tics);     // an initial state of 0 could cycle through
}

//
// P_BringUpWeapon
// Starts bringing the pending weapon up
// from the bottom of the screen.
// Uses player
//

static void P_BringUpWeapon(player_t *player)
{
   statenum_t newstate;

   if (player->pendingweapon == wp_nochange)
      player->pendingweapon = player->readyweapon;

   if (player->pendingweapon == wp_chainsaw)
      S_StartSound (player->mo, sfx_sawup);

   newstate = weaponinfo[player->pendingweapon].upstate;

   player->pendingweapon = wp_nochange;
   // killough 12/98: prevent pistol from starting visibly at bottom of screen:
   player->psprites[ps_weapon].sy =
      mbf_features ? WEAPONBOTTOM+FRACUNIT*2 : WEAPONBOTTOM;

   P_SetPsprite(player, ps_weapon, newstate);
}

// The first set is where the weapon preferences from             // killough,
// default.cfg are stored. These values represent the keys used   // phares
// in DOOM2 to bring up the weapon, i.e. 6 = plasma gun. These    //    |
// are NOT the wp_* constants.                                    //    V

int weapon_preferences[2][NUMWEAPONS+1] = {
         {6, 9, 4, 3, 2, 8, 5, 7, 1, 0},  // !compatibility preferences
         {6, 9, 4, 3, 2, 8, 5, 7, 1, 0},  //  compatibility preferences
      };

// P_SwitchWeapon checks current ammo levels and gives you the
// most preferred weapon with ammo. It will not pick the currently
// raised weapon. When called from P_CheckAmmo this won't matter,
// because the raised weapon has no ammo anyway. When called from
// G_BuildTiccmd you want to toggle to a different weapon regardless.

int P_SwitchWeapon(player_t *player)
{
   int *prefer = weapon_preferences[demo_compatibility!=0]; // killough 3/22/98
   int currentweapon = player->readyweapon;
   int newweapon = currentweapon;
   int i = NUMWEAPONS+1;   // killough 5/2/98

   // killough 2/8/98: follow preferences and fix BFG/SSG bugs

   do
      switch (*prefer++)
      {
      case 1:
         if (!player->powers[pw_strength])      // allow chainsaw override
            break;
      case 0:
         newweapon = wp_fist;
         break;
      case 2:
         if (player->ammo[am_clip])
            newweapon = wp_pistol;
         break;
      case 3:
         if (player->weaponowned[wp_shotgun] && player->ammo[am_shell])
            newweapon = wp_shotgun;
         break;
      case 4:
         if (player->weaponowned[wp_chaingun] && player->ammo[am_clip])
            newweapon = wp_chaingun;
         break;
      case 5:
         if (player->weaponowned[wp_missile] && player->ammo[am_misl])
            newweapon = wp_missile;
         break;
      case 6:
         if (player->weaponowned[wp_plasma] && player->ammo[am_cell] &&
               gamemode != shareware)
            newweapon = wp_plasma;
         break;
      case 7:
         if (player->weaponowned[wp_bfg] && gamemode != shareware &&
               player->ammo[am_cell] >= (demo_compatibility ? 41 : 40))
            newweapon = wp_bfg;
         break;
      case 8:
         if (player->weaponowned[wp_chainsaw])
            newweapon = wp_chainsaw;
         break;
      case 9:
         if (player->weaponowned[wp_supershotgun] && gamemode == commercial &&
               player->ammo[am_shell] >= (demo_compatibility ? 3 : 2))
            newweapon = wp_supershotgun;
         break;
      }
   while (newweapon==currentweapon && --i);          // killough 5/2/98
   return newweapon;
}

// killough 5/2/98: whether consoleplayer prefers weapon w1 over weapon w2.
int P_WeaponPreferred(int w1, int w2)
{
   return
      (weapon_preferences[0][0] != ++w2 && (weapon_preferences[0][0] == ++w1 ||
      (weapon_preferences[0][1] !=   w2 && (weapon_preferences[0][1] ==   w1 ||
      (weapon_preferences[0][2] !=   w2 && (weapon_preferences[0][2] ==   w1 ||
      (weapon_preferences[0][3] !=   w2 && (weapon_preferences[0][3] ==   w1 ||
      (weapon_preferences[0][4] !=   w2 && (weapon_preferences[0][4] ==   w1 ||
      (weapon_preferences[0][5] !=   w2 && (weapon_preferences[0][5] ==   w1 ||
      (weapon_preferences[0][6] !=   w2 && (weapon_preferences[0][6] ==   w1 ||
      (weapon_preferences[0][7] !=   w2 && (weapon_preferences[0][7] ==   w1
      ))))))))))))))));
}

//
// P_CheckAmmo
// Returns true if there is enough ammo to shoot.
// If not, selects the next weapon to use.
// (only in demo_compatibility mode -- killough 3/22/98)
//

boolean P_CheckAmmo(player_t *player)
{
   ammotype_t ammo = weaponinfo[player->readyweapon].ammo;
   int count = 1;  // Regular

   if (player->readyweapon == wp_bfg)  // Minimal amount for one shot varies.
      count = BFGCELLS;
   else
      if (player->readyweapon == wp_supershotgun)        // Double barrel.
         count = 2;

   // Some do not need ammunition anyway.
   // Return if current ammunition sufficient.

   if (ammo == am_noammo || player->ammo[ammo] >= count)
      return true;

   // Out of ammo, pick a weapon to change to.
   //
   // killough 3/22/98: for old demos we do the switch here and now;
   // for Boom games we cannot do this, and have different player
   // preferences across demos or networks, so we have to use the
   // G_BuildTiccmd() interface instead of making the switch here.

   if (demo_compatibility)
   {
      player->pendingweapon = P_SwitchWeapon(player);      // phares
      // Now set appropriate weapon overlay.
      P_SetPsprite(player,ps_weapon,weaponinfo[player->readyweapon].downstate);
   }

   return false;
}

//
// P_FireWeapon.
//

int lastshottic; // killough 3/22/98

static void P_FireWeapon(player_t *player)
{
   statenum_t newstate;

   if (!P_CheckAmmo(player))
      return;

   P_SetMobjState(player->mo, S_PLAY_ATK1);
   newstate = weaponinfo[player->readyweapon].atkstate;
   P_SetPsprite(player, ps_weapon, newstate);
   P_NoiseAlert(player->mo, player->mo);
   lastshottic = gametic;                       // killough 3/22/98
}

//
// P_DropWeapon
// Player died, so put the weapon away.
//

void P_DropWeapon(player_t *player)
{
   P_SetPsprite(player, ps_weapon, weaponinfo[player->readyweapon].downstate);
}

//
// A_WeaponReady
// The player can fire the weapon
// or change to another weapon at this time.
// Follows after getting weapon up,
// or after previous attack/fire sequence.
//

void A_WeaponReady(player_t *player, pspdef_t *psp)
{
   // get out of attack state
   if (player->mo->state == &states[S_PLAY_ATK1]
         || player->mo->state == &states[S_PLAY_ATK2] )
      P_SetMobjState(player->mo, S_PLAY);

   if (player->readyweapon == wp_chainsaw && psp->state == &states[S_SAW])
      S_StartSound(player->mo, sfx_sawidl);

   // check for change
   //  if player is dead, put the weapon away

   if (player->pendingweapon != wp_nochange || !player->health)
   {
      // change weapon (pending weapon should already be validated)
      statenum_t newstate = weaponinfo[player->readyweapon].downstate;
      P_SetPsprite(player, ps_weapon, newstate);
      return;
   }

   // check for fire
   //  the missile launcher and bfg do not auto fire

   if (player->cmd.buttons & BT_ATTACK)
   {
      if (!player->attackdown || (player->readyweapon != wp_missile &&
                                  player->readyweapon != wp_bfg))
      {
         player->attackdown = true;
         P_FireWeapon(player);
         return;
      }
   }
   else
      player->attackdown = false;

   // bob the weapon based on movement speed
   {
      int angle = (128*leveltime) & FINEMASK;
      psp->sx = FRACUNIT + FixedMul(player->bob, finecosine[angle]);
      angle &= FINEANGLES/2-1;
      psp->sy = WEAPONTOP + FixedMul(player->bob, finesine[angle]);
   }
}

//
// A_ReFire
// The player can re-fire the weapon
// without lowering it entirely.
//

void A_ReFire(player_t *player, pspdef_t *psp)
{
   (void)psp;
   // check for fire
   //  (if a weaponchange is pending, let it go through instead)

   if ( (player->cmd.buttons & BT_ATTACK)
         && player->pendingweapon == wp_nochange && player->health)
   {
      player->refire++;
      P_FireWeapon(player);
   }
   else
   {
      player->refire = 0;
      P_CheckAmmo(player);
   }
}

void A_CheckReload(player_t *player, pspdef_t *psp)
{
   (void)psp;
   P_CheckAmmo(player);
}

//
// A_Lower
// Lowers current weapon,
//  and changes weapon at bottom.
//

void A_Lower(player_t *player, pspdef_t *psp)
{
   psp->sy += LOWERSPEED;

   // Is already down.
   if (psp->sy < WEAPONBOTTOM)
      return;

   // Player is dead.
   if (player->playerstate == PST_DEAD)
   {
      psp->sy = WEAPONBOTTOM;
      return;      // don't bring weapon back up
   }

   // The old weapon has been lowered off the screen,
   // so change the weapon and start raising it

   if (!player->health)
   {      // Player is dead, so keep the weapon off screen.
      P_SetPsprite(player,  ps_weapon, S_NULL);
      return;
   }

   player->readyweapon = player->pendingweapon;

   P_BringUpWeapon(player);
}

//
// A_Raise
//

void A_Raise(player_t *player, pspdef_t *psp)
{
   statenum_t newstate;

   psp->sy -= RAISESPEED;

   if (psp->sy > WEAPONTOP)
      return;

   psp->sy = WEAPONTOP;

   // The weapon has been raised all the way,
   //  so change to the ready state.

   newstate = weaponinfo[player->readyweapon].readystate;

   P_SetPsprite(player, ps_weapon, newstate);
}


// Weapons now recoil, amount depending on the weapon.              // phares
//                                                                  //   |
// The P_SetPsprite call in each of the weapon firing routines      //   V
// was moved here so the recoil could be synched with the
// muzzle flash, rather than the pressing of the trigger.
// The BFG delay caused this to be necessary.

static void A_FireSomething(player_t* player,int adder)
{
   P_SetPsprite(player, ps_flash,
                weaponinfo[player->readyweapon].flashstate+adder);

   // killough 3/27/98: prevent recoil in no-clipping mode
   if (!(player->mo->flags & MF_NOCLIP))
      if (!compatibility && weapon_recoil)
         P_Thrust(player,
                  ANG180+player->mo->angle,                          //   ^
                  2048*recoil_values[player->readyweapon]);          //   |
}                                                                 // phares

//
// A_GunFlash
//

void A_GunFlash(player_t *player, pspdef_t *psp)
{
   (void)psp;
   P_SetMobjState(player->mo, S_PLAY_ATK2);

   A_FireSomething(player,0);                                      // phares
}

//
// WEAPON ATTACKS
//

//
// A_Punch
//

void A_Punch(player_t *player, pspdef_t *psp)
{
   (void)psp;
   angle_t angle;
   int t, slope, damage = (P_Random(pr_punch)%10+1)<<1;

   if (player->powers[pw_strength])
      damage *= 10;

   angle = player->mo->angle;

   // killough 5/5/98: remove dependence on order of evaluation:
   t = P_Random(pr_punchangle);
   angle += (t - P_Random(pr_punchangle))<<18;

   /* killough 8/2/98: make autoaiming prefer enemies */
   if (!mbf_features ||
         (slope = P_AimLineAttack(player->mo, angle, MELEERANGE, MF_FRIEND),
          !linetarget))
      slope = P_AimLineAttack(player->mo, angle, MELEERANGE, 0);

   P_LineAttack(player->mo, angle, MELEERANGE, slope, damage);

   if (!linetarget)
      return;

   S_StartSound(player->mo, sfx_punch);

   // turn to face target

   player->mo->angle = R_PointToAngle2(player->mo->x, player->mo->y,
                                       linetarget->x, linetarget->y);
}

//
// A_Saw
//

void A_Saw(player_t *player, pspdef_t *psp)
{
   (void)psp;
   int slope, damage = 2*(P_Random(pr_saw)%10+1);
   angle_t angle = player->mo->angle;
   // killough 5/5/98: remove dependence on order of evaluation:
   int t = P_Random(pr_saw);
   angle += (t - P_Random(pr_saw))<<18;

   /* Use meleerange + 1 so that the puff doesn't skip the flash
    * killough 8/2/98: make autoaiming prefer enemies */
   if (!mbf_features ||
         (slope = P_AimLineAttack(player->mo, angle, MELEERANGE+1, MF_FRIEND),
          !linetarget))
      slope = P_AimLineAttack(player->mo, angle, MELEERANGE+1, 0);

   P_LineAttack(player->mo, angle, MELEERANGE+1, slope, damage);

   if (!linetarget)
   {
      S_StartSound(player->mo, sfx_sawful);
      return;
   }

   S_StartSound(player->mo, sfx_sawhit);

   // turn to face target
   angle = R_PointToAngle2(player->mo->x, player->mo->y,
                           linetarget->x, linetarget->y);

   if (angle - player->mo->angle > ANG180) {
      if (angle - player->mo->angle < (unsigned)(-ANG90/20))
         player->mo->angle = angle + ANG90/21;
      else
         player->mo->angle -= ANG90/20;
   } else {
      if (angle - player->mo->angle > ANG90/20)
         player->mo->angle = angle - ANG90/21;
      else
         player->mo->angle += ANG90/20;
   }

   player->mo->flags |= MF_JUSTATTACKED;
}

//
// A_FireMissile
//

void A_FireMissile(player_t *player, pspdef_t *psp)
{
   (void)psp;
   player->ammo[weaponinfo[player->readyweapon].ammo]--;
   P_SpawnPlayerMissile(player->mo, MT_ROCKET);
}

//
// A_FireBFG
//

void A_FireBFG(player_t *player, pspdef_t *psp)
{
   (void)psp;
   player->ammo[weaponinfo[player->readyweapon].ammo] -= BFGCELLS;
   P_SpawnPlayerMissile(player->mo, MT_BFG);
}

/*
 * A_FireOldBFG
 *
 * This function emulates Doom's Pre-Beta BFG
 * By Lee Killough 6/6/98, 7/11/98, 7/19/98, 8/20/98
 *
 * This code may not be used in other mods without appropriate credit given.
 * Code leeches will be telefragged.
 */

void A_FireOldBFG(player_t *player, pspdef_t *psp)
{
   (void)psp;
   (void)player;
}

//
// A_FirePlasma
//

void A_FirePlasma(player_t *player, pspdef_t *psp)
{
   (void)psp;
   player->ammo[weaponinfo[player->readyweapon].ammo]--;

   A_FireSomething(player,P_Random(pr_plasma)&1);              // phares
   P_SpawnPlayerMissile(player->mo, MT_PLASMA);
}

//
// P_BulletSlope
// Sets a slope so a near miss is at aproximately
// the height of the intended target
//

static fixed_t bulletslope;

static void P_BulletSlope(mobj_t *mo)
{
   angle_t an = mo->angle;    // see which target is to be aimed at

   /* killough 8/2/98: make autoaiming prefer enemies */
   uint_64_t mask = mbf_features ? MF_FRIEND : 0;

   do
   {
      bulletslope = P_AimLineAttack(mo, an, 16*64*FRACUNIT, mask);
      if (!linetarget)
         bulletslope = P_AimLineAttack(mo, an += 1<<26, 16*64*FRACUNIT, mask);
      if (!linetarget)
         bulletslope = P_AimLineAttack(mo, an -= 2<<26, 16*64*FRACUNIT, mask);
   }
   while (mask && (mask=0, !linetarget));  /* killough 8/2/98 */
}

//
// P_GunShot
//

void P_GunShot(mobj_t *mo, boolean accurate)
{
   int damage = 5*(P_Random(pr_gunshot)%3+1);
   angle_t angle = mo->angle;

   if (!accurate)
   {  // killough 5/5/98: remove dependence on order of evaluation:
      int t = P_Random(pr_misfire);
      angle += (t - P_Random(pr_misfire))<<18;
   }

   P_LineAttack(mo, angle, MISSILERANGE, bulletslope, damage);
}

//
// A_FirePistol
//

void A_FirePistol(player_t *player, pspdef_t *psp)
{
   (void)psp;
   S_StartSound(player->mo, sfx_pistol);

   P_SetMobjState(player->mo, S_PLAY_ATK2);
   player->ammo[weaponinfo[player->readyweapon].ammo]--;

   A_FireSomething(player,0);                                      // phares
   P_BulletSlope(player->mo);
   P_GunShot(player->mo, !player->refire);
}

//
// A_FireShotgun
//

void A_FireShotgun(player_t *player, pspdef_t *psp)
{
   (void)psp;
   int i;

   S_StartSound(player->mo, sfx_shotgn);
   P_SetMobjState(player->mo, S_PLAY_ATK2);

   player->ammo[weaponinfo[player->readyweapon].ammo]--;

   A_FireSomething(player,0);                                      // phares

   P_BulletSlope(player->mo);

   for (i=0; i<7; i++)
      P_GunShot(player->mo, false);
}

//
// A_FireShotgun2
//

void A_FireShotgun2(player_t *player, pspdef_t *psp)
{
   (void)psp;
   int i;

   S_StartSound(player->mo, sfx_dshtgn);
   P_SetMobjState(player->mo, S_PLAY_ATK2);
   player->ammo[weaponinfo[player->readyweapon].ammo] -= 2;

   A_FireSomething(player,0);                                      // phares

   P_BulletSlope(player->mo);

   for (i=0; i<20; i++)
   {
      int damage = 5*(P_Random(pr_shotgun)%3+1);
      angle_t angle = player->mo->angle;
      // killough 5/5/98: remove dependence on order of evaluation:
      int t = P_Random(pr_shotgun);
      angle += (t - P_Random(pr_shotgun))<<19;
      t = P_Random(pr_shotgun);
      P_LineAttack(player->mo, angle, MISSILERANGE, bulletslope +
                   ((t - P_Random(pr_shotgun))<<5), damage);
   }
}

//
// A_FireCGun
//

void A_FireCGun(player_t *player, pspdef_t *psp)
{
   if (player->ammo[weaponinfo[player->readyweapon].ammo] || comp[comp_sound])
      S_StartSound(player->mo, sfx_pistol);

   if (!player->ammo[weaponinfo[player->readyweapon].ammo])
      return;

   P_SetMobjState(player->mo, S_PLAY_ATK2);
   player->ammo[weaponinfo[player->readyweapon].ammo]--;

   A_FireSomething(player,psp->state - &states[S_CHAIN1]);           // phares

   P_BulletSlope(player->mo);

   P_GunShot(player->mo, !player->refire);
}

void A_Light0(player_t *player, pspdef_t *psp)
{
   (void)psp;
   player->extralight = 0;
}

void A_Light1 (player_t *player, pspdef_t *psp)
{
   (void)psp;
   player->extralight = 1;
}

void A_Light2 (player_t *player, pspdef_t *psp)
{
   (void)psp;
   player->extralight = 2;
}

//
// A_BFGSpray
// Spawn a BFG explosion on every monster in view
//

void A_BFGSpray(mobj_t *mo)
{
   int i;

   for (i=0 ; i<40 ; i++)  // offset angles from its attack angle
   {
      int j, damage;
      angle_t an = mo->angle - ANG90/2 + ANG90/40*i;

      // mo->target is the originator (player) of the missile

      // killough 8/2/98: make autoaiming prefer enemies
      if (!mbf_features ||
            (P_AimLineAttack(mo->target, an, 16*64*FRACUNIT, MF_FRIEND),
             !linetarget))
         P_AimLineAttack(mo->target, an, 16*64*FRACUNIT, 0);

      if (!linetarget)
         continue;

      P_SpawnMobj(linetarget->x, linetarget->y,
                  linetarget->z + (linetarget->height>>2), MT_EXTRABFG);

      for (damage=j=0; j<15; j++)
         damage += (P_Random(pr_bfg)&7) + 1;

      P_DamageMobj(linetarget, mo->target, mo->target, damage);
   }
}

//
// A_BFGsound
//

void A_BFGsound(player_t *player, pspdef_t *psp)
{
   (void)psp;
   S_StartSound(player->mo, sfx_bfg);
}

//
// P_SetupPsprites
// Called at start of level for each player.
//

void P_SetupPsprites(player_t *player)
{
   int i;

   // remove all psprites
   for (i=0; i<NUMPSPRITES; i++)
      player->psprites[i].state = NULL;

   // spawn the gun
   player->pendingweapon = player->readyweapon;
   P_BringUpWeapon(player);
}

//
// P_MovePsprites
// Called every tic by player thinking routine.
//

void P_MovePsprites(player_t *player)
{
   pspdef_t *psp = player->psprites;
   int i;

   // a null state means not active
   // drop tic count and possibly change state
   // a -1 tic count never changes

   for (i=0; i<NUMPSPRITES; i++, psp++)
      if (psp->state && psp->tics != -1 && !--psp->tics)
         P_SetPsprite(player, i, psp->state->nextstate);

   player->psprites[ps_flash].sx = player->psprites[ps_weapon].sx;
   player->psprites[ps_flash].sy = player->psprites[ps_weapon].sy;
}
