/*

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/gamesys.c $
 * $Revision: 1.128 $
 * $Author: xemu $
 * $Date: 1994/11/28 06:42:41 $
 *
 */

#include <stdbool.h>
#include <stdlib.h>

#include "ai.h"
#include "cyber.h"
#include "cybstrng.h"
#include "damage.h"
#include "diffq.h" // for time limit
#include "drugs.h"
#include "effect.h"
#include "email.h"
#include "faketime.h"
#include "frflags.h"
#include "frprotox.h"
#include "fullscrn.h"
#include "gameloop.h"
#include "gameobj.h"
#include "gamerend.h"
#include "gamesys.h"
#include "hud.h"
#include "input.h"
#include "lvldata.h"
#include "mainloop.h"
#include "map.h"
#include "mfdfunc.h"
#include "miscqvar.h"
#include "musicai.h"
#include "newmfd.h"
#include "objcrit.h"
#include "objects.h"
#include "objgame.h"
#include "objprop.h"
#include "objsim.h"
#include "objstuff.h"
#include "objuse.h"
#include "otrip.h"
#include "palfx.h"
#include "pathfind.h"
#include "player.h"
#include "physics.h"
#include "rendfx.h"
#include "rendtool.h"
#include "schedule.h"
#include "sfxlist.h"
#include "shodan.h"
#include "tools.h"
#include "tpolys.h"
#include "trigger.h"
#include "visible.h"
#include "wares.h"
#include "weapons.h"
#include "cutsloop.h"

// -------
// DEFINES
// -------
#define SECOND_UPDATE_FREQ APPROX_CIT_CYCLE_HZ
#define HEALTH_RESTORE_PRECISION (APPROX_CIT_CYCLE_SHFT)
#define HEALTH_RESTORE_SHF (6u)
#define HEALTH_RESTORE_UNIT (1u << HEALTH_RESTORE_SHF) // unit time for hp/energy regen
#define HEALTH_RESTORE_MASK (HEALTH_RESTORE_UNIT - 1)
#define sqr(x) ((x) * (x))
#define MAX_FATIGUE 10000
#define FATIGUE_WARNING_LEVEL 7000

// -------
// GLOBALS
// -------
int run_fatigue_rate = 5;
extern short fr_solidfr_time;
extern short fr_sfx_time;
ulong fr_shake_time;
uchar gamesys_on = TRUE;

// hud vars
short enviro_edrain_rate = 0;
short enviro_absorb_rate = 0;

LevelData level_gamedata;
Schedule game_seconds_schedule;

// prototypes
void check_nearby_objects(void);
void fatigue_player(void);
uchar shodan_phase_in(uchar *bitmask, short x, short y, short w, short h, short num, uchar dir);
uchar panel_ref_sanity(ObjID obj);
int apply_rate(int var, int rate, int t0, int t1, int vmin, int vmax);
void do_stuff_every_second(void);
void expose_player_real(short damage, ubyte type, ushort tsecs);

void game_sched_init(void) { schedule_init(&game_seconds_schedule, GAME_SCHEDULE_SIZE, FALSE); }

void game_sched_free(void) {
    //extern errtype schedule_free(Schedule * s);
    schedule_free(&game_seconds_schedule);
}

short fatigue_accum_rate = 100;

#define OBJ_CHECK_TICKS CIT_CYCLE
ulong obj_check_time = 0;

#define MACHINE_LAYER_BASE 25
#define OBJ_CHECK_RADIUS 7

void unshodanizing_callback(ObjID id, intptr_t user_data) {
    bool ud = (bool)user_data;
    if (ud) {
        objBigstuffs[objs[id].specID].data2 = SHODAN_STATIC_MAGIC_COOKIE | (TPOLY_TYPE_CUSTOM_MAT << TPOLY_INDEX_BITS);
        objBigstuffs[objs[id].specID].cosmetic_value = 0;
        objs[id].info.current_frame = 0;
    } else
        add_obj_to_animlist(id, FALSE, TRUE, FALSE, 0, 3, TRUE, ANIMCB_REMOVE);
}

#define STOCHASTIC_SHODAN_MASK 0xF
#define STOCHASTIC_MONSTER_MASK 0xF

#define STOCHASTIC_SPARKING_MASK 0xFF
#define STOCHASTIC_SPARKING_LEVEL 0x10
#define SPARKING_RADIUS 5

#define STOCHASTIC_FROG_MASK 0xF
#define STOCHASTIC_FROG_LEVEL 0x2
#define FIRST_GROVE_LEVEL 11

#define NEAR_NOISE_RADIUS 5
#define PERIL_RADIUS 4

#define MONSTER_THEME_PATH_LENGTH 12

