/* #FILENAME# This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to: Free Software Foundation, Inc. 59 Temple Place - Suite 330 Boston, MA 02111-1307, USA $Id$ */ /* =========================================================================== grapple.qc 12/29/96 Quakeworld-friendly grapple hook code by Wedge (Steve Bond) visit Quake Command http://www.nuc.net/quake Original 'Morning Star' (Grapple Hook) by "Mike" I took care to preserve the speed and damage values of the original Morning Star. Depending on latency, performance should be near exact. =========================================================================== */ // prototypes for WEAPONS.QC functions float() crandom; void(vector org, float damage) SpawnBlood; // // Reset_Grapple - Removes the hook and resets its owner's state. // expects a pointer to the hook // void (entity rhook) Reset_Grapple = { if (rhook.owner == world) return; sound (rhook.owner, CHAN_NO_PHS_ADD+CHAN_WEAPON, "weapons/bounce2.wav", 1, ATTN_NORM); rhook.owner.on_hook = FALSE; rhook.owner.hook_out = FALSE; rhook.owner.weaponframe = 0; rhook.owner.attack_finished = time + 0.25; rhook.think = SUB_Remove; rhook.nextthink = time; }; // // Grapple_Track - Constantly updates the hook's position relative to // what it's hooked to. Inflicts damage if attached to // a player that is not on the same team as the hook's // owner. // void () Grapple_Track = { // Release dead targets if (self.enemy.classname == "player" && self.enemy.health <= 0) { self.owner.on_hook = FALSE; self.owner.attack_finished = time +0.75; } // drop the hook if owner is dead or has released the button if (!self.owner.on_hook || self.owner.health <= 0) { Reset_Grapple (self); return; } // bring the pAiN! if (self.enemy.classname == "player") { // 4.1, if we can't see our enemy, unlock if (!CanDamage(self.enemy, self.owner)) { Reset_Grapple(self); return; } // move the hook along with the player. It's invisible, but // we need this to make the sound come from the right spot setorigin(self, self.enemy.origin); sound (self, CHAN_WEAPON, "blob/land1.wav", 1, ATTN_NORM); T_Damage (self.enemy, self, self.owner, 1); makevectors (self.v_angle); SpawnBlood(self.enemy.origin, 1); } // If the hook is not attached to the player, constantly copy // copy the target's velocity. Velocity copying DOES NOT work properly // for a hooked client. if (self.enemy.classname != "player") self.velocity = self.enemy.velocity; self.nextthink = time + 0.1; }; // // MakeLink - spawns the chain link entities // entity (float head) MakeLink = { newmis = spawn (); newmis.movetype = MOVETYPE_FLYMISSILE; newmis.solid = SOLID_NOT; newmis.owner = self;// SELF is the hook! newmis.avelocity = '200 200 200'; setmodel (newmis, "progs/bit.mdl"); setorigin (newmis, self.origin); setsize (newmis, '0 0 0' , '0 0 0'); return newmis; }; // // Remove_Chain - Removes all chain link entities; this is a separate // function because CLIENT also needs to be able // to remove the chain. Only one function required to // remove all links. // void () Remove_Chain = { self.think = SUB_Remove; self.nextthink = time; if (self.goalentity) { self.goalentity.think = SUB_Remove; self.goalentity.nextthink = time; if (self.goalentity.goalentity) { self.goalentity.goalentity.think = SUB_Remove; self.goalentity.goalentity.nextthink = time; } } }; // // Update_Chain - Repositions the chain links each frame. This single function // maintains the positions of all of the links. Only one link // is thinking every frame. // void () Update_Chain = { local vector temp; if (!self.owner.hook_out) { self.think = Remove_Chain; self.nextthink = time; return; } temp = (self.owner.hook.origin - self.owner.origin); // These numbers are correct assuming 3 links. // 4 links would be *20 *40 *60 and *80 setorigin (self, self.owner.origin + temp * 0.25); setorigin (self.goalentity, self.owner.origin + temp * 0.5); setorigin (self.goalentity.goalentity, self.owner.origin + temp * 0.75); self.nextthink = time + 0.1; }; // // Build_Chain - Builds the chain (linked list) // void () Build_Chain = { self.goalentity = MakeLink(); self.goalentity.think = Update_Chain; self.goalentity.nextthink = time + 0.1; self.goalentity.owner = self.owner; self.goalentity.goalentity = MakeLink(); self.goalentity.goalentity.goalentity = MakeLink(); }; // // Check_Overhead - Makes sure there is sufficient headroom above the player // so that setorigin doesn't stick them into a wall. I tried // to compare pointcontents, but that was too flaky. // float () Check_Overhead = { local vector src; local vector end; makevectors (self.owner.angles); // The following comparisons could be optimized by doing away with // SRC and END, and plugging the values directly into the traceline // function calls. Using SRC and END made debugging easier. You // decide if it's worth it. // quick check right above head src = self.owner.origin - '0 0 24'; end = self.owner.origin - '0 0 24'; traceline (src, end, FALSE, self.owner); if (trace_fraction != 1.0) return FALSE; src = self.owner.origin - '0 0 24' - v_forward * 16; end = self.owner.origin - '0 0 24' - v_forward * 16 + '0 0 58'; traceline (src, end, FALSE, self.owner); if (trace_fraction != 1.0) return FALSE; src = self.owner.origin - '0 0 24' + v_forward * 16; end = self.owner.origin - '0 0 24' + v_forward * 16 + '0 0 58'; traceline (src, end, FALSE, self.owner); if (trace_fraction != 1.0) return FALSE; src = self.owner.origin - '0 0 24' - v_right * 16; end = self.owner.origin - '0 0 24' - v_right * 16 + '0 0 58'; traceline (src, end, FALSE, self.owner); if (trace_fraction != 1.0) return FALSE; src = self.owner.origin - '0 0 24' + v_right * 16; end = self.owner.origin - '0 0 24' + v_right * 16 + '0 0 58'; traceline (src, end, FALSE, self.owner); if (trace_fraction != 1.0) return FALSE; return TRUE; }; // // Anchor_Grapple - Tries to anchor the grapple to whatever it touches // void () Anchor_Grapple = { local float test; if (other == self.owner) return; // DO NOT allow the grapple to hook to any projectiles, no matter WHAT! // if you create new types of projectiles, make sure you use one of the // classnames below or write code to exclude your new classname so // grapples will not stick to them. if (other.classname == "missile" || other.classname == "grenade" || other.classname == "spike" || other.classname == "hook") return; // Don't stick the the sky. if (pointcontents(self.origin) == CONTENT_SKY) { Reset_Grapple (self); return; } if (other.classname == "player") { // glance off of teammates if (other.steam == self.owner.steam) return; sound (self, CHAN_WEAPON, "player/axhit1.wav", 1, ATTN_NORM); T_Damage (other, self, self.owner, 10); // make hook invisible since we will be pulling directly // towards the player the hook hit. Quakeworld makes it // too quirky to try to match hook's velocity with that of // the client that it hit. setmodel (self, ""); } else if (other.classname != "player") { sound (self, CHAN_WEAPON, "player/axhit2.wav", 1, ATTN_NORM); // One point of damage inflicted upon impact. Subsequent // damage will only be done to PLAYERS... this way secret // doors and triggers will only be damaged once. if (other.takedamage) T_Damage (other, self, self.owner, 1); self.velocity = '0 0 0'; self.avelocity = '0 0 0'; } // conveniently clears the sound channel of the CHAIN1 sound, // which is a looping sample and would continue to play. Tink1 is // the least offensive choice, ass NULL.WAV loops and clogs the // channel with silence sound (self.owner, CHAN_NO_PHS_ADD+CHAN_WEAPON, "weapons/tink1.wav", 1, ATTN_NORM); if (!self.owner.button0) { Reset_Grapple (self); return; } /* // our last chance to avoid being picked up off of the ground. // check over the client's head to make sure there is one unit // clearance so we can lift him off of the ground. test = Check_Overhead (); if (!test) { Reset_Grapple (self); return; } */ if (self.owner.flags & FL_ONGROUND) { self.owner.flags = self.owner.flags - FL_ONGROUND; // setorigin(self.owner,self.owner.origin + '0 0 1'); } self.owner.on_hook = TRUE; sound (self.owner, CHAN_WEAPON, "weapons/chain2.wav", 1, ATTN_NORM); // CHAIN2 is a looping sample. Use LEFTY as a flag so that client.qc // will know to only play the tink sound ONCE to clear the weapons // sound channel. (Lefty is a leftover from AI.QC, so I reused it to // avoid adding a field) self.owner.lefty = TRUE; self.enemy = other;// remember this guy! self.think = Grapple_Track; self.nextthink = time; self.solid = SOLID_NOT; self.touch = SUB_Null; }; // // Throw_Grapple - called from WEAPONS.QC, 'fires' the grapple // void () Throw_Grapple = { if (self.hook_out)// reject subsequent calls from player.qc return; msg_entity = self; WriteByte (MSG_ONE, SVC_SMALLKICK); // chain out sound (loops) sound (self, CHAN_WEAPON, "weapons/chain1.wav", 1, ATTN_NORM); newmis = spawn(); newmis.movetype = MOVETYPE_FLYMISSILE; newmis.solid = SOLID_BBOX; newmis.owner = self;// newmis belongs to me self.hook = newmis;// This is my newmis newmis.classname = "hook"; makevectors (self.v_angle); newmis.velocity = v_forward * 800; newmis.avelocity = '0 0 -500'; newmis.touch = Anchor_Grapple; newmis.think = Build_Chain; newmis.nextthink = time + 0.1;// don't jam newmis and links into same packet setmodel (newmis,"progs/star.mdl"); setorigin (newmis, self.origin + v_forward * 16 + '0 0 16'); setsize(newmis, '0 0 0' , '0 0 0 '); self.hook_out = TRUE; }; // // Service_Grapple - called each frame by CLIENT.QC if client is ON_HOOK // void () Service_Grapple = { local vector hook_dir; // drop the hook if player lets go of button if (!self.button0) { if (self.weapon == IT_GRAPPLE) { Reset_Grapple (self.hook); return; } } // If hooked to a player, track them directly! if (self.hook.enemy.classname == "player") hook_dir = (self.hook.enemy.origin - self.origin); // else, track to hook else if (self.hook.enemy.classname != "player") hook_dir = (self.hook.origin - self.origin); self.velocity = normalize(hook_dir) * 750; if ( vlen(hook_dir) <= 100 && self.lefty)// cancel chain sound { // If there is a chain, ditch it now. We're // close enough. Having extra entities lying around // is never a good idea. if (self.hook.goalentity) { self.hook.goalentity.think = Remove_Chain; self.hook.goalentity.nextthink = time; } sound(self, CHAN_NO_PHS_ADD+CHAN_WEAPON, "weapons/chain3.wav", 1, ATTN_NORM); self.lefty = FALSE;// we've reset the sound channel. } };