/* =======================================================================
 *
 * Targets.
 *
 * =======================================================================
 */

#include "header/local.h"

#define TARGET_HELP_PRIMARY 1
#define TARGET_HELP_THINK_DELAY 0.3f

#define LASER_ON 0x0001
#define LASER_RED 0x0002
#define LASER_GREEN 0x0004
#define LASER_BLUE 0x0008
#define LASER_YELLOW 0x0010
#define LASER_ORANGE 0x0020
#define LASER_FAT 0x0040
#define LASER_STOPWINDOW 0x0080

void ED_CallSpawn(edict_t *ent);

/*
 * QUAKED target_temp_entity (1 0 0) (-8 -8 -8) (8 8 8)
 *
 * Fire an origin based temp entity event to the clients.
 * "style"		type byte
 */
void
Use_Target_Tent(edict_t *ent, edict_t *other, edict_t *activator)
{
	gi.WriteByte(svc_temp_entity);
	gi.WriteByte(ent->style);
	gi.WritePosition(ent->s.origin);
	gi.multicast(ent->s.origin, MULTICAST_PVS);
}

void
SP_target_temp_entity(edict_t *ent)
{
	ent->use = Use_Target_Tent;
}

/*
 * QUAKED target_speaker (1 0 0) (-8 -8 -8) (8 8 8) looped-on looped-off reliable
 *
 * "noise"		wav file to play
 * "attenuation"
 * -1 = none, send to whole level
 * 1 = normal fighting sounds
 * 2 = idle sound level
 * 3 = ambient sound level
 * "volume"	0.0 to 1.0
 *
 * Normal sounds play each time the target is used. The reliable flag can be set for
 * crucial voiceovers.
 *
 * Looped sounds are always atten 3 / vol 1, and the use function toggles it on/off.
 * Multiple identical looping sounds will just increase volume without any speed cost.
 */
void
Use_Target_Speaker(edict_t *ent, edict_t *other /* unused */, edict_t *activator /* unused */)
{
	int chan;

	if (!ent)
	{
		return;
	}

	if (ent->spawnflags & 3)
	{
	 	/* looping sound toggles */
		if (ent->s.sound)
		{
			ent->s.sound = 0; /* turn it off */
		}
		else
		{
			ent->s.sound = ent->noise_index; /* start it */
		}
	}
	else
	{
	 	/* normal sound */
		if (ent->spawnflags & 4)
		{
			chan = CHAN_VOICE | CHAN_RELIABLE;
		}
		else
		{
			chan = CHAN_VOICE;
		}

		/* use a positioned_sound, because this entity won't
		   normally be  sent to any clients because it is invisible */
		gi.positioned_sound(ent->s.origin, ent, chan, ent->noise_index,
				ent->volume, ent->attenuation, 0);
	}
}

void
SP_target_speaker(edict_t *ent)
{
	char buffer[MAX_QPATH];

	if (!ent)
	{
		return;
	}

	if (!st.noise)
	{
		gi.dprintf("target_speaker with no noise set at %s\n", vtos(ent->s.origin));
		return;
	}

	if (!strstr(st.noise, ".wav"))
	{
		Com_sprintf(buffer, sizeof(buffer), "%s.wav", st.noise);
	}
	else
	{
		Q_strlcpy(buffer, st.noise, sizeof(buffer));
	}

	ent->noise_index = gi.soundindex(buffer);

	if (!ent->volume)
	{
		ent->volume = 1.0;
	}

	if (!ent->attenuation)
	{
		ent->attenuation = 1.0;
	}
	else if (ent->attenuation == -1) /* use -1 so 0 defaults to 1 */
	{
		ent->attenuation = 0;
	}

	/* check for prestarted looping sound */
	if (ent->spawnflags & 1)
	{
		ent->s.sound = ent->noise_index;
	}

	ent->use = Use_Target_Speaker;

	/* must link the entity so we get areas and clusters so
	   the server can determine who to send updates to */
	gi.linkentity(ent);
}

/* ========================================================== */

