/*======================================================================
 Pushables bmodels

 * Looked at the code from Hipnotic Interactive (hip_push.qc)
   - It uses a proxy entity for the player to interact with
   - Movement is done by constantly updating origin
   - Code seems really experimental and a horrible mess

   * Looked at the code from the Nehara mod (push.qc)
   - It uses the original bmodel entity with velocity instead
   - Has one push sound type and moves XY directions

   * AD solution is inspired by Nehara code 
   - It uses the original bmodel and velocity movement
   - Switches movetype/solid so players can stand on top
   - Fades the bmodel (via alpha) so it does not block view
   - Works with entity state system, reset very useful if stuck
   - Works really close with breakables (target2 triggers)
 
======================================================================*/
float PUSH_DAMAGE = 2;		// Can receive damage (from anything) 
float PUSH_NOFLRCHECK = 8;	// No floor below check for movement
float PUSH_FLOATING = 16;	// Start floating on spawn (like items)
float PUSH_NOMONSTER = 32;	// Cannot be damaged by monsters

// Pushable sound/impact types
float PTYPE_ROCK = 1;		// Default rock
float PTYPE_WOOD = 2;
float PTYPE_GLASS = 3;
float PTYPE_METAL = 4;
float PTYPE_BRICK = 5;
float PTYPE_CUSTOM = 10;	// Custom push sounds

// Three sounds to randomly pick from 0.5s each
string SOUND_PUSH_ROCK1 = "pushable/rock_m1.wav";
string SOUND_PUSH_ROCK2 = "pushable/rock_m2.wav";
string SOUND_PUSH_ROCK3 = "pushable/rock_m3.wav";
string SOUND_PUSH_WOOD1 = "pushable/wood_m1.wav";
string SOUND_PUSH_WOOD2 = "pushable/wood_m2.wav";
string SOUND_PUSH_WOOD3 = "pushable/wood_m3.wav";
string SOUND_PUSH_GLASS1 = "pushable/glass_m1.wav";
string SOUND_PUSH_GLASS2 = "pushable/glass_m2.wav";
string SOUND_PUSH_GLASS3 = "pushable/glass_m3.wav";
string SOUND_PUSH_METAL1 = "pushable/metal_m1.wav";
string SOUND_PUSH_METAL2 = "pushable/metal_m2.wav";
string SOUND_PUSH_METAL3 = "pushable/metal_m3.wav";
string SOUND_PUSH_BRICK1 = "pushable/brick_m1.wav";
string SOUND_PUSH_BRICK2 = "pushable/brick_m2.wav";
string SOUND_PUSH_BRICK3 = "pushable/brick_m3.wav";

// Which touch state (used for fading)
float STATE_PUSH = 1;
float STATE_SOLID = 2;

//======================================================================
/*QUAKED func_pushable (0 .5 .8) ? x DAMAGE x NOFLRCHECK FLOATING NOMONSTER STARTOFF x Not_Easy Not_Normal Not_Hard Not_DM
Moveable brushwork (pushable)
-------- KEYS --------
targetname : trigger entity (works with entity state system)
style      : sound move/impact types - 1=rock, 2=wood, 3=glass, 4=metal, 5=brick, 10=custom
noise1     : Custom move sound 1 (style=10)
noise2     : Custom move sound 2
noise3     : Custom move sound 3
bleedcolour: Impact particle colour (starting point on palette)
target     : target(s) to fire when pushable is dead (once only)
deathtarget: target(s) to fire when pushable is dead (once only)
target2    : points to a func_breakable_spawner to trigger on death
message    : Message to display when killed by player
message2   : Message to display when touched by player
wait       : Time for showing touch/death message (=-1 once, def=2)
health     : damage taken before death
death_dmg  : will explode on death (reqs health)
speed      : pushing speed (def = 1)
waitmin    : length of pushable sounds (def=0.5s)
waitmin2   : Alpha value to fade block down to when pushing
-------- SPAWNFLAGS --------
DAMAGE    : Pushable can be damaged or destroyed
NOFLRCHECK: No floor below check for extra velocity
FLOATING  : Pushable starts off floating (until touched)
NOMONSTER : Cannot be damaged by monsters
STARTOFF  : Starts off and waits for trigger
-------- NOTES --------
Moveable brushwork (pushable)
*/
//======================================================================

