/*
===========================================================================
Copyright (C) 2023 the OpenMoHAA team

This file is part of OpenMoHAA source code.

OpenMoHAA source code 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.

OpenMoHAA source code 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 OpenMoHAA source code; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
===========================================================================
*/

// misc.cpp: Basically the big stew pot of the DLLs, or maybe a garbage bin, whichever
// metaphore you prefer.  This really should be cleaned up.  Anyway, this
// should contain utility functions that could be used by any entity.
// Right now it contains everything from entities that could be in their
// own file to my mother's pot roast recipes.
//

#include "g_local.h"
#include "entity.h"
#include "trigger.h"
#include "explosion.h"
#include "misc.h"
#include "navigate.h"
#include "specialfx.h"
#include "player.h"
#include "weaputils.h"
#include "g_spawn.h"
#include "g_phys.h"
#include "../script/scriptexception.h"

/*****************************************************************************/
/*QUAKED detail (0.5 0 1.0) ?

Used to fake detail brushes, convenient for grouping

******************************************************************************/

/*****************************************************************************/
/*QUAKED func_group (0.5 0.5 0.5) ?

Used to group brushes together just for editor convenience.

******************************************************************************/

/*****************************************************************************/
/*QUAKED func_remove (0.75 0.75 0.75) ?

Used for lighting and such

******************************************************************************/

CLASS_DECLARATION(Entity, FuncRemove, "func_remove") {
    {NULL, NULL}
};

FuncRemove::FuncRemove()
{
    PostEvent(EV_Remove, EV_REMOVE);
}

/*****************************************************************************/
/*QUAKED misc_model (1 0.5 1) (0 0 0) (0 0 0)
"model"     arbitrary .tik file to display
******************************************************************************/

CLASS_DECLARATION(Entity, MiscModel, "misc_model") {
    {NULL, NULL}
};

MiscModel::MiscModel()
{
    PostEvent(EV_Remove, EV_REMOVE);
}

/*****************************************************************************/
/*QUAKED info_null (0 0.5 0) (-4 -4 -4) (4 4 4)

Used as a positional target for spotlights, etc.

******************************************************************************/

CLASS_DECLARATION(Listener, InfoNull, "info_null") {
    {NULL, NULL}
};

InfoNull::InfoNull()
{
    PostEvent(EV_Remove, EV_REMOVE);
}

/*****************************************************************************/
/*QUAKED info_notnull (0 0.5 0) (-4 -4 -4) (4 4 4)

Used as a positional target for lightning.

******************************************************************************/

CLASS_DECLARATION(Entity, InfoNotNull, "info_notnull") {
    {NULL, NULL}
};

/*****************************************************************************/
/*QUAKED func_explodingwall (0 0.25 0.5) ? RANDOMANGLES LANDSHATTER NOT_PLAYERS MONSTERS PROJECTILES INVISIBLE ACCUMALATIVE TWOSTAGE

Blows up on activation or when attacked

"explosions"      number of explosions to spawn ( default 1 )
"land_angles"     The angles you want this piece to\
                  orient to when it lands on the ground
"land_radius"     The distance of the ground the piece\
                  should be when on the ground ( default 16 )
"anglespeed"      Speed at which pieces rotate ( default 100 ) \
                  if RANDOMANGLES ( default is 600 )
"key"             The item needed to activate this. (default nothing)
"base_velocity"   The speed that the debris will have when triggered.  (default 0 0 280)
"random_velocity" The variation of the velocity. x & y will be from -n < X,Y < n and z is 0 <= Z < n.  (default 140 140 140)


IF RANDOMANGLES is set, object randomly spins while in the air.
IF LANDSHATTER is set, object shatters when it hits the ground.
IF TWOSTAGE is set, object can be shattered once it lands on the ground.
IF ACCUMALATIVE is set, damage is accumlative not threshold
IF INVISIBLE is set, these are invisible and not solid until triggered
If NOT_PLAYERS is set, the trigger does not respond to players
If MONSTERS is set, the trigger will respond to monsters
If PROJECTILES is set, the trigger will respond to projectiles (rockets, grenades, etc.)

******************************************************************************/
#define RANDOMANGLES (1 << 0)
#define LANDSHATTER  (1 << 1)
#define INVISIBLE    (1 << 5)
#define ACCUMULATIVE (1 << 6)
#define TWOSTAGE     (1 << 7)

Event EV_ExplodingWall_StopRotating
(
    "stoprotating",
    EV_DEFAULT,
    NULL,
    NULL,
    "Stop rotating the wall.",
    EV_NORMAL
);
Event EV_ExplodingWall_OnGround
(
    "checkonground",
    EV_DEFAULT,
    NULL,
    NULL,
    "Check if exploding wall is on ground.",
    EV_NORMAL
);
Event EV_ExplodingWall_AngleSpeed
(
    "anglespeed",
    EV_DEFAULT,
    "f",
    "speed",
    "Set the angle speed.",
    EV_NORMAL
);
Event EV_ExplodingWall_LandRadius
(
    "land_radius",
    EV_DEFAULT,
    "f",
    "radius",
    "Set the land radius.",
    EV_NORMAL
);
Event EV_ExplodingWall_LandAngles
(
    "land_angles",
    EV_DEFAULT,
    "v",
    "angles",
    "Set the land angles.",
    EV_NORMAL
);
Event EV_ExplodingWall_BaseVelocity
(
    "base_velocity",
    EV_DEFAULT,
    "v",
    "velocity",
    "Set the base velocity.",
    EV_NORMAL
);
Event EV_ExplodingWall_RandomVelocity
(
    "random_velocity",
    EV_DEFAULT,
    "v",
    "velocity",
    "Set the amount of random variation of the base velocity.",
    EV_NORMAL
);
Event EV_ExplodingWall_SetDmg
(
    "dmg",
    EV_DEFAULT,
    "i",
    "dmg",
    "Set the damage from the exploding wall.",
    EV_NORMAL
);
Event EV_ExplodingWall_SetExplosions
(
    "explosions",
    EV_DEFAULT,
    "i",
    "explosions",
    "Set the number of explosions.",
    EV_NORMAL
);
Event EV_ExplodingWall_Setup
(
    "setup",
    EV_CODEONLY,
    NULL,
    NULL,
    "Initializes the exploding wall.",
    EV_NORMAL
);

CLASS_DECLARATION(Trigger, ExplodingWall, "func_explodingwall") {
    {&EV_ExplodingWall_Setup,          &ExplodingWall::Setup         },
    {&EV_Trigger_Effect,               &ExplodingWall::Explode       },
    {&EV_Damage,                       &ExplodingWall::DamageEvent   },
    {&EV_Touch,                        &ExplodingWall::TouchFunc     },
    {&EV_ExplodingWall_StopRotating,   &ExplodingWall::StopRotating  },
    {&EV_ExplodingWall_OnGround,       &ExplodingWall::CheckOnGround },
    {&EV_ExplodingWall_AngleSpeed,     &ExplodingWall::AngleSpeed    },
    {&EV_ExplodingWall_LandRadius,     &ExplodingWall::LandRadius    },
    {&EV_ExplodingWall_LandAngles,     &ExplodingWall::LandAngles    },
    {&EV_ExplodingWall_BaseVelocity,   &ExplodingWall::BaseVelocity  },
    {&EV_ExplodingWall_RandomVelocity, &ExplodingWall::RandomVelocity},
    {&EV_ExplodingWall_SetDmg,         &ExplodingWall::SetDmg        },
    {&EV_ExplodingWall_SetExplosions,  &ExplodingWall::SetExplosions },
    {NULL,                             NULL                          }
};

void ExplodingWall::AngleSpeed(Event *ev)

{
    angle_speed = ev->GetFloat(1);
}

void ExplodingWall::LandRadius(Event *ev)

{
    land_radius = ev->GetFloat(1);
}

void ExplodingWall::LandAngles(Event *ev)

{
    land_angles = ev->GetVector(1);
}

void ExplodingWall::BaseVelocity(Event *ev)

{
    base_velocity = ev->GetVector(1);
}

void ExplodingWall::RandomVelocity(Event *ev)

{
    random_velocity = ev->GetVector(1);
}

void ExplodingWall::SetDmg(Event *ev)

{
    dmg = ev->GetInteger(1);
}

void ExplodingWall::SetExplosions(Event *ev)

{
    explosions = ev->GetInteger(1);
}

void ExplodingWall::Explode(Event *ev)

{
    Entity *other;
    Vector  pos;
    Vector  mins, maxs;
    int     i;

    if (spawnflags & INVISIBLE) {
        showModel();
        setSolidType(SOLID_BSP);
        takedamage = DAMAGE_YES;
    }

    if (takedamage == DAMAGE_NO) {
        return;
    }

    other = ev->GetEntity(1);

    health     = 0;
    takedamage = DAMAGE_NO;

    // Create explosions
    for (i = 0; i < explosions; i++) {
        pos[0] = absmin[0] + G_Random(size[0]);
        pos[1] = absmin[1] + G_Random(size[1]);
        pos[2] = absmin[2] + G_Random(size[2]);

        CreateExplosion(pos, dmg, this, other, this);
    }

    // throw itself
    state     = 1;
    on_ground = false;
    PostEvent(EV_ExplodingWall_OnGround, FRAMETIME);

    velocity.x = base_velocity.x + G_CRandom(random_velocity.x);
    velocity.y = base_velocity.y + G_CRandom(random_velocity.y);
    velocity.z = base_velocity.z + G_Random(random_velocity.z);

    setMoveType(MOVETYPE_BOUNCE);
    setSolidType(SOLID_BBOX);
    if (spawnflags & RANDOMANGLES) {
        avelocity[0] = G_Random(angle_speed);
        avelocity[1] = G_Random(angle_speed);
        avelocity[2] = G_Random(angle_speed);
    } else {
        Vector delta;
        float  most;
        float  time;
        int    t;

        delta = land_angles - angles;
        if (delta[0] > 180) {
            delta[0] -= 360;
        }
        if (delta[0] < -180) {
            delta[0] += 360;
        }
        if (delta[1] > 180) {
            delta[1] -= 360;
        }
        if (delta[1] < -180) {
            delta[1] += 360;
        }
        if (delta[2] > 180) {
            delta[2] -= 360;
        }
        if (delta[2] < -180) {
            delta[2] += 360;
        }
        most = MaxValue(delta);
        if (!angle_speed) {
            angle_speed = 1;
        }
        t         = 10 * most / angle_speed;
        time      = (float)t / 10;
        delta     = delta * (1.0 / time);
        avelocity = delta;
        PostEvent(EV_ExplodingWall_StopRotating, time);
        state = 2;
    }

    ActivateTargets(ev);

    if (land_radius > 0) {
        mins[0] = mins[1] = mins[2] = -land_radius;
        maxs[0] = maxs[1] = maxs[2] = land_radius;
        setSize(mins, maxs);
    }

    attack_finished = 0;
}

