Casser des portes dans Half-Life

Écrit le 26/08/2004 par FreeZeBiT
Dernière mise à jour : 31/01/2006

Casser des portes dans Half-Life

1. INTRODUCTION:
Je suis sûr que vous avez déjà tous vu des émissions où le GIGN défonçait une porte à coup de fusil à pompe afin d'investir une salle...
Et vous avez sûrement envie de faire de même quand vous jouer à CS et que votre équipe de 16 potes tente désespérément de rentrer rapidement dans une salle. Manque de pot, faut attendre que la porte s'ouvre...

Dans ce tutorial, je vous propose de réaliser ce rêve et rendre l'entité 'func_door' destructible !!
En fait, ça paraît dur, mais c'est très simple... il faut dire que le code de l'entité 'func_breakable' y ait pour beaucoup ;)
On va beaucoup jouer avec le copier/coller entre les fichiers 'func_breakable.cpp' et 'doors.cpp'...
Nous allons donc modifier notre 'func_door' afin que le mappeur puisse décider si sa porte sera cassable ou non, et si elle est cassable, de quel matériau elle est conçu et si elle explose ou non en se cassant (bref, les bases de 'func_breakable').

2. COPIER/COLLER:
Commençons par le commencement, les déclarations...
Afin de pouvoir faire exploser notre porte, nous avons besoin d'inclure le fichier 'explode.h' ; on a aussi dit que l'on se servait des matériaux, donc on déclare tout ça au début de 'doors.cpp' :

#include "extdll.h"
#include "util.h"
#include "cbase.h"
#include "doors.h"

// freezebit
#include "explode.h"
typedef enum { matGlass = 0, matWood, matMetal, matFlesh, matCinderBlock, matCeilingTile, matComputer, matRocks, matNone, matLastMaterial } Materials;
// fin freezebit

extern void SetMovedir(entvars_t* ev);

Petite remarque au passage : faîtes attention, j'ai retiré le matériau 'matUnbreakableGlass' qui ne nous sert pas...

Ensuite, nous avons besoin de quelques fonctions et des tableaux des sons, on ajoute leur déclaration dans la déclaration de la classe 'CBaseDoor' :

class CBaseDoor : public CBaseToggle
{
public:
    void Spawn( void );
    void Precache( void );
    virtual void KeyValue( KeyValueData *pkvd );
    virtual void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value );
    virtual void Blocked( CBaseEntity *pOther );

    // freezebit
    inline int ExplosionMagnitude( void ) { return pev->impulse; }

    static const char **MaterialSoundList( Materials precacheMaterial, int &soundCount );
    int TakeDamage( entvars_t* pevInflictor, entvars_t* pevAttacker, float flDamage, int bitsDamageType );
    void DamageSound( void );
    void MaterialSoundPrecache( Materials precacheMaterial );
    void BreakTouch( CBaseEntity *pOther );
    void Die( void );

    static const char *pSoundsWood[];
    static const char *pSoundsFlesh[];
    static const char *pSoundsGlass[];
    static const char *pSoundsMetal[];
    static const char *pSoundsConcrete[];
    static const char *pSpawnObjects[];

    int m_idShard;
    Materials m_Material;
    bool m_bBreakable;
    bool m_bExplodable;
    // fin freezebit

    virtual int ObjectCaps( void )
    {
        if (pev->spawnflags & SF_ITEM_USE_ONLY)
            return (CBaseToggle::ObjectCaps() & ~FCAP_ACROSS_TRANSITION) | FCAP_IMPULSE_USE;
        else
            return (CBaseToggle::ObjectCaps() & ~FCAP_ACROSS_TRANSITION);
    };
    ...

Maintenant, ça va être en grande partie très simple, il suffit de faire un copier/coller de tout ce que vous venez de déclarer depuis 'func_breakable.cpp' vers 'doors.cpp'... puis de prendre soin quand même de remplacer toutes les occurences de 'CBreakable' en 'CBaseDoor'...