//----------------------------------------------------------------------
void() func_pushable_push =
{
	self.movetype = MOVETYPE_STEP;	// Default monster move type
	self.solid = SOLID_SLIDEBOX;	// Discrete movement stepping
	self.state = STATE_PUSH;		// Can be moved by player
};

//----------------------------------------------------------------------
void() func_pushable_solid = 
{
	self.movetype = MOVETYPE_PUSH;	// Blocks projectiles
	self.solid = SOLID_BSP;			// Square bounding box
	self.state = STATE_SOLID;		// No world interaction
	self.inpain = FALSE;			// Reset touch flag
};

//----------------------------------------------------------------------
void() func_pushable_movement =
{
	// Block entity state exceptions
	if (self.estate & ESTATE_BLOCK) return;

	// Block movement if nothing has changed
	if (self.oldorigin == self.origin && !self.inpain) return;
	self.oldorigin = self.origin;
	// Reset touch function flag
	self.inpain = FALSE;
	
	// Check for moving sound
	if (self.attack_finished < time && self.state == STATE_PUSH) {
		// Random start to setup blending and overlap
		self.attack_finished = time + self.waitmin + (random()*self.waitmin);
		// Make sure sound WAVs don't repeat in a row
		self.lefty = self.meleeattack;
		while (self.lefty == self.meleeattack) { 
			self.lefty = rint(random()*3);
		}
		// CHAN_AUTO to allow for sound blending
		self.meleeattack = self.lefty;
		if (self.meleeattack == 1) 
			sound (self, CHAN_AUTO, self.noise1, 1, ATTN_NORM);
		else if (self.meleeattack == 2) 
			sound (self, CHAN_AUTO, self.noise2, 1, ATTN_NORM);
		else sound (self, CHAN_AUTO, self.noise3, 1, ATTN_NORM);
	}	

	// Remove onground flag so bmodel can move
	self.flags = self.flags - (self.flags & FL_ONGROUND);

	// Transfer velocity from player to pushable
	// Read velocity from saved touch entity
	self.velocity_x = self.finaldest_x * self.speed;
	self.velocity_y = self.finaldest_y * self.speed;

	// Enable floor below check?
	if (!(self.spawnflags & PUSH_NOFLRCHECK)) {
		// Work out push direction
		makevectors(self.finalangle);
		self.movedir = normalize(v_forward);
		self.movedir_z = 0;
		
		// Work out true min/max for bmodel using origin (offset)
		self.bbmins = self.origin + self.mins;
		self.bbmaxs = self.origin + self.maxs;

		// Find the middle of the bottom edge
		self.move_state = self.bbmins;
		self.move_state_x = self.move_state_x + (self.size_x*0.5);
		self.move_state_y = self.move_state_y + (self.size_y*0.5);
		
		// Trace down and see if anything is underneath it
		traceline(self.move_state, self.move_state+'0 0 -1024', FALSE, self);
		// Work out length of traceline downward
		self.t_length = fabs(trace_endpos_z - self.move_state_z);
		
		// Is there space below middle point?
		if (self.t_length > 0) {
			// Keep velocity active while ground is missing
			self.velocity = self.velocity + (self.movedir*self.t_width);
			// Keep looping around until on ground
			self.think = func_pushable_movement;
			self.nextthink = time + 0.05;
		}
	}
};

//----------------------------------------------------------------------
void() func_pushable_touch =
{
	// Block entity state exceptions
	if (self.estate & ESTATE_BLOCK) return;

	// Allow for pushables to stack
	if (other.classtype == CT_FUNCPUSHABLE && other.state == STATE_PUSH) {
		// Switch to solid block
		func_pushable_solid();
		return;
	}
	
	// Only works for the player
	if ( !(other.flags & FL_CLIENT) ) return;

	// Touch function is active
	self.inpain = TRUE;

	// Keep updating touch condition for fade controller
	// Create an overlap for the touch to stop working
	self.pain_finished = time + 0.5;
	
	// Check for a touch message
	if (self.message2 != "" && self.idletimer < time) {
		if (self.wait > 0) self.idletimer = time + self.wait;
		centerprint(other, self.message2);
		if (self.wait < 0) self.message2 = "";
	}

	// Slow down the detection of the touching object
	//
	// This is not an ideal solution, its a horrible hack!
	// The touching entity should be consistent with its Z axis
	// origin if the entity is not moving up or down.
	// For some reason the Z axis is inconsistent +/- 18 units
	// Cannot slowdown the touch function because the movement
	// of the pushable stutters and looks terrible.
	// Touch functions cycle at really high frame rates so
	// its easier to slowdown the reading the entity instead!
	//
	if (self.oldenemy != other || self.meleetimer < time) {
		// Save player details for later
		self.oldenemy = other;
		self.dest1 = self.oldenemy.origin;
		self.dest2 = self.oldenemy.view_ofs;
		// Slowdown the reading of the touching entity
		self.meleetimer = time + 0.01;
	}
	
	self.finalangle = self.oldenemy.angles;
	self.finaldest = self.oldenemy.velocity;

	// Setup origin of player (at feet level)
	self.pos3 = self.dest1 - self.dest2;
	
	// Work out true min/max for bmodel using origin (offset)
	self.bbmins = self.origin + self.mins;
	self.bbmaxs = self.origin + self.maxs;

	// Check if player is above/below pushable
	if (self.pos3_z >= self.bbmaxs_z) 
		func_pushable_solid();		// Stop the bmodel interacting with player
	else {
		func_pushable_push();		// bmodel can be moved (again)
		func_pushable_movement();	// Move the pushable block
	}
};