void check_nearby_objects() {
    short x, y;
    int dist, best_dist;
    ObjRefID oref;
    int trip;
    extern char mlimbs_machine;
    extern int mlimbs_monster;
    int new_monster;
#ifdef USE_3DREP_FOR_SHODANIZING
    short rep;
#endif
    ObjID id;
    ObjSpecID osid;
    LGPoint dest_pt, source_pt;
    int pf_id;
    extern uchar music_on;

    // Should probably distribute different kinds of checks so that there is not a big hit every OBJ_CHECK_TIME
    if (obj_check_time < player_struct.game_time) {
        mlimbs_machine = 0;
        mlimbs_peril = 0;
        best_dist = OBJ_CHECK_RADIUS + 1;
        source_pt.x = OBJ_LOC_BIN_X(objs[PLAYER_OBJ].loc);
        source_pt.y = OBJ_LOC_BIN_Y(objs[PLAYER_OBJ].loc);
        new_monster = -1;
        for (x = PLAYER_BIN_X - OBJ_CHECK_RADIUS; x < PLAYER_BIN_X + OBJ_CHECK_RADIUS; x++) {
            for (y = PLAYER_BIN_Y - OBJ_CHECK_RADIUS; y < PLAYER_BIN_Y + OBJ_CHECK_RADIUS; y++) {
                // Make sure we ain't lookin' off the edge of the map...
                if ((x < 0) || (x >= MAP_XSIZE) || (y < 0) || (y >= MAP_YSIZE))
                    continue;

                // Hmm, I should probably modularize out the individual kinds of checks....
                oref = me_objref(MAP_GET_XY(x, y));
                while (oref != OBJ_REF_NULL) {
                    id = objRefs[oref].obj;
                    dist = long_fast_pyth_dist(x - PLAYER_BIN_X, y - PLAYER_BIN_Y);
                    if (id != PLAYER_OBJ) {
                        osid = objs[id].specID;
                        trip = ID2TRIP(id);
                        switch (objs[id].obclass) {
                        case CLASS_CRITTER:
                            if (music_on) {
                                if ((new_monster == -1) && (dist < best_dist)) {
                                    // Boy, I wonder how gruesomely slow this is going to be...
                                    dest_pt.x = OBJ_LOC_BIN_X(objs[id].loc);
                                    dest_pt.y = OBJ_LOC_BIN_Y(objs[id].loc);
                                    pf_id = -1;
                                    if (pf_id != -1) {
                                        check_requests(TRUE);
                                        if ((paths[pf_id].num_steps != 0) &&
                                            (paths[pf_id].num_steps <= MONSTER_THEME_PATH_LENGTH)) {
                                            delete_path(pf_id);
                                            pf_id = -1;
                                        }
                                    }
                                    // go ahead if we couldn't allocate a path or the path was sufficiently short
                                    if (pf_id == -1) {
                                        switch (ID2TRIP(id)) {
                                        case SERVBOT_TRIPLE:
                                        case REPAIRBOT_TRIPLE:
                                        case REPAIRBOT2_TRIPLE:
                                        case FLIER_TRIPLE:
                                        case AUTOBOMB_TRIPLE:
                                            new_monster = MONSTER_MUSIC_SMALL_ROBOT;
                                            break;
                                        default:
                                            new_monster = objs[id].subclass;
                                            break;
                                        }
                                        best_dist = dist;
                                    } else {
                                        delete_path(pf_id);
                                        pf_id = -1;
                                    }
                                }
                                if ((dist < PERIL_RADIUS) && (objCritters[osid].mood != AI_MOOD_FRIENDLY) &&
                                    (objCritters[osid].orders != AI_ORDERS_SLEEP)) {
                                    mlimbs_peril = DEFAULT_PERIL_MAX;
                                }
                            }
                            if ((dist < NEAR_NOISE_RADIUS) && (CritterProps[CPTRIP(trip)].near_sound != 255) &&
                                (objCritters[osid].mood != AI_MOOD_ATTACKING) &&
                                (get_crit_posture(osid) != DEATH_CRITTER_POSTURE) &&
                                ((rand() & STOCHASTIC_MONSTER_MASK) == 1))
                                play_digi_fx_obj(CritterProps[CPTRIP(trip)].near_sound, 1, objCritters[osid].id);
                            break;
                        }
                        switch (trip) {
                        case MUSIC_MARK_TRIPLE:
                            // Hm, should we have this use comparator_check
                            if (player_struct.level >= FIRST_GROVE_LEVEL) {
                                if ((rand() & STOCHASTIC_FROG_MASK) < STOCHASTIC_FROG_LEVEL)
                                    play_digi_fx_obj(SFX_GROVE_1, 1, id);
                            } else
                                mlimbs_machine = MACHINE_LAYER_BASE + objTraps[objs[id].specID].p1;
                            break;
                        case SPARK_CABLE_TRIPLE:
                            if ((dist < SPARKING_RADIUS) &&
                                ((rand() & STOCHASTIC_SPARKING_MASK) < STOCHASTIC_SPARKING_LEVEL))
                                play_digi_fx_obj(SFX_SPARKING_CABLE, 1, id);
                            break;
                        case HORZ_KLAXON_TRIPLE: {
                            // uchar digi_fx_playing(int fx_id, int *handle_ptr);
                            if (!digi_fx_playing(SFX_KLAXON, NULL))
                                play_digi_fx_obj(SFX_KLAXON, 1, id);
                        } break;
                        case TV_TRIPLE:
                        case MONITOR2_TRIPLE:
                        case SCREEN_TRIPLE:
                        case BIGSCREEN_TRIPLE:
                        case SUPERSCREEN_TRIPLE:
#ifdef USE_3DREP_FOR_SHODANIZING
                            rep = compute_3drep(&(objs[id]), id, ObjProps[OPNUM(id)].render_type);
                            if ((rep & TPOLY_INDEX_MASK) == SHODAN_STATIC_MAGIC_COOKIE)
#else
                            if (((objBigstuffs[objs[id].specID].data2 & TPOLY_INDEX_MASK) ==
                                 SHODAN_STATIC_MAGIC_COOKIE) &&
                                (((objBigstuffs[objs[id].specID].data2 & TPOLY_TYPE_MASK) >> TPOLY_INDEX_BITS) ==
                                 TPOLY_TYPE_CUSTOM_MAT))
#endif
                            {
                                // Chance of shodanizing....
                                if ((rand() & STOCHASTIC_SHODAN_MASK) == 1) {
                                    objBigstuffs[objs[id].specID].data2 = FIRST_SHODAN_ANIM;
                                    objBigstuffs[objs[id].specID].cosmetic_value = NUM_SHODAN_FRAMES;
                                    objs[id].info.current_frame = 0;
                                    // animate me:  cycle, but don't repeat
                                    add_obj_to_animlist(id, FALSE, FALSE, FALSE, 0, 3, 0, ANIMCB_REMOVE);
                                }
                            }
                            break;
                        default:
                            ;
                        }
                    }
                    oref = objRefs[oref].next;
                }
            }
        }
        mlimbs_monster = new_monster;
        obj_check_time = player_struct.game_time + OBJ_CHECK_TICKS;
    }
}