void ExplodingWall::DamageEvent(Event *ev)

{
    Event  *event;
    Entity *inflictor;
    Entity *attacker;
    int     damage;

    if (takedamage == DAMAGE_NO) {
        return;
    }

    if (on_ground) {
        GroundDamage(ev);
        return;
    }

    attacker  = ev->GetEntity(1);
    damage    = ev->GetInteger(2);
    inflictor = ev->GetEntity(3);

    if (spawnflags & ACCUMULATIVE) {
        health -= damage;
        if (health > 0) {
            return;
        }
    } else {
        if (damage < health) {
            return;
        }
    }

    event = new Event(EV_Activate);
    event->AddEntity(attacker);
    ProcessEvent(event);
}

void ExplodingWall::GroundDamage(Event *ev)

{
    Entity *inflictor;
    Entity *attacker;
    Vector  pos;
    int     damage;

    if (takedamage == DAMAGE_NO) {
        return;
    }

    damage    = ev->GetInteger(1);
    inflictor = ev->GetEntity(2);
    attacker  = ev->GetEntity(3);

    if (spawnflags & ACCUMULATIVE) {
        health -= damage;
        if (health > 0) {
            return;
        }
    } else {
        if (damage < health) {
            return;
        }
    }

    if (explosions) {
        pos[0] = absmin[0] + G_Random(size[0]);
        pos[1] = absmin[1] + G_Random(size[1]);
        pos[2] = absmin[2] + G_Random(size[2]);

        CreateExplosion(pos, damage, this, attacker, this);
    }
    takedamage = DAMAGE_NO;
    hideModel();
    BroadcastAIEvent();
    PostEvent(EV_Remove, 0);
}

void ExplodingWall::SetupSecondStage(void)

{
    health     = max_health;
    takedamage = DAMAGE_YES;
}

void ExplodingWall::StopRotating(Event *ev)

{
    avelocity = vec_zero;
    setAngles(land_angles);
    if (spawnflags & TWOSTAGE) {
        SetupSecondStage();
    }
}

void ExplodingWall::CheckOnGround(Event *ev)

{
    if ((velocity == vec_zero) && groundentity) {
        Vector delta;
        float  most;
        float  time;
        int    t;

        delta = land_angles - angles;
        if (delta.length() > 1) {
            if (delta[0] > 180) {
                delta[0] -= 360;
            }
            if (delta[0] < -180) {
                delta[0] += 360;
            }
            if (delta[1] > 180) {
                delta[1] -= 360;
            }
            if (delta[1] < -180) {
                delta[1] += 360;
            }
            if (delta[2] > 180) {
                delta[2] -= 360;
            }
            if (delta[2] < -180) {
                delta[2] += 360;
            }
            most = MaxValue(delta);
            if (angle_speed > 3) {
                t = 10.0f * most / (angle_speed / 3);
            } else {
                t = 10.0f * most;
            }
            time      = (float)t / 10;
            delta     = delta * (1.0 / time);
            avelocity = delta;
            PostEvent(EV_ExplodingWall_StopRotating, time);
        }
        state = 2;
        setSize(orig_mins, orig_maxs);
        on_ground = true;
    } else {
        PostEvent(ev, FRAMETIME);
    }
}

void ExplodingWall::TouchFunc(Event *ev)

{
    Entity *other;

    if ((velocity == vec_zero) || (level.time < attack_finished)) {
        return;
    }

    other = ev->GetEntity(1);

    if ((spawnflags & LANDSHATTER) && (other == world)) {
        Vector pos;

        takedamage = DAMAGE_NO;

        if (explosions) {
            pos[0] = absmin[0] + G_Random(size[0]);
            pos[1] = absmin[1] + G_Random(size[1]);
            pos[2] = absmin[2] + G_Random(size[2]);

            CreateExplosion(pos, dmg, this, other, this);
        }
        hideModel();
        BroadcastAIEvent();
        PostEvent(EV_Remove, 0);
        return;
    }

    if (other->takedamage) {
        other->Damage(this, activator, dmg, origin, vec_zero, vec_zero, 20, 0, MOD_EXPLODEWALL);
        Sound("debris_generic", CHAN_WEAPON);
        attack_finished = level.time + FRAMETIME;
    }
}

void ExplodingWall::Setup(Event *ev)

{
    if (spawnflags & INVISIBLE) {
        if (Targeted()) {
            takedamage = DAMAGE_YES;
        } else {
            takedamage = DAMAGE_NO;
        }
        hideModel();
        setSolidType(SOLID_NOT);
    } else {
        showModel();
        setSolidType(SOLID_BSP);
        takedamage = DAMAGE_YES;
    }

    setMoveType(MOVETYPE_PUSH);
    setOrigin();
}

ExplodingWall::ExplodingWall()
{
    if (LoadingSavegame) {
        return;
    }

    health     = 60;
    max_health = health;
    on_ground  = false;

    state       = 0;
    angle_speed = (spawnflags & RANDOMANGLES) ? 600 : 100;
    land_radius = 16;
    dmg         = 10;
    explosions  = 1;

    base_velocity   = Vector(0, 0, 280);
    random_velocity = Vector(140, 140, 140);

    orig_mins = mins;
    orig_maxs = maxs;

    respondto = spawnflags ^ TRIGGER_PLAYERS;

    PostEvent(EV_ExplodingWall_Setup, EV_POSTSPAWN);
}

/*****************************************************************************/
/*QUAKED trigger_teleport (0.5 0.5 0.5) ? VISIBLE x NOT_PLAYERS NOT_MONSTERS NOT_PROJECTILES NO_EFFECTS

Touching this entity will teleport players to the targeted object.

"key"						The item needed to activate this. (default nothing)

"teleportthread"		The thread that is run when the player is teleported

If NOT_PLAYERS is set, the teleporter does not teleport players
If NOT_MONSTERS is set, the teleporter does not teleport monsters
If NOT_PROJECTILES is set, the teleporter does not teleport projectiles (rockets, grenades, etc.)
If NO_EFFECTS is set, the special effect will not happen and the teleport will be instant

******************************************************************************/

#define NO_EFFECTS (1 << 5)

Event EV_Teleporter_Teleport
(
    "teleport",
    EV_CODEONLY,
    "e",
    "entity",
    "Teleports the entity to destination.",
    EV_NORMAL
);
Event EV_Teleporter_StopTeleport
(
    "stopteleport",
    EV_CODEONLY,
    "e",
    "entity",
    "Releases the entity at the end of the teleport.",
    EV_NORMAL
);
Event EV_Teleporter_SetThread
(
    "teleportthread",
    EV_CODEONLY,
    "s",
    "thread_name",
    "Sets the thread to run when the player is teleported.",
    EV_NORMAL
);

CLASS_DECLARATION(Trigger, Teleporter, "trigger_teleport") {
    {&EV_Trigger_Effect,          &Teleporter::StartTeleport},
    {&EV_Teleporter_Teleport,     &Teleporter::Teleport     },
    {&EV_Teleporter_StopTeleport, &Teleporter::StopTeleport },
    {&EV_Teleporter_SetThread,    &Teleporter::SetThread    },
    {NULL,                        NULL                      }
};

void Teleporter::SetThread(Event *ev)
{
    teleport_label.SetThread(ev->GetValue(1));
}

void Teleporter::StartTeleport(Event *ev)
{
    Animate *fx;
    Entity  *other;
    Event   *event;
    qboolean is_sentient;
    Vector   new_position;

    if (in_use) {
        return;
    }

    in_use = true;

    other = ev->GetEntity(1);

    if (!other) {
        return;
    }

    if (spawnflags & NO_EFFECTS) {
        event = new Event(EV_Teleporter_Teleport);
        event->AddEntity(other);
        ProcessEvent(event);
        return;
    }

    if (other->isSubclassOf(Sentient)) {
        is_sentient = true;
    } else {
        is_sentient = false;
    }

    if (is_sentient) {
        new_position = origin;
        new_position.z += mins.z;
        other->setOrigin(new_position);
        other->NoLerpThisFrame();
    }

    // Create the teleport special effect

    fx = new Animate;
    fx->setOrigin(other->origin);

    fx->NewAnim("idle", EV_Remove);

    if (is_sentient) {
        fx->setModel("fx_bigteleport.tik");
        //fx->Sound( "sound/environment/electric/singles/dimming.wav" );
    } else {
        fx->setModel("fx_teleport2.tik");
    }

    if (is_sentient) {
        // Freeze the entity that went into the teleporter

        other->flags |= FL_IMMOBILE;
        other->takedamage = DAMAGE_NO;
    }

    // Make the entity teleport

    event = new Event(EV_Teleporter_Teleport);
    event->AddEntity(other);

    if (is_sentient) {
        PostEvent(event, 4);
    } else {
        PostEvent(event, 0);
    }

    if (is_sentient) {
        other->PostEvent(EV_CODEONLY, 2);

        /* if ( !( spawnflags & NO_EFFECTS ) )
			{
			event = new Event( EV_Sound );
			event->AddString( "snd_teleport" );
			other->PostEvent( event, 2 );
			} */
    }
}

void Teleporter::Teleport(Event *ev)

