502
%{
#include "StdH.h"
#define BLOOD_SPILL_RED RGBAToColor(250,20,20,255)
#define BLOOD_SPILL_GREEN RGBAToColor(0,250,0,255)
%}

uses "Entities/BasicEffects";
uses "Engine/Classes/MovableEntity";


// input parameters for bullet
event EBulletInit {
  CEntityPointer penOwner,        // who launched it
  FLOAT fDamage,                  // damage
};

// hit enum
enum BulletHitType {
  0 BHT_NONE                "",     // none
  1 BHT_FLESH               "",     // flesh
  2 BHT_BRUSH_STONE         "",     // brush stone
  3 BHT_BRUSH_SAND          "",     // brush sand
  4 BHT_BRUSH_WATER         "",     // brush water
  5 BHT_BRUSH_UNDER_WATER   "",     // brush under water
  6 BHT_ACID                "",     // acid
  7 BHT_BRUSH_RED_SAND      "",     // brush red sand
};

%{
void CBullet_OnPrecache(CDLLEntityClass *pdec, INDEX iUser) 
{
  pdec->PrecacheClass(CLASS_BASIC_EFFECT, BET_BULLETSTAINSTONE);
  pdec->PrecacheClass(CLASS_BASIC_EFFECT, BET_BULLETSTAINSAND);
  pdec->PrecacheClass(CLASS_BASIC_EFFECT, BET_BULLETSTAINREDSAND);
  pdec->PrecacheClass(CLASS_BASIC_EFFECT, BET_BULLETSTAINWATER);
  pdec->PrecacheClass(CLASS_BASIC_EFFECT, BET_BULLETSTAINSTONENOSOUND);
  pdec->PrecacheClass(CLASS_BASIC_EFFECT, BET_BULLETSTAINSANDNOSOUND);
  pdec->PrecacheClass(CLASS_BASIC_EFFECT, BET_BULLETSTAINREDSANDNOSOUND);
  pdec->PrecacheClass(CLASS_BASIC_EFFECT, BET_BULLETSTAINWATERNOSOUND);
  pdec->PrecacheClass(CLASS_BASIC_EFFECT, BET_BLOODSPILL);
  pdec->PrecacheClass(CLASS_BASIC_EFFECT, BET_BULLETTRAIL);
}
%}

class export CBullet : CEntity {
name      "Bullet";
thumbnail "";
features "ImplementsOnPrecache";

properties:
  1 CEntityPointer m_penOwner,        // entity which owns it
  2 FLOAT m_fDamage = 0.0f,                   // damage
  3 FLOAT3D m_vTarget = FLOAT3D(0,0,0),       // bullet target point in space
  4 FLOAT3D m_vTargetCopy = FLOAT3D(0,0,0),   // copy of bullet target point in space for jitter
  6 FLOAT3D m_vHitPoint = FLOAT3D(0,0,0),     // hit point
  8 INDEX m_iBullet = 0,                // bullet for lerped launch
  9 enum DamageType m_EdtDamage = DMT_BULLET,   // damage type
  10 FLOAT m_fBulletSize = 0.0f,      // bullet can have radius, for hitting models only

components:
  1 class   CLASS_BASIC_EFFECT "Classes\\BasicEffect.ecl"

functions:

/************************************************************
 *                      BULLET LAUNCH                       *
 ************************************************************/
  // set bullet damage
  void SetDamage(FLOAT fDamage) {
    m_fDamage = fDamage;
  };

  // calc jitter target
  void CalcTarget(FLOAT fRange) {
    // destination in bullet direction
    AnglesToDirectionVector(GetPlacement().pl_OrientationAngle, m_vTarget);
    m_vTarget *= fRange;
    m_vTarget += GetPlacement().pl_PositionVector;
    m_vTargetCopy = m_vTarget;
  };

  void CalcTarget(CEntity *pen, FLOAT fRange) {
    FLOAT3D vTarget;

    // target body
    EntityInfo *peiTarget = (EntityInfo*) (pen->GetEntityInfo());
    GetEntityInfoPosition(pen, peiTarget->vTargetCenter, vTarget);

    // calculate
    m_vTarget = (vTarget - GetPlacement().pl_PositionVector).Normalize();
    m_vTarget *= fRange;
    m_vTarget += GetPlacement().pl_PositionVector;
    m_vTargetCopy = m_vTarget;
  };

