/*

Copyright (C) 2015-2018 Night Dive Studios, LLC.

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

*/
/*
 * $Source: r:/prj/cit/src/RCS/damage.c $
 * $Revision: 1.206 $
 * $Author: xemu $
 * $Date: 1994/10/27 04:56:12 $
 */

#include <stdlib.h>

#include "Shock.h"

#include "effect.h"
#include "newmfd.h"
#include "damage.h"
#include "objwpn.h"
#include "objects.h"
#include "objsim.h"
#include "objprop.h"
#include "weapons.h"
#include "player.h"
#include "combat.h"
#include "cybrnd.h"
#include "mainloop.h"
#include "trigger.h"
#include "musicai.h"
#include "sfxlist.h"
#include "ai.h"
#include "frflags.h"
#include "gamerend.h"
#include "gameloop.h"
#include "target.h"
#include "objbit.h"
#include "physunit.h"
#include "hud.h"
#include "faketime.h"
#include "otrip.h"
#include "grenades.h"
#include "softdef.h"
#include "aiflags.h"
#include "input.h"
#include "mapflags.h"
#include "wares.h"
#include "drugs.h"
#include "diffq.h"
#include "hudobj.h"
#include "amap.h"
#include "cutsloop.h"

#define SQUARE(x) ((x) * (x))
#define PLAYER_DEFENSE_VALUE 4
#define MAX_DAMAGE 2500


short destroyed_obj_count = 0;
ObjID destroyed_ids[MAX_DESTROYED_OBJS];

//----------------
//  Internal Prototypes
//----------------
int random_bell_modifier(uchar attack_on_player);
int randomize_damage(int damage, uchar attack_on_player);
int armor_absorption(int raw_damage, int obj_triple, ubyte penetrate);
int shield_absorb_damage(int damage, ubyte dtype, byte shield_absorb, ubyte shield_threshold);
void player_dies();
ubyte damage_player(int damage, ubyte dtype, ubyte flags);
void critter_hit_effect(ObjID target, ubyte effect, Combat_Pt location, int damage, int max_damage);

// -------------------------------------------
// destroy_destroyed_objects()
//

void destroy_destroyed_objects(void) {
    int i, j;
    uchar change_target = FALSE;
    uchar dupe;
    ObjID id;

    if (destroyed_obj_count != 0) {
        for (i = 0; i < destroyed_obj_count; i++) {
            // Look through the rest of the list for duplicates
            // if so, don't delete us now since we'll get what's coming
            // to us later...
            // Yeah, yeah, so this is an N-squared algorithm, but hopefully the size of
            // the list is pretty damn short...
            dupe = FALSE;
            id = destroyed_ids[i];

            for (j = i + 1; j < destroyed_obj_count; j++) {
                if (id == destroyed_ids[j]) {
                    dupe = TRUE;
                    break;
                }
            }
            if (!dupe) {
                {
                    // check if the physics object contributed to the lighting of the map
                    if (objs[id].obclass == CLASS_PHYSICS) {
                        ObjSpecID osid = objs[id].specID;
                        if (objPhysicss[osid].p2.x) {
                            MapElem *mmp;
                            mmp = MAP_GET_XY(OBJ_LOC_TO_LIGHT_LOC(objPhysicss[osid].p1.x),
                                             OBJ_LOC_TO_LIGHT_LOC(objPhysicss[osid].p1.y));
                            me_rend3_set(mmp, me_bits_rend3(mmp) - 1);
                            objPhysicss[osid].p2.x = 0;
                        }
                    } else if (objs[id].obclass == CLASS_GRENADE) {
                        do_grenade_explosion(id, TRUE);
                    }

                    // If we destroyed a creature which was being targeted, we
                    // need to switch the current target
                    if (destroyed_ids[i] == player_struct.curr_target)
                        change_target = TRUE;

                    obj_destroy(destroyed_ids[i]);

                    if (change_target) {
                        toggle_current_target();
                        change_target = FALSE;
                    }
                }
            }

            // object_dead();
        }

        chg_set_flg(DEMOVIEW_UPDATE);

        // "clear" the list
        destroyed_obj_count = 0;
    }
}

// -------------------------------------------
// is_obj_destroyed()
//

uchar is_obj_destroyed(ObjID id) {
    int i;
    uchar found = FALSE;

    for (i = 0; i < destroyed_obj_count; i++) {
        if (destroyed_ids[i] == id) {
            found = TRUE;
            break;
        }
    }
    return (found);
}

// ---------------------------------------------
// random_bell_modifier()
//

int random_bell_modifier(uchar attack_on_player) {
    int rtotal;
    int i;
    int rval;
    int retval;
    ubyte dies, die_value, handicap;
    ubyte difficulty = (global_fullmap->cyber) ? player_struct.difficulty[CYBER_DIFF_INDEX]
                                               : player_struct.difficulty[COMBAT_DIFF_INDEX];

    if (attack_on_player)
        difficulty = (difficulty == 0) ? 3 : 4 - difficulty;

    switch (difficulty) {
    case (0):
        dies = 2;
        die_value = 50;
        handicap = 15;
        break;
    case (1):
        dies = 3;
        die_value = 33;
        handicap = 6;
        break;
    case (2):
        dies = 5;
        die_value = 20;
        handicap = 0;
        break;
    case (3):
        dies = 8;
        die_value = 12;
        handicap = 0;
        break;
    }

    rtotal = handicap;
    for (i = 0; i < dies; i++) {
        rval = RndRange(&damage_rnd, 0, die_value);
        rtotal += rval;
    }

    if (rtotal <= 2)
        retval = -12; // 00-02
    else if (rtotal <= 8)
        retval = -8; // 03-08
    else if (rtotal <= 16)
        retval = -6; // 09-16
    else if (rtotal <= 28)
        retval = -3; // 17-28
    else if (rtotal <= 40)
        retval = -1; // 29-40
    else if (rtotal <= 60)
        retval = 0; // 41-60
    else if (rtotal <= 72)
        retval = 1; // 61-72
    else if (rtotal <= 84)
        retval = 3; // 73-84
    else if (rtotal <= 92)
        retval = 6; // 85-92
    else if (rtotal <= 98)
        retval = 8; // 93-98
    else
        retval = 12; // 99-100

    return (retval);
}