{
    Entity  *dest;
    int      i;
    Entity  *other;
    Vector   mid;
    Animate *fx;
    Event   *event;

    other = ev->GetEntity(1);

    if (!other || (other == world)) {
        return;
    }

    dest = (Entity *)G_FindTarget(NULL, Target());
    if (!dest) {
        warning("Teleport", "Couldn't find destination\n");
        return;
    }

    assert(dest);

    // unlink to make sure it can't possibly interfere with KillBox
    other->unlink();

    if (other->isSubclassOf(Sentient)) {
        other->origin   = dest->origin + Vector(0, 0, 1);
        other->velocity = vec_zero;
    } else {
        mid           = (absmax - absmin) * 0.5;
        other->origin = dest->origin + Vector(0, 0, 1);
        other->origin += mid;
    }

    // set angles
    other->setAngles(dest->angles);

    if (other->client) {
        client = other->client;

        // clear the velocity and hold them in place briefly
        client->ps.pm_time = 100;
        client->ps.pm_flags |= PMF_RESPAWNED;

        // cut the camera on the client
        ((Player *)other)->CameraCut();

        for (i = 0; i < 3; i++) {
            client->ps.delta_angles[i] = ANGLE2SHORT(dest->angles[i] - client->cmd_angles[i]);
        }

        VectorCopy(angles, client->ps.viewangles);
    }

    if (dest->isSubclassOf(TeleporterDestination)) {
        float len;

        len = other->velocity.length();
        //
        // give them a bit of a push
        //
        if (len < 400) {
            len = 400;
        }
        other->velocity = ((TeleporterDestination *)dest)->movedir * len;
    }

    // kill anything at the destination
    KillBox(other);

    other->setOrigin(other->origin);
    other->origin.copyTo(other->edict->s.origin2);

    // Run the teleport thread if set

    if (teleport_label.IsSet() && other->isSubclassOf(Player)) {
        teleport_label.Execute(this);
    }

    // Skip effects if no_effects set

    if (spawnflags & NO_EFFECTS) {
        Event *event = new Event(EV_Teleporter_StopTeleport);
        event->AddEntity(other);
        ProcessEvent(event);
        return;
    }

    // Spawn in effect

    fx = new Animate;

    fx->setOrigin(other->origin);

    fx->NewAnim("idle", EV_Remove);

    if (other->isSubclassOf(Sentient)) {
        fx->setModel("fx_bigteleport.tik");
    } else {
        fx->setModel("fx_teleport2.tik");
    }

    event = new Event(EV_Teleporter_StopTeleport);
    event->AddEntity(other);

    if (other->isSubclassOf(Sentient)) {
        PostEvent(event, 1.75);
    } else {
        PostEvent(event, FRAMETIME);
    }
}

void Teleporter::StopTeleport(Event *ev)

{
    Entity *other;

    other = ev->GetEntity(1);

    if (other->isSubclassOf(Sentient)) {
        other->flags &= ~FL_IMMOBILE;
        other->takedamage = DAMAGE_AIM;
        other->showModel();
    }

    //if ( !( spawnflags & NO_EFFECTS ) )
    //	other->Sound( "snd_teleport" );

    in_use = false;
}

Teleporter::Teleporter()
{
    AddWaitTill(STRING_TELEPORT);

    if (LoadingSavegame) {
        return;
    }

    if (spawnflags & 1) {
        PostEvent(EV_Show, EV_POSTSPAWN);
    }

    //respondto = spawnflags ^ ( TRIGGER_PLAYERS | TRIGGER_MONSTERS | TRIGGER_PROJECTILES );
    respondto = spawnflags ^ (TRIGGER_PLAYERS);

    in_use = false;

    // Cache all needed stuff

    if (!(spawnflags & NO_EFFECTS)) {
        CacheResource("models/fx_bigteleport.tik");
        //CacheResource( "sound/environment/electric/singles/dimming.wav", this );
        CacheResource("fx_teleport2.tik");
        //CacheResource( "snd_teleport", this );
    }
}

/*****************************************************************************/
/*QUAKED func_teleportdest (0 0.25 0.5) (-32 -32 0) (32 32 8)

Point trigger_teleport at these.

******************************************************************************/

CLASS_DECLARATION(Entity, TeleporterDestination, "func_teleportdest") {
    {&EV_SetAngle, &TeleporterDestination::SetMoveDir},
    {NULL,         NULL                              }
};

TeleporterDestination::TeleporterDestination()
{
    movedir = G_GetMovedir(0);
}

void TeleporterDestination::SetMoveDir(Event *ev)
{
    float angle;

    angle   = ev->GetFloat(1);
    movedir = G_GetMovedir(angle);
    setAngles(movedir.toAngles());
}

/*****************************************************************************/
/*QUAKED func_useanim (0 0.5 0) ? VISIBLE TOUCHABLE CONTINUOUS

This object allows you to place the player into a specific animation for the
purposes of using an object within the world->

This object should point at a func_useanimdest which contains specific
information about how the player is supposed to be posed.

"count" - how many times this should trigger (default -1, infinite)
"thread" - thread to fire when used
"triggertarget" - what to trigger when used.
"delay" - how long it takes to be re-triggered ( default 3 seconds )
"key" - item needed to activate this

VISIBLE - if this is checked the trigger itself will be visible
TOUCHABLE - if this is set we can activate the trigger by standing in it.
CONTINUOUS - if this is checked the thing will re-trigger continously, otherwise
it waits until the player has left the trigger field.

******************************************************************************/

Event EV_UseAnim_Reset
(
    "_reset",
    EV_CODEONLY,
    NULL,
    NULL,
    "Reset's the Use Anim after it has no longer been touched.",
    EV_NORMAL
);
Event EV_UseAnim_Thread
(
    "setthread",
    EV_DEFAULT,
    "s",
    "label",
    "Sets which thread to use when this UseAnim is triggered.",
    EV_NORMAL
);
Event EV_UseAnim_Count
(
    "count",
    EV_DEFAULT,
    "i",
    "newCount",
    "Sets how many times the UseAnim can be triggered.",
    EV_NORMAL
);
Event EV_UseAnim_TriggerTarget
(
    "triggertarget",
    EV_DEFAULT,
    "s",
    "targetname",
    "Sets what should be triggered, when this UseAnim is triggered.",
    EV_NORMAL
);
Event EV_UseAnim_SetAnim
(
    "anim",
    EV_DEFAULT,
    "s",
    "animName",
    "set the animation to use for player.",
    EV_NORMAL
);
Event EV_UseAnim_SetKey
(
    "key",
    EV_DEFAULT,
    "s",
    "keyName",
    "set the key needed to make this UseAnim function.",
    EV_NORMAL
);
Event EV_UseAnim_SetState
(
    "state",
    EV_CHEAT,
    "s",
    "stateName",
    "set the state to use for the player.",
    EV_NORMAL
);
Event EV_UseAnim_SetCamera
(
    "camera",
    EV_DEFAULT,
    "s",
    "cameraPosition",
    "set the camera to use when in this animation.\n"
    "topdown, behind, front, side, behind_fixed, side_left, side_right",
    EV_NORMAL
);
Event EV_UseAnim_SetNumLoops
(
    "num_loops",
    EV_DEFAULT,
    "i",
    "loopCount",
    "set the number of times to loop an animation per use.",
    EV_NORMAL
);
Event EV_UseAnim_SetDelay
(
    "delay",
    EV_DEFAULT,
    "f",
    "delayTime",
    "how long it takes for the UseAnim to be retriggered once you leave it.",
    EV_NORMAL
);

CLASS_DECLARATION(Entity, UseAnim, "func_useanim") {
    {&EV_Use,                   NULL                      },
    {&EV_Touch,                 &UseAnim::Touched         },
    {&EV_UseAnim_Reset,         &UseAnim::Reset           },
    {&EV_UseAnim_Thread,        &UseAnim::SetThread       },
    {&EV_UseAnim_TriggerTarget, &UseAnim::SetTriggerTarget},
    {&EV_UseAnim_Count,         &UseAnim::SetCount        },
    {&EV_UseAnim_SetAnim,       &UseAnim::SetAnim         },
    {&EV_UseAnim_SetState,      &UseAnim::SetState        },
    {&EV_UseAnim_SetKey,        &UseAnim::SetKey          },
    {&EV_UseAnim_SetNumLoops,   &UseAnim::SetNumLoops     },
    {&EV_UseAnim_SetCamera,     &UseAnim::SetCamera       },
    {NULL,                      NULL                      }
};

UseAnim::UseAnim()
{
    AddWaitTill(STRING_TRIGGER);
    if (LoadingSavegame) {
        return;
    }

    setMoveType(MOVETYPE_NONE);

    anim      = "stand_use";
    num_loops = 1;
    hideModel();

    //
    // make it not solid unless we want it touchable
    //
    if (!(spawnflags & 2)) {
        setSolidType(SOLID_BBOX);
        setContents(CONTENTS_BODY);
    } else {
        setSolidType(SOLID_TRIGGER);
        edict->r.svFlags |= SVF_NOCLIENT;
    }

    // by default this can activated infinitely
    count = -1;
    // clear out the triggertarget
    triggertarget = "";
    // clear out the thread
    thread.Set("");
    // set the default delay
    delay = 3;
    // initialize the last time the door was triggered
    last_active_time = -delay;
    // initially its not active
    active = 0;
    // set the default camera to be side view
    camera = "behind";
    //
    // only make it visible if so desired
    //
    if (spawnflags & 1) {
        PostEvent(EV_Show, EV_POSTSPAWN);
    }
}

void UseAnim::Touched(Event *ev)

{
    Entity *other;

    if (active && (!(spawnflags & 4))) {
        CancelEventsOfType(EV_UseAnim_Reset);
        PostEvent(EV_UseAnim_Reset, 0.25f);
        return;
    }

    // don't retrigger to soon
    if (level.time < last_active_time) {
        return;
    }

    other = ev->GetEntity(1);
    if (other->isSubclassOf(Player)) {
        ((Player *)other)->TouchedUseAnim(this);
    }
}

bool UseAnim::canBeUsed(Entity *activator)

{
    Entity *dest;

    // if this is no longer usable, return false
    if (!count) {
        return false;
    }

    // don't retrigger to soon
    if (level.time < last_active_time) {
        return false;
    }

    if (key.length()) {
        if (!activator->isSubclassOf(Sentient)) {
            return false;
        }
        if (!(((Sentient *)activator)->HasItem(key.c_str()))) {
            qboolean  setModel;
            Item     *item;
            ClassDef *cls;
            str       dialog;

            cls = FindClass(key.c_str(), &setModel);
            if (!cls || !checkInheritance("Item", cls->classname)) {
                gi.DPrintf("No item named '%s'\n", key.c_str());
                return true;
            }
            item = (Item *)cls->newInstance();
            if (setModel) {
                item->setModel(key.c_str());
            }
            item->CancelEventsOfType(EV_Item_DropToFloor);
            item->CancelEventsOfType(EV_Remove);
            item->ProcessPendingEvents();
            dialog = item->GetDialogNeeded();
            if (dialog.length() > 0) {
                activator->Sound(dialog);
            } else {
                gi.centerprintf(activator->edict, "You need the %s", item->getName().c_str());
            }
            delete item;

            // don't retrigger for 5 seconds
            last_active_time = level.time + 5;
            return false;
        } else {
            return qtrue;
        }
    }

    if (isSubclassOf(TouchAnim)) {
        return true;
    }

    dest = (Entity *)G_FindTarget(NULL, Target());
    if (!dest || !dest->isSubclassOf(UseAnimDestination)) {
        warning("UseAnim", "Couldn't find destination\n");
        return false;
    }

    return true;
}