Et comme je suis un mec sympa ;), je l'ai fait pour vous (après la fonction 'void CBaseDoor::Blocked( CBaseEntity *pOther )') :

// freezebit
// tableaux des sons dispo pour chaque matériau
const char *CBaseDoor::pSoundsWood[] = 
{
    "debris/wood1.wav",
    "debris/wood2.wav",
    "debris/wood3.wav",
};

const char *CBaseDoor::pSoundsFlesh[] = 
{
    "debris/flesh1.wav",
    "debris/flesh2.wav",
    "debris/flesh3.wav",
    "debris/flesh5.wav",
    "debris/flesh6.wav",
    "debris/flesh7.wav",
};

const char *CBaseDoor::pSoundsMetal[] = 
{
    "debris/metal1.wav",
    "debris/metal2.wav",
    "debris/metal3.wav",
};

const char *CBaseDoor::pSoundsConcrete[] = 
{
    "debris/concrete1.wav",
    "debris/concrete2.wav",
    "debris/concrete3.wav",
};

const char *CBaseDoor::pSoundsGlass[] = 
{
    "debris/glass1.wav",
    "debris/glass2.wav",
    "debris/glass3.wav",
};

// de quelle liste de sons va-t-on avoir besoin ?
const char **CBaseDoor::MaterialSoundList( Materials precacheMaterial, int &soundCount )
{
    const char **pSoundList = NULL;

    switch ( precacheMaterial ) 
    {
    case matWood:
        pSoundList = pSoundsWood;
        soundCount = ARRAYSIZE(pSoundsWood);
        break;

    case matFlesh:
        pSoundList = pSoundsFlesh;
        soundCount = ARRAYSIZE(pSoundsFlesh);
        break;

    case matGlass:
        pSoundList = pSoundsGlass;
        soundCount = ARRAYSIZE(pSoundsGlass);
        break;

    case matComputer:
    case matMetal:
        pSoundList = pSoundsMetal;
        soundCount = ARRAYSIZE(pSoundsMetal);
        break;

    case matCinderBlock:
    case matRocks:
        pSoundList = pSoundsConcrete;
        soundCount = ARRAYSIZE(pSoundsConcrete);
        break;
    
    case matCeilingTile:
    case matNone:
    default:
        soundCount = 0;
        break;
    }

    return pSoundList;
}

// precache de tous les sons qui sont dans la liste dont on a besoin
void CBaseDoor::MaterialSoundPrecache( Materials precacheMaterial )
{
    const char **pSoundList;
    int i, soundCount = 0;

    pSoundList = MaterialSoundList( precacheMaterial, soundCount );

    for ( i = 0; i < soundCount; i++ )
    {
        PRECACHE_SOUND( (char *)pSoundList[i] );
    }
}

// fonction qui gère si la porte se prend un coup
void CBaseDoor::BreakTouch( CBaseEntity *pOther )
{
    float flDamage;
    entvars_t* pevToucher = pOther->pev;

    // only players can break these right now
    if ( !pOther->IsPlayer() || !m_bBreakable ) // freezebit (ou porte non destructible, on sort de suite)
    {
        return;
    }

    if ( FBitSet ( pev->spawnflags, SF_BREAK_TOUCH ) )
    {// can be broken when run into 
        flDamage = pevToucher->velocity.Length() * 0.01;

        if (flDamage >= pev->health)
        {
            SetTouch( NULL );
            TakeDamage(pevToucher, pevToucher, flDamage, DMG_CRUSH);

            // do a little damage to player if we broke glass or computer
            pOther->TakeDamage( pev, pev, flDamage/4, DMG_SLASH );
        }
    }

    if ( FBitSet ( pev->spawnflags, SF_BREAK_PRESSURE ) && pevToucher->absmin.z >= pev->maxs.z - 2 )
    {// can be broken when stood upon
        
        // play creaking sound here.
        DamageSound();

        SetThink ( Die );
        SetTouch( NULL );
        
        if ( m_flDelay == 0 )
        {// !!!BUGBUG - why doesn't zero delay work?
            m_flDelay = 0.1;
        }

        pev->nextthink = pev->ltime + m_flDelay;
    }
}