// -------------------------------------------
// randomize_damage()
//

int randomize_damage(int damage, uchar attack_on_player) {
    int dtotal;
    ubyte iterations;
    int i;
    ubyte difficulty = (global_fullmap->cyber) ? player_struct.difficulty[CYBER_DIFF_INDEX]
                                               : player_struct.difficulty[COMBAT_DIFF_INDEX];

    dtotal = damage / 2;
    iterations = damage / 8;

    // if damage div 8 is non-zero add an extra iteration
    // yes, we might have a value that's a little bigger than normal
    if (damage % 8)
        iterations++;

    for (i = 0; i < iterations; i++)
        dtotal += RndRange(&damage_rnd, 1, 7);

    // if we're playing on difficulty 3 - reduce damage by a third
    if (!attack_on_player && (difficulty == 3))
        dtotal = (dtotal << 1) / 3;

    return (dtotal);
}

// ---------------------------------------------
// object_affect()
//
// there has been a hit, now we check how much an object is affected
//

ubyte object_affect(ObjID target_id, short dtype) {
    ubyte affected = 0;
    int resis;

    if (!target_id) {
        return 0;
    }

    resis = ObjProps[OPNUM(target_id)].resistances;
    affected = (resis & dtype & DAMAGE_TYPE_FIELD) ? 1 : 0;
    if (affected) {
        // are the super damage the same, and we have a non-zero primary damage
        if ((SUPER_DAMAGE(resis) == SUPER_DAMAGE(dtype)) && (SUPER_DAMAGE(resis))) {
            ubyte difficulty = (global_fullmap->cyber) ? player_struct.difficulty[CYBER_DIFF_INDEX]
                                                       : player_struct.difficulty[COMBAT_DIFF_INDEX];

            // don't do as much super damage if we're on combat diff 3
            affected = (difficulty == 3) ? 3 : 4;
        }
        // are the primary damage the same, and we have a non-zero primary damage
        else if ((PRIMARY_DAMAGE(resis) == PRIMARY_DAMAGE(dtype)) && (PRIMARY_DAMAGE(resis))) {
            affected = 2;
        }
    }
    //   mprintf("attack on %d, affect : %d\n", target_id, affected);
    return (affected);
}

// -----------------------------------------------------------------------------------------
// armor_absorption()
//

int armor_absorption(int raw_damage, int obj_triple, ubyte penetrate) {
    short real_penetration = (((short)penetrate * (90 + RndRange(&damage_rnd, 0, 20))) / 100);
    int damage;

    damage = (ObjProps[OPTRIP(obj_triple)].armor - real_penetration);
    damage = (damage > 0) ? raw_damage - damage : raw_damage;

    if (damage < 0)
        damage = 0;

    return (damage);
}

// some globals
uchar sound_hurt_threshold = 10;
uchar static_pain_time = 64;
uchar static_pain_base = 30;
uchar static_pain_delta = 30;
uchar shield_blowout_threshold = 15;

short fr_solidfr_time;
short fr_sfx_time;

// this is the voodoo threshold for shields, so that we have a reference
// to do effects with. (static)
#define VOODOO_SHIELD_THRESHOLD 100

// -------------------------------------------------------------------------------------------
// shield_absorb_damage()
//
// returns damage to the player after shields

short shield_absorb_perc = 0;
uchar shield_used;

int shield_absorb_damage(int damage, ubyte dtype, byte shield_absorb, ubyte shield_threshold) {
    ubyte shield_drain = 0;

    if ((damage | player_struct.hit_points) == 0)
        return 0; // 0 hp is already dead, eh?, 0 damage no matter

    if (damage > shield_threshold) {
        // absorption rate will be given a +/- 8 percentage for variation - unless we're at zero already
        shield_absorb = (shield_absorb) ? shield_absorb + RndRange(&damage_rnd, 0, 16) - 8 : 0;

        if (shield_absorb > 0) {
            // figure out how much damage is absorbed by shields
            // hey - it's even a percentage
            // WHY - make it 0-255, dont divide needlessly, arghgqghgghdsghgfdgfjfghdfgj
            shield_drain = (damage * shield_absorb) / 100;
            // let's absorb the damage... now!
            damage -= shield_drain;
        } else
            shield_absorb = 0;
    } else {
        if (shield_absorb) {
            // if we're below threshold, we've absorbed all damage...
            shield_absorb = 100;
            shield_drain = damage;
        } else
            shield_drain = 0;
    }

    // Hud me baby
    // oh,and sound too.

    if (shield_absorb > 0) {
        shield_absorb_perc = shield_absorb;
        hud_set_time(HUD_SHIELD, CIT_CYCLE);

        // do the sound if we're not in cyberspace
        if (!global_fullmap->cyber)
            (shield_absorb > 80) ? play_digi_fx(SFX_SHIELD_2, 1) : play_digi_fx(SFX_SHIELD_1, 1);
    } else if (((rand() & 0x1A) < damage) && !global_fullmap->cyber)
        play_digi_fx(SFX_PLAYER_HURT, 1);

    // let's make sure that we don't go over the 100% shield flash
    if (shield_drain > VOODOO_SHIELD_THRESHOLD)
        shield_drain = VOODOO_SHIELD_THRESHOLD;

    // let's reuse the shield_drain variable
    // let's get the percentage absorbed (w.r.t. the VOODOO_SHIELD_THRESHOLD)
    shield_drain = (ubyte)(((int)shield_drain << 8) / VOODOO_SHIELD_THRESHOLD);

    if ((shield_used = (shield_drain > 0)) == TRUE)
        set_dmg_percentage(DMG_SHIELD, shield_drain);

    return (damage);
}

