/* AMX Mod X
*   Dropped Bomb Defuse
*
* (c) Copyright 2006 by VEN
*
* This file is provided as is (no warranties)
*
*     DESCRIPTION
*       Plugin allows to CTs defuse the dropped bomb. Once bomb is defused CT team will win
*       the round (can be disabled) and CT defuser will gain 1 frag (configurable). Plugin
*       emits standard sounds like defuse start/end/denied, draws progress bar, plays/shows
*       "Bomb has been defused" audio/text. Defuser kit requirement can be forced with a CVAR.
*
*     INSTALLATION
*       Install(/enable) "Force Team Win": http://forums.alliedmods.net/showthread.php?t=40287
*       Download/copy ftw.inc into amxmodx/scripting/include folder.
*       Note: if you do not wish to perform the above steeps, you can skip that but then you
*       should disable(comment) the FORCE_TEAM_WIN option. This will prevent CT win on defuse.
*       Compile(/install/enable) droppedbombdefuse.sma using compile.exe or amxxpc.exe.
*       Enable fakemeta module and change the map or restart the server.
*       Also make sure that you have AMX Mod X version 1.71 or higher.
*
*     CVARs
*       dbd_time (N: seconds, default: 10, 0: disable the plugin) - defuse time
*       dbd_defkit_required (0: OFF, 1: ON, default: 0) - controls defkit requirement
*       Below with an examples is explained how CVARs works, remember it's just an examples.
*       By default defuse time is 10 seconds, but with defuser kit it will be 5 seconds.
*       If defuser kit requirement is forced with the CVAR, defuse time with a defkit will be
*       not 5 but 10 seconds. If you still want 5 seconds, comment IGNORE_MULTIPLIER option.
*       You can alter 5 seconds defuse w/ a defkit time by the DEFKIT_TIME_MULTIPLIER option.
*       So if multiplier is 0.4 and dbd_time is 15, you will get 0.4 * 15, i.e. 6 seconds.
*
*     OPTIONS
*       The following options can be altered at the top of the plugin's source code.
*       Bomb map check, defuse with a defkit time multiplier, ignore multiplier on defkit
*       requirement, frags bonus, player/bomb max. distance/view angle diff. and a few more.
*
*     VERSIONS
*       0.3.1 now frags on the scoreboard updated immediately
*       0.3   added an option to disable the CT's team win on dropped bomb defuse
*             now bomb can be defused even when CT's team win can't be forced (server is full)
*       0.2   defkit requirement can be forced with the dbd_defkit_required CVAR (default: 0)
*       0.1   first release
*/

#include <amxmodx>	// AMX Mod X 1.71+ required, check your addons/metamod/plugins.ini
#include <fakemeta>	// fakemeta module required, check your configs/modules.ini
#include <xs>		// this is not a module!

// plugin's main information
#define PLUGIN_NAME "Dropped Bomb Defuse"
#define PLUGIN_VERSION "0.3.1"
#define PLUGIN_AUTHOR "VEN"

// OPTIONS BELOW

// comment to disable force team win on dropped bomb defuse
#define FORCE_TEAM_WIN

// comment to disable check for bomb maps
#define BOMB_MAP_CHECK

// multiplier * dbd_time == defuse time with a defkit
#define DEFKIT_TIME_MULTIPLIER 0.5

// comment to not ignore the above multiplier when dbd_defkit_required CVAR is 1
#define IGNORE_MULTIPLIER

// defuser frag bonus, comment or set to 0 to disable this feature
#define DEFUSER_FRAG_BONUS 1

// max. allowed distance from a CT to a dropped bomb
#define MAX_DISTANCE 70

// max. allowed angle difference between CT's view vector and bomb position
#define MAX_VIEWANGLE_DIFF 35

// name of the CVAR which controls dropped bomb defuse time
#define CVAR_TIME_NAME "dbd_time"

// default value of the "dbd_time" CVAR
#define CVAR_TIME_DEF "25"

// name of the CVAR which controls defkit requirement
#define CVAR_DEFKIT_NAME "dbd_defkit_required"

// default value of the "dbd_defkit_required" CVAR
#define CVAR_DEFKIT_DEF "0"

// requirement center text message
new g_requirement[] = "The defuser kit is required!"

// uncomment to disable automatic 32/64bit processor detection
// possible values are <0: 32bit | 1: 64bit>
//#define PROCESSOR_TYPE 0

// OPTIONS ABOVE

// private data deaths offset
#define OFFSET_DEATHS_32BIT 444
#define OFFSET_DEATHS_64BIT 493

// deaths offset linux difference
#define OFFSET_DEATHS_LINUXDIFF 5