// aïe !!
int CBaseDoor :: TakeDamage( entvars_t* pevInflictor, entvars_t* pevAttacker, float flDamage, int bitsDamageType )
{
    Vector vecTemp;

    // if Attacker == Inflictor, the attack was a melee or other instant-hit attack.
    // (that is, no actual entity projectile was involved in the attack so use the shooter's origin). 
    if ( pevAttacker == pevInflictor )
    {
        vecTemp = pevInflictor->origin - ( pev->absmin + ( pev->size * 0.5 ) );
        
        // if a client hit the breakable with a crowbar, and breakable is crowbar-sensitive, break it now.
        if ( FBitSet ( pevAttacker->flags, FL_CLIENT ) &&
                 FBitSet ( pev->spawnflags, SF_BREAK_CROWBAR ) && (bitsDamageType & DMG_CLUB))
            flDamage = pev->health;
    }
    else
    // an actual missile was involved.
    {
        vecTemp = pevInflictor->origin - ( pev->absmin + ( pev->size * 0.5 ) );
    }
    
    // porte non destructible, on sort de suite
    if ( !m_bBreakable ) // freezebit
        return 0;

    // Breakables take double damage from the crowbar
    if ( bitsDamageType & DMG_CLUB )
        flDamage *= 2;

    // Boxes / glass / etc. don't take much poison damage, just the impact of the dart - consider that 10%
    if ( bitsDamageType & DMG_POISON )
        flDamage *= 0.1;

// this global is still used for glass and other non-monster killables, along with decals.
    /*g_vecAttackDir = vecTemp.Normalize();*/ // freezebit (direction de l'explosion... on ne gère pas)
        
// do the damage
    pev->health -= flDamage;
    if (pev->health <= 0)
    {
        Killed( pevAttacker, GIB_NORMAL );
        Die();
        return 0;
    }

    // Make a shard noise each time func breakable is hit.
    // Don't play shard noise if cbreakable actually died.

    DamageSound();

    return 1;
}

// la fonction qui joue un son quand on tire sur la porte
void CBaseDoor::DamageSound( void )
{
    int pitch;
    float fvol;
    char *rgpsz[6];
    int i;
    int material = m_Material;

//    if (RANDOM_LONG(0,1))
//        return;

    if (RANDOM_LONG(0,2))
        pitch = PITCH_NORM;
    else
        pitch = 95 + RANDOM_LONG(0,34);

    fvol = RANDOM_FLOAT(0.75, 1.0);

    if (material == matComputer && RANDOM_LONG(0,1))
        material = matMetal;

    switch (material)
    {
    case matComputer:
    case matGlass:
        rgpsz[0] = "debris/glass1.wav";
        rgpsz[1] = "debris/glass2.wav";
        rgpsz[2] = "debris/glass3.wav";
        i = 3;
        break;

    case matWood:
        rgpsz[0] = "debris/wood1.wav";
        rgpsz[1] = "debris/wood2.wav";
        rgpsz[2] = "debris/wood3.wav";
        i = 3;
        break;

    case matMetal:
        rgpsz[0] = "debris/metal1.wav";
        rgpsz[1] = "debris/metal3.wav";
        rgpsz[2] = "debris/metal2.wav";
        i = 2;
        break;

    case matFlesh:
        rgpsz[0] = "debris/flesh1.wav";
        rgpsz[1] = "debris/flesh2.wav";
        rgpsz[2] = "debris/flesh3.wav";
        rgpsz[3] = "debris/flesh5.wav";
        rgpsz[4] = "debris/flesh6.wav";
        rgpsz[5] = "debris/flesh7.wav";
        i = 6;
        break;

    case matRocks:
    case matCinderBlock:
        rgpsz[0] = "debris/concrete1.wav";
        rgpsz[1] = "debris/concrete2.wav";
        rgpsz[2] = "debris/concrete3.wav";
        i = 3;
        break;

    case matCeilingTile:
        // UNDONE: no ceiling tile shard sound yet
        i = 0;
        break;
    }

    if (i)
        EMIT_SOUND_DYN(ENT(pev), CHAN_VOICE, rgpsz[RANDOM_LONG(0,i-1)], fvol, ATTN_NORM, 0, pitch);
}