static void
Target_Help_Apply(const char *msg, int is_primary)
{
	char *curr;
	size_t sz;

	if (!msg)
	{
		return;
	}

	if (is_primary)
	{
		curr = game.helpmessage1;
		sz = sizeof(game.helpmessage1);
	}
	else
	{
		curr = game.helpmessage2;
		sz = sizeof(game.helpmessage2);
	}

	if (strcmp(curr, msg) == 0)
	{
		return;
	}

	Q_strlcpy(curr, msg, sz - 1);

	game.helpchanged++;
}

void
Target_Help_Think(edict_t *ent)
{
	if (!ent)
	{
		return;
	}

	Target_Help_Apply(ent->message, ent->spawnflags & TARGET_HELP_PRIMARY);
	ent->think = NULL;
}

void
Use_Target_Help(edict_t *ent, edict_t *other /* unused */, edict_t *activator /* unused */)
{
	if (!ent)
	{
		return;
	}

	if (level.time > TARGET_HELP_THINK_DELAY)
	{
		Target_Help_Apply(ent->message, ent->spawnflags & TARGET_HELP_PRIMARY);
	}
	else
	{
		/* The game is still pre-loading so delay the help message a bit,
		   otherwise its changes to game structure will leak past save loads
		*/
		ent->think = Target_Help_Think;
		ent->nextthink = TARGET_HELP_THINK_DELAY;
	}
}

/*
 * QUAKED target_help (1 0 1) (-16 -16 -24) (16 16 24) help1
 *
 * When fired, the "message" key becomes the current personal computer string,
 * and the message light will be set on all clients status bars.
 */
void
SP_target_help(edict_t *ent)
{
	if (!ent)
	{
		return;
	}

	if (deathmatch->value)
	{
	 	/* auto-remove for deathmatch */
		G_FreeEdict(ent);
		return;
	}

	if (!ent->message)
	{
		gi.dprintf("%s with no message at %s\n", ent->classname, vtos(ent->s.origin));
		G_FreeEdict(ent);
		return;
	}

	ent->use = Use_Target_Help;
}

/* ========================================================== */

/*
 * QUAKED target_secret (1 0 1) (-8 -8 -8) (8 8 8)
 *
 * Counts a secret found.
 * These are single use targets.
 */
void
use_target_secret(edict_t *ent, edict_t *other /* unused */, edict_t *activator /* unused */)
{
	if (!ent)
	{
		return;
	}

	gi.sound(ent, CHAN_VOICE, ent->noise_index, 1, ATTN_NORM, 0);

	level.found_secrets++;

	G_UseTargets(ent, activator);
	G_FreeEdict(ent);
}

void
SP_target_secret(edict_t *ent)
{
	if (!ent)
	{
		return;
	}

	if (deathmatch->value)
	{
	 	/* auto-remove for deathmatch */
		G_FreeEdict(ent);
		return;
	}

	ent->use = use_target_secret;

	if (!st.noise)
	{
		st.noise = "misc/secret.wav";
	}

	ent->noise_index = gi.soundindex(st.noise);
	ent->svflags = SVF_NOCLIENT;
	level.total_secrets++;

	/* map bug hack */
	if (!Q_stricmp(level.mapname, "mine3") && (ent->s.origin[0] == 280) &&
		(ent->s.origin[1] == -2048) && (ent->s.origin[2] == -624))
	{
		ent->message = "You have found a secret area.";
	}
}

/* ========================================================== */

/*
 * QUAKED target_goal (1 0 1) (-8 -8 -8) (8 8 8)
 * Counts a goal completed.
 * These are single use targets.
 */
void
use_target_goal(edict_t *ent, edict_t *other /* unused */, edict_t *activator /* unused */)
{
	if (!ent)
	{
		return;
	}

	gi.sound(ent, CHAN_VOICE, ent->noise_index, 1, ATTN_NORM, 0);

	level.found_goals++;

	if (level.found_goals == level.total_goals)
	{
		gi.configstring(CS_CDTRACK, "0");
	}

	G_UseTargets(ent, activator);
	G_FreeEdict(ent);
}

void
SP_target_goal(edict_t *ent)
{
	if (!ent)
	{
		return;
	}

	if (deathmatch->value)
	{
	 	/* auto-remove for deathmatch */
		G_FreeEdict(ent);
		return;
	}

	ent->use = use_target_goal;

	if (!st.noise)
	{
		st.noise = "misc/secret.wav";
	}

	ent->noise_index = gi.soundindex(st.noise);
	ent->svflags = SVF_NOCLIENT;
	level.total_goals++;
}