// determination of actual offsets
#if !defined PROCESSOR_TYPE // is automatic 32/64bit processor detection?
	#if cellbits == 32 // is the size of a cell are 32 bits?
		// then considering processor as 32bit
		#define OFFSET_DEATHS OFFSET_DEATHS_32BIT
	#else // in other case considering the size of a cell as 64 bits
		// and then considering processor as 64bit
		#define OFFSET_DEATHS OFFSET_DEATHS_64BIT
	#endif
#else // processor type specified by PROCESSOR_TYPE define
	#if PROCESSOR_TYPE == 0 // 32bit processor defined
		#define OFFSET_DEATHS OFFSET_DEATHS_32BIT
	#else // considering that 64bit processor defined
		#define OFFSET_DEATHS OFFSET_DEATHS_64BIT
	#endif
#endif

#define CS_GET_USER_DEATHS_(%1) get_pdata_int(%1, OFFSET_DEATHS, OFFSET_DEATHS_LINUXDIFF)

// checks whether force team win is enabled and if so includes a native
#if defined FORCE_TEAM_WIN
	#include <ftw> // ftw.inc, includes cs_force_team_win native, uses "Force Team Win"
#endif

#define TASK_INTERVAL 0.2
#define TASK_INDEX 349576

#define IN_USE (1<<5)
#define SOLID_NOT 0
#define EF_NODRAW 128

new CsTeams:CS_TEAM_CT = CsTeams:2

new g_wpn_denyselect[] = "common/wpn_denyselect.wav"
new g_wpn_select[] = "common/wpn_select.wav"

new g_classname[] = "classname"
new g_weapon_c4[] = "weapon_c4"

new g_disarm[] = "weapons/c4_disarm.wav"
new g_disarmed[] = "weapons/c4_disarmed.wav"

new g_defused_audio[] = "%!MRAD_BOMBDEF"
new g_defused_text[] = "#Bomb_Defused"

new g_pcvar_time
new g_pcvar_defkit

new g_msgid_bartime
new g_msgid_scoreinfo
new g_msgid_bombpickup
new g_msgid_audio
new g_msgid_text

new g_maxplayers

new g_defuser
new Float:g_time_left

#define MAX_PLAYERS 32
new bool:g_has_defuser[MAX_PLAYERS + 1]

public plugin_init() {
	register_plugin(PLUGIN_NAME, PLUGIN_VERSION, PLUGIN_AUTHOR)

	g_pcvar_time = register_cvar(CVAR_TIME_NAME, CVAR_TIME_DEF)
	g_pcvar_defkit = register_cvar(CVAR_DEFKIT_NAME, CVAR_DEFKIT_DEF)

#if defined BOMB_MAP_CHECK
	if (!engfunc(EngFunc_FindEntityByString, -1, g_classname, "func_bomb_target"))
		return
#endif

	register_event("StatusIcon", "event_deficon", "b", "2=defuser")
	register_event("DeathMsg", "event_death", "a")

	register_forward(FM_EmitSound, "forward_emit_sound")

	g_msgid_bartime = get_user_msgid("BarTime")
	g_msgid_scoreinfo = get_user_msgid("ScoreInfo")
	g_msgid_bombpickup = get_user_msgid("BombPickup")
	g_msgid_audio = get_user_msgid("SendAudio")
	g_msgid_text = get_user_msgid("TextMsg")

	g_maxplayers = get_maxplayers()
}

/* *************************************************** Base **************************************************** */

public event_deficon(id) {
	g_has_defuser[id] = bool:read_data(1)
}

public event_death() {
	new id = read_data(2)
	if (g_defuser == id && task_exists(TASK_INDEX)) {
		remove_task(TASK_INDEX)
		msg_bartime(id, 0)
	}
}

public client_disconnect(id) {
	if (task_exists(TASK_INDEX) && g_defuser == id)
		remove_task(TASK_INDEX)
}

public forward_emit_sound(id, channel, sound[]) {
	if (!id || !equali(sound, g_wpn_denyselect) || !is_user_alive(id) || !(pev(id, pev_button) & IN_USE) || get_user_team(id) != _:CS_TEAM_CT)
		return FMRES_IGNORED

	new itime = get_pcvar_num(g_pcvar_time)
	if (!itime || !can_disarm(id))
		return FMRES_IGNORED

	if (task_exists(TASK_INDEX)) {
		if (id != g_defuser)
			emit_sound(id, CHAN_ITEM, g_wpn_select, 0.4, ATTN_NORM, 0, PITCH_NORM)

		return FMRES_SUPERCEDE
	}

	new bool:dk_required = bool:get_pcvar_num(g_pcvar_defkit)
	if (g_has_defuser[id]) {
#if defined IGNORE_MULTIPLIER
		if (dk_required)
			g_time_left = float(itime)
		else {
			g_time_left = itime * DEFKIT_TIME_MULTIPLIER
			itime = floatround(g_time_left)
		}
#else
		g_time_left = itime * DEFKIT_TIME_MULTIPLIER
		itime = floatround(g_time_left)
#endif
	}
	else {
		if (dk_required) {
			client_print(id, print_center, g_requirement)
			return FMRES_IGNORED
		}

		g_time_left = float(itime)
	}

	g_defuser = id

	msg_bartime(id, itime)
	emit_sound(id, CHAN_ITEM, g_disarm, VOL_NORM, ATTN_NORM, 0, PITCH_NORM)
	set_task(TASK_INTERVAL, "task_check", TASK_INDEX, _, _, "a", floatround(g_time_left / TASK_INTERVAL))

	return FMRES_SUPERCEDE
}