// ah, la porte n'a plus de vie, on la détruit
void CBaseDoor::Die( void )
{
    Vector vecSpot;// shard origin
    Vector vecVelocity;// shard velocity
    CBaseEntity *pEntity = NULL;
    char cFlag = 0;
    int pitch;
    float fvol;
    
    pitch = 95 + RANDOM_LONG(0,29);

    if (pitch > 97 && pitch < 103)
        pitch = 100;

    // The more negative pev->health, the louder
    // the sound should be.

    fvol = RANDOM_FLOAT(0.85, 1.0) + (abs(pev->health) / 100.0);

    if (fvol > 1.0)
        fvol = 1.0;

    switch (m_Material)
    {
    case matGlass:
        switch ( RANDOM_LONG(0,1) )
        {
        case 0:    EMIT_SOUND_DYN(ENT(pev), CHAN_VOICE, "debris/bustglass1.wav", fvol, ATTN_NORM, 0, pitch);
            break;
        case 1:    EMIT_SOUND_DYN(ENT(pev), CHAN_VOICE, "debris/bustglass2.wav", fvol, ATTN_NORM, 0, pitch);
            break;
        }
        cFlag = break_GLASS;
        break;

    case matWood:
        switch ( RANDOM_LONG(0,1) )
        {
        case 0:    EMIT_SOUND_DYN(ENT(pev), CHAN_VOICE, "debris/bustcrate1.wav", fvol, ATTN_NORM, 0, pitch);
            break;
        case 1:    EMIT_SOUND_DYN(ENT(pev), CHAN_VOICE, "debris/bustcrate2.wav", fvol, ATTN_NORM, 0, pitch);
            break;
        }
        cFlag = break_WOOD;
        break;

    case matComputer:
    case matMetal:
        switch ( RANDOM_LONG(0,1) )
        {
        case 0:    EMIT_SOUND_DYN(ENT(pev), CHAN_VOICE, "debris/bustmetal1.wav", fvol, ATTN_NORM, 0, pitch);
            break;
        case 1:    EMIT_SOUND_DYN(ENT(pev), CHAN_VOICE, "debris/bustmetal2.wav", fvol, ATTN_NORM, 0, pitch);
            break;
        }
        cFlag = break_METAL;
        break;

    case matFlesh:
        switch ( RANDOM_LONG(0,1) )
        {
        case 0:    EMIT_SOUND_DYN(ENT(pev), CHAN_VOICE, "debris/bustflesh1.wav", fvol, ATTN_NORM, 0, pitch);
            break;
        case 1:    EMIT_SOUND_DYN(ENT(pev), CHAN_VOICE, "debris/bustflesh2.wav", fvol, ATTN_NORM, 0, pitch);
            break;
        }
        cFlag = break_FLESH;
        break;

    case matRocks:
    case matCinderBlock:
        switch ( RANDOM_LONG(0,1) )
        {
        case 0:    EMIT_SOUND_DYN(ENT(pev), CHAN_VOICE, "debris/bustconcrete1.wav", fvol, ATTN_NORM, 0, pitch);
            break;
        case 1:    EMIT_SOUND_DYN(ENT(pev), CHAN_VOICE, "debris/bustconcrete2.wav", fvol, ATTN_NORM, 0, pitch);
            break;
        }
        cFlag = break_CONCRETE;
        break;

    case matCeilingTile:
        EMIT_SOUND_DYN(ENT(pev), CHAN_VOICE, "debris/bustceiling.wav", fvol, ATTN_NORM, 0, pitch);
        break;
    }
    
        // freezebit
    // ça, c'était lorsque l'on pouvait choisir la direction de l'explosion,
    // ici, on s'en fiche... mais on pourrait le remettre
    /*if (m_Explosion == expDirected)
        vecVelocity = g_vecAttackDir * 200;
    else
    {*/
        vecVelocity.x = 0;
        vecVelocity.y = 0;
        vecVelocity.z = 0;
    /*}*/

    vecSpot = pev->origin + (pev->mins + pev->maxs) * 0.5;
    MESSAGE_BEGIN( MSG_PVS, SVC_TEMPENTITY, vecSpot );
        WRITE_BYTE( TE_BREAKMODEL);

        // position
        WRITE_COORD( vecSpot.x );
        WRITE_COORD( vecSpot.y );
        WRITE_COORD( vecSpot.z );

        // size
        WRITE_COORD( pev->size.x);
        WRITE_COORD( pev->size.y);
        WRITE_COORD( pev->size.z);

        // velocity
        WRITE_COORD( vecVelocity.x );
        WRITE_COORD( vecVelocity.y );
        WRITE_COORD( vecVelocity.z );

        // randomization
        WRITE_BYTE( 10 );

        // Model
        WRITE_SHORT( m_idShard ); //model id#

        // # of shards
        WRITE_BYTE( 0 ); // let client decide

        // duration
        WRITE_BYTE( 25 ); // 2.5 seconds

        // flags
        WRITE_BYTE( cFlag );
    MESSAGE_END();

    float size = pev->size.x;
    if ( size < pev->size.y )
        size = pev->size.y;
    if ( size < pev->size.z )
        size = pev->size.z;

    // !!! HACK  this should work!
    // Build a box above the entity that looks like an 8 pixel high sheet
    Vector mins = pev->absmin;
    Vector maxs = pev->absmax;
    mins.z = pev->absmax.z;
    maxs.z += 8;

    // BUGBUG -- can only find 256 entities on a breakable -- should be enough
    CBaseEntity *pList[256];
    int count = UTIL_EntitiesInBox( pList, 256, mins, maxs, FL_ONGROUND );
    if ( count )
    {
        for ( int i = 0; i < count; i++ )
        {
            ClearBits( pList[i]->pev->flags, FL_ONGROUND );
            pList[i]->pev->groundentity = NULL;
        }
    }

    // Don't fire something that could fire myself
    pev->targetname = 0;

    pev->solid = SOLID_NOT;
    // Fire targets on break
    SUB_UseTargets( NULL, USE_TOGGLE, 0 );

    SetThink( SUB_Remove );
    pev->nextthink = pev->ltime + 0.1;

    // heu... non, on ne va pas mettre qu'une porte peut lâcher un objet...
    /*if ( m_iszSpawnObject )
        CBaseEntity::Create( (char *)STRING(m_iszSpawnObject), VecBModelOrigin(pev), pev->angles, edict() );*/

    if ( m_bExplodable ) // freezebit (si la porte explose)
        ExplosionCreate( Center(), pev->angles, edict(), ExplosionMagnitude(), true );
}
// fin freezebit