/* ========================================================== */

/*
 * QUAKED target_explosion (1 0 0) (-8 -8 -8) (8 8 8)
 * Spawns an explosion temporary entity when used.
 *
 * "delay"		wait this long before going off
 * "dmg"		how much radius damage should be done, defaults to 0
 */
void
target_explosion_explode(edict_t *self)
{
	float save;

	if (!self)
	{
		return;
	}

	gi.WriteByte(svc_temp_entity);
	gi.WriteByte(TE_EXPLOSION1);
	gi.WritePosition(self->s.origin);
	gi.multicast(self->s.origin, MULTICAST_PHS);

	T_RadiusDamage(self, self->activator, self->dmg,
			NULL, self->dmg + 40, MOD_EXPLOSIVE);

	save = self->delay;
	self->delay = 0;
	G_UseTargets(self, self->activator);
	self->delay = save;
}

void
use_target_explosion(edict_t *self, edict_t *other /* unused */, edict_t *activator)
{
	if (!self || !activator)
	{
		return;
	}

	self->activator = activator;

	if (!self->delay)
	{
		target_explosion_explode(self);
		return;
	}

	self->think = target_explosion_explode;
	self->nextthink = level.time + self->delay;
}

void
SP_target_explosion(edict_t *ent)
{
	if (!ent)
	{
		return;
	}

	ent->use = use_target_explosion;
	ent->svflags = SVF_NOCLIENT;
}

/* ========================================================== */

/*
 * QUAKED target_changelevel (1 0 0) (-8 -8 -8) (8 8 8)
 *
 * Changes level to "map" when fired
 */
void
use_target_changelevel(edict_t *self, edict_t *other, edict_t *activator)
{
	if (!self || !other)
	{
		return;
	}

	if (level.intermissiontime)
	{
		return; /* already activated */
	}

	if (!deathmatch->value && !coop->value)
	{
		if (g_edicts[1].health <= 0)
		{
			return;
		}
	}

	/* if noexit, do a ton of damage to other */
	if (deathmatch->value && !((int)dmflags->value & DF_ALLOW_EXIT) &&
		(other != world))
	{
		T_Damage(other, self, self, vec3_origin, other->s.origin,
				vec3_origin, 10 * other->max_health, 1000, 0, MOD_EXIT);
		return;
	}

	/* if multiplayer, let everyone know who hit the exit */
	if (deathmatch->value)
	{
		if (activator && activator->client)
		{
			gi.bprintf(PRINT_HIGH, "%s exited the level.\n",
					activator->client->pers.netname);
		}
	}

	/* if going to a new unit, clear cross triggers */
	if (strstr(self->map, "*"))
	{
		game.serverflags &= ~(SFL_CROSS_TRIGGER_MASK);
	}

	BeginIntermission(self);
}

void
SP_target_changelevel(edict_t *ent)
{
	if (!ent)
	{
		return;
	}

	if (!ent->map)
	{
		gi.dprintf("target_changelevel with no map at %s\n", vtos(ent->s.origin));
		G_FreeEdict(ent);
		return;
	}

	/* ugly hack because *SOMEBODY* screwed up their map */
	if ((Q_stricmp(level.mapname, "fact1") == 0) && (Q_stricmp(ent->map, "fact3") == 0))
	{
		ent->map = "fact3$secret1";
	}

	ent->use = use_target_changelevel;
	ent->svflags = SVF_NOCLIENT;
}

/* ========================================================== */

/*
 * QUAKED target_splash (1 0 0) (-8 -8 -8) (8 8 8)
 * Creates a particle splash effect when used.
 *
 * Set "sounds" to one of the following:
 * 1) sparks
 * 2) blue water
 * 3) brown water
 * 4) slime
 * 5) lava
 * 6) blood
 *
 * "count"	how many pixels in the splash
 * "dmg"	if set, does a radius damage at this location when it splashes
 *          useful for lava/sparks
 */