uchar alternate_death = FALSE;

// kill_player()
// kills the player, checks for traps and stuff, so on
// returns true if player is really dead dead dead
uchar kill_player(void) {
    ObjSpecID osid;
    uchar quick_death = TRUE;
    uchar dummy;
    extern uchar clear_player_data;

    INFO("Player died!\n");

    // Look for a player death trigger.  If so, do it.
    // If not, play appropriate dying cutscene.
    osid = objTraps[0].id;
    while (osid != OBJ_SPEC_NULL) {
        if (ID2TRIP(objTraps[osid].id) == PLRDETH_TRIG_TRIPLE)
            if (trap_activate(objTraps[osid].id, &dummy))
                quick_death = FALSE;
        osid = objTraps[osid].next;
    }

        // if we died not from a trap - then we should clear the player data
#ifdef TEST_REBIRTH
    clear_player_data = FALSE;
    return FALSE;
#else
    clear_player_data = (quick_death | alternate_death);

    if (quick_death) {
        amap_reset();
        secret_render_fx = 0;
        play_cutscene(DEATH_CUTSCENE, FALSE);
    }

    return (quick_death | alternate_death);
#endif
}

void regenerate_player(void) {
    extern void wear_off_drug(int i);
    extern void regenetron_door_hack(void);
    int i;
    for (i = 0; i < NUM_DAMAGE_TYPES; i++)
        player_struct.hit_points_lost[i] = 0;
    hud_unset(HUD_RADPOISON | HUD_BIOPOISON);
    player_struct.curr_target = OBJ_NULL;
    player_struct.fatigue = 0;

    // clear physics state
    player_struct.posture = 0;
    player_struct.foot_planted = 0;
    player_struct.leanx = 0;
    player_struct.leany = 0;
    player_struct.eye_pos = 0;

    for (i = 0; i < NUM_DRUGZ; i++)
        wear_off_drug(i);
    regenetron_door_hack();
}

    // -----------------------------------------------------------------------------------------
    // damage_player()
    //
    // returns 0 if player is still alive
    // returns 1 if player is dead

#define CSHIELD_THRESHOLDS(lev) 0

#define MAX_CSHIELD_ABSORB 120
#define NUM_CSHIELD_LEVELS 10
#define CSHIELD_ABSORB_RATES(lev) ((lev) == 0) ? 0 : (MAX_CSHIELD_ABSORB / (NUM_CSHIELD_LEVELS - (lev)))

#define MAX_FATIGUE 10000
#define DEATH_TICKS CIT_CYCLE

ulong player_death_time = 0;

// Something has caused the player to become a fatality
// typically this is damage, but can be delayed-death due to craze
void player_dies() {
    extern void physics_zero_all_controls();
    extern void clear_digi_fx();
    extern short inventory_page;
#ifdef AUDIOLOGS
    extern char secret_pending_hack;
    secret_pending_hack = 0;
#endif

    // we should play funky death music
    mai_player_death();
    reset_input_system();
    chg_set_sta(GL_CHG_2); // disable the input system

    extern uchar weapon_button_up;
    weapon_button_up = TRUE;

    // clear off hud & prep for funky regen FX
    //   hud_unset(HUD_ALL);
    physics_zero_all_controls();

    // clear edms_state
    LG_memset(player_struct.edms_state, 0, sizeof(fix) * 12);

    // reset the inventory page
    inventory_page = 0;

    // hey no more things to do to player while he's dead
    // it's really a secret - don't do things to player
    // while there are cool secret_render_fx going on
    player_struct.dead = TRUE;
    set_dmg_percentage(DMG_BLOOD, 100); // 100 is an arbitrary number - we're trying to get it to do red static
    secret_render_fx = DYING_REND_SFX;

    clear_digi_fx();
    player_struct.num_deaths++;
}

    // -------------------------------------------------------
    // damage_player()
    //

#define DAMAGE_DIFFICULTY ((global_fullmap->cyber) ? 3 : 0)