Notez quand même que j'ai effectué quelques retouches afin de simplifier la chose...
Par exemple, on ne gère pas la direction de l'explosion donc cette partie est commentée...
Pour le reste, en général, j'ai un peu commenté donc voyez par vous-même.

On va aussi copier/coller le code qui permet le precache des sons et models...
Attention, là aussi j'ai fait quelques modifications, ajoutez ceci à la fin de la fonction 'Precache' :

    switch (m_bUnlockedSentence)
    {
        case 1: m_ls.sUnlockedSentence = ALLOC_STRING("EA"); break; // access granted
        case 2: m_ls.sUnlockedSentence = ALLOC_STRING("ED"); break; // security door
        case 3: m_ls.sUnlockedSentence = ALLOC_STRING("EF"); break; // blast door
        case 4: m_ls.sUnlockedSentence = ALLOC_STRING("EFIRE"); break; // fire door
        case 5: m_ls.sUnlockedSentence = ALLOC_STRING("ECHEM"); break; // chemical door
        case 6: m_ls.sUnlockedSentence = ALLOC_STRING("ERAD"); break; // radiation door
        case 7: m_ls.sUnlockedSentence = ALLOC_STRING("ECON"); break; // gen containment
        case 8: m_ls.sUnlockedSentence = ALLOC_STRING("EH"); break; // maintenance door
        
        default: m_ls.sUnlockedSentence = 0; break;
    }

    // freezebit
    // precache des sons et du model en fonction du matériau
    const char *pGibName;

    switch (m_Material)
    {
    case matWood:
        pGibName = "models/woodgibs.mdl";
        
        PRECACHE_SOUND("debris/bustcrate1.wav");
        PRECACHE_SOUND("debris/bustcrate2.wav");
        break;

    case matFlesh:
        pGibName = "models/fleshgibs.mdl";
        
        PRECACHE_SOUND("debris/bustflesh1.wav");
        PRECACHE_SOUND("debris/bustflesh2.wav");
        break;

    case matComputer:
        PRECACHE_SOUND("buttons/spark5.wav");
        PRECACHE_SOUND("buttons/spark6.wav");
        pGibName = "models/computergibs.mdl";
        
        PRECACHE_SOUND("debris/bustmetal1.wav");
        PRECACHE_SOUND("debris/bustmetal2.wav");
        break;

    case matGlass:
        pGibName = "models/glassgibs.mdl";
        
        PRECACHE_SOUND("debris/bustglass1.wav");
        PRECACHE_SOUND("debris/bustglass2.wav");
        break;

    case matMetal:
        pGibName = "models/metalplategibs.mdl";
        
        PRECACHE_SOUND("debris/bustmetal1.wav");
        PRECACHE_SOUND("debris/bustmetal2.wav");
        break;

    case matCinderBlock:
        pGibName = "models/cindergibs.mdl";
        
        PRECACHE_SOUND("debris/bustconcrete1.wav");
        PRECACHE_SOUND("debris/bustconcrete2.wav");
        break;

    case matRocks:
        pGibName = "models/rockgibs.mdl";
        
        PRECACHE_SOUND("debris/bustconcrete1.wav");
        PRECACHE_SOUND("debris/bustconcrete2.wav");
        break;

    case matCeilingTile:
        pGibName = "models/ceilinggibs.mdl";
        
        PRECACHE_SOUND ("debris/bustceiling.wav");
        break;
    }
    MaterialSoundPrecache( m_Material );
    
    m_idShard = PRECACHE_MODEL( (char *)pGibName );
    // fin freezebit
}