//----------------------------------------------------------------------
void(entity inflictor, entity attacker, float damage) func_pushable_pain =
{
	local float loop_count;
	local vector vel;

	// default = Pushables cannot die
	if (!(self.spawnflags & PUSH_DAMAGE)) {self.health = MEGADEATH; return;}
	// Block entity state exceptions
	if (self.estate & ESTATE_BLOCK) return;

	// Something is trying to wear down the pushable with damage
	// work out facing angle and project particles upward
	makevectors(inflictor.angles);
	vel = -v_up*2;
	while(loop_count < 4) {
		particle (inflictor.origin, vel*0.1, self.bleedcolour + rint(random()*7), damage);
		loop_count = loop_count + 1;
	}
};

//----------------------------------------------------------------------
void() func_pushable_death =
{
	// default = Pushables cannot die
	if (!(self.spawnflags & PUSH_DAMAGE)) {self.health = MEGADEATH; return;}
	// Block entity state exceptions
	if (self.estate & ESTATE_BLOCK) return;
	
	// Work out current bmodel origin
	self.oldorigin = bmodel_origin(self);
	
	// Check for breakable spawners
	if (self.target2 != "") {
		self.enemy = find (world, targetname, self.target2);
		// Make sure its a breakable spawner ONLY!
		if (self.enemy.classtype == CT_FUNCBREAKSPN) {
			// Copy over pushable bmodel origin and volume to breakable
			self.enemy.origin = self.oldorigin;
			setorigin(self.enemy, self.enemy.origin);
			self.enemy.brkvol = self.size;
			// Use entity which killed pushable for breakable direction
			trigger_ent(self.enemy, self.activate);
		}
	}

	// Check for explosion sprite/particle effect
	if (self.death_dmg > 0) {
		WriteByte (MSG_BROADCAST, SVC_TEMPENTITY);
		WriteByte (MSG_BROADCAST, TE_EXPLOSION);
		WriteCoord (MSG_BROADCAST, self.oldorigin_x);
		WriteCoord (MSG_BROADCAST, self.oldorigin_y);
		WriteCoord (MSG_BROADCAST, self.oldorigin_z);
		
		SpawnExplosion(EXPLODE_SMALL, self.oldorigin, SOUND_REXP3);
		T_RadiusDamage (self, self, self.death_dmg, world, DAMAGEALL);
	}
	
	// Fire death targets (check DT first, because target can have uses)
	if (self.deathtarget != "") trigger_strs(self.deathtarget, self.activate);
	if (self.target != "") trigger_strs(self.target, self.activate);

	// Any final words ...
	if (self.activate.flags & FL_CLIENT && self.message != "")
		centerprint (self.activate, self.message);
	
	// Fire once condition?
	if (self.wait < 0) {
		self.target = self.deathtarget = self.message = "";
	}
	
	// Check for any pushable above? (fake physics)
	self.move_state = bmodel_origin(self);
	
	// Trace up and see if anything is above
	traceline(self.move_state, self.move_state + '0 0 1024', FALSE, self);

	// Check for a pushable entity above (but not touching)
	if (trace_ent != self && trace_ent.classtype == CT_FUNCPUSHABLE)
		trigger_ent(trace_ent, self);
	
	// Switch off entity instead of hide/remove
	// So the pushable can be reset again
	self.estate_off();
};