void
use_target_splash(edict_t *self, edict_t *other /* unused */, edict_t *activator)
{
	if (!self || !activator)
	{
		return;
	}

	gi.WriteByte(svc_temp_entity);
	gi.WriteByte(TE_SPLASH);
	gi.WriteByte(self->count);
	gi.WritePosition(self->s.origin);
	gi.WriteDir(self->movedir);
	gi.WriteByte(self->sounds);
	gi.multicast(self->s.origin, MULTICAST_PVS);

	if (self->dmg)
	{
		T_RadiusDamage(self, activator, self->dmg, NULL,
				self->dmg + 40, MOD_SPLASH);
	}
}

void
SP_target_splash(edict_t *self)
{
	if (!self)
	{
		return;
	}

	self->use = use_target_splash;
	G_SetMovedir(self->s.angles, self->movedir);

	if (!self->count)
	{
		self->count = 32;
	}

	self->svflags = SVF_NOCLIENT;
}

/* ========================================================== */

/*
 * QUAKED target_spawner (1 0 0) (-8 -8 -8) (8 8 8)
 * Set target to the type of entity you want spawned.
 * Useful for spawning monsters and gibs in the factory levels.
 *
 * For monsters:
 *  Set direction to the facing you want it to have.
 *
 * For gibs:
 *  Set direction if you want it moving and
 *  speed how fast it should be moving otherwise it
 *  will just be dropped
 */

void
use_target_spawner(edict_t *self, edict_t *other /* unused */, edict_t *activator /* unused */)
{
	edict_t *ent;

	if (!self)
	{
		return;
	}

	ent = G_Spawn();
	ent->classname = self->target;
	VectorCopy(self->s.origin, ent->s.origin);
	VectorCopy(self->s.angles, ent->s.angles);
	ED_CallSpawn(ent);
	gi.unlinkentity(ent);
	KillBox(ent);
	gi.linkentity(ent);

	if (self->speed)
	{
		VectorCopy(self->movedir, ent->velocity);
	}

	ent->s.renderfx |= RF_IR_VISIBLE; /* PGM */
}

void
SP_target_spawner(edict_t *self)
{
	if (!self)
	{
		return;
	}

	self->use = use_target_spawner;
	self->svflags = SVF_NOCLIENT;

	if (self->speed)
	{
		G_SetMovedir(self->s.angles, self->movedir);
		VectorScale(self->movedir, self->speed, self->movedir);
	}
}

/* ========================================================== */

/*
 * QUAKED target_blaster (1 0 0) (-8 -8 -8) (8 8 8) NOTRAIL NOEFFECTS
 * Fires a blaster bolt in the set direction when triggered.
 *
 * dmg		default is 15
 * speed	default is 1000
 */

void
use_target_blaster(edict_t *self, edict_t *other, edict_t *activator)
{
	if (!self)
	{
		return;
	}

	fire_blaster(self, self->s.origin, self->movedir,
			self->dmg, self->speed, EF_BLASTER, MOD_TARGET_BLASTER);
	gi.sound(self, CHAN_VOICE, self->noise_index, 1, ATTN_NORM, 0);
}

void
SP_target_blaster(edict_t *self)
{
	if (!self)
	{
		return;
	}

	self->use = use_target_blaster;
	G_SetMovedir(self->s.angles, self->movedir);
	self->noise_index = gi.soundindex("weapons/laser2.wav");

	if (!self->dmg)
	{
		self->dmg = 15;
	}

	if (!self->speed)
	{
		self->speed = 1000;
	}

	self->svflags = SVF_NOCLIENT;
}

/* ========================================================== */

/*
 * QUAKED target_crosslevel_trigger (.5 .5 .5) (-8 -8 -8) (8 8 8) trigger1 trigger2 trigger3 trigger4 trigger5 trigger6 trigger7 trigger8
 *
 * Once this trigger is touched/used, any trigger_crosslevel_target
 * with the same trigger number is automatically used when a level
 * is started within the same unit. It is OK to check multiple triggers.
 * Message, delay, target, and killtarget also work.
 */
void
trigger_crosslevel_trigger_use(edict_t *self, edict_t *other /* unused */, edict_t *activator /* unused */)
{
	if (!self)
	{
		return;
	}

	game.serverflags |= self->spawnflags;
	G_FreeEdict(self);
}