/*
#define CFG_FATIGUE_VAR "fatigue"
extern ubyte fatigue_threshold;
void reload_fatigue_parms()
{
   int i;
   int vec[5];
   i = 5;
//   player_struct.fatigue_regen = DEFAULT_FATIGUE_REGEN;
   if (config_get_value(CFG_FATIGUE_VAR,CONFIG_INT_TYPE,vec,&i))
   {
      switch (i)
      {
      case 5:
         fatigue_accum_rate                  = vec[4];
      case 4:
         player_struct.fatigue_regen_max     = vec[3];
      case 3:
         player_struct.fatigue_regen_base    = vec[2];
      case 2:
         fatigue_threshold                   = vec[1];
      case 1:
         run_fatigue_rate                    = vec[0];
      default:
         break;
      }
   }

}
*/

uchar fatigue_warning;
#define fatigue_val(x) (((x) > SPRINT_CONTROL_THRESHOLD) ? ((int)(x)-SPRINT_CONTROL_THRESHOLD) : 0)
#define FATIGUE_DENOM (CONTROL_MAX_VAL - SPRINT_CONTROL_THRESHOLD)
uchar gamesys_fatigue = TRUE;

#define SKATE_MOD 8

void fatigue_player(void) {
    byte *c = player_struct.controls;
    int deltat, deltaf;
    extern uchar jumpjets_active;
    if (gamesys_fatigue && !jumpjets_active && !EDMS_pelvis_is_climbing()) {
        deltat = player_struct.deltat;
        deltaf = run_fatigue_rate * (fatigue_val(c[CONTROL_YVEL]) + fatigue_val(2 * c[CONTROL_ZVEL]) +
                                     // fatigue_val(c[CONTROL_XVEL])/64 +
                                     // fatigue_val(c[CONTROL_XYROT])/256 +
                                     // fatigue_val(c[CONTROL_XZROT])/256 +
                                     // fatigue_val(c[CONTROL_YZROT])/256 +
                                     0);
        if (player_struct.posture != POSTURE_STAND)
            deltaf /= sqr(player_struct.posture + 1);
        if (motionware_mode == MOTION_SKATES)
            deltaf /= SKATE_MOD;
        player_struct.fatigue += deltaf * deltat / FATIGUE_DENOM + player_struct.fatigue_spend * deltat;
        if (player_struct.fatigue > MAX_FATIGUE)
            player_struct.fatigue = MAX_FATIGUE;
        if (player_struct.drug_status[DRUG_STAMINUP] <= 0 && player_struct.fatigue > FATIGUE_WARNING_LEVEL) {
            if (!fatigue_warning) {
                hud_set(HUD_FATIGUE);
                fatigue_warning = TRUE;
            }
        } else if (fatigue_warning) {
            hud_unset(HUD_FATIGUE);
            fatigue_warning = FALSE;
        }
    }
}

uchar gamesys_render_fx = TRUE;
uchar gamesys_restore_health = TRUE;
uchar gamesys_slow_proj = TRUE;
uchar gamesys_beam_wpns = TRUE;
uchar gamesys_drugs = TRUE;

ulong next_contin_trig;

#define NUM_CONTIN_SECONDS 5
#define CONTIN_INTERVAL CIT_CYCLE *NUM_CONTIN_SECONDS

short fr_surge_time = 0;
char surg_fx_frame = 0;
short surge_duration = 60;