3. AJOUTS NECESSAIRES:
Arf !! Oui, quand même, il a fallut réfléchir un peu ;) ...
Bon, ce n'est pas grand chose, mais si vous tentez de compiler ça, je ne sais pas ce que ça va donner, mais votre porte ne sera sûrement pas cassable...

En effet, notez que nul part nous ne faisons appelle à l'une ou l'autre de ces fonctions.
On va tout d'abord ajouter nos variables au tableau 'm_SaveData' :

TYPEDESCRIPTION CBaseDoor::m_SaveData[] = 
{
    DEFINE_FIELD( CBaseDoor, m_bHealthValue, FIELD_CHARACTER ),
    DEFINE_FIELD( CBaseDoor, m_bMoveSnd, FIELD_CHARACTER ),
    DEFINE_FIELD( CBaseDoor, m_bStopSnd, FIELD_CHARACTER ),

    DEFINE_FIELD( CBaseDoor, m_bLockedSound, FIELD_CHARACTER ),
    DEFINE_FIELD( CBaseDoor, m_bLockedSentence, FIELD_CHARACTER ),
    DEFINE_FIELD( CBaseDoor, m_bUnlockedSound, FIELD_CHARACTER ),
    DEFINE_FIELD( CBaseDoor, m_bUnlockedSentence, FIELD_CHARACTER ),

    // freezebit
    // on garde en mémoire ces valeurs
    DEFINE_FIELD( CBaseDoor, m_Material, FIELD_INTEGER ),
    DEFINE_FIELD( CBaseDoor, m_bBreakable, FIELD_BOOLEAN ),
    DEFINE_FIELD( CBaseDoor, m_bExplodable, FIELD_BOOLEAN ),
    // fin freezebit
};