bool UseAnim::GetInformation(
    Entity *activator, Vector *org, Vector *angles, str *animation, int *loopcount, str *state, str *camera
)

{
    Entity             *dest;
    UseAnimDestination *uadest;

    // if this is no longer usable, return false
    if (!count) {
        return false;
    }

    dest = (Entity *)G_FindTarget(NULL, Target());

    if (!dest || !dest->isSubclassOf(UseAnimDestination)) {
        // grab the information from this entity instead
        // set the destination origin
        *org = origin;
        // set the destination angles
        *angles = this->angles;
        // set the desination animation
        *animation = anim;
        // set the number of loops
        *loopcount = num_loops;
        // get the state if necessary
        *state = this->state;
        // set the camera
        *camera = this->camera;
    } else {
        uadest = (UseAnimDestination *)dest;

        // set the destination origin
        *org = uadest->origin;
        // set the destination angles
        *angles = uadest->angles;
        // set the desination animation
        *animation = uadest->GetAnim();
        // set the number of loops
        *loopcount = uadest->GetNumLoops();
        // get the state if necessary
        *state = uadest->GetState();
        // set the camera
        *camera = this->camera;
    }

    // make this guy active
    active = qtrue;

    // if this is a TouchAnim see if it is linked to another TouchAnim
    if (isSubclassOf(TouchAnim)) {
        dest = NULL;
        do {
            dest = (Entity *)G_FindTarget(dest, Target());
            if (dest) {
                if (dest->isSubclassOf(UseAnim)) {
                    // make our linked UseAnim's active as well
                    ((UseAnim *)dest)->active = qtrue;
                }
            } else {
                break;
            }
        } while (1);
    }
    //
    // decrement the use
    //
    if (count > 0) {
        count--;
    }

    return true;
}

void UseAnim::TriggerTargets(Entity *activator)

{
    //
    // fire off our trigger target if appropriate
    //
    if (triggertarget.length()) {
        Event  *event;
        Entity *ent;

        ent = NULL;
        do {
            ent = (Entity *)G_FindTarget(ent, triggertarget.c_str());
            if (!ent) {
                break;
            }
            event = new Event(EV_Activate);
            event->AddEntity(activator);
            ent->PostEvent(event, 0);
        } while (1);
    }

    //
    // fire off a thread if necessary
    //
    if (thread.IsSet()) {
        thread.Execute(this);
    }
}

void UseAnim::Reset(Event *ev)

{
    //
    // find out if our triggertarget is of type door and only reset if the door is closed
    //
    if (triggertarget.length()) {
        Entity *ent;

        ent = NULL;
        do {
            ent = (Entity *)G_FindTarget(ent, triggertarget.c_str());
            if (!ent) {
                break;
            }
            if (ent->isSubclassOf(Door)) {
                if (!((Door *)ent)->isCompletelyClosed()) {
                    CancelEventsOfType(EV_UseAnim_Reset);
                    PostEvent(EV_UseAnim_Reset, 0.25f);
                    //
                    // wait for a little bit
                    //
                    return;
                }
            }
        } while (1);
    }

    active           = qfalse;
    last_active_time = level.time + delay;

    // if this is a TouchAnim see if it is linked to another TouchAnim
    if (isSubclassOf(TouchAnim)) {
        Entity *dest;

        dest = NULL;
        do {
            dest = (Entity *)G_FindTarget(dest, Target());
            if (dest) {
                if (dest->isSubclassOf(UseAnim)) {
                    // make our linked UseAnim's reset as well
                    ((UseAnim *)dest)->active           = qfalse;
                    ((UseAnim *)dest)->last_active_time = level.time + delay;
                }
            } else {
                break;
            }
        } while (1);
    }
}

void UseAnim::SetThread(Event *ev)

{
    thread.SetThread(ev->GetValue(1));
}

void UseAnim::SetDelay(Event *ev)

{
    delay = ev->GetFloat(1);
}

void UseAnim::SetTriggerTarget(Event *ev)

{
    triggertarget = ev->GetString(1);
}

void UseAnim::SetCount(Event *ev)

{
    count = ev->GetInteger(1);
}

void UseAnim::SetAnim(Event *ev)
{
    anim = ev->GetString(1);
}

void UseAnim::SetState(Event *ev)
{
    state = ev->GetString(1);
}

void UseAnim::SetKey(Event *ev)
{
    key = ev->GetString(1);
}

void UseAnim::SetCamera(Event *ev)
{
    camera = ev->GetString(1);
}

void UseAnim::SetNumLoops(Event *ev)
{
    num_loops = ev->GetInteger(1);
}

CLASS_DECLARATION(UseAnim, TouchAnim, "func_touchanim") {
    {NULL, NULL}
};

TouchAnim::TouchAnim()
{
    if (LoadingSavegame) {
        return;
    }

    spawnflags |= 2;

    if (spawnflags & 8) {
        setSize(Vector("-32 -32 0"), Vector("32 32 96"));
    } else {
        setSize(Vector("-16 -16 0"), Vector("16 16 96"));
    }

    setSolidType(SOLID_TRIGGER);
    edict->r.svFlags |= SVF_NOCLIENT;
}

/*****************************************************************************/
/*       func_useanimdest (0 0.25 0.5) (0 0 0) (0 0 0)

Point func_useanim's at these.

The player will be lerped to this position and this orientation
and placed into the specified animation

"anim" specifies the animation that the player should be in.
"state" instead of an animation, sets a state the player should go into
"camera" camera position to use when player is in animation
"num_loops" number of animation loops to play.


******************************************************************************/

CLASS_DECLARATION(Entity, UseAnimDestination, "func_useanimdest") {
    {&EV_UseAnim_SetAnim,     &UseAnimDestination::SetAnim    },
    {&EV_UseAnim_SetState,    &UseAnimDestination::SetState   },
    {&EV_UseAnim_SetNumLoops, &UseAnimDestination::SetNumLoops},
    {NULL,                    NULL                            }
};

UseAnimDestination::UseAnimDestination()
{
    if (LoadingSavegame) {
        // Archive function will setup all necessary data
        return;
    }
    //
    // default animation to use
    //
    anim      = "stand_use";
    num_loops = 1;
    setSolidType(SOLID_NOT);
    hideModel();
}

void UseAnimDestination::SetAnim(Event *ev)
{
    anim = ev->GetString(1);
}

void UseAnimDestination::SetState(Event *ev)
{
    state = ev->GetString(1);
}

str UseAnimDestination::GetAnim(void)
{
    return anim;
}

str UseAnimDestination::GetState(void)
{
    return state;
}

void UseAnimDestination::SetNumLoops(Event *ev)
{
    num_loops = ev->GetInteger(1);
}

int UseAnimDestination::GetNumLoops(void)
{
    return num_loops;
}

Event EV_UseObject_MoveThread
(
    "move_thread",
    EV_DEFAULT,
    "s",
    "label",
    "Sets which move thread to use when this UseObject has finshed looping.",
    EV_NORMAL
);
Event EV_UseObject_StopThread
(
    "stop_thread",
    EV_DEFAULT,
    "s",
    "label",
    "Sets which stop thread to use when this UseObject is finished.",
    EV_NORMAL
);
Event EV_UseObject_ResetThread
(
    "reset_thread",
    EV_DEFAULT,
    "s",
    "label",
    "Sets which thread to call when resetting.",
    EV_NORMAL
);
Event EV_UseObject_Count
(
    "count",
    EV_DEFAULT,
    "i",
    "newCount",
    "Sets how many times the UseObject can be triggered.",
    EV_NORMAL
);
Event EV_UseObject_Cone
(
    "cone",
    EV_DEFAULT,
    "f",
    "newCone",
    "Sets the cone in angles of where the Useobject can be used.",
    EV_NORMAL
);
Event EV_UseObject_Offset
(
    "offset",
    EV_DEFAULT,
    "v",
    "newOffset",
    "Sets the offset to use for this UseObject.",
    EV_NORMAL
);
Event EV_UseObject_YawOffset
(
    "yaw_offset",
    EV_DEFAULT,
    "f",
    "newYawOffset",
    "Sets the yaw offset to use for this UseObject.",
    EV_NORMAL
);
Event EV_UseObject_State
(
    "state",
    EV_DEFAULT,
    "s",
    "newState",
    "Sets the state to use for this UseObject.",
    EV_NORMAL
);
Event EV_UseObject_StateBackwards
(
    "state_backwards",
    EV_DEFAULT,
    "s",
    "newState",
    "Sets the backward state to use for this UseObject.",
    EV_NORMAL
);
Event EV_UseObject_TriggerTarget
(
    "triggertarget",
    EV_DEFAULT,
    "s",
    "targetname",
    "Sets what should be triggered, when this UseObject is triggered.",
    EV_NORMAL
);
Event EV_UseObject_ResetTime
(
    "reset_time",
    EV_DEFAULT,
    "f",
    "newResetTime",
    "Sets the time it takes for the UseObject to reset itself.",
    EV_NORMAL
);
Event EV_UseObject_DamageType
(
    "damage_type",
    EV_DEFAULT,
    "s",
    "newDamageType",
    "Sets what kind of damage is needed to activate the trigger.",
    EV_NORMAL
);
Event EV_UseObject_Reset
(
    "_useobject_reset",
    EV_DEFAULT,
    NULL,
    NULL,
    "Resets the useobject to the start state after a certain amount of time.",
    EV_NORMAL
);
Event EV_UseObject_Resetting
(
    "_useobject_resetting",
    EV_DEFAULT,
    NULL,
    NULL,
    "Intermediate function for useobject reset.",
    EV_NORMAL
);
Event EV_UseObject_DamageTriggered
(
    "_useobject_damagetriggered",
    EV_DEFAULT,
    "e",
    "activator",
    "Intermediate function for when the useobject was triggered by damage.",
    EV_NORMAL
);
Event EV_UseObject_Activate
(
    "activate",
    EV_DEFAULT,
    NULL,
    NULL,
    "Allow the useobject to be used.",
    EV_NORMAL
);
Event EV_UseObject_Deactivate
(
    "deactivate",
    EV_DEFAULT,
    NULL,
    NULL,
    "Do not allow the useobject to be used.",
    EV_NORMAL
);
Event EV_UseObject_UseMaterial
(
    "usematerial",
    EV_DEFAULT,
    "s",
    "nameOfUseMaterial",
    "the name of the material that glows when active.",
    EV_NORMAL
);
Event EV_UseObject_SetActiveState
(
    "_setactivestate",
    EV_DEFAULT,
    NULL,
    NULL,
    "event that sets up the proper skin for the useobject.",
    EV_NORMAL
);