#define NUM_SURG_FX_FRAMES 7
// WH: Unportable, replaced with actual values
// short surge_vals[NUM_SURG_FX_FRAMES] = {-1 << 8, -5 << 8, 0 << 8, 2 << 8, 2 << 8, 1 << 8, 1 << 8};
short surge_vals[NUM_SURG_FX_FRAMES] = {-256, -1280, 0, 512, 512, 256, 256};

#define CONQUER_THRESHOLD 512
#define UNCONQUER_THRESHOLD 32
#define MAX_SHODAN_FAILURES 10
char thresh_fail = 0;
uchar shodan_phase_in(uchar *bitmask, short x, short y, short w, short h, short num, uchar dir) {
    int i = 0, nx, ny, val, oval;
    while (i < num) {
        nx = rand() % w;
        ny = rand() % h;
        val = x + (y * FULL_VIEW_WIDTH);
        val += (nx + (ny * FULL_VIEW_WIDTH));
        oval = val;

        if (dir) {
            while (SHODAN_CONQUER_GET(bitmask, val) && (val < SHODAN_BITMASK_SIZE) && (val - oval <= CONQUER_THRESHOLD))
                val++;
            if (val < SHODAN_BITMASK_SIZE)
                SHODAN_CONQUER_SET(bitmask, val);
            if (val - oval > CONQUER_THRESHOLD) {
                i = num;
                thresh_fail++;
            }
        } else {
            while (!SHODAN_CONQUER_GET(bitmask, val) && (val < SHODAN_BITMASK_SIZE) &&
                   (val - oval <= UNCONQUER_THRESHOLD))
                val++;
            if (val < SHODAN_BITMASK_SIZE)
                SHODAN_CONQUER_UNSET(bitmask, val);
            if (val - oval > UNCONQUER_THRESHOLD)
                i = num;
        }
        i++;
    }
    if (thresh_fail > MAX_SHODAN_FAILURES) {
        return (TRUE);
    }
    return (FALSE);
}

#define NUM_SHODAN_REGIONS 4
short shodan_region_full_x[] = {0, 0, FULL_VIEW_WIDTH * 7 / 8, 0};
short shodan_region_full_y[] = {0, 0, 0, FULL_VIEW_HEIGHT * 7 / 8};
short shodan_region_full_width[] = {FULL_VIEW_WIDTH, FULL_VIEW_WIDTH / 8, FULL_VIEW_WIDTH / 8, FULL_VIEW_WIDTH};
short shodan_region_full_height[] = {FULL_VIEW_HEIGHT / 8, FULL_VIEW_HEIGHT, FULL_VIEW_HEIGHT, FULL_VIEW_HEIGHT / 8};

// stolen from trigger.c
#define GAME_OVER_HACK 0x6

#define KEY_CODE_ESC 0x1b

#define DETECT_AUDIOLOG_QVAR_CHANGE

errtype gamesys_run(void) {
    ObjSpecID osi;
    uchar dummy;
    extern uchar *shodan_bitmask;

#ifdef AUTOCORRECT_DIFF_TRASH
    for (int i = 0; i < 4; i++) {
        extern char diff_qvars[4];
        if (player_struct.difficulty[i] != QUESTVAR_GET(diff_qvars[i]))
            QUESTVAR_SET(diff_qvars[i], player_struct.difficulty[i]);
    }
#endif

    //   page_amount = 0;

    if (!gamesys_on)
        return (OK);

    if (gamesys_render_fx) {
        if (fr_solidfr_time > 0) {
            fr_solidfr_time -= player_struct.deltat;
            if (fr_solidfr_time <= 0)
                fr_global_mod_flag(0, FR_SOLIDFR_MASK);
        }

        if (fr_sfx_time > 0) {
            fr_sfx_time -= player_struct.deltat;
            if (fr_sfx_time <= 0)

            {
                fr_global_mod_flag(0, FR_SFX_MASK);
            }
        }
        if (fr_surge_time > 0) {
            fr_surge_time -= player_struct.deltat;
            if (fr_surge_time <= 0) {
                if (surg_fx_frame >= NUM_SURG_FX_FRAMES)
                    fr_surge_time = 0;
                else {
                    fr_surge_time = surge_duration;
                    set_global_lighting(surge_vals[surg_fx_frame]);
                }
                surg_fx_frame++;
            }
        }
    }

    if (shodan_bitmask != NULL) {
        if (player_struct.game_time > time_until_shodan_avatar) {
            char i;
            if (thresh_fail) {
                errtype trap_hack_func(int p1, int p2, int p3, int p4);
                begin_shodan_conquer_fx(FALSE);
                shodan_bitmask = NULL;
                trap_hack_func(GAME_OVER_HACK, 0, 0, 0);
                palfx_fade_down();
            } else {
                for (i = 0; i < NUM_SHODAN_REGIONS; i++) {
                    shodan_phase_in(shodan_bitmask, shodan_region_full_x[i], shodan_region_full_y[i],
                                    shodan_region_full_width[i], shodan_region_full_height[i],
                                    QUESTVAR_GET(CYBER_DIFF_QVAR) + 1, TRUE);
                    shodan_phase_in(shodan_bitmask, 0, 0, FULL_VIEW_WIDTH, FULL_VIEW_HEIGHT,
                                    (3 * QUESTVAR_GET(CYBER_DIFF_QVAR)) + 1, TRUE);
                }
            }
            if (thresh_fail) {
                // extern errtype mai_player_death();
                mai_player_death();
                time_until_shodan_avatar = player_struct.game_time + (CIT_CYCLE * 8);
            } else
                time_until_shodan_avatar = player_struct.game_time + SHODAN_INTERVAL;
        }
    }

    // update fatigue
    if (!global_fullmap->cyber) {

        fatigue_player();
        // update drug effects
        if (gamesys_drugs)
            drugs_update();

        // cool off all beam weapons
        if (gamesys_beam_wpns)
            cool_off_beam_weapons();
    }

    do_stuff_every_second();

    check_nearby_objects();

    // destroy old slow projectiles
    if (gamesys_slow_proj) {
        osi = objPhysicss[0].id;
        while (osi != OBJ_SPEC_NULL) {
            if (objPhysicss[osi].duration < player_struct.game_time) {
                ADD_DESTROYED_OBJECT(objPhysicss[osi].id);
            }
            osi = objPhysicss[osi].next;
        }
        destroy_destroyed_objects();
    }

    // Run continuous triggers
    if (player_struct.game_time > next_contin_trig) {
        osi = objTraps[0].id;
        while (osi != OBJ_SPEC_NULL) {
            if (ID2TRIP(objTraps[osi].id) == CONTIN_TRIG_TRIPLE)
                trap_activate(objTraps[osi].id, &dummy);
            osi = objTraps[osi].next;
        }
        next_contin_trig = player_struct.game_time + CONTIN_INTERVAL;
    }

    return (OK);
}