  // calc jitter target - !!! must call CalcTarget first !!!
  void CalcJitterTarget(FLOAT fR) {
    FLOAT3D vJitter;
/* My Sphere
    FLOAT fXZ = FRnd()*360.0f;
    FLOAT fXY = FRnd()*360.0f;

    // sphere
    fR *= FRnd();
    vJitter(1) = CosFast(fXZ)*CosFast(fXY)*fR;
    vJitter(2) = CosFast(fXZ)*SinFast(fXY)*fR;
    vJitter(3) = SinFast(fXZ)*fR;*/
// comp graphics algorithms sphere
    FLOAT fZ = FRnd()*2.0f - 1.0f;
    FLOAT fA = FRnd()*360.0f;
    FLOAT fT = Sqrt(1-(fZ*fZ));
    vJitter(1) = fT * CosFast(fA);
    vJitter(2) = fT * SinFast(fA);
    vJitter(3) = fZ;
    vJitter = vJitter*fR*FRnd();

    // target
    m_vTarget = m_vTargetCopy + vJitter;
  };

  // calc jitter target asymetric - !!! must call CalcTarget first !!!
  void CalcJitterTargetFixed(FLOAT fX, FLOAT fY, FLOAT fJitter) {
    FLOAT fRndX = FRnd()*2.0f - 1.0f;
    FLOAT fRndY = FRnd()*2.0f - 1.0f;
    FLOAT3D vX, vY;
    const FLOATmatrix3D &m=GetRotationMatrix();
    vX(1) = m(1,1); vX(2) = m(2,1); vX(3) = m(3,1);
    vY(1) = m(1,2); vY(2) = m(2,2); vY(3) = m(3,2);
    // target
    m_vTarget = m_vTargetCopy + (vX*(fX+fRndX*fJitter)) + (vY*(fY+fRndY*fJitter));
  };

  // launch one bullet
  void LaunchBullet(BOOL bSound, BOOL bTrail, BOOL bHitFX)
  {
    // cast a ray to find bullet target
    CCastRay crRay( m_penOwner, GetPlacement().pl_PositionVector, m_vTarget);
    crRay.cr_bHitPortals = TRUE;
    crRay.cr_bHitTranslucentPortals = TRUE;
    crRay.cr_ttHitModels = CCastRay::TT_COLLISIONBOX;
    crRay.cr_bPhysical = FALSE;
    crRay.cr_fTestR = m_fBulletSize;
    FLOAT3D vHitDirection;
    AnglesToDirectionVector(GetPlacement().pl_OrientationAngle, vHitDirection);

    INDEX ctCasts = 0;
    while( ctCasts<10)
    {
      if(ctCasts == 0)
      {
        // perform first cast
        GetWorld()->CastRay(crRay);       
      }
      else
      {
        // next casts
        GetWorld()->ContinueCast(crRay);
      }
      ctCasts++;

      // stop casting if nothing hit
      if (crRay.cr_penHit==NULL)
      {
        break;
      }
      // apply damage
      InflictDirectDamage(crRay.cr_penHit, m_penOwner, m_EdtDamage, m_fDamage,
                            crRay.cr_vHit, vHitDirection);

      m_vHitPoint = crRay.cr_vHit;

      // if brush hitted
      if (crRay.cr_penHit->GetRenderType()==RT_BRUSH && crRay.cr_pbpoBrushPolygon!=NULL)
      {
        CBrushPolygon *pbpo = crRay.cr_pbpoBrushPolygon;
        FLOAT3D vHitNormal = FLOAT3D(pbpo->bpo_pbplPlane->bpl_plAbsolute);
        // obtain surface type
        INDEX iSurfaceType = pbpo->bpo_bppProperties.bpp_ubSurfaceType;
        BulletHitType bhtType = BHT_BRUSH_STONE;
        // get content type
        INDEX iContent = pbpo->bpo_pbscSector->GetContentType();
        CContentType &ct = GetWorld()->wo_actContentTypes[iContent];
        // if this is under water polygon
        if( ct.ct_ulFlags&CTF_BREATHABLE_GILLS)
        {
          // if we hit water surface
          if( iSurfaceType==SURFACE_WATER) 
          {
            vHitNormal = -vHitNormal;
            bhtType=BHT_BRUSH_WATER;
          }   
          // if we hit stone under water
          else
          {
            bhtType=BHT_BRUSH_UNDER_WATER;
          }
        }
        else
        {
          if( iSurfaceType==SURFACE_SAND)     {bhtType=BHT_BRUSH_SAND;}
          if( iSurfaceType==SURFACE_RED_SAND) {bhtType=BHT_BRUSH_RED_SAND;}
          if( iSurfaceType==SURFACE_WATER)    {bhtType=BHT_BRUSH_WATER;}
        }
        // spawn hit effect
        BOOL bPassable = pbpo->bpo_ulFlags & (BPOF_PASSABLE|BPOF_SHOOTTHRU);
        if (!bPassable || iSurfaceType==SURFACE_WATER) {
          SpawnHitTypeEffect(bhtType, bSound, vHitNormal, crRay.cr_vHit, vHitDirection, FLOAT3D(0.0f, 0.0f, 0.0f));
        }
        if(!bPassable) {
          break;
        }
      // if not brush
      } else {

        // if flesh entity
        if (crRay.cr_penHit->GetEntityInfo()!=NULL) {
          if( ((EntityInfo*)crRay.cr_penHit->GetEntityInfo())->Eeibt == EIBT_FLESH)
          {
            CEntity *penOfFlesh = crRay.cr_penHit;
            FLOAT3D vHitNormal = (GetPlacement().pl_PositionVector-m_vTarget).Normalize();
            FLOAT3D vOldHitPos = crRay.cr_vHit;
            FLOAT3D vDistance;

            // look behind the entity (for back-stains)
            GetWorld()->ContinueCast(crRay);
            if( crRay.cr_penHit!=NULL && crRay.cr_pbpoBrushPolygon!=NULL && 
                crRay.cr_penHit->GetRenderType()==RT_BRUSH)
            {
              vDistance = crRay.cr_vHit-vOldHitPos;
              vHitNormal = FLOAT3D(crRay.cr_pbpoBrushPolygon->bpo_pbplPlane->bpl_plAbsolute);
            }
            else
            {
              vDistance = FLOAT3D(0.0f, 0.0f, 0.0f);
              vHitNormal = FLOAT3D(0,0,0);
            }

            if(IsOfClass(penOfFlesh, "Gizmo") ||
               IsOfClass(penOfFlesh, "Beast"))
            {
              // spawn green blood hit spill effect
              SpawnHitTypeEffect(BHT_ACID, bSound, vHitNormal, crRay.cr_vHit, vHitDirection, vDistance);
            }
            else
            {
              // spawn red blood hit spill effect
              SpawnHitTypeEffect(BHT_FLESH, bSound, vHitNormal, crRay.cr_vHit, vHitDirection, vDistance);
            }
            break;
          }
        }

        // stop casting ray if not brush
        break;
      }
    }

    if( bTrail)
    {
      SpawnTrail();
    }
  };