#define MULTI_STATE (1 << 0)

/*****************************************************************************/
/*QUAKED func_useobject (0 0.5 0) ? MULTI-STATE

Allows you to setup a special object that places the player into a specific state
sequence.  Primarily used for levers and cranks.  

Object starts out in the "start" animation, when used the following occurs:

It is determined whether or not the player is in the right position to activate
the object, if it is, the player is moved to the exact offset and angle specified
by "offset" and "yaw_offset".  The right position is determined by a dot product
with "offset" and "yaw_offset".  The "cone" parameter controls the cone in which the
object can be triggered. Once the player is in the right position, the player is placed
into "state" and the "move" animation is played. Once the player animation ends, the
"move_thread" will be called.  If the use button is continued to be held down and count
is not finite, the animation will be continued to be played until the use key is held
down.  Once the use key is let go, the "stop" animation will be played on the lever and
the "stop_thread" will be called.

"activate" - turns the useobject on
"deactivate" - turns the useobject off
"offset" - vector offset of where the player should stand
"state" - state to go into when used
"state_backwards" - what state to use when reversing the object
"yaw_offset" - what direction the player should be facing when using the object
"cone" - the cone in which the object can be used
"count" - how many times this should trigger (default -1, infinite)
"move_thread" - thread that is fired when the object has cycled one animation
"stop_thread" - thread that is fired when the object has finished animating
"reset_thread" - thread that is fired when the object is resetting itself
"reset_time" - the time it takes for the object to reset, (default 0, it doesn't)
"triggertarget" - target to trigger when finished animating, if reset_time is set, target
will be fired again when resetting
"damage_type" - if set, can be triggered by using a weapon to activate it.  If set to "all",
any damage will activate it.

MULTI-STATE - the object has two different states and must be used each time to set the state
when multi state is active, the reset_thread is called instead of stop_thread.  All UseObjects
have two states on and off.  When reset_time is set, the object will automatically return to the 
off state after a preset amount of time.  When multi-state is set this must be done manually.

******************************************************************************/

CLASS_DECLARATION(Animate, UseObject, "func_useobject") {
    {&EV_Use,                       NULL                         },
    {&EV_UseObject_MoveThread,      &UseObject::SetMoveThread    },
    {&EV_UseObject_StopThread,      &UseObject::SetStopThread    },
    {&EV_UseObject_ResetThread,     &UseObject::SetResetThread   },
    {&EV_UseObject_TriggerTarget,   &UseObject::SetTriggerTarget },
    {&EV_UseObject_Offset,          &UseObject::SetOffset        },
    {&EV_UseObject_YawOffset,       &UseObject::SetYawOffset     },
    {&EV_UseObject_Count,           &UseObject::SetCount         },
    {&EV_UseObject_Cone,            &UseObject::SetCone          },
    {&EV_UseObject_State,           &UseObject::SetState         },
    {&EV_UseObject_StateBackwards,  &UseObject::SetBackwardsState},
    {&EV_UseObject_ResetTime,       &UseObject::SetResetTime     },
    {&EV_UseObject_Reset,           &UseObject::Reset            },
    {&EV_UseObject_DamageType,      &UseObject::DamageType       },
    {&EV_UseObject_Resetting,       &UseObject::Resetting        },
    {&EV_UseObject_DamageTriggered, &UseObject::DamageTriggered  },
    {&EV_Damage,                    &UseObject::DamageFunc       },
    {&EV_UseObject_Activate,        &UseObject::ActivateEvent    },
    {&EV_UseObject_Deactivate,      &UseObject::DeactivateEvent  },
    {&EV_UseObject_UseMaterial,     &UseObject::UseMaterialEvent },
    {&EV_UseObject_SetActiveState,  &UseObject::SetActiveState   },
    {NULL,                          NULL                         }
};

UseObject::UseObject()
{
    Event *e;

    AddWaitTill(STRING_RESET);
    AddWaitTill(STRING_MOVE);
    AddWaitTill(STRING_STOP);

    if (LoadingSavegame) {
        // Archive function will setup all necessary data
        return;
    }

    setMoveType(MOVETYPE_NONE);
    setSolidType(SOLID_BBOX);
    setContents(CONTENTS_BODY);

    // make sure the bounds get rotated with the object
    flags |= FL_ROTATEDBOUNDS;
    // by default this can activated infinitely
    count = -1;
    // clear out the triggertarget
    triggertarget = "";
    // clear out the move thread
    move_thread.Set("");
    // clear out the stop thread
    stop_thread.Set("");
    // clear out the reset thread
    reset_thread.Set("");
    // clear out the damage type, by default useobjects do not respond to damage
    damage_type = MOD_NONE;
    // turn on damage taking ability
    takedamage = DAMAGE_YES;
    // give it some health
    health = 100;
    // set the default yaw offset
    yaw_offset = 0;
    // set the cone
    cone = cos(DEG2RAD(90));
    // set the state
    state = "";
    // set the backwards state
    state_backwards = "";
    // clear out the reset_time
    reset_time = 0;
    // clear out the object state
    objectState = 0;
    // the useobject is active by default
    active = qtrue;

    // start off in the start animation
    e = new Event(EV_SetAnim);
    e->AddString("start");
    PostEvent(e, 0);
    PostEvent(EV_Show, 0);
    // setup our skins once we are spawned
    PostEvent(EV_UseObject_SetActiveState, 0);
}

void UseObject::SetActiveState(Event *ev)
{
    if (!useMaterial.length()) {
        return;
    }

    if (active && count) {
        SurfaceCommand(useMaterial.c_str(), "+skin1");
    } else {
        SurfaceCommand(useMaterial.c_str(), "-skin1");
    }
    if (objectState) {
        SurfaceCommand(useMaterial.c_str(), "+skin2");
    } else {
        SurfaceCommand(useMaterial.c_str(), "-skin2");
    }
}

void UseObject::SetMoveThread(Event *ev)

{
    move_thread.SetThread(ev->GetValue(1));
}

void UseObject::SetStopThread(Event *ev)

{
    stop_thread.SetThread(ev->GetValue(1));
}

void UseObject::SetResetThread(Event *ev)

{
    reset_thread.SetThread(ev->GetValue(1));
}

void UseObject::ActivateEvent(Event *ev)

{
    active = qtrue;
    PostEvent(EV_UseObject_SetActiveState, 0);
}

void UseObject::DeactivateEvent(Event *ev)

{
    active = qfalse;
    PostEvent(EV_UseObject_SetActiveState, 0);
}

void UseObject::SetTriggerTarget(Event *ev)

{
    triggertarget = ev->GetString(1);
}

void UseObject::SetOffset(Event *ev)

{
    offset = ev->GetVector(1);
}

void UseObject::SetYawOffset(Event *ev)

{
    yaw_offset = ev->GetFloat(1);
}

void UseObject::SetCount(Event *ev)

{
    count = ev->GetInteger(1);
}

void UseObject::SetCone(Event *ev)

{
    cone = cos(DEG2RAD(ev->GetFloat(1)));
}

void UseObject::SetState(Event *ev)

{
    state = ev->GetString(1);
}

void UseObject::SetBackwardsState(Event *ev)

{
    state_backwards = ev->GetString(1);
}

void UseObject::UseMaterialEvent(Event *ev)

{
    useMaterial = ev->GetString(1);
}

void UseObject::SetResetTime(Event *ev)

{
    reset_time = ev->GetFloat(1);
}

void UseObject::Reset(Event *ev)
{
    NewAnim("move_backward", EV_UseObject_Resetting);
}

void UseObject::Resetting(Event *ev)

{
    SetActiveState(NULL);
    NewAnim("start");

    // reset the count
    count = 1;

    //
    // fire off our trigger target if appropriate
    //
    if (triggertarget.length()) {
        Event  *event;
        Entity *ent;

        ent = NULL;
        do {
            ent = (Entity *)G_FindTarget(ent, triggertarget.c_str());
            if (!ent) {
                break;
            }
            event = new Event(EV_Activate);
            event->AddEntity(this);
            ent->PostEvent(event, 0);
        } while (1);
    }

    //
    // fire off a thread if necessary
    //
    if (reset_thread.IsSet()) {
        reset_thread.Execute(this);
    }
}

bool UseObject::canBeUsed(Vector org, Vector dir)

{
    float  dot;
    Vector forward;
    Vector diff;
    Vector ang;

    // see if it is active
    if (!active) {
        return false;
    }

    // if this is no longer usable, return false
    if (!count) {
        return false;
    }

    // convert our yawoffset to a vector
    ang      = vec_zero;
    ang[YAW] = angles[YAW] + yaw_offset;
    ang.AngleVectors(&forward);
    dot = forward * dir;
    if (dot < cone) {
        return false;
    }

    /*
   // convert our offset to a vector in worldspace
   forward = getLocalVector( offset );
   forward.normalize();
   diff = org - origin;
//   diff = origin - org;
   diff.normalize();
   dot = forward * dir;
   if ( dot < cone )
      {
      return false;
      }
*/

    return true;
}

void UseObject::DamageFunc(Event *ev)

{
    Event  *e;
    Entity *attacker;
    int     mod;

    // if this is no longer usable, return false
    if (!count) {
        return;
    }

    // what kind of damage hit us
    mod = ev->GetInteger(9);

    // if we don't respond to any kind of damage, and our damage types do not match, return
    if (!MOD_matches(mod, damage_type)) {
        return;
    }

    // get the attacker
    attacker = ev->GetEntity(1);

    //
    // decrement the use
    //
    if (count > 0) {
        count--;
    }
    // setup our damage triggered event
    e = new Event(EV_UseObject_DamageTriggered);
    // add our attacker
    e->AddEntity(attacker);
    // start up the object with our special event
    Start(e);
}

void UseObject::DamageTriggered(Event *ev)

{
    // grab the attacker from our event
    Stop(ev->GetEntity(1));
}

void UseObject::Setup(Entity *activator, Vector *org, Vector *ang, str *newstate)