public task_check() {
	g_time_left -= TASK_INTERVAL
	new bomb = can_disarm(g_defuser)
	if (!bomb || !(pev(g_defuser, pev_button) & IN_USE)) {
		msg_bartime(g_defuser, 0)
		remove_task(TASK_INDEX)
	}
	else if (g_time_left < TASK_INTERVAL) {
		remove_task(TASK_INDEX)
		msg_bartime(g_defuser, 0)
		emit_sound(g_defuser, CHAN_WEAPON, g_disarmed, VOL_NORM, ATTN_NORM, 0, PITCH_NORM)

#if defined DEFUSER_FRAG_BONUS
	#if DEFUSER_FRAG_BONUS
		new Float:frags
		pev(g_defuser, pev_frags, frags)
		frags += DEFUSER_FRAG_BONUS
		set_pev(g_defuser, pev_frags, frags)

		message_begin(MSG_ALL, g_msgid_scoreinfo)
		write_byte(g_defuser)
		write_short(floatround(frags))
		write_short(CS_GET_USER_DEATHS_(g_defuser))
		write_short(0)
		write_short(get_user_team(g_defuser))
		message_end()

	#endif
#endif

		set_pev(bomb, pev_solid, SOLID_NOT)
		set_pev(bomb, pev_effects, EF_NODRAW)
		message_begin(MSG_ALL, g_msgid_bombpickup)
		message_end()

#if defined FORCE_TEAM_WIN
		cs_force_team_win(CS_TEAM_CT)
#endif

		message_begin(MSG_BROADCAST, g_msgid_audio)
		write_byte(0)
		write_string(g_defused_audio)
		write_short(100)
		message_end()

		message_begin(MSG_ALL, g_msgid_text)
		write_byte(print_center)
		write_string(g_defused_text)
		message_end()
	}
}

can_disarm(id) {
	new c4 = engfunc(EngFunc_FindEntityByString, -1, g_classname, g_weapon_c4)
	if (!c4)
		return 0

	new bomb = pev(c4, pev_owner)
	if (bomb <= g_maxplayers || !pev(bomb, pev_solid))
		return 0

	new Float:origin[3], Float:bombpos[3]
	pev(id, pev_origin, origin)
	pev(bomb, pev_origin, bombpos)

	if (get_distance_f(origin, bombpos) > MAX_DISTANCE || !fm_is_visible(id, bombpos) || get_view_angle_diff(id, bombpos) > MAX_VIEWANGLE_DIFF)
		return 0

	return bomb
}

msg_bartime(index, scale) {
	message_begin(MSG_ONE, g_msgid_bartime, _, index)
	write_short(scale)
	message_end()
}

/* ************************************************** Stocks *************************************************** */

// checks whether no obstacles between eyes and given point
stock bool:fm_is_visible(index, const Float:point[3]) {
	new Float:origin[3], Float:view_ofs[3], Float:eyespos[3]
	pev(index, pev_origin, origin)
	pev(index, pev_view_ofs, view_ofs)
	xs_vec_add(origin, view_ofs, eyespos)

	engfunc(EngFunc_TraceLine, eyespos, point, 0, index)

	new Float:fraction
	global_get(glb_trace_fraction, fraction)
	if (fraction == 1.0)
		return true

	return false
}

// returns a float degree angle between view vector and given point
stock Float:get_view_angle_diff(index, Float:vec_c[3]) {
	new Float:vec_a[3], Float:vec_b[3], viewend[3]
	new Float:origin[3], Float:view_ofs[3]
	pev(index, pev_origin, origin)
	pev(index, pev_view_ofs, view_ofs)
	xs_vec_add(origin, view_ofs, vec_a)

	get_user_origin(index, viewend, 3)
	vec_b[0] = float(viewend[0])
	vec_b[1] = float(viewend[1])
	vec_b[2] = float(viewend[2])
	
	new Float:a = get_distance_f(vec_b, vec_c)
	new Float:b = get_distance_f(vec_a, vec_c)
	new Float:c = get_distance_f(vec_a, vec_b)

	return floatacos((b*b + c*c - a*a) / (2 * b * c), _:degrees)
}

/* **************************************************** EOF **************************************************** */