  // destroy yourself
  void DestroyBullet(void) {
    Destroy();
  };



/************************************************************
 *                        EFFECTS                           *
 ************************************************************/
  // spawn effect from hit type
  void SpawnHitTypeEffect(enum BulletHitType bhtType, BOOL bSound, FLOAT3D vHitNormal, FLOAT3D vHitPoint,
    FLOAT3D vDirection, FLOAT3D vDistance)
  {
    switch (bhtType)
    {
      case BHT_BRUSH_STONE:
      case BHT_BRUSH_SAND:
      case BHT_BRUSH_RED_SAND:
      case BHT_BRUSH_WATER:
      case BHT_BRUSH_UNDER_WATER:
      {
        // bullet stain
        ESpawnEffect ese;
        if( bSound)
        {
          if( bhtType == BHT_BRUSH_STONE)         {ese.betType = BET_BULLETSTAINSTONE;};
          if( bhtType == BHT_BRUSH_SAND)          {ese.betType = BET_BULLETSTAINSAND;};
          if( bhtType == BHT_BRUSH_RED_SAND)      {ese.betType = BET_BULLETSTAINREDSAND;};
          if( bhtType == BHT_BRUSH_WATER)         {ese.betType = BET_BULLETSTAINWATER;};
          if( bhtType == BHT_BRUSH_UNDER_WATER)   {ese.betType = BET_BULLETSTAINUNDERWATER;};
        }
        else
        {
          if( bhtType == BHT_BRUSH_STONE)         {ese.betType = BET_BULLETSTAINSTONENOSOUND;};
          if( bhtType == BHT_BRUSH_SAND)          {ese.betType = BET_BULLETSTAINSANDNOSOUND;};
          if( bhtType == BHT_BRUSH_RED_SAND)      {ese.betType = BET_BULLETSTAINREDSANDNOSOUND;};
          if( bhtType == BHT_BRUSH_WATER)         {ese.betType = BET_BULLETSTAINWATERNOSOUND;};
          if( bhtType == BHT_BRUSH_UNDER_WATER)   {ese.betType = BET_BULLETSTAINUNDERWATERNOSOUND;};
        }

        ese.vNormal = vHitNormal;
        ese.colMuliplier = C_WHITE|CT_OPAQUE;
        FLOAT3D vDirection = (vHitPoint-GetPlacement().pl_PositionVector).Normalize();
        // reflect direction arround normal
        FLOAT fNx = vHitNormal(1);
        FLOAT fNy = vHitNormal(2);
        FLOAT fNz = vHitNormal(3);
        FLOAT fNV  = fNx*vDirection(1) + fNy*vDirection(2) + fNz*vDirection(3);
        FLOAT fRVx = vDirection(1) - 2*fNx*fNV;
        FLOAT fRVy = vDirection(2) - 2*fNy*fNV;
        FLOAT fRVz = vDirection(3) - 2*fNz*fNV;
        ese.vStretch = FLOAT3D( fRVx, fRVy, fRVz);
        SpawnEffect(vHitPoint, ese);
        break;
      }
      case BHT_FLESH:
      case BHT_ACID:
      {
        // spawn bullet entry wound
        ESpawnEffect ese;
        ese.colMuliplier = C_WHITE|CT_OPAQUE;
        // if there is exit wound blood spill place
        FLOAT fDistance = vDistance.Length();
        if( fDistance>0.01f && !(IRnd()%2) )
        {
          // spawn bullet exit wound blood patch
          ese.betType = BET_BLOODSPILL;
          if( bhtType == BHT_ACID)
          {
            ese.colMuliplier = BLOOD_SPILL_GREEN;
          }
          else
          {
            ese.colMuliplier = BLOOD_SPILL_RED;
          }
          ese.vNormal = vHitNormal;
          if (fDistance<25.0f)
          {
            GetNormalComponent( vDistance/fDistance, vHitNormal, ese.vDirection);
            FLOAT fLength = ese.vDirection.Length();
            fLength   = Clamp( fLength*3, 1.0f, 3.0f);
            fDistance = Clamp( log10(fDistance), 0.5, 2.0);
            ese.vStretch = FLOAT3D( fDistance, fLength*fDistance, 1.0f);
            SpawnEffect(vHitPoint, ese);
          }
        }
        break;
      }
    }
  };
  // spawn trail of this bullet
  void SpawnTrail(void) 
  {
    // get bullet path positions
    const FLOAT3D &v0 = GetPlacement().pl_PositionVector;
    const FLOAT3D &v1 = m_vHitPoint;
    // calculate distance
    FLOAT3D vD = v1-v0;
    FLOAT fD = vD.Length();
    // if too short
    if (fD<1.0f) {
      // no trail
      return;
    }

    // length must be such that it doesn't get out of path
    FLOAT fLen = Min(20.0f, fD);
    // position is random, but it must not make trail go out of path
    FLOAT3D vPos;
    if (fLen<fD) {
      vPos = Lerp(v0, v1, FRnd()*(fD-fLen)/fD);
    } else {
      vPos = v0;
    }

    ESpawnEffect ese;
    UBYTE ubRndH = UBYTE( 8+FRnd()*32);
    UBYTE ubRndS = UBYTE( 8+FRnd()*32);
    UBYTE ubRndV = UBYTE( 224+FRnd()*32);
    UBYTE ubRndA = UBYTE( 32+FRnd()*128);
    ese.colMuliplier = HSVToColor(ubRndH, ubRndS, ubRndV)|ubRndA;
    ese.betType = BET_BULLETTRAIL;
    ese.vNormal = vD/fD;
    ese.vStretch = FLOAT3D(0.1f, fLen, 1.0f);
    SpawnEffect(vPos , ese);
  }

  // spawn effect
  void SpawnEffect(const FLOAT3D &vHit, const ESpawnEffect &eSpawnEffect)
  {
    FLOAT3D vDirection;
    vDirection = (m_vTarget-GetPlacement().pl_PositionVector).Normalize();
    CPlacement3D plHit = CPlacement3D(vHit-vDirection*0.1f, GetPlacement().pl_OrientationAngle);
    CEntityPointer penHit = CreateEntity(plHit , CLASS_BASIC_EFFECT);
    penHit->Initialize(eSpawnEffect);
  };

procedures:

  Main(EBulletInit eInit)
  {
    // remember the initial parameters
    ASSERT(eInit.penOwner!=NULL);
    m_penOwner = eInit.penOwner;
    m_fDamage = eInit.fDamage;

    InitAsVoid();
    SetPhysicsFlags(EPF_MODEL_IMMATERIAL);
    SetCollisionFlags(ECF_IMMATERIAL);

    // for lerped launch
    m_iBullet = 0;
    return;
  };
};