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