void
SP_target_crosslevel_trigger(edict_t *self)
{
	if (!self)
	{
		return;
	}

	self->svflags = SVF_NOCLIENT;
	self->use = trigger_crosslevel_trigger_use;
}

/*
 * QUAKED target_crosslevel_target (.5 .5 .5) (-8 -8 -8) (8 8 8) trigger1 trigger2 trigger3 trigger4 trigger5 trigger6 trigger7 trigger8
 *
 * Triggered by a trigger_crosslevel elsewhere within a unit.
 * If multiple triggers are checked, all must be true.
 * Delay, target and killtarget also work.
 *
 * "delay" delay before using targets if the trigger has been activated (default 1)
 */
void
target_crosslevel_target_think(edict_t *self)
{
	if (!self)
	{
		return;
	}

	if (self->spawnflags == (game.serverflags & SFL_CROSS_TRIGGER_MASK & self->spawnflags))
	{
		G_UseTargets(self, self);
		G_FreeEdict(self);
	}
}

void
SP_target_crosslevel_target(edict_t *self)
{
	if (!self)
	{
		return;
	}

	if (!self->delay)
	{
		self->delay = 1;
	}

	self->svflags = SVF_NOCLIENT;

	self->think = target_crosslevel_target_think;
	self->nextthink = level.time + self->delay;
}

/* ========================================================== */

/*
 * QUAKED target_laser (0 .5 .8) (-8 -8 -8) (8 8 8) START_ON RED GREEN BLUE YELLOW ORANGE FAT WINDOWSTOP
 * When triggered, fires a laser.  You can either set a target
 * or a direction.
 *
 * WINDOWSTOP - stops at CONTENTS_WINDOW
 */
void
target_laser_think(edict_t *self)
{
	edict_t *ignore;
	vec3_t start;
	vec3_t end;
	trace_t tr;
	vec3_t point;
	vec3_t last_movedir;
	int count;

	if (!self)
	{
		return;
	}

	if (self->spawnflags & 0x80000000)
	{
		count = 8;
	}
	else
	{
		count = 4;
	}

	if (self->enemy)
	{
		VectorCopy(self->movedir, last_movedir);
		VectorMA(self->enemy->absmin, 0.5, self->enemy->size, point);
		VectorSubtract(point, self->s.origin, self->movedir);
		VectorNormalize(self->movedir);

		if (!VectorCompare(self->movedir, last_movedir))
		{
			self->spawnflags |= 0x80000000;
		}
	}

	ignore = self;
	VectorCopy(self->s.origin, start);
	VectorMA(start, 2048, self->movedir, end);

	while (1)
	{
		if (self->spawnflags & LASER_STOPWINDOW)
		{
			tr = gi.trace(start, NULL, NULL, end, ignore, MASK_SHOT);
		}
		else
		{
			tr = gi.trace(start, NULL, NULL, end, ignore,
					CONTENTS_SOLID | CONTENTS_MONSTER | CONTENTS_DEADMONSTER);
		}

		if (!tr.ent)
		{
			break;
		}

		/* hurt it if we can */
		if ((tr.ent->takedamage) && !(tr.ent->flags & FL_IMMUNE_LASER))
		{
			T_Damage(tr.ent, self, self->activator, self->movedir, tr.endpos,
					vec3_origin, self->dmg, 1, DAMAGE_ENERGY, MOD_TARGET_LASER);
		}

		/* if we hit something that's not a monster or player or is immune to lasers, we're done */
		if (!(tr.ent->svflags & SVF_MONSTER) && (!tr.ent->client) &&
			!(tr.ent->svflags & SVF_DAMAGEABLE))
		{
			if (self->spawnflags & 0x80000000)
			{
				self->spawnflags &= ~0x80000000;
				gi.WriteByte(svc_temp_entity);
				gi.WriteByte(TE_LASER_SPARKS);
				gi.WriteByte(count);
				gi.WritePosition(tr.endpos);
				gi.WriteDir(tr.plane.normal);
				gi.WriteByte(self->s.skinnum);
				gi.multicast(tr.endpos, MULTICAST_PVS);
			}

			break;
		}

		ignore = tr.ent;
		VectorCopy(tr.endpos, start);
	}

	VectorCopy(tr.endpos, self->s.old_origin);

	self->nextthink = level.time + FRAMETIME;
}