{
    if ((spawnflags & MULTI_STATE) && objectState) {
        *newstate = state_backwards;
    } else {
        *newstate = state;
    }

    // convert our offset to a vector in worldspace
    MatrixTransformVector(offset, orientation, *org);
    *org += origin;

    *ang = angles;
    ang->y += yaw_offset;

    //
    // decrement the use
    //
    if (count > 0) {
        count--;
    }
}

void UseObject::Start(Event *ev)

{
    //
    // fire off the move_thread
    //
    if (move_thread.IsSet()) {
        move_thread.Execute(this);
    }

    if ((spawnflags & MULTI_STATE) && objectState) {
        NewAnim("move_backward", ev);
    } else {
        NewAnim("move", ev);
    }

    SetActiveState(NULL);
}

bool UseObject::Loop(void)

{
    if (!count) {
        return qfalse;
    }

    return qtrue;
}

void UseObject::Stop(Entity *activator)

{
    if ((spawnflags & MULTI_STATE) && objectState) {
        NewAnim("start");
    } else {
        NewAnim("stop");
    }

    //
    // fire off our trigger target if appropriate
    //
    if (triggertarget.length()) {
        Event  *event;
        Entity *ent;

        ent = NULL;
        do {
            ent = (Entity *)G_FindTarget(ent, triggertarget.c_str());
            if (!ent) {
                break;
            }
            event = new Event(EV_Activate);
            event->AddEntity(activator);
            ent->PostEvent(event, 0);
        } while (1);
    }

    //
    // fire off a thread if necessary
    //
    if ((spawnflags & MULTI_STATE) && objectState) {
        if (reset_thread.IsSet()) {
            reset_thread.Execute(this);
        }
    } else {
        if (stop_thread.IsSet()) {
            stop_thread.Execute(this);
        }
    }

    // toggle the state
    objectState ^= 1;

    if (reset_time) {
        count = 0;
        PostEvent(EV_UseObject_Reset, reset_time);
    }

    SetActiveState(NULL);
}

/*****************************************************************************/
/*QUAKED info_waypoint (0 0.5 0) (-8 -8 -8) (8 8 8)

Used as a positioning device for objects

******************************************************************************/

CLASS_DECLARATION(SimpleArchivedEntity, Waypoint, "info_waypoint") {
    {NULL, NULL}
};

Waypoint::Waypoint()
{
    entflags |= ECF_WAYPOINT;
}

CLASS_DECLARATION(SimpleArchivedEntity, TempWaypoint, NULL) {
    {NULL, NULL}
};

TempWaypoint::TempWaypoint()
{
    entflags |= ECF_TEMPWAYPOINT;
}

/*****************************************************************************/
/*QUAKED info_vehiclepoint (0.15 0.5 0.25) (-8 -8 -8) (8 8 8) START_STOPPING START_SKIDDING STOP_SKIDDING

Like info_waypoints, but with spawnflags for vehicles.

******************************************************************************/

Event EV_VehiclePoint_SetSpawnFlags
(
    "spawnflags",
    EV_DEFAULT,
    "i",
    "spawn_flags",
    "Sets the spawn flags."
);

CLASS_DECLARATION(Waypoint, VehiclePoint, "info_vehiclepoint") {
    {&EV_VehiclePoint_SetSpawnFlags, &VehiclePoint::SetSpawnFlags},
    {NULL,                           NULL                        }
};

VehiclePoint::VehiclePoint()
{
    entflags |= ECF_VEHICLEPOINT;
    spawnflags = 0;
}

void VehiclePoint::SetSpawnFlags(Event *ev)
{
    spawnflags = ev->GetInteger(1);
}

/*****************************************************************************/
/*QUAKED func_monkeybars (0.75 0.75 0.75) ?

Monkey bars

******************************************************************************/

CLASS_DECLARATION(Entity, MonkeyBars, "func_monkeybars") {
    {&EV_SetAngle, &MonkeyBars::SetAngleEvent},
    {NULL,         NULL                      }
};

MonkeyBars::MonkeyBars()
{
    if (LoadingSavegame) {
        // Archive function will setup all necessary data
        return;
    }
    setMoveType(MOVETYPE_NONE);
    setContents(CONTENTS_SHOOTONLY | MASK_SOLID);
    PostEvent(EV_BecomeSolid, 0);
    dir = 0;
}

void MonkeyBars::SetAngleEvent(Event *ev)

{
    dir = ev->GetFloat(1);
}

/*****************************************************************************/
/*QUAKED func_horizontalpipe (0.75 0.75 0.75) ?

Horizontal pipe that play can crawl upside down on.

******************************************************************************/

CLASS_DECLARATION(Entity, HorizontalPipe, "func_horizontalpipe") {
    {&EV_SetAngle, &HorizontalPipe::SetAngleEvent},
    {NULL,         NULL                          }
};

HorizontalPipe::HorizontalPipe()
{
    if (LoadingSavegame) {
        // Archive function will setup all necessary data
        return;
    }
    setMoveType(MOVETYPE_NONE);
    setContents(CONTENTS_SHOOTONLY | MASK_SOLID);
    PostEvent(EV_BecomeSolid, 0);
    dir = 0;
}

void HorizontalPipe::SetAngleEvent(Event *ev)

{
    dir = ev->GetFloat(1);
}

/*****************************************************************************/
// TossObject
/*****************************************************************************/

Event EV_TossObject_SetBounceSound
(
    "bouncesound", EV_DEFAULT,
    "s",
    "sound",
    "When bouncing, what sound to play on impact",
    EV_NORMAL
);
Event EV_TossObject_SetBounceSoundChance
(
    "bouncesoundchance",
    EV_DEFAULT,
    "f[0,1]",
    "chance",
    "When bouncing, the chance that the bounce sound will be played",
    EV_NORMAL
);

CLASS_DECLARATION(Animate, TossObject, "TossObject") {
    {&EV_Touch,                           &TossObject::Touch               },
    {&EV_Stop,                            &TossObject::Stop                },
    {&EV_TossObject_SetBounceSound,       &TossObject::SetBounceSound      },
    {&EV_TossObject_SetBounceSoundChance, &TossObject::SetBounceSoundChance},
    {NULL,                                NULL                             }
};

TossObject::TossObject()
{
    if (LoadingSavegame) {
        // Archive function will setup all necessary data
        return;
    }
    setMoveType(MOVETYPE_GIB);
    setSolidType(SOLID_NOT);
    bouncesound       = "";
    bouncesoundchance = 1.0f;
}

TossObject::TossObject(str model)
{
    setMoveType(MOVETYPE_GIB);
    setSolidType(SOLID_NOT);
    bouncesound       = "";
    bouncesoundchance = 1.0f;
    setModel(model);
}

void TossObject::SetBounceSound(str bounce)

{
    bouncesound = bounce;
}

void TossObject::SetBounceSound(Event *ev)

{
    bouncesound = ev->GetString(1);
}

void TossObject::SetBounceSoundChance(float chance)

{
    bouncesoundchance = chance;
}

void TossObject::SetBounceSoundChance(Event *ev)

{
    bouncesoundchance = ev->GetFloat(1);
}

void TossObject::Stop(Event *ev)

{
    setMoveType(MOVETYPE_NONE);
    setSolidType(SOLID_NOT);
    // cancel the previous fade out command
    CancelEventsOfType(EV_FadeOut);
    PostEvent(EV_FadeOut, 7 + G_Random(5));
    setAngles(Vector("0 0 0"));
    NewAnim("landed");
}

void TossObject::Touch(Event *ev)

{
    Entity *ent;

    ent = ev->GetEntity(1);

    // only touch the world
    if (!ent || (ent != world)) {
        return;
    }
    //
    // every time we bounce try to go back to our nominal angles
    //
    setAngles(angles * 0.5f);

    if (bouncesound.length()) {
        if (G_Random(1) < bouncesoundchance) {
            Sound(bouncesound);
        }
    }
}

void TossObject::SetVelocity(float severity)

{
    setSolidType(SOLID_BBOX);
    velocity[0] = 100.0 * crandom();
    velocity[1] = 100.0 * crandom();
    velocity[2] = 200.0 + 100.0 * random();

    avelocity = Vector(G_Random(600), G_Random(600), G_Random(600));

    velocity *= severity;

    if (velocity[0] < -400) {
        velocity[0] = -400;
    } else if (velocity[0] > 400) {
        velocity[0] = 400;
    }
    if (velocity[1] < -400) {
        velocity[1] = -400;
    } else if (velocity[1] > 400) {
        velocity[1] = 400;
    }
    if (velocity[2] < 200) {
        velocity[2] = 200; // always some upwards
    } else if (velocity[2] > 600) {
        velocity[2] = 600;
    }

    NewAnim("idle");

    // we give it 8 seconds to fall, if not it will get faded out
    PostEvent(EV_FadeOut, 8);
}

/*****************************************************************************/
/*QUAKED func_pushobject (0.75 0.75 0.75) ?

Pushable object

"dmg" how much damage to cause when blocked.  (default 2)
"pushsound" Sound to play when object is pushed (default is none)

******************************************************************************/

Event EV_PushObject_Start
(
    "start",
    EV_DEFAULT,
    NULL,
    NULL,
    "Sets up the pushobject.",
    EV_NORMAL
);

Event EV_PushObject_SetDamage
(
    "dmg",
    EV_DEFAULT,
    "i",
    "damage",
    "Set the damage.",
    EV_NORMAL
);

Event EV_PushObject_SetPushSound
(
    "pushsound",
    EV_DEFAULT,
    "s",
    "sound",
    "Set the pushing sound",
    EV_NORMAL
);

CLASS_DECLARATION(Entity, PushObject, "func_pushobject") {
    {&EV_PushObject_Start,        &PushObject::Start       },
    {&EV_Blocked,                 &PushObject::BlockFunc   },
    {&EV_PushObject_SetDamage,    &PushObject::SetDamage   },
    {&EV_PushObject_SetPushSound, &PushObject::SetPushSound},
    {NULL,                        NULL                     }
};

PushObject::PushObject()
{
    if (LoadingSavegame) {
        // Archive function will setup all necessary data
        return;
    }
    dmg             = 2;
    attack_finished = 0;

    pushsound = "object_slide";

    PostEvent(EV_PushObject_Start, EV_POSTSPAWN);
}

void PushObject::SetPushSound(Event *ev)

{
    pushsound = ev->GetString(1);
}

void PushObject::Start(Event *ev)