On va ensuite rendre notre porte vivante... à savoir qu'elle peut prendre des coups, ça se passe dans la fonction 'Spawn' :

    if ( pev->skin == 0 )
    {//normal door
        if ( FBitSet (pev->spawnflags, SF_DOOR_PASSABLE) )
            pev->solid = SOLID_NOT;
        else
            pev->solid = SOLID_BSP;
    }
    else
    {// special contents
        pev->solid = SOLID_NOT;
        SetBits( pev->spawnflags, SF_DOOR_SILENT ); // water is silent for now
    }

    // freezebit
    // la porte est prête à prendre des coups ;)
    pev->takedamage = DAMAGE_YES;
    // fin freezebit

    pev->movetype = MOVETYPE_PUSH;

Maintenant, on va faire en sorte que si on tire sur la porte, elle soit "blessée" ; pour cela, il faut faire un 'SetTouch( BreakTouch );'.
Pourtant, ce n'est pas ce que nous allons faire, sinon, le 'SetTouch( DoorTouch );' qui permet au joueur d'ouvrir la porte, ne fonctionnera plus...
On va donc tout simplement faire appel à notre fonction 'BreakTouch' à la fin de la fonction 'DoorTouch' :

    if (DoorActivate( ))
        SetTouch( NULL ); // Temporarily disable the touch function, until movement is finished.

    // freezebit
    // plutôt que de faire un SetTouch( BreakTouch ), on l'ajoute ici histoire de conserver la possibilité
    // d'ouvrir et de fermer la porte
    BreakTouch( pOther );
    // fin freezebit
}

4. COMMUNICATION AVEC LA MAP:
Oui, nous n'avons pas tout à fait fini.
Comme je le disais au début, nous allons permettre au mappeur de décider si la porte est cassable ou non...

Nous allons donc ajouter une brindille de code dans la fonction 'KeyValue' (qui sert à récupérer les propriétés des entités de la map, soit ici de la 'func_door') :

void CBaseDoor::KeyValue( KeyValueData *pkvd )
{
    int i; // freezebit

    if (FStrEq(pkvd->szKeyName, "skin"))//skin is used for content type
    {
        pev->skin = atof(pkvd->szValue);
        pkvd->fHandled = true;
    }
    else if (FStrEq(pkvd->szKeyName, "movesnd"))
    {
        m_bMoveSnd = atof(pkvd->szValue);
        pkvd->fHandled = true;
    }
    else if (FStrEq(pkvd->szKeyName, "stopsnd"))
    {
        m_bStopSnd = atof(pkvd->szValue);
        pkvd->fHandled = true;
    }
    else if (FStrEq(pkvd->szKeyName, "healthvalue"))
    {
        m_bHealthValue = atof(pkvd->szValue);
        pkvd->fHandled = true;
    }
    else if (FStrEq(pkvd->szKeyName, "locked_sound"))
    {
        m_bLockedSound = atof(pkvd->szValue);
        pkvd->fHandled = true;
    }
    else if (FStrEq(pkvd->szKeyName, "locked_sentence"))
    {
        m_bLockedSentence = atof(pkvd->szValue);
        pkvd->fHandled = true;
    }
    else if (FStrEq(pkvd->szKeyName, "unlocked_sound"))
    {
        m_bUnlockedSound = atof(pkvd->szValue);
        pkvd->fHandled = true;
    }
    else if (FStrEq(pkvd->szKeyName, "unlocked_sentence"))
    {
        m_bUnlockedSentence = atof(pkvd->szValue);
        pkvd->fHandled = true;
    }
    else if (FStrEq(pkvd->szKeyName, "WaveHeight"))
    {
        pev->scale = atof(pkvd->szValue) * (1.0/8.0);
        pkvd->fHandled = true;
    }
    // freezebit
    // la porte est-elle cassable ?
    else if (FStrEq(pkvd->szKeyName, "breakable"))
    {
        i = atoi( pkvd->szValue );
        m_bBreakable = (i == 1);
    }
    // on récupère la puissance de l'explosion (si 0 => pas d'explosion)
    else if (FStrEq(pkvd->szKeyName, "explodemagnitude"))
    {
        i = atoi( pkvd->szValue );
        if( i < 0)
            i *= -1;
        pev->impulse = i;
        m_bExplodable = (i > 0);
    }
    // on récupère ici le matériau
    else if (FStrEq(pkvd->szKeyName, "material"))
    {
        i = atoi( pkvd->szValue );

        // 0:glass, 1:metal, 2:flesh, 3:wood

        if ((i < 0) || (i >= matLastMaterial))
            m_Material = matWood;
        else
            m_Material = (Materials)i;

        pkvd->fHandled = true;
    }
    // fin freezebit
    else
        CBaseToggle::KeyValue( pkvd );
}