ubyte damage_player(int damage, ubyte dtype, ubyte flags) {
    ubyte *cur_hp;
    short rawval;
    uchar dead = 0, damage_dealt = FALSE;
    char dlev, dmg_type = DMG_BLOOD;

    if (secret_render_fx > 0)
        return 0;

    if ((damage <= 0) || (player_struct.hit_points == 0)) // 0 hp is already dead, eh?
        return 0;

    if (global_fullmap->cyber && !player_struct.difficulty[CYBER_DIFF_INDEX])
        return 0;

    cur_hp = (global_fullmap->cyber) ? &player_struct.cspace_hp : &player_struct.hit_points;

    shield_used = FALSE;

    // check if shields should play a role
    if ((dtype == RADIATION_TYPE) || (dtype == BIO_TYPE))
        dmg_type = DMG_RAD;
    else if (!(flags & NO_SHIELD_ABSORBTION)) {
        byte absorb_rate, thresh_val;

        // compensate for difficulty level
        dlev = player_struct.difficulty[DAMAGE_DIFFICULTY];
        if (dlev != 3)
            damage >>= (2 - dlev);
        if (global_fullmap->cyber) {
            if (player_struct.softs.defense[SOFTWARE_CSHIELD]) {
                absorb_rate = CSHIELD_ABSORB_RATES(player_struct.softs.defense[SOFTWARE_CSHIELD]);
                thresh_val = CSHIELD_THRESHOLDS(player_struct.softs.defense[SOFTWARE_CSHIELD]);
            } else
                absorb_rate = thresh_val = 0;
        } else {
            absorb_rate = (byte)player_struct.shield_absorb_rate;
            thresh_val = player_struct.shield_threshold;
        }
        damage = shield_absorb_damage(damage, dtype, absorb_rate, thresh_val);
    }

    if (damage <= 0)
        return 0;

#ifdef WACKY_STATIC_USAGE
    // Play digi FX should go in here when we have appropriate SFX
    if ((!global_fullmap->cyber) && (damage > static_pain_base + rand() % static_pain_delta)) {
        extern char static_density, static_color, static_grouping;
        // Turn on fullscreen static & turn off any SFX that might be otherwise going on.
        fr_global_mod_flag(FR_SOLIDFR_STATIC, FR_SOLIDFR_MASK | FR_SFX_MASK);
        fr_solidfr_time = (static_pain_time);
        play_digi_fx(SFX_STATIC, -1);
    }
#endif

    // did we take more damage than hit points?? - eeeegggads! we're dead
    if ((*cur_hp) <= damage) {
        damage_dealt = TRUE;
        {
            if (global_fullmap
                    ->cyber) { // No matter what, the player can't be killed in one shot.....it takes at least two shots
                player_struct.cspace_hp = (player_struct.cspace_hp == 1) ? 0 : 1;
            } else // normal (non-cyberspace) damage - player's dead dead dead
            {
                if (*cur_hp > 0) {
#ifdef CRAZE_NODEATH
                    if ((player_struct.drug_status[DRUG_LSD] > 0) && (QUESTVAR_GET(COMBAT_DIFF_QVAR) < 3))
                        *cur_hp = 1;
                    else
#endif
                    {
                        *cur_hp = 0;
                        dead = TRUE;

                        // if we're carrying an object - let's drop it here!
                        if (object_on_cursor != OBJ_NULL) {
                            obj_move_to(object_on_cursor, &objs[PLAYER_OBJ].loc, TRUE);
                            pop_cursor_object();
                        }

                        // signal for the effects that happen with death
                        player_dies();
                    }
                }
            }
        }
    }
    if (!damage_dealt) {
        extern int mai_damage_sum;

        *cur_hp -= damage;
        mai_damage_sum += damage;
    }

    if (*cur_hp == 0)
        rawval = damage << 8;
    else
        rawval = ((damage << 8) / (*cur_hp)); // what is this minmax3/20xff thing?
                                              // it's my voodoo - minman
    if (!shield_used)
        set_dmg_percentage(dmg_type, (ubyte)lg_min(lg_max(rawval, ((damage * 3) / 2)), 0x00FF));

    // makes sure gamescreen knows that it should be updated
    chg_set_flg(VITALS_UPDATE);
    return (dead);
}

// --------------------------------------------------
// damage_object()
//
// return 0 - if object is still alive after damage
// return 1 - if object has been destroyed
//
// also does the appropriate texture map change if
// object is destroyed???????

ubyte damage_object(ObjID target_id, int damage, int dtype, ubyte flags) {
    int obclass = objs[target_id].obclass;
    int dead = 0;
    uchar tranq = FALSE;
    uchar stun = FALSE;
    short target_hp = ObjProps[OPNUM(target_id)].hit_points;

    // If we've already been destroyed, or don't care, thendon't bother us.
    if ((objs[target_id].info.inst_flags & INDESTRUCT_FLAG) ||
        ((target_id != player_struct.rep) && (objs[target_id].info.current_hp == 0)))
        return (0);

    // let the player get his/her own special treatment
    if (target_id == PLAYER_OBJ)
        dead = damage_player(damage, (ubyte)PRIMARY_DAMAGE(dtype), flags);
    else {
        // are we still alive - then do special stuff that we only care about if
        // we're still alive, makes too much sense
        if (objs[target_id].info.current_hp > damage) {
            // damage object - but it's not dead yet.
            dead = 0;
            if (obclass == CLASS_CRITTER) {
                int pct = (450L * damage) / objs[target_id].info.current_hp;
                tranq = dtype & TRANQ_FLAG;
                stun = (flags & STUN_ATTACK);

                if (tranq) {
                    tranq = FALSE;
                    // okay tranq - only if we've done damage, and we're lucky and the damage we're doing
                    // is a decent amount of the remaining life
                    if (damage)
                        tranq = (RndRange(&damage_rnd, 0, 100) < pct);
                } else if (stun) {
                    if (objs[target_id].subclass == CRITTER_SUBCLASS_ROBOT)
                        stun = FALSE;
                    else
                        stun = (RndRange(&damage_rnd, 0, 100) < pct);
                }
                ai_critter_hit(objs[target_id].specID, damage, tranq, stun);
            }
            // get rid of the hit points
            objs[target_id].info.current_hp -= damage;
        } else {
            objs[target_id].info.current_hp = 0;
            dead = 1;

            // Check to see whether or not there is cool special stuff to do when this thing
            // gets destroyed.  obj_combat_destroy returns whether or not to go ahead and
            // continue the destruction process
            if (obj_combat_destroy(target_id))
                ADD_DESTROYED_OBJECT(target_id);

            if (DESTROY_SOUND_EFFECT(ObjProps[OPNUM(target_id)].destroy_effect)) {
                extern ObjID damage_sound_id;
                extern char damage_sound_fx;

                damage_sound_fx = SFX_CPU_EXPLODE;
                damage_sound_id = target_id;
            }
        }

        if ((obclass == CLASS_CRITTER) && !global_fullmap->cyber) {
            ubyte seriousness = 0;
            extern void hud_report_damage(ObjID target, byte seriousness);

            // marc's desired code
            if (stun)
                seriousness = 7;
            else if (tranq)
                seriousness = 6;
            else if (!object_affect(target_id, dtype))
                seriousness = 5;
            else if (damage > target_hp)
                seriousness = 4;
            else if (damage > (target_hp * 4) / 5)
                seriousness = 3;
            else if (damage > target_hp / 5)
                seriousness = 2;
            else if (damage > 0)
                seriousness = 1;

            hud_report_damage(target_id, seriousness);
        }

        // If we damaged the currently targeted creature, let mfds know...
        if ((target_id == player_struct.curr_target) && !global_fullmap->cyber)
            mfd_notify_func(MFD_TARGET_FUNC, MFD_TARGET_SLOT, FALSE, MFD_ACTIVE, TRUE);
    }
    return (dead);
}