{
    // make sure that this touches triggers
    flags |= FL_TOUCH_TRIGGERS;
    edict->clipmask = MASK_SOLID;
    setSolidType(SOLID_BSP);
    setMoveType(MOVETYPE_PUSH);

    // fix the bounding box so that the object isn't stuck in the ground
    setSize(mins + Vector(1, 1, 2), maxs - Vector(1, 1, 1));
}

qboolean PushObject::canPush(Vector dir)

{
    trace_t trace;

    Vector end(origin.x + dir.x, origin.y + dir.y, origin.z);

    trace = G_Trace(origin, mins, maxs, end, this, MASK_DEADSOLID, false, "PushObject::Push");
    return (!trace.startsolid && (trace.fraction == 1.0f));
}

qboolean PushObject::Push(Entity *pusher, Vector move)

{
    trace_t trace;

    if (pushsound.length()) {
        if (!edict->s.loopSound) {
            LoopSound(pushsound);
            PostEvent(EV_StopLoopSound, level.frametime * 5);
        }
    }

    Vector end(origin.x + move.x, origin.y + move.y, origin.z);

    trace = G_Trace(origin, mins, maxs, end, this, MASK_DEADSOLID, false, "PushObject::Push");
    if (!trace.startsolid && (trace.fraction > 0)) {
        owner = pusher;

        G_PushMove(this, trace.endpos - origin, vec_zero);

        if (edict->s.loopSound) {
            PostEvent(EV_StopLoopSound, 0.f);
        }

        return qtrue;
    }

    return qfalse;
}

Entity *PushObject::getOwner(void)

{
    return (Entity *)owner;
}

void PushObject::BlockFunc(Event *ev)

{
    Entity *other;

    if ((dmg != 0) && (level.time >= attack_finished)) {
        attack_finished = level.time + 0.5f;
        other           = ev->GetEntity(1);
        if (other != owner) {
            other->Damage(this, this, dmg, origin, vec_zero, vec_zero, 0, 0, MOD_CRUSH);
        }
    }
}

void PushObject::SetDamage(Event *ev)

{
    dmg = ev->GetInteger(1);
}

#define SPAWN_AUTO_RESET (1 << 0)
#define NO_RANDOMNESS    (1 << 1)
#define REMOVE_ON_GROUND (1 << 2)
/*****************************************************************************/
/*QUAKED func_fallingrock (0.75 0.75 0.75) ? AUTO_RESET NO_RANDOMNESS REMOVE_ON_GROUND

Creates a rock that, when triggered, begins falling and bounces along a path
specified by targetname.  Use info_waypoint for the path.

"targetname" the path to follow.
"dmg" how much damage to cause creatures it hits (default 20).
"speed" how fast to move (default 200).
"wait" how long to wait before falling when triggered (default 0).
"noise" sound to play when rock touches the world

AUTO_RESET - when done falling, automatically return to the start
NO_RANDOMNESS - don't use any randomness when making the rocks fall
REMOVE_ON_GROUND - remove the rocks when done

******************************************************************************/

Event EV_FallingRock_Bounce
(
    "bounce",
    EV_DEFAULT,
    NULL,
    NULL,
    "sent to entity when touched.",
    EV_NORMAL
);
Event EV_FallingRock_Rotate
(
    "rotate",
    EV_DEFAULT,
    NULL,
    NULL,
    "rotates the falling rock.",
    EV_NORMAL
);
Event EV_FallingRock_SetWait
(
    "wait",
    EV_DEFAULT,
    "f",
    "wait",
    "How long to wait before rock starts falling.",
    EV_NORMAL
);
Event EV_FallingRock_Start
(
    "start",
    EV_DEFAULT,
    NULL,
    NULL,
    "Starts rock falling.",
    EV_NORMAL
);
Event EV_FallingRock_SetDmg
(
    "dmg",
    EV_DEFAULT,
    "i",
    "dmg",
    "Set the damage from the rock.",
    EV_NORMAL
);
Event EV_FallingRock_SetSpeed
(
    "speed",
    EV_DEFAULT,
    "f",
    "speed",
    "Set the speed that the rock moves at.",
    EV_NORMAL
);
Event EV_FallingRock_SetBounceSound
(
    "noise",
    EV_DEFAULT,
    "s",
    "sound",
    "Set the sound to play when the rock bounces",
    EV_NORMAL
);

CLASS_DECLARATION(Entity, FallingRock, "func_fallingrock") {
    {&EV_Activate,                   &FallingRock::Activate      },
    {&EV_Touch,                      &FallingRock::Touch         },
    {&EV_FallingRock_Bounce,         &FallingRock::Bounce        },
    {&EV_FallingRock_Rotate,         &FallingRock::Rotate        },
    {&EV_FallingRock_Start,          &FallingRock::StartFalling  },
    {&EV_FallingRock_SetWait,        &FallingRock::SetWait       },
    {&EV_FallingRock_SetSpeed,       &FallingRock::SetSpeed      },
    {&EV_FallingRock_SetDmg,         &FallingRock::SetDmg        },
    {&EV_FallingRock_SetBounceSound, &FallingRock::SetBounceSound},
    {NULL,                           NULL                        }
};

FallingRock::FallingRock()
{
    if (LoadingSavegame) {
        // Archive function will setup all necessary data
        return;
    }
    active  = 0;
    current = NULL;
    setMoveType(MOVETYPE_NONE);
    wait            = 0;
    dmg             = 20;
    speed           = 200;
    activator       = NULL;
    attack_finished = 0;

    SetBounceSound("impact_rock");
}

Entity *FallingRock::SetNextBounceDir(void)

{
    Entity *ent;

    if (!current->target.length()) {
        return NULL;
    }

    ent = (Entity *)G_FindTarget(NULL, current->target.c_str());
    if (!ent) {
        gi.Error(ERR_DROP, "FallingRock :: Entity with targetname of '%s' not found", current->target.c_str());
    }

    bounce_dir = ent->origin - current->origin;
    bounce_dir.normalize();

    return ent;
}

void FallingRock::NextBounce(void)

{
    float  time;
    float  distance;
    Vector delta, xydelta;
    float  xy_speed;
    float  vertical_speed;

    delta     = current->origin - origin;
    xydelta   = delta;
    xydelta.z = 0;
    xy_speed  = speed;

    distance = xydelta.normalize();

    time = distance / xy_speed;

    if (!(spawnflags & NO_RANDOMNESS)) {
        if (time > 1.0f) {
            time = 0.75f + G_Random(1);
        }

        if (time < 0.4f) {
            time = 0.4f;
        }
    }

    vertical_speed = (delta.z / time) + (0.5f * gravity * sv_gravity->value * time);
    if (vertical_speed < 0) {
        vertical_speed = 0;
    }

    velocity   = xydelta * xy_speed;
    velocity.z = vertical_speed;

    Vector ang(0, vectoyaw(delta), 0);
    ang.AngleVectors(NULL, &rotateaxis);

    // make sure it leaves the ground
    groundentity = NULL;
}

void FallingRock::Rotate(Event *ev)

{
    float mat[3][3];
    float ang;

    ang = 360.0f * FRAMETIME;
    RotatePointAroundVector(mat[0], rotateaxis, orientation[0], ang);
    RotatePointAroundVector(mat[1], rotateaxis, orientation[1], ang);
    RotatePointAroundVector(mat[2], rotateaxis, orientation[2], ang);
    MatrixToEulerAngles(mat, angles);
    setAngles(angles);

    if (velocity != vec_zero) {
        PostEvent(EV_FallingRock_Rotate, FRAMETIME);
    }
}

void FallingRock::SetWait(Event *ev)

{
    wait = ev->GetFloat(1);
}

void FallingRock::SetSpeed(Event *ev)

{
    speed = ev->GetFloat(1);
}

void FallingRock::SetDmg(Event *ev)

{
    dmg = ev->GetInteger(1);
}

void FallingRock::SetBounceSound(str sound)
{
    bouncesound = sound;
    // cache the sound in
    CacheResource(bouncesound.c_str());
}

void FallingRock::SetBounceSound(Event *ev)

{
    SetBounceSound(ev->GetString(1));
}

void FallingRock::Activate(Event *ev)

{
    if (active == 1) {
        return;
    }

    if ((active == 2) && (spawnflags & SPAWN_AUTO_RESET)) {
        current   = NULL;
        activator = NULL;
        setMoveType(MOVETYPE_NONE);
        NoLerpThisFrame();
        setOrigin(start_origin);
    }

    activator = ev->GetEntity(1);

    if (wait) {
        PostEvent(EV_FallingRock_Start, wait);
    } else {
        ProcessEvent(EV_FallingRock_Start);
    }
}

void FallingRock::StartFalling(Event *ev)

{
    if (current) {
        return;
    }

    if (!active) {
        start_origin = origin;
    }

    active = 1;
    setMoveType(MOVETYPE_BOUNCE);
    setSolidType(SOLID_BBOX);
    PostEvent(EV_FallingRock_Rotate, FRAMETIME);
    edict->clipmask = MASK_SOLID | CONTENTS_BODY;

    last_bounce_origin = origin;
    current            = this;
    current            = SetNextBounceDir();
    if (current) {
        NextBounce();
    }
}

void FallingRock::Touch(Event *ev)

{
    Entity *other;

    other = ev->GetEntity(1);

    if (other != world) {
        if ((velocity != vec_zero) && (other->takedamage) && (level.time >= attack_finished)) {
            other->Damage(this, activator, dmg, origin, vec_zero, vec_zero, 20, 0, MOD_THROWNOBJECT);
            attack_finished = level.time + FRAMETIME;
        }
    }

    if (!current || (other != world)) {
        return;
    }
    if (bouncesound.length()) {
        Vector delta(origin - last_bounce_origin);

        if (delta.length() > 8) {
            last_bounce_origin = origin;
            Sound(bouncesound.c_str(), CHAN_VOICE);
        }
    }

    // we have to wait to set the velocity since the physics code
    // will modify it when we return.
    PostEvent(EV_FallingRock_Bounce, 0);
}

void FallingRock::Bounce(Event *ev)

{
    Vector delta;

    if (!current) {
        return;
    }

    do {
        // check if we've passed the waypoint
        delta = origin - current->origin;
        if ((delta * bounce_dir) >= (-2 * edict->r.radius)) {
            // call any threads on the current waypoint
            if (current->isSubclassOf(Trigger)) {
                current->ProcessEvent(EV_Trigger_StartThread);
            }
            current = SetNextBounceDir();
            if (!current) {
                velocity = vec_zero;
                if (spawnflags & SPAWN_AUTO_RESET) {
                    active = 2;
                } else if (spawnflags & REMOVE_ON_GROUND) {
                    PostEvent(EV_Remove, 0);
                }
                break;
            }
        } else {
            NextBounce();
            break;
        }
    } while (1);
}

