Files
quakemapping/mod_ad_dev/my_progs/pushable.qc
2019-12-30 22:24:44 +01:00

576 lines
19 KiB
Plaintext

/*======================================================================
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();
};