void
target_laser_on(edict_t *self)
{
	if (!self)
	{
		return;
	}

	if (!self->activator)
	{
		self->activator = self;
	}

	self->spawnflags |= 0x80000001;
	self->svflags &= ~SVF_NOCLIENT;
	target_laser_think(self);
}

void
target_laser_off(edict_t *self)
{
	if (!self)
	{
		return;
	}

	self->spawnflags &= ~1;
	self->svflags |= SVF_NOCLIENT;
	self->nextthink = 0;
}

void
target_laser_use(edict_t *self, edict_t *other /* unused */, edict_t *activator)
{
	if (!self || !activator)
	{
		return;
	}

	self->activator = activator;

	if (self->spawnflags & 1)
	{
		target_laser_off(self);
	}
	else
	{
		target_laser_on(self);
	}
}

void
target_laser_start(edict_t *self)
{
	edict_t *ent;

	if (!self)
	{
		return;
	}

	self->movetype = MOVETYPE_NONE;
	self->solid = SOLID_NOT;
	self->s.renderfx |= RF_BEAM | RF_TRANSLUCENT;
	self->s.modelindex = 1; /* must be non-zero */

	/* set the beam diameter */
	if (self->spawnflags & 64)
	{
		self->s.frame = 16;
	}
	else
	{
		self->s.frame = 4;
	}

	/* set the color */
	if (self->spawnflags & 2)
	{
		self->s.skinnum = 0xf2f2f0f0;
	}
	else if (self->spawnflags & 4)
	{
		self->s.skinnum = 0xd0d1d2d3;
	}
	else if (self->spawnflags & 8)
	{
		self->s.skinnum = 0xf3f3f1f1;
	}
	else if (self->spawnflags & 16)
	{
		self->s.skinnum = 0xdcdddedf;
	}
	else if (self->spawnflags & 32)
	{
		self->s.skinnum = 0xe0e1e2e3;
	}

	if (!self->enemy)
	{
		if (self->target)
		{
			ent = G_Find(NULL, FOFS(targetname), self->target);

			if (!ent)
			{
				gi.dprintf("%s at %s: %s is a bad target\n", self->classname,
						vtos(self->s.origin), self->target);
			}

			self->enemy = ent;
		}
		else
		{
			G_SetMovedir(self->s.angles, self->movedir);
		}
	}

	self->use = target_laser_use;
	self->think = target_laser_think;

	if (!self->dmg)
	{
		self->dmg = 1;
	}

	VectorSet(self->mins, -8, -8, -8);
	VectorSet(self->maxs, 8, 8, 8);
	gi.linkentity(self);

	if (self->spawnflags & 1)
	{
		target_laser_on(self);
	}
	else
	{
		target_laser_off(self);
	}
}

void
SP_target_laser(edict_t *self)
{
	if (!self)
	{
		return;
	}

	self->think = target_laser_start;
	self->nextthink = level.time + 1;
}

/* ========================================================== */

/*
 * QUAKED target_lightramp (0 .5 .8) (-8 -8 -8) (8 8 8) TOGGLE
 *
 * speed		How many seconds the ramping will take
 * message		two letters; starting lightlevel and ending lightlevel
 */

void
target_lightramp_think(edict_t *self)
{
	char style[2];

	if (!self)
	{
		return;
	}

	style[0] = 'a' + self->movedir[0] + (level.time - self->timestamp) / FRAMETIME * self->movedir[2];
	style[1] = 0;
	gi.configstring(CS_LIGHTS + self->enemy->style, style);

	if ((level.time - self->timestamp) < self->speed)
	{
		self->nextthink = level.time + FRAMETIME;
	}
	else if (self->spawnflags & 1)
	{
		char temp;

		temp = self->movedir[0];
		self->movedir[0] = self->movedir[1];
		self->movedir[1] = temp;
		self->movedir[2] *= -1;
	}
}