/*****************************************************************************/
/*QUAKED func_ladder (0.75 0.75 0.75) ?

Ladder trigger volume

******************************************************************************/

Event EV_LadderSetup
(
    "_ladder_setup",
    EV_CODEONLY,
    NULL,
    NULL,
    "Does the post spawn setup of the ladder"
);

CLASS_DECLARATION(Entity, FuncLadder, "func_ladder") {
    {&EV_Use,         NULL                        },
    {&EV_Touch,       NULL                        },
    {&EV_SetAngle,    &FuncLadder::SetLadderFacing},
    {&EV_LadderSetup, &FuncLadder::LadderSetup    },
    {NULL,            NULL                        }
};

FuncLadder::FuncLadder()
{
    if (LoadingSavegame) {
        return;
    }

    PostEvent(EV_LadderSetup, EV_POSTSPAWN);
}

void FuncLadder::LadderSetup(Event *ev)
{
    setMoveType(MOVETYPE_PUSH);

    setContents(CONTENTS_LADDER);
    setSolidType(SOLID_BSP);

    // never send to clients
    edict->r.svFlags |= SVF_NOCLIENT;
}

void FuncLadder::SetLadderFacing(Event *ev)
{
    m_vFacingAngles    = vec_zero;
    m_vFacingAngles[1] = AngleMod(ev->GetFloat(1));

    AngleVectorsLeft(m_vFacingAngles, m_vFacingDir, NULL, NULL);
}

qboolean FuncLadder::CanUseLadder(Entity *pUser)
{
    int     iMask;
    Vector  vDelta;
    Vector  vUserFacing;
    Vector  start, end;
    trace_t trace;

    vDelta    = origin - pUser->origin;
    vDelta[2] = 0;

    if (vDelta.length() > 52.0f) {
        return qfalse;
    }

    AngleVectorsLeft(pUser->angles, vUserFacing, NULL, NULL);

    iMask = pUser->client ? MASK_PLAYERSOLID : MASK_MONSTERSOLID;

    if (pUser->origin[2] + pUser->maxs[2] > absmax[2]) {
        if (DotProduct(vUserFacing, m_vFacingDir) <= 0.15f) {
            vDelta    = pUser->origin - origin;
            vDelta[2] = 0.0f;

            if (DotProduct(m_vFacingDir, vDelta) >= 0.0f) {
                start    = origin + m_vFacingDir * 26.0f;
                start[2] = absmax[2] + 16.0f;

                end = start - Vector(0, 0, 16);

                trace = G_Trace(
                    start, pUser->mins, pUser->maxs, end, pUser, MASK_PLAYERSOLID, qfalse, "FuncLadder::CanUseLadder"
                );

                start = trace.endpos;
                trace = G_Trace(
                    start, pUser->mins, pUser->maxs, start, pUser, MASK_PLAYERSOLID, qfalse, "FuncLadder::CanUseLadder"
                );

                if (!trace.startsolid) {
                    // client can use ladder
                    return qtrue;
                }
            }
        }

        return qfalse;
    }

    vDelta    = (pUser->origin - origin);
    vDelta[2] = 0.0f;

    if (DotProduct(vUserFacing, m_vFacingDir) < -0.15f || DotProduct(vDelta, m_vFacingDir) > 0.0f) {
        return qfalse;
    }

    start    = origin - m_vFacingDir * 29.0f;
    start[2] = absmin[2] + 16.0f;

    end    = start;
    end[2] = start[2] - 16.0f;

    trace = G_Trace(start, pUser->mins, pUser->maxs, end, pUser, MASK_PLAYERSOLID, qfalse, "FuncLadder::CanUseLadder");

    if (!trace.allsolid) {
        // client can use ladder
        return qtrue;
    }

    gi.DPrintf("ladder start position is blocked by a solid object\n");
    return qfalse;
}

void FuncLadder::PositionOnLadder(Entity *pUser)
{
    Vector  vPos;
    Vector  vStart;
    Vector  vEnd;
    trace_t trace;

    if (pUser->origin[2] + pUser->maxs[2] > absmax[2]) {
        vEnd    = origin + m_vFacingDir * 26.0f;
        vEnd[2] = absmax[2] + 2.0f;
    } else if (absmin[2] + 32.0f > pUser->origin[2]) {
        vEnd    = origin - m_vFacingDir * 29.0f;
        vEnd[2] = absmin[2] + 2.0f;
    } else {
        vEnd    = origin - m_vFacingDir * 16.0f;
        vEnd[2] = pUser->origin[2] + 8.0f;
    }

    vStart = vEnd;

    trace =
        G_Trace(vStart, pUser->mins, pUser->maxs, vEnd, pUser, MASK_PLAYERSOLID, qtrue, "FuncLadder::PositionOnLadder");

    if (trace.allsolid) {
        vStart    = pUser->origin;
        vStart[2] = pUser->origin[2] + 4.0f;

        vEnd    = origin - m_vFacingDir * 16.0f;
        vEnd[2] = pUser->origin[2] + 4.0f;

        trace = G_Trace(
            vStart, pUser->mins, pUser->maxs, vEnd, pUser, MASK_PLAYERSOLID, qtrue, "FuncLadder::PositionOnLadder2"
        );
    }

    pUser->setOrigin(trace.endpos);
    pUser->setAngles(m_vFacingAngles);
}

void FuncLadder::AdjustPositionOnLadder(Entity *pUser)
{
    float   fHeight;
    Vector  vDelta;
    Vector  vPos;
    Vector  vStart;
    trace_t trace;

    vDelta    = pUser->origin - origin;
    vDelta[2] = 0;

    fHeight = ((int)(pUser->origin[2] + 8.0f) & ~15);

    if (vDelta.length() == 16.0f && pUser->origin[2] == fHeight) {
        return;
    }

    vPos    = origin - m_vFacingDir * 16.0f;
    vPos[2] = fHeight;

    vStart = vPos + m_vFacingDir * -16.0f;

    trace = G_Trace(
        vStart, pUser->mins, pUser->maxs, vPos, pUser, MASK_PLAYERSOLID, qtrue, "FuncLadder::AdjustPositionOnLadder"
    );

    if (trace.allsolid || trace.startsolid) {
        trace = G_Trace(
            pUser->origin,
            pUser->mins,
            pUser->maxs,
            vPos,
            pUser,
            MASK_PLAYERSOLID,
            qtrue,
            "FuncLadder::AdjustPositionOnLadder"
        );
    }

    pUser->setOrigin(trace.endpos);
}

void FuncLadder::EnsureOverLadder(Entity *pUser)
{
    float   fHeight = absmax[2];
    Vector  vStart;
    Vector  vPos;
    trace_t trace;

    if (pUser->origin[2] - fHeight >= 8.0f) {
        return;
    }

    vPos    = pUser->origin;
    vPos[2] = fHeight + 16.0f;

    vStart = pUser->origin + m_vFacingDir * -16.0f;

    trace = G_Trace(
        vStart, pUser->mins, pUser->maxs, vPos, pUser->edict, MASK_PLAYERSOLID, true, "FuncLadder::EnsureOverLadder"
    );

    if (trace.startsolid || trace.allsolid) {
        trace = G_Trace(
            origin, pUser->mins, pUser->maxs, vPos, pUser->edict, MASK_PLAYERSOLID, true, "FuncLadder::EnsureOverLadder"
        );
    }

    pUser->setOrigin(trace.endpos);
}

void FuncLadder::EnsureForwardOffLadder(Entity *pUser)
{
    int     iMask;
    Vector  vStart;
    Vector  vPos;
    Vector  vDelta;
    trace_t trace;

    if (pUser->origin[2] - absmax[2] >= 8.0f) {
        return;
    }

    iMask = pUser->client ? MASK_PLAYERSOLID : MASK_MONSTERSOLID;

    vPos    = pUser->origin;
    vPos[2] = absmax[2] + 16.0f;

    vStart = vPos + m_vFacingDir * -16.0f;

    trace = G_Trace(vStart, pUser->mins, pUser->maxs, vPos, pUser, iMask, qtrue, "FuncLadder::EnsureOverLadder");

    if (trace.allsolid || trace.startsolid) {
        trace =
            G_Trace(pUser->origin, pUser->mins, pUser->maxs, vPos, pUser, iMask, qtrue, "FuncLadder::EnsureOverLadder");
    }

    pUser->setOrigin(trace.endpos);
}

Event EV_InfoLandmark_Name
(
    "landmark_name",
    EV_DEFAULT,
    "s",
    "name",
    "Set the name of this landmark",
    EV_NORMAL
);
Event EV_InfoLandmark_SetOrigin
(
    "origin",
    EV_DEFAULT,
    "v",
    "origin",
    "Set the origin of the landmark.",
    EV_NORMAL
);

CLASS_DECLARATION(Listener, InfoLandmark, "info_landmark") {
    {&EV_InfoLandmark_Name,      &InfoLandmark::SetLandmarkName},
    {&EV_InfoLandmark_SetOrigin, &InfoLandmark::SetOrigin      },
    {NULL,                       NULL                          }
};

InfoLandmark::InfoLandmark()
{
    m_bNameSet   = false;
    m_bOriginSet = false;
}

void InfoLandmark::SetLandmarkName(Event *ev)
{
    m_sName    = ev->GetString(1);
    m_bNameSet = true;

    if (m_bOriginSet) {
        level.AddLandmarkName(m_sName, m_vOrigin);
        PostEvent(EV_Remove, EV_REMOVE);
    }
}

void InfoLandmark::SetOrigin(Event *ev)
{
    m_vOrigin = ev->GetVector(1);

    if (m_vOrigin == Vector(99999, 99999, 99999)) {
        ScriptError(
            "Illegal location (%f, %f, %f) for Landmark. Please place in valid XYZ coordinates\n",
            m_vOrigin[0],
            m_vOrigin[1],
            m_vOrigin[2]
        );
    }

    level.AddLandmarkOrigin(m_vOrigin);
    m_bOriginSet = true;

    if (m_bNameSet) {
        level.AddLandmarkName(m_sName, m_vOrigin);
        PostEvent(EV_Remove, EV_REMOVE);
    }
}