// ----------------------------------------
// check_hazard_regions()
//
// Checks to see if we're in a bio/radiation zone

void check_hazard_regions(MapElem *newElem) {
    fix hdiff = fix_from_obj_height(PLAYER_OBJ) - fix_from_map_height(me_height_flr(newElem));
    if (me_hazard_rad(newElem) > 0 && hdiff <= fix_make(level_gamedata.hazard.rad_h, 0) / 8) {
        short exp = (short)me_hazard_rad(newElem) * (short)level_gamedata.hazard.rad;
        exp -= (short)player_struct.hit_points_lost[RADIATION_TYPE - 1];
        if (exp > 0) {
            expose_player_real(exp, RADIATION_TYPE, 0);
        }

        hud_set(HUD_RADIATION);
        if (rand() % 0xFF < 0x80)
            play_digi_fx(SFX_RADIATION, 1);
    } else
        hud_unset(HUD_RADIATION);

    if (!level_gamedata.hazard.zerogbio) {
        if (me_hazard_bio(newElem) > 0 && hdiff <= fix_make(level_gamedata.hazard.bio_h, 0) / 8) {
            short exp = me_hazard_bio(newElem) * level_gamedata.hazard.bio;
            exp -= player_struct.hit_points_lost[BIO_TYPE - 1];
            if (exp > 0)
                expose_player_real(exp, BIO_TYPE, 0);
            hud_set(HUD_BIOHAZARD);
        } else
            hud_unset(HUD_BIOHAZARD);
    } else {
        if (me_hazard_bio(newElem))
            hud_set(HUD_ZEROGRAV);
        else
            hud_unset(HUD_ZEROGRAV);
    }
    if ((player_struct.hud_modes & (HUD_RADIATION | HUD_BIOHAZARD | HUD_ENVIROUSE)) == 0) {
        enviro_edrain_rate = 0;
    }
}

#define Z_THRESHOLD FIX_UNIT

uchar panel_ref_sanity(ObjID obj) {
    int objtrip = OPNUM(obj), obj_type;
    obj_type = ObjProps[objtrip].render_type;
    if ((obj_type == FAUBJ_TPOLY) || (obj_type == FAUBJ_TEXBITMAP))
        if (objs[obj].obclass == CLASS_FIXTURE) {
            fixang obj_to_p, objh, delt;
            fix dy, dx;

            objh = fixang_from_phys_angle(phys_angle_from_obj(objs[obj].loc.h));
            dy = fix_from_obj_coord(objs[PLAYER_OBJ].loc.y) - fix_from_obj_coord(objs[obj].loc.y);
            dx = fix_from_obj_coord(objs[PLAYER_OBJ].loc.x) - fix_from_obj_coord(objs[obj].loc.x);

            // x and y swapped here to transform coordinate system
            obj_to_p = fix_atan2(dx, dy);
            delt = (objh - FIXANG_PI / 2) - obj_to_p;

            //	      mprintf("Called with %d, locs %d %d and %d %d, got delt %x from %x and %x\n",
            //	         obj,objs[PLAYER_OBJ].loc.x,objs[PLAYER_OBJ].loc.y,objs[obj].loc.x,objs[obj].loc.y,delt,objh,obj_to_p);

            // hmmm?
            if (delt > FIXANG_PI)
                return FALSE;
        }
    return TRUE;
}