// -----------------------------------------------
// simple_damage_object() takes damage of a particular type with particular flags,
// and applies it to the object if it is vulnerable to the type.
// returns whether the object was destroyed.

uchar simple_damage_object(ObjID target, int damage, ubyte dtype, ubyte flags) {
    if (object_affect(target, 1 << (dtype - 1)))
        return damage_object(target, damage, dtype, flags);
    return FALSE;
}

#define FIRST_ENRG_PROJ_TYPE 7
#define DAMAGE_PROJ_DISTANCE (fix_make(0, 0x6000))

// OBJ_NULL victim means terrain
void slow_proj_hit(ObjID id, ObjID victim) {
    Combat_Pt origin;
    ObjLoc loc = objs[id].loc;
    ObjRefID current_ref;
    ObjID current_id;
    ubyte affect;
    ubyte dtype;
    ubyte proj_power;
    ubyte special_effect = EFFECT_VAL(ObjProps[OPNUM(id)].destroy_effect);
    int a;
    int weapon_triple;

    current_ref = MAP_GET_XY(OBJ_LOC_BIN_X(objs[id].loc), OBJ_LOC_BIN_Y(objs[id].loc))->objRef;

    if (objPhysicss[objs[id].specID].owner != PLAYER_OBJ) {
        if (special_effect)
            do_special_effect_location(id, special_effect, 0xFF, &loc, 0);
        return;
    }

    a = objPhysicss[objs[id].specID].bullet_triple;
    if (objs[id].info.type >= FIRST_ENRG_PROJ_TYPE) {
        weapon_triple = MAKETRIP(CLASS_GUN, GUN_SUBCLASS_BEAMPROJ, TRIP2TY(a));
        proj_power = TRIP2CL(a);
        dtype = BeamprojGunProps[SCTRIP(weapon_triple)].damage_type;
    } else {
        weapon_triple = MAKETRIP(CLASS_GUN, GUN_SUBCLASS_SPECIAL, TRIP2TY(a));
        proj_power = 100;
        dtype = SpecialGunProps[SCTRIP(weapon_triple)].damage_type;
    }
    if (weapon_triple == RAILGUN_TRIPLE) {
        do_explosion(loc, id, 0, &(game_explosions[1]));
        play_digi_fx_obj(SFX_EXPLOSION_1, 1, id);
    } else if (victim == OBJ_NULL) {
        while (current_ref != OBJ_REF_NULL) {
            current_id = objRefs[current_ref].obj;

            if (current_id != id) {
                affect = object_affect(current_id, dtype);
                if (affect) {
                    fix dist, deltax, deltay, deltaz;

                    deltax = fix_from_obj_coord(objs[id].loc.x - objs[current_id].loc.x);
                    deltay = fix_from_obj_coord(objs[id].loc.y - objs[current_id].loc.y);
                    deltaz = fix_from_obj_height_val(objs[id].loc.z - objs[current_id].loc.z);
                    dist = fix_mul(deltax, deltax) + fix_mul(deltay, deltay) + fix_mul(deltaz, deltaz);

                    if (dist < DAMAGE_PROJ_DISTANCE) {

                        origin.x = fix_make(-1, 0);
                        origin.y = fix_make(-1, 0);
                        origin.z = fix_make(-1, 0);

                        player_attack_object(current_id, weapon_triple, proj_power, origin);
                    }
                }
            }
            current_ref = objRefs[current_ref].next;
        }
    } else {
        origin.x = fix_from_obj_coord(loc.x);
        origin.y = fix_from_obj_coord(loc.y);
        origin.z = fix_from_obj_height(loc.z);

        player_attack_object(victim, weapon_triple, proj_power, origin);
    }
    if (special_effect)
        do_special_effect_location(id, special_effect, 0xFF, &loc, 0);
}

// returns whether it is being killed...
uchar special_terrain_hit(ObjID cobjid) {
    if (is_obj_destroyed(cobjid))
        return TRUE;

    if (objs[cobjid].obclass == CLASS_GRENADE) {
        if (objGrenades[objs[cobjid].specID].flags & GREN_ACTIVE_FLAG)
            ADD_DESTROYED_OBJECT(cobjid);
        else {
            return FALSE; // dead grenades stay alive
                          // in the sense that they are not live, but should remain physics live, see
        }
    } else if (objs[cobjid].obclass == CLASS_PHYSICS) {
        // only one terrain hit per turn!
        if (objPhysicss[objs[cobjid].specID].p3.x)
            return FALSE;
        else
            objPhysicss[objs[cobjid].specID].p3.x = 3;

        slow_proj_hit(cobjid, OBJ_NULL);
        EDMS_obey_collisions(objs[cobjid].info.ph);

        if (PhysicsProps[CPNUM(cobjid)].flags & PROJ_PRESERVE_WALL)
            return FALSE;

        // Signal a miss to our controller
        ai_misses(objs[objPhysicss[objs[cobjid].specID].owner].specID);
    }
    ADD_DESTROYED_OBJECT(cobjid);
    return TRUE;
}