//======================================================================
// The pushable needs a controller to reach to the touch function
// and to reset the alpha fade if the pushable is not being touched
//======================================================================
void() pushable_controller_think =
{
	// Keep the controller alive
	self.think = pushable_controller_think;
	self.nextthink = time + 0.1;
	
	// Block entity state exceptions
	if (self.owner.estate & ESTATE_BLOCK) return;
	
	// Check if the player is touching the pushable?
	// If the player is standing on the block, no alpha
	if (self.owner.pain_finished > time && self.owner.state == STATE_PUSH) {
		// Is the alpha set the right amount?
		if (self.owner.alpha > self.owner.waitmin2) {
			self.owner.alpha = self.owner.alpha - 0.01;
			self.nextthink = time + FADEMODEL_TIME;
		}
	}
	else {
		if (self.owner.alpha < 1.00) {
			self.owner.alpha = self.owner.alpha + 0.01;
			self.nextthink = time + FADEMODEL_TIME;
		}
	}
};

//----------------------------------------------------------------------
void() func_pushable_controller_setup =
{
	// Check if controller entity been setup
	if (!self.attachment) self.attachment = spawn();

	// Setup link back to pushable
	self.attachment.owner = self;
	self.attachment.classtype = CT_CONTROL;
	self.attachment.movetype = MOVETYPE_NONE;
	self.attachment.solid = SOLID_NOT;
	// Move to pushable bmodel origin location
	self.attachment.origin = bmodel_origin(self);
	setorigin(self.attachment, self.attachment.origin);
	setsize (self.attachment, VEC_ORIGIN, VEC_ORIGIN);
	
	// Start fade controller think loop
	self.attachment.think = pushable_controller_think;
	self.attachment.nextthink = time + 0.1;
};

//======================================================================
// Entity state functions
//======================================================================
void() func_pushable_use =
{
	// Switch on pushable first
	if (self.estate == ESTATE_OFF) entity_state_on();
	else {
		// Let the pushable move around
		func_pushable_push();
		self.flags = self.flags - (self.flags & FL_ONGROUND);
		// Push upward using size as force
		self.velocity_z = self.t_width;
	}
};

//----------------------------------------------------------------------
void() func_pushable_reset = 
{	
	// If the block starts off(disabled) then do nothing
	if (self.spawnflags & ENT_STARTOFF) return;

	// reset to original position
	setorigin(self, VEC_ORIGIN);
	// Make sure the bmodel drops down, no weird ofset
	self.velocity = self.avelocity = '0 0 0';
	self.estate_on();	
};

//----------------------------------------------------------------------
void() func_pushable_off = 
{
	// Do nothing if pushable is already off
	if (self.estate == ESTATE_OFF) return;
	
	self.estate = ESTATE_OFF;
	self.movetype = MOVETYPE_NONE;
	self.solid = SOLID_NOT;
	setmodel (self, "");
	// Switch off impact particles
	self.takedamage = DAMAGE_NO;
};

//----------------------------------------------------------------------
void() func_pushable_on = 
{
	// Do nothing if pushable is already on
	if (self.estate == ESTATE_ON) return;
	
	// No longer need this spawnflag, remove it
	self.spawnflags = self.spawnflags - (self.spawnflags & ENT_STARTOFF);

	self.estate = ESTATE_ON;
	func_pushable_push();
	setmodel (self, self.mdl);
	
	// Setup touch fade controller
	if (!self.attachment) func_pushable_controller_setup();

	// Switch on impact particles
	self.takedamage = DAMAGE_AIM;
	// Reset health to spawn 
	self.health = self.max_health;

	if (!(self.spawnflags & PUSH_FLOATING)) {
		// Make sure it drops to ground on spawn
		self.origin_z = self.origin_z + 4;
		self.flags = self.flags - (self.flags & FL_ONGROUND);
	}
	else func_pushable_solid();
};