Nous avons donc ajouter nos 3 propriétés identifiés par 'breakable', 'explodemagnitude' et 'material'...
Enfin, il faut les ajouter dans notre fichier FGD.
Et vu que l'on a modifier la classe 'CBaseDoor' plus haut, nous allons modifier l'entité de base qui est associée aux portes... à savoir 'Door' ('@BaseClass base(Appearflags, Targetname, RenderFields, Global) = Door').

Ajoutons donc nos propriétés à la fin de cette entité :

    _minlight(string) : "Minimum light level"
    // freezebit
    // on ajoute ici les propriétés que l'on veut en plus
    breakable(choices) :"Breakable" : 0 =
    [
        0: "No"
        1: "Yes"
    ]

    explodemagnitude(integer) : "Explode Magnitude (0=none)" : 0

    material(choices) :"Material type" : 0 =
    [
        0: "Glass"
        1: "Wood"
        2: "Metal"
        3: "Flesh"
        4: "Cinder Block"
        5: "Ceiling Tile"
        6: "Computer"
        7: "Rocks"
    ]
    // fin freezebit
]

On aura maintenant ces 3 nouvelles propriétés dans Wordcraft (ou Hammer)...

5. EXTENSION A L'ENTITE 'func_door_rotating':
Eh oui, comment étendre cette possibilité à l'entité 'func_door_rotating' ?
Pour les plus perspicaces, vous l'aurez sûrement déjà deviné. Pour ceux qui n'y pensent pas, remarquez que la classe 'CRotDoor' correspond à l'entité en question...
Et comme par miracle, cette classe hérite de la classe 'CBaseDoor' que l'on vient de rendre cassable !!!
Vous n'avez donc plus qu'à ajouter une (et une seule) ligne de code dans la fonction 'Spawn' de 'CRotDoor', à savoir celle-ci :

    if ( FBitSet (pev->spawnflags, SF_DOOR_PASSABLE) )
        pev->solid = SOLID_NOT;
    else
        pev->solid = SOLID_BSP;

    // freezebit
    // la porte est prête à prendre des coups ;)
    pev->takedamage = DAMAGE_YES;
    // fin freezebit

    pev->movetype = MOVETYPE_PUSH;

Et le tour est joué !
Pas besoin de modifier en plus le FGD vu que nous avons ajouté nos propriétés à celle de base des portes... l'entité 'func_door_rotating' en hérite aussi !

6. POUR FINIR:
Voilà, c'est fini, compilez votre dll, créez une map et testez, vous verrez que ça fonctionne (cf. cette vidéo : cliquez ici) !!
Toutefois, notez qu'il y a un petit problème : dans ce nouveau cas, la vie de la porte est utilisée pour savoir quand est-ce qu'elle casse (logique)...
Eh bien sachez que la propriété 'Health (shoot open)' ne sert plus à ouvrir la porte si on tire dessus, mais à définir réellement les points de vie de la porte, augmentez cette valeur pour la rendre plus costaud...
Ce code supprime l'action du 'shoot open'... c'est le seul bug que j'ai trouvé pour le moment...

Vous pouvez télécharger le mod de test que j'ai fais (contient les sources) en cliquant ici.