#define DMG_THRESH 0x60000
#define HACK_THRESH 0x1800
#define SPCL_THRESH 0x80

// HEY COMMENTED OUT PROCEDURE
#ifdef CALLS_WERENT_SLOW
uchar terrain_damage_object(physics_handle ph, fix raw_damage) {
    uchar dead = FALSE;
    ObjID target = physics_handle_to_id(ph);

    if (ObjProps[OPNUM(cobjid)].flags & SPCL_TERR_DMG) {
        if (raw_damage > SPCL_THRESH) {
            objs[target].info.current_hp = 0;
            ADD_DESTROYED_OBJECT(target);
            dead = TRUE;
        }
    } else
        dead = simple_damage_object(target, (raw_damage - HACK_THRESH) >> 10, EXPLOSION_FLAG, NO_SHIELD_ABSORBTION);

    return (dead);
}
#endif

// ------------------------------
// compute_damage()
//
// computes damage to be inflicted on target
//
// target         ObjID of object that will be damaged
// damage_type    damage type of the attack       example : energy, explosion, physical, needle, etc...
// damage_mod     raw value of damage inflicted
// offense        offensive value of the attack
// penet          penetration value of the attack
// power_level    percentage of damage to be inflicted
// *effect        returns the effect number to be played
// *effect_row    a pointer to the effect row

int compute_damage(ObjID target, int damage_type, int damage_mod, ubyte offense, ubyte penet, int power_level,
                   ubyte *effect, ubyte *effect_row, ubyte attack_effect_type) {
    int damage = 0;
    int delta;
    int modifier;
    ubyte affect;

    // AFFECTIVENESS
    //
    affect = object_affect(target, damage_type);
    if (affect) {
        damage = (damage_mod * power_level * affect) / 100;

        damage = armor_absorption(damage, ID2TRIP(target), penet);

        if (!global_fullmap->cyber) {
            // Compute the CRITICAL HIT affector
            //
            delta = random_bell_modifier((target == PLAYER_OBJ));
            modifier = (target == PLAYER_OBJ) ? (offense + delta - PLAYER_DEFENSE_VALUE)
                                              : ((offense - ObjProps[OPNUM(target)].defense_value) + delta);

            if (modifier < -3)
                damage /= SQUARE(modifier + 3); // plus because it's negative
            else if ((modifier > 3) && (ObjProps[OPNUM(target)].defense_value != 0xFF)) {
                // we are going to do critical damage, but we must check that the defense value
                // isn't 0xFF (meaning it's invulnerable to critical hits).

                // just put an upper bound to be safe.
                if (modifier > 12)
                    modifier = 12;
                damage = (damage * modifier) / 3;
            }
        }

        // TOUGHNESS
        if (ObjProps[OPNUM(target)].toughness != 3)
            damage >>= ObjProps[OPNUM(target)].toughness;
        else
            damage = 0;

        if (damage < 0) {
            damage = 0;
        } else {
            damage = randomize_damage(damage, target == PLAYER_OBJ);

            // bound to realistic max
            if (damage > MAX_DAMAGE)
                damage = MAX_DAMAGE;
        }

        // take either the normal effect, unless we did lots of damage - then play bigger one
        if ((effect_row != NULL) && (attack_effect_type != SPECIAL_TYPE) && (effect != NULL) &&
            !global_fullmap->cyber) {
            // check to see if we did damage - if so - do appropriate hit effect
            // otherwise do a wall hit effect - MEANS NO DAMAGE/EFFECT
            if (damage)
                *effect = (damage < (damage_mod * 7) / 6) ? *(effect_row) : *(effect_row + 1);
            else
                *effect = effect_matrix[NON_CRITTER_EFFECT][attack_effect_type][0];
        } else if (effect != NULL)
            *effect = 0;
    } else if (attack_effect_type != SPECIAL_TYPE) {
        // we didn't affect - so do the no effect one!
        if (effect)
            *effect = (!global_fullmap->cyber) ? effect_matrix[NON_CRITTER_EFFECT][attack_effect_type][0] : 0;
    } else {
	// Special damage type with no damage = no effect.
	if (effect) {
	    *effect = 0;
	}
    }

    return (damage);
}

// --------------------------------------------------------------
// critter_hit_effect()
//
void critter_hit_effect(ObjID target, ubyte effect, Combat_Pt location, int damage, int max_damage) {
    fix radius, height;
    byte ht;
    ObjLoc loc = objs[target].loc;

    // temporary - to hit effect_center - will take care of later
    SET_EFFECT_LOC(target, EFFECT_CENTER);

    SET_EFFECT_NUM(target, effect);
    SET_EFFECT_FRAME(target, 0);

    radius = fix_make(ObjProps[OPNUM(target)].physics_xr, 0) / (PHYSICS_RADIUS_UNIT * 4);

    height = fix_from_obj_height_val(loc.z);
    ht = ((height - location.z) / radius) + 4;
    if (ht < 1)
        ht = 1;
    else if (ht > 7)
        ht = 7;

    SET_EFFECT_HEIGHT(target, ht);

    if (damage < (max_damage / 3)) {
        SET_EFFECT_DUAL(target, 0);
        SET_EFFECT_SCALE(target, 1);
    } else if (damage < max_damage) {
        SET_EFFECT_DUAL(target, 0);
        SET_EFFECT_SCALE(target, 2);
    } else {
        SET_EFFECT_DUAL(target, 1);
        SET_EFFECT_SCALE(target, 3);
    }
}