void
target_lightramp_use(edict_t *self, edict_t *other /* unused */, edict_t *activator /* unused */)
{
	if (!self)
	{
		return;
	}

	if (!self->enemy)
	{
		edict_t *e;

		/* check all the targets */
		e = NULL;

		while (1)
		{
			e = G_Find(e, FOFS(targetname), self->target);

			if (!e)
			{
				break;
			}

			if (strcmp(e->classname, "light") != 0)
			{
				gi.dprintf("%s at %s ", self->classname, vtos(self->s.origin));
				gi.dprintf("target %s (%s at %s) is not a light\n", self->target,
						e->classname, vtos(e->s.origin));
			}
			else
			{
				self->enemy = e;
			}
		}

		if (!self->enemy)
		{
			gi.dprintf("%s target %s not found at %s\n", self->classname,
					self->target, vtos(self->s.origin));
			G_FreeEdict(self);
			return;
		}
	}

	self->timestamp = level.time;
	target_lightramp_think(self);
}

void
SP_target_lightramp(edict_t *self)
{
	if (!self)
	{
		return;
	}

	if (!self->message || (strlen(self->message) != 2) ||
		(self->message[0] < 'a') || (self->message[0] > 'z') ||
		(self->message[1] < 'a') || (self->message[1] > 'z') ||
		(self->message[0] == self->message[1]))
	{
		gi.dprintf("target_lightramp has bad ramp (%s) at %s\n", self->message, vtos(self->s.origin));
		G_FreeEdict(self);
		return;
	}

	if (deathmatch->value)
	{
		G_FreeEdict(self);
		return;
	}

	if (!self->target)
	{
		gi.dprintf("%s with no target at %s\n", self->classname, vtos(self->s.origin));
		G_FreeEdict(self);
		return;
	}

	self->svflags |= SVF_NOCLIENT;
	self->use = target_lightramp_use;
	self->think = target_lightramp_think;

	self->movedir[0] = self->message[0] - 'a';
	self->movedir[1] = self->message[1] - 'a';
	self->movedir[2] = (self->movedir[1] - self->movedir[0]) / (self->speed / FRAMETIME);
}

/* ========================================================== */

/*
 * QUAKED target_earthquake (1 0 0) (-8 -8 -8) (8 8 8) SILENT
 *
 * When triggered, this initiates a level-wide earthquake.
 * All players and monsters are affected.
 * "speed"		severity of the quake (default:200)
 * "count"		duration of the quake (default:5)
 */
void
target_earthquake_think(edict_t *self)
{
	int i;
	edict_t *e;

	if (!self)
	{
		return;
	}

	if (!(self->spawnflags & 1))
	{
		if (self->last_move_time < level.time)
		{
			gi.positioned_sound(self->s.origin, self, CHAN_AUTO, self->noise_index,
					1.0, ATTN_NONE, 0);
			self->last_move_time = level.time + 0.5;
		}
	}

	for (i = 1, e = g_edicts + i; i < globals.num_edicts; i++, e++)
	{
		if (!e->inuse)
		{
			continue;
		}

		if (!e->client)
		{
			continue;
		}

		if (!e->groundentity)
		{
			continue;
		}

		e->groundentity = NULL;
		e->velocity[0] += crandom() * 150;
		e->velocity[1] += crandom() * 150;
		e->velocity[2] = self->speed * (100.0 / e->mass);
	}

	if (level.time < self->timestamp)
	{
		self->nextthink = level.time + FRAMETIME;
	}
}

void
target_earthquake_use(edict_t *self, edict_t *other /* unused */, edict_t *activator)
{
	if (!self || !activator)
	{
		return;
	}

	self->timestamp = level.time + self->count;
	self->nextthink = level.time + FRAMETIME;
	self->activator = activator;
	self->last_move_time = 0;
}

void
SP_target_earthquake(edict_t *self)
{
	if (!self)
	{
		return;
	}

	if (!self->targetname)
	{
		gi.dprintf("untargeted %s at %s\n", self->classname, vtos( self->s.origin));
	}

	if (!self->count)
	{
		self->count = 5;
	}

	if (!self->speed)
	{
		self->speed = 200;
	}

	self->svflags |= SVF_NOCLIENT;
	self->think = target_earthquake_think;
	self->use = target_earthquake_use;

	if (!(self->spawnflags & 1))
	{
		self->noise_index = gi.soundindex("world/quake.wav");
	}
}