// -------------------------------------
// check_panel_ref
//
// Checks to see if we've walked away from a panel
// in an mfd, and closes the mfd.

void check_panel_ref(uchar puntme) {
    // static short old_x, old_y;
    // extern void restore_mfd_slot(int mfd_id);

    ObjID id = player_struct.panel_ref;

    if (id != OBJ_NULL && (id != PLAYER_OBJ || puntme)) {
        uchar punt = puntme;
        if (objs[id].active) {
            punt = punt || !check_object_dist(id, PLAYER_OBJ, MAX_USE_DIST);
            punt = punt || !panel_ref_sanity(id);
        }
        if (punt) {
            uchar punt_mfd[NUM_MFDS], punting = 0;
            uint8_t mfd_id;

            for (mfd_id = 0; mfd_id < NUM_MFDS; mfd_id++) {
                punt_mfd[mfd_id] = mfd_distance_remove(mfd_get_func(mfd_id, MFD_INFO_SLOT));
                punting = punting || punt_mfd[mfd_id];
            }
            if (punting) {
                mfd_notify_func(MFD_EMPTY_FUNC, MFD_INFO_SLOT, TRUE, MFD_ACTIVE, TRUE);
            }
            for (mfd_id = 0; mfd_id < NUM_MFDS; mfd_id++) {
                if (punt_mfd[mfd_id] && player_struct.mfd_current_slots[mfd_id] == MFD_INFO_SLOT) {
                    restore_mfd_slot(mfd_id);
                }
            }
            player_struct.panel_ref = OBJ_NULL;
        }
    }
    // old_x = PLAYER_BIN_X;
    // old_y = PLAYER_BIN_Y;
}

// =================================================
// do_stuff_every_second()
//
// deals with game system stuff that gets done every nerd second.
// (A nerd second is 256 clock ticks.  A nerd minute is 64 nerd seconds)

// 1 nerd minute = 64*256/(60*280) = 2*256/(15*35) = 512/525 minutes.

// Rates at which exposure levels degrade by type, in units of
// percent per nerd minute, compounded every nerd second.

uchar exposure_degrade_rates[] = {
    100, //   EXPLOSION_TYPE
    100, //   ENERGY_BEAM_TYPE
    100, //   MAGNETIC_TYPE
    100, //   RADIATION_TYPE
    100, //
    100, //   ARMOR_PIERCING_TYPE
    100, //   NEEDLE_TYPE
    100, //   BIO_TYPE
};

// Integrates the function f(t) = num >> denom_shf from t0 to t1,
// dealing with roundoff in a graceful way so that
// integrating over lots of small intervals roughly equals
// the large interval.
#define find_delta(t0, t1, num, denom_shf) ((((t1) * (num)) >> (denom_shf)) - (((t0) * (num)) >> (denom_shf)))

// computes the change to var by the specified rate as accrued over the
// time from t0 to t1, clamped by [vmin,vmax].
// returns the new value of var.

// t0 and t1 are times in nerd seconds, rate is in units per minute.

int apply_rate(int var, int rate, int t0, int t1, int vmin, int vmax) {
    int delta = find_delta(t0, t1, rate, HEALTH_RESTORE_SHF);
    int final = lg_min(vmax, lg_max(var + delta, vmin));
    return final;
}

#define ENERGY_VAR_RATE 50

extern ObjID shodan_avatar_id;

#define MY_FORALLOBJSPECS(pmo, objspec) \
    for (pmo = (objspec[OBJ_SPEC_NULL]).id; pmo != OBJ_SPEC_NULL; pmo = objspec[pmo].next)

#define REACTOR_BOOM_QB 0x14 // stolen from trigger.c
#define BRIDGE_SEPARATED_QB 0x98

#define REALSPACE_HUDS \
    (HUD_RADPOISON | HUD_BIOPOISON | HUD_FATIGUE | HUD_BIOHAZARD | HUD_RADIATION | HUD_ZEROGRAV | HUD_ENVIROUSE)