// ---------------------------------------------------------------------------
// get_damage_estimate()
//
// Returns a number from DAMAGE_MIN to DAMAGE_MAX indicating how wounded
// a creature is.

int get_damage_estimate(ObjSpecID osid) {
    ObjID id = objCritters[osid].id;
    int triple = ID2TRIP(id);

    return (DAMAGE_MAX - ((objs[id].info.current_hp * DAMAGE_MAX) / ObjProps[OPTRIP(triple)].hit_points));
}

// -------------------------------------------------------
// attack_object()
//
// target         ObjID of object that will be damaged
// damage_type    damage type of the attack       example : energy, explosion, physical, needle, etc...
// damage_mod     raw value of damage inflicted
// offense        offensive value of the attack
// penet          penetration value of the attack
// flags          flags for the attack          (currently just to see if player's shields absorb damage)
// power_level    percentage of damage to be inflicted
// *effect        returns the effect number to be played
// *effect_row    a pointer to the effect row
//
// returns whether target died

ubyte attack_object(ObjID target, int damage_type, int damage_mod, ubyte offense, ubyte penet, ubyte flags,
                    int power_level, ubyte *effect_row, ubyte *effect, ubyte attack_effect_type,
                    int *damage_inflicted) {
    int damage;
    char diff;

    if (effect)
        *effect = 0;
    // check to see if we have a valid target
    if (target == OBJ_NULL)
        return (0);
    else if (is_obj_destroyed(target))
        return (0);
    // the next is same as dead!
    else if ((objs[target].info.current_hp == 0) && DESTROY_OBJ_EFFECT(ObjProps[OPNUM(target)].destroy_effect))
        return (0);

    // get the difficulty level
    diff = (global_fullmap->cyber) ? player_struct.difficulty[CYBER_DIFF_INDEX]
                                   : player_struct.difficulty[COMBAT_DIFF_INDEX];

    // look combat difficult 0 setting - KILL OBJECTS IN ONE SHOT!!! - if object's toughness isn't 3
    if ((ObjProps[OPNUM(target)].toughness != 3) && !diff && (target != PLAYER_OBJ)) {
        damage = objs[target].info.current_hp;
        if (effect)
            *effect = (effect_row) ? *(effect_row + 1) : 0;
    } else {
#ifdef SELFRUN // we do max damage if we're in self run
        damage = ((objs[target].obclass == CLASS_CRITTER) && (target != PLAYER_OBJ)) ? 0xFF : 0;
#else
        damage = compute_damage(target, damage_type, damage_mod, offense, penet, power_level, effect, effect_row,
                                attack_effect_type);
#endif
    }

    if (damage_inflicted)
        *damage_inflicted = damage;

    // okay let's check if we're destroying an object that has a flagged destroy effect
    // if the high bit is flagged then we will destroy the object during the animation
    if ((objs[target].info.current_hp <= damage) && DESTROY_OBJ_EFFECT(ObjProps[OPNUM(target)].destroy_effect)) {
        objs[target].info.current_hp = 0;
        if (effect)
            *effect = ObjProps[OPNUM(target)].destroy_effect;
        return (TRUE);
    }
    return (damage_object(target, damage, damage_type, flags));
}

    // -------------------------------------------------------------
    // player_attack_object()
    //

#define CRAZE_DAMAGE_MOD 2