//======================================================================
// Main entity definition
//======================================================================
void() func_pushable =
{
	if (check_bmodel_keys()) return;	// Check for bmodel errors

	//------------------------------------------------------------------
	// make sure initial break sound is within range types (def=rock)
	if (self.style < PTYPE_ROCK || self.style > PTYPE_CUSTOM) 
		self.style = PTYPE_ROCK;
		
	// Medium drag sound with light brown impact particles
	if (self.style == PTYPE_WOOD) {
		self.noise1 = SOUND_PUSH_WOOD1;
		self.noise2 = SOUND_PUSH_WOOD2;
		self.noise3 = SOUND_PUSH_WOOD3;
		if (self.bleedcolour <= 0) self.bleedcolour = 112;
	}
	// Light drag sound with light yellow impact particles
	else if (self.style == PTYPE_GLASS) {
		self.noise1 = SOUND_PUSH_GLASS1;
		self.noise2 = SOUND_PUSH_GLASS2;
		self.noise3 = SOUND_PUSH_GLASS3;
		if (self.bleedcolour <= 0) self.bleedcolour = 96;
	}
	// Medium drag sound with green/brown impact particles
	else if (self.style == PTYPE_METAL) {
		self.noise1 = SOUND_PUSH_METAL1;
		self.noise2 = SOUND_PUSH_METAL2;
		self.noise3 = SOUND_PUSH_METAL3;
		if (self.bleedcolour <= 0) self.bleedcolour = 80;
	}
	// Light drag sound with brown impact particles
	else if (self.style == PTYPE_BRICK) {
		self.noise1 = SOUND_PUSH_BRICK1;
		self.noise2 = SOUND_PUSH_BRICK2;
		self.noise3 = SOUND_PUSH_BRICK3;
		if (self.bleedcolour <= 0) self.bleedcolour = 80;
	}
	// Custom sounds with white/grey impact particles
	else if (self.style == PTYPE_CUSTOM) {
		if (self.noise1 == "") self.noise1 = SOUND_EMPTY;
		if (self.noise2 == "") self.noise2 = SOUND_EMPTY;
		if (self.noise3 == "") self.noise3 = SOUND_EMPTY;
		if (self.bleedcolour <= 0) self.bleedcolour = 1;
	}
	// *** DEFAULT ***
	// Heavy drag sound with brown impact particles
	else {
		self.noise1 = SOUND_PUSH_ROCK1;
		self.noise2 = SOUND_PUSH_ROCK2;
		self.noise3 = SOUND_PUSH_ROCK3;
		if (self.bleedcolour <= 0) self.bleedcolour = 80;
	}
	
	// Cache all sounds
	precache_sound (self.noise1);
	precache_sound (self.noise2);
	precache_sound (self.noise3);

	self.classtype = CT_FUNCPUSHABLE;
	self.classgroup = CG_BREAKABLE;
	self.bsporigin = TRUE;
	self.mdl = self.model;

	// Save starting position and reset angles
	self.mangle = self.angles;
	self.angles = '0 0 0';

	// Default parameters and states
	if (!self.speed) self.speed = 1;
	if (!self.wait) self.wait = 2;
	
	// Length of dragging sounds
	if (!self.waitmin) self.waitmin = 0.5;
	// Create halway point for random start
	self.waitmin = self.waitmin * 0.5;
	// Start with pushing state
	self.state = STATE_PUSH;
	self.pain_finished = -1;
	// Setup default alpha fade (lower) value
	if (!self.waitmin2) self.waitmin2 = 0.6;
	self.alpha = 1;

	// setup pain/die functions
	self.th_pain = func_pushable_pain;
	self.th_die = func_pushable_death;
	
	// The pushable needs to take damage for impact sounds
	// to work correctly. Check for health default first
	// Pain/death needs to reset health if indestructible
	if (self.health <= 0) {
		if (self.spawnflags & PUSH_DAMAGE) self.health = 1;
		else self.health = MEGADEATH;
	}
	self.max_health = self.health;
	
	// Setup the bmodel ready for interaction
	func_pushable_solid();
	setmodel(self, self.mdl);
	setorigin(self, VEC_ORIGIN);
	setsize(self, self.mins, self.maxs);
	
	// Work out average size of block
	self.t_width = rint((self.size_x + self.size_y) * 0.5);
	
	// Check for spawning conditions (nightmare, coop)
	// Needs to exist after entity has been added to work for BSPorigin
	if (check_nightmare() == TRUE) return;
	if (check_coop() == TRUE) return;
	
	// Setup Entity State functionality
	self.use = entity_state_use;
	self.estate_use = func_pushable_use;
	self.estate_on = func_pushable_on;
	self.estate_off = func_pushable_off;
	self.estate_reset = func_pushable_reset;
	self.touch = func_pushable_touch;
	
	// Setup pushable in correct state
	if (self.spawnflags & ENT_STARTOFF) self.estate_off();
	else self.estate_on();
};