void do_stuff_every_second() {
    long running_dt = player_struct.game_time - player_struct.last_second_update;
    extern int bio_energy_var;
    extern int bio_absorb;
    extern int rad_absorb;
    int last = (player_struct.last_second_update >> HEALTH_RESTORE_PRECISION) & HEALTH_RESTORE_MASK;
    int next = (player_struct.game_time >> HEALTH_RESTORE_PRECISION) & HEALTH_RESTORE_MASK;

    if (running_dt > SECOND_UPDATE_FREQ) {
        if (global_fullmap->cyber) {
            char i;
            ObjSpecID osid;
            ObjID new_id, shrine_obj = OBJ_NULL;
            extern uchar *shodan_bitmask;

            for (i = 0; i < NUM_CS_EFFECTS; i++)
                if ((cspace_effect_times[i] != 0) && (cspace_effect_times[i] <= player_struct.game_time)) {
                    cspace_effect_times[i] = 0;
                    if (cspace_effect_turnoff[i] != NULL)
                        cspace_effect_turnoff[i](TRUE, TRUE);
                }

            if ((time_until_shodan_avatar != 0) && (player_struct.game_time > time_until_shodan_avatar) &&
                (shodan_avatar_id == OBJ_NULL) && (shodan_bitmask == NULL)) {
                time_until_shodan_avatar = 0;
                MY_FORALLOBJSPECS(osid, objSmallstuffs) {
                    if (ID2TRIP(objSmallstuffs[osid].id) == SHODO_SHRINE_TRIPLE) {
                        shrine_obj = objSmallstuffs[osid].id;
                    }
                }
                if (shrine_obj != OBJ_NULL) {
                    LGPoint sq;
                    sq.x = OBJ_LOC_BIN_X(objs[shrine_obj].loc);
                    sq.y = OBJ_LOC_BIN_Y(objs[shrine_obj].loc);
                    new_id = object_place(CYBER_SHODAN_TRIPLE, sq);
                    objCritters[objs[new_id].specID].mood = AI_MOOD_HOSTILE;
                    shodan_avatar_id = new_id;
                }
            }
            hud_unset(REALSPACE_HUDS);
        } else {
            if ((QUESTBIT_GET(REACTOR_BOOM_QB)) && (!QUESTBIT_GET(BRIDGE_SEPARATED_QB)) && ((rand() & 0x3F) == 1)) {
                play_digi_fx(SFX_RUMBLE, 2);
                fr_global_mod_flag(FR_SFX_SHAKE, FR_SFX_MASK);
                fr_sfx_time = CIT_CYCLE * 5; // 2 seconds of shake
            }
            if (next < last)
                next += HEALTH_RESTORE_UNIT;
            if (player_struct.energy_regen + player_struct.energy_spend != 0) {
                int num = player_struct.energy_regen - player_struct.energy_spend;
                if (num != 0) {
                    int finale = apply_rate(player_struct.energy, num, last, next, 0, MAX_ENERGY);
                    bio_energy_var -= ENERGY_VAR_RATE * (finale - player_struct.energy);
                    player_struct.energy = (ubyte)finale;
                    chg_set_flg(VITALS_UPDATE);
                }
            }
            if (player_struct.energy == 0) {
                if (!player_struct.energy_out) {
                    string_message_info(REF_STR_PowerRanOut);
                    play_digi_fx(SFX_POWER_OUT, 1);
                    player_struct.energy_out = TRUE;
                }
                hardware_power_outage();
                gear_power_outage();
            } else
                player_struct.energy_out = FALSE;
            if (player_struct.hit_points < PLAYER_MAX_HP && player_struct.hit_points_regen != 0) {

                int num = player_struct.hit_points_regen;
                player_struct.hit_points = apply_rate(player_struct.hit_points, num, last, next, 0, PLAYER_MAX_HP);
                mfd_notify_func(MFD_BIOWARE_FUNC, MFD_INFO_SLOT, FALSE, MFD_ACTIVE, FALSE);
            }
            if (player_struct.hit_points > 0) {
                int i;
                for (i = 0; i < NUM_DAMAGE_TYPES; i++)
                    if (player_struct.hit_points_lost[i] > 0) {
                        int num = player_struct.hit_points_lost[i];
                        int degrade = exposure_degrade_rates[i];
                        int deltahp = player_struct.hit_points -
                                      apply_rate(player_struct.hit_points, -num, last, next, 0, PLAYER_MAX_HP);
                        degrade = lg_min(100, find_delta(last, next, degrade, HEALTH_RESTORE_SHF));
                        damage_player((ubyte)deltahp, i + 1, NO_SHIELD_ABSORBTION);
                        player_struct.hit_points_lost[i] = num * (100 - degrade) / 100;
                    }
                if (player_struct.hit_points_lost[RADIATION_TYPE - 1] > 0)
                    hud_set(HUD_RADPOISON);
                else
                    hud_unset(HUD_RADPOISON);
                if (player_struct.hit_points_lost[BIO_TYPE - 1] > 0)
                    hud_set(HUD_BIOPOISON);
                else
                    hud_unset(HUD_BIOPOISON);
            }
            update_email_ware();
        }
        if (player_struct.fatigue > 0 && player_struct.controls[CONTROL_YVEL] <= SPRINT_CONTROL_THRESHOLD &&
            !EDMS_pelvis_is_climbing()) {
            int newf = player_struct.fatigue - player_struct.fatigue_regen;
            player_struct.fatigue_regen += fatigue_accum_rate;
            if (player_struct.fatigue_regen > player_struct.fatigue_regen_max)
                player_struct.fatigue_regen = player_struct.fatigue_regen_max;
            if (newf <= 0) {
                newf = 0;
            }
            if (newf != player_struct.fatigue) {
                mfd_notify_func(MFD_BIOWARE_FUNC, MFD_INFO_SLOT, FALSE, MFD_ACTIVE, FALSE);
                player_struct.fatigue = newf;
            }
        } else
            player_struct.fatigue_regen = player_struct.fatigue_regen_base;

        if (QUESTVAR_GET(MISSION_DIFF_QVAR) == 3) {
            int remain = MISSION_3_TICKS - player_struct.game_time;

            if ((remain / CIT_CYCLE) <= 30)
                secret_render_fx = TIMELIMIT_REND_SFX;

            if (remain < CIT_CYCLE) {
                secret_render_fx = 0;
                play_cutscene(ENDGAME_CUTSCENE,TRUE);

                // gDeadPlayerQuit = TRUE; // Pretend the player is dead.
                // gPlayingGame = FALSE; // Hop out of the game loop.
            }
        }

        player_struct.last_second_update = player_struct.game_time;

        // what is this code trying to do? ie. r_a -= (0-r_a)-r_a/4; looks like it can increase or decrease?, end <0, so
        // on
        if (rad_absorb > 0)
            rad_absorb -= rand() % (rad_absorb)-rad_absorb / 4;
        else
            rad_absorb = 0;
        if (bio_absorb > 0)
            bio_absorb -= rand() % (bio_absorb)-bio_absorb / 4;
        else
            bio_absorb = 0;

        check_hazard_regions(MAP_GET_XY(PLAYER_BIN_X, PLAYER_BIN_Y));
        check_panel_ref(FALSE);
    }
}

    // ---------------------------------------------
    // Expose_player exposes the player to damage of a specified type.
    // if tsecs is non-zero, it is the amount of time in which the damage will go away.

    // TSECS IS NO LONGER SUPPORTED, ALL EXPOSURE HAS A BUILT-IN DECAY RATE.