ubyte player_attack_object(ObjID target, int wpn_triple, int power_level, Combat_Pt origin) {
    ubyte offense;
    int damage_mod;
    int wpn_class = TRIP2CL(wpn_triple);
    int dtype;
    int damage_inflicted;
    int prop_val;
    ubyte penet;
    ubyte effect;
    ubyte *effect_row;
    ubyte attack_effect_type;
    ubyte special_effect = 0;
    ubyte flags = 0;
    ObjID effect_id = OBJ_NULL;
    uchar dead = FALSE;
    uchar new_loc = FALSE;
    ObjLoc loc;
    ubyte effect_class =
        (objs[target].obclass == CLASS_CRITTER) ? CritterProps[CPNUM(target)].hit_effect : NON_CRITTER_EFFECT;

    // Special targeting ware hack
    if ((objs[target].obclass == CLASS_CRITTER) && (get_player_ware_version(WARE_HARD, HARDWARE_TARGET) > 3) &&
        (player_struct.curr_target == OBJ_NULL) && (!global_fullmap->cyber)) {
        select_current_target(target, FALSE);
    }

    switch (wpn_class) {
    case (CLASS_GUN): // Beam weapon is the only gun with damage type
        switch (TRIP2SC(wpn_triple)) {
        case (GUN_SUBCLASS_HANDTOHAND):
            prop_val = SCTRIP(wpn_triple);
            damage_mod = HandtohandGunProps[prop_val].damage_modifier;
            offense = HandtohandGunProps[prop_val].offense_value;
            if (player_struct.drug_status[DRUG_LSD] > 0) {
                damage_mod <<= CRAZE_DAMAGE_MOD;
                offense += CRAZE_DAMAGE_MOD;
            }
            dtype = HandtohandGunProps[prop_val].damage_type;
            penet = HandtohandGunProps[prop_val].penetration;
            attack_effect_type = HAND_TYPE;
            break;
        case (GUN_SUBCLASS_BEAM):
            prop_val = SCTRIP(wpn_triple);
            damage_mod = BeamGunProps[prop_val].damage_modifier;
            offense = BeamGunProps[prop_val].offense_value;
            dtype = BeamGunProps[prop_val].damage_type;
            penet = BeamGunProps[prop_val].penetration;
            attack_effect_type = BEAM_TYPE;
            break;
        case (GUN_SUBCLASS_SPECIAL):
            prop_val = SCTRIP(wpn_triple);
            damage_mod = SpecialGunProps[prop_val].damage_modifier;
            offense = SpecialGunProps[prop_val].offense_value;
            dtype = SpecialGunProps[prop_val].damage_type;
            penet = SpecialGunProps[prop_val].penetration;
            attack_effect_type = SPECIAL_TYPE;
            special_effect = EFFECT_VAL(ObjProps[OPTRIP(SpecialGunProps[prop_val].proj_triple)].destroy_effect);
            break;
        case (GUN_SUBCLASS_BEAMPROJ):
            prop_val = SCTRIP(wpn_triple);
            damage_mod = BeamprojGunProps[prop_val].damage_modifier;
            offense = BeamprojGunProps[prop_val].offense_value;
            dtype = BeamprojGunProps[prop_val].damage_type;
            penet = BeamprojGunProps[prop_val].penetration;
            attack_effect_type = SPECIAL_TYPE;
            special_effect = EFFECT_VAL(ObjProps[OPTRIP(BeamprojGunProps[prop_val].proj_triple)].destroy_effect);
            if (BeamprojGunProps[prop_val].flags & 0x02)
                flags |= STUN_ATTACK;
            break;
        }
        break;
    case (CLASS_AMMO):
        damage_mod = AmmoProps[CPTRIP(wpn_triple)].damage_modifier;
        offense = AmmoProps[CPTRIP(wpn_triple)].offense_value;
        dtype = AmmoProps[CPTRIP(wpn_triple)].damage_type;
        penet = AmmoProps[CPTRIP(wpn_triple)].penetration;
        attack_effect_type = PROJ_TYPE;
        break;
    case (CLASS_GRENADE):
        damage_mod = GrenadeProps[CPTRIP(wpn_triple)].damage_modifier;
        offense = GrenadeProps[CPTRIP(wpn_triple)].offense_value;
        dtype = GrenadeProps[CPTRIP(wpn_triple)].damage_type;
        penet = GrenadeProps[CPTRIP(wpn_triple)].penetration;
        attack_effect_type = GREN_TYPE;
        break;
    default:
        return (0);
        break;
    }
    effect_row = effect_matrix[effect_class][attack_effect_type];
    dead = attack_object(target, dtype, damage_mod, offense, penet, flags, power_level, effect_row, &effect,
                         attack_effect_type, &damage_inflicted);

    // this is for a slow projectile spang - not for objects exploding
    if (attack_effect_type == SPECIAL_TYPE)
        effect = 0; // special_effect;

    if (dead) {
        ubyte old_effect = effect;

        // check to see if we're suppose to play an animation if object is destroyed....
        if (EFFECT_VAL(ObjProps[OPNUM(target)].destroy_effect))
            effect = ObjProps[OPNUM(target)].destroy_effect;

        if (old_effect != effect) {
            new_loc = TRUE;
            loc = objs[target].loc;
        }
    }

    if (effect != 0) {
        //      if (objs[target].obclass == CLASS_CRITTER)
        //         critter_hit_effect(target, effect, origin, damage_inflicted, damage_mod);
        //#ifdef REMOVE_OLD_EFFECT
        //      else
        //#endif
        {
            fix deltax, deltay, dist;

            // if it's a 3-d model - let's get the right place
            if (ObjProps[OPNUM(target)].render_type == 1) {
                loc = objs[target].loc;

                if (ObjProps[OPNUM(target)].physics_xr > 20) {
                    // let's randomize the hit by a bit if object is big enough
                    loc.x += (RndRange(&damage_rnd, 0, 0x40) - 0x20);
                    loc.y += (RndRange(&damage_rnd, 0, 0x40) - 0x20);
                    loc.z +=
                        ((RndRange(&damage_rnd, 0, 0x10) - 0x08) +
                         obj_height_from_fix(fix_make(ObjProps[OPNUM(target)].physics_xr, 0) / PHYSICS_RADIUS_UNIT));
                }

                // and in the end - let's bring the effect up to the center of the object
            } else if (!new_loc) {
                // if we've been given bad info - let's just go on
                if ((origin.x < 0) || (fix_int(origin.y) > 64)) {
                    return (dead);
                }

                loc.x = obj_coord_from_fix(origin.x);
                loc.y = obj_coord_from_fix(origin.y);
                loc.z = obj_height_from_fix(origin.z);
            }

            deltax = OBJ_LOC_VAL_TO_FIX(objs[PLAYER_OBJ].loc.x - loc.x);
            deltay = OBJ_LOC_VAL_TO_FIX(objs[PLAYER_OBJ].loc.y - loc.y);
            dist = fix_fast_pyth_dist(deltax, deltay) << 2;

            // move explosion towards player
            loc.x += obj_coord_from_fix(fix_div(deltax, dist));
            loc.y += obj_coord_from_fix(fix_div(deltay, dist));

            effect_id = do_special_effect_location(target, effect, 0xFF, &loc, 0);
        }
    }

    if (attack_effect_type == BEAM_TYPE) {
        extern ObjID beam_effect_id;
        if (effect_id != OBJ_NULL) {
            beam_effect_id = effect_id;
            hudobj_set_id(beam_effect_id, TRUE);
        }
    }

    return (dead);
}