#define MAX_UBYTE 0xFF

int enviro_suit_absorb(int damage, int exposure, ubyte dtype);

void expose_player_real(short damage, ubyte type, ushort tsecs) {
    int cval = player_struct.hit_points_lost[type - 1];
    if (damage == 0)
        return;
    damage = lg_max(-cval, lg_min(damage, MAX_UBYTE - cval));
    if (damage > 0 && (type == BIO_TYPE || type == RADIATION_TYPE)) {
        damage = enviro_suit_absorb(damage, cval, type);
    }
    cval += damage;
    player_struct.hit_points_lost[type - 1] = (ubyte)cval;
#ifdef SCHEDULED_DECAY
    if (tsecs > 0) {
        SchedEvent ev;
        SchedExposeData *xd = (SchedExposeData *)&ev.data;
        int count = 1;
        ev.timestamp = TICKS2TSTAMP(player_struct.game_time + tsecs * CIT_CYCLE);
        ev.type = EXPOSE_SCHED_EVENT;
        xd->damage = -(damage / count); // plus or minus exposure increment
        xd->type = type;
        xd->tsecs = tsecs;
        xd->count = count;
        schedule_event(&game_seconds_schedule, &ev);
    }
#endif // SCHEDULED_DECAY
}

void expose_player(byte damage, ubyte type, ushort tsecs) { expose_player_real(damage, type, tsecs); }

//-------------------------------------------------------
// enviro_suit_absorb()
//
// returns damage to player after enviro-suit

#define ENVIRO_ABSORB_DENOM 5
#define ENVIRO_DRAIN_DENOM 1
#define ENVIRO_DRAIN_RATE 32

// biorhythm vars
int bio_absorb = 0;
int rad_absorb = 0;

int enviro_suit_absorb(int damage, int exposure, ubyte dtype) {
    short drain;
    short absorb;
    short denom;
    short energy;
    short old_edrain_rate = enviro_edrain_rate;
    ubyte version = player_struct.hardwarez[CPTRIP(ENV_HARD_TRIPLE)];

    if (dtype == RADIATION_TYPE && version > 0)
        version--;
    if (version == 0)
        return damage;

    // Absorb all but 1/nth of damage
    denom = ENVIRO_ABSORB_DENOM + version;
    absorb = ((damage + exposure) * (denom - 1)) / denom;
    if (absorb == 0)
        return damage;

    // Compute drain for that absorption amount.
    denom = ENVIRO_DRAIN_DENOM + version + 1;
    enviro_edrain_rate = exposure * 60 / (ENVIRO_DRAIN_RATE * denom);
    drain = (rand() % 60 + enviro_edrain_rate) / 60;

    // drain energy
    energy = drain_energy(drain);
    // did we have enough?
    if (energy < drain) {
        // if not, recompute absorption.
        absorb = energy * denom / (denom - 1);
    }
    enviro_absorb_rate = lg_min(damage, absorb) >> 1;
    damage -= lg_min(damage, absorb);
    switch (dtype) {
    case BIO_TYPE:
        bio_absorb = 8 + long_sqrt((int)damage);
        break;
    case RADIATION_TYPE:
        rad_absorb = 8 + long_sqrt((int)damage);
        break;
    default:
        break;
    }
    if (enviro_absorb_rate > 0) {
        if (old_edrain_rate == 0) {
            uint32_t time = 5u << APPROX_CIT_CYCLE_SHFT;
            hud_set_time(HUD_ENVIROUSE, time);
            hud_set_time(HUD_ENERGYUSE, time);
        }
    } else
        hud_unset(HUD_ENVIROUSE);
    return (byte)damage;
}
