#include "sv_keepaway.qh"
#include <common/effects/all.qh>
+#include <server/client.qh>
+#include <server/gamelog.qh>
+#include <server/damage.qh>
+#include <server/items/items.qh>
+#include <server/world.qh>
.entity ballcarried;
GameLogEcho(strcat(":ka:", mode, ((actor != NULL) ? (strcat(":", ftos(actor.playerid))) : "")));
}
-void ka_TouchEvent(entity this, entity toucher);
void ka_RespawnBall(entity this) // runs whenever the ball needs to be relocated
{
if(game_stopped) return;
vector oldballorigin = this.origin;
if(!MoveToRandomMapLocation(this, DPCONTENTS_SOLID | DPCONTENTS_CORPSE | DPCONTENTS_PLAYERCLIP, DPCONTENTS_SLIME | DPCONTENTS_LAVA | DPCONTENTS_SKY | DPCONTENTS_BODY | DPCONTENTS_DONOTENTER, Q3SURFACEFLAG_SKY, 10, 1024, 256))
- {
- entity spot = SelectSpawnPoint(this, true);
- setorigin(this, spot.origin);
- this.angles = spot.angles;
- }
+ setorigin(this, SelectSpawnPoint(this, true).origin);
- makevectors(this.angles);
set_movetype(this, MOVETYPE_BOUNCE);
this.velocity = '0 0 200';
this.angles = '0 0 0';
sound(this, CH_TRIGGER, SND_KA_RESPAWN, VOL_BASE, ATTEN_NONE); // ATTEN_NONE (it's a sound intended to be heard anywhere)
}
+.float timepoints_counter;
+MUTATOR_HOOKFUNCTION(ka, reset_map_global)
+{
+ FOREACH_CLIENT(true,
+ {
+ it.timepoints_counter = 0;
+ });
+ return true;
+}
+
void ka_TimeScoring(entity this)
{
if(this.owner.ballcarried)
{ // add points for holding the ball after a certain amount of time
+ float timescore = 0;
if(autocvar_g_keepaway_score_timepoints)
- GameRules_scoring_add(this.owner, SCORE, autocvar_g_keepaway_score_timepoints);
+ timescore = autocvar_g_keepaway_score_timepoints / max(0.001, autocvar_g_keepaway_score_timeinterval);
- GameRules_scoring_add(this.owner, KEEPAWAY_BCTIME, (autocvar_g_keepaway_score_timeinterval / 1)); // interval is divided by 1 so that time always shows "seconds"
- this.nextthink = time + autocvar_g_keepaway_score_timeinterval;
+ if (timescore)
+ GameRules_scoring_add_float2int(this.owner, SCORE, timescore, timepoints_counter, 1);
+
+ GameRules_scoring_add(this.owner, KEEPAWAY_BCTIME, 1);
+ this.nextthink = time + 1;
}
}
+void ka_DamageEvent(entity this, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
+{
+ if(ITEM_DAMAGE_NEEDKILL(deathtype))
+ ka_RespawnBall(this);
+}
+
void ka_TouchEvent(entity this, entity toucher) // runs any time that the ball comes in contact with something
{
if (!this || game_stopped)
ka_RespawnBall(this);
return;
}
+ if(toucher.ballcarried) { return; }
if(IS_DEAD(toucher)) { return; }
if(STAT(FROZEN, toucher)) { return; }
if (!IS_PLAYER(toucher))
this.effects |= EF_NODRAW;
settouch(this, func_null);
setthink(this, ka_TimeScoring);
- this.nextthink = time + autocvar_g_keepaway_score_timeinterval;
+ this.nextthink = time + 1;
this.takedamage = DAMAGE_NO;
+ this.event_damage = func_null;
+ this.damagedbycontents = false;
+ IL_REMOVE(g_damagedbycontents, this);
navigation_dynamicgoal_unset(this);
// apply effects to player
WaypointSprite_Kill(this.waypointsprite_attachedforcarrier);
}
-void ka_PlayerReset(entity plyr)
+void ka_PlayerReset(entity player)
{
- plyr.ballcarried = NULL;
- GameRules_scoring_vip(plyr, false);
- WaypointSprite_Kill(plyr.waypointsprite_attachedforcarrier);
+ player.ballcarried = NULL;
+ GameRules_scoring_vip(player, false);
+ WaypointSprite_Kill(player.waypointsprite_attachedforcarrier);
// reset the player effects
- plyr.glow_trail = false;
- plyr.effects &= ~autocvar_g_keepaway_ballcarrier_effects;
+ player.glow_trail = false;
+ player.effects &= ~autocvar_g_keepaway_ballcarrier_effects;
}
-void ka_DropEvent(entity plyr) // runs any time that a player is supposed to lose the ball
+void ka_DropEvent(entity player) // runs any time that a player is supposed to lose the ball
{
- entity ball;
- ball = plyr.ballcarried;
+ entity ball = player.ballcarried;
if(!ball) { return; }
setthink(ball, ka_RespawnBall);
ball.nextthink = time + autocvar_g_keepawayball_respawntime;
ball.takedamage = DAMAGE_YES;
+ ball.event_damage = ka_DamageEvent;
+ ball.damagedbycontents = true;
+ IL_PUSH(g_damagedbycontents, ball);
ball.effects &= ~EF_NODRAW;
- setorigin(ball, plyr.origin + '0 0 10');
+ setorigin(ball, player.origin + '0 0 10');
+ nudgeoutofsolid(ball); // a ball has a horizontally bigger bbox than a player
ball.velocity = '0 0 200' + '0 100 0'*crandom() + '100 0 0'*crandom();
ball.owner = NULL;
- navigation_dynamicgoal_set(ball, plyr);
+ navigation_dynamicgoal_set(ball, player);
// messages and sounds
- ka_EventLog("dropped", plyr);
- Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_KEEPAWAY_DROPPED, plyr.netname);
- Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_KEEPAWAY_DROPPED, plyr.netname);
+ ka_EventLog("dropped", player);
+ Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_KEEPAWAY_DROPPED, player.netname);
+ Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_KEEPAWAY_DROPPED, player.netname);
sound(NULL, CH_TRIGGER, SND_KA_DROPPED, VOL_BASE, ATTEN_NONE); // ATTEN_NONE (it's a sound intended to be heard anywhere)
// waypoints
WaypointSprite_UpdateRule(ball.waypointsprite_attachedforcarrier, 0, SPRITERULE_DEFAULT);
WaypointSprite_Ping(ball.waypointsprite_attachedforcarrier);
- ka_PlayerReset(plyr);
+ ka_PlayerReset(player);
}
.bool pushable;
MODEL(KA_BALL, "models/orbs/orbblue.md3");
-void ka_RemoveBall()
+void ka_RemoveBalls()
{
- entity plyr = ka_ball.owner;
- if (plyr) // it was attached
- ka_PlayerReset(plyr);
- else
- WaypointSprite_DetachCarrier(ka_ball);
- delete(ka_ball);
- ka_ball = NULL;
+ IL_EACH(g_kaballs, true,
+ {
+ if (it.owner) // it was attached
+ ka_PlayerReset(it.owner);
+ else
+ WaypointSprite_DetachCarrier(it);
+ delete(it);
+ });
}
-void ka_SpawnBall()
+void ka_SpawnBalls()
{
- entity e = new(keepawayball);
- setmodel(e, MDL_KA_BALL);
- setsize(e, '-16 -16 -20', '16 16 20'); // 20 20 20 was too big, player is only 16 16 24... gotta cheat with the Z (20) axis so that the particle isn't cut off
- e.damageforcescale = autocvar_g_keepawayball_damageforcescale;
- e.takedamage = DAMAGE_YES;
- e.solid = SOLID_TRIGGER;
- set_movetype(e, MOVETYPE_BOUNCE);
- e.glow_color = autocvar_g_keepawayball_trail_color;
- e.glow_trail = true;
- e.flags = FL_ITEM;
- IL_PUSH(g_items, e);
- e.pushable = true;
- settouch(e, ka_TouchEvent);
- e.owner = NULL;
- ka_ball = e;
- navigation_dynamicgoal_init(ka_ball, false);
-
- InitializeEntity(e, ka_RespawnBall, INITPRIO_SETLOCATION); // is this the right priority? Neh, I have no idea.. Well-- it works! So.
+ int i = 0;
+ do // never allow less than 1 ball to spawn
+ {
+ entity e = new(keepawayball);
+ setmodel(e, MDL_KA_BALL);
+ e.solid = SOLID_TRIGGER; // before setsize to ensure area grid linking
+ // 20 20 20 was too big, player is only 16 16 24... gotta cheat with the Z (20) axis so that the particle isn't cut off
+ // bones_was_here: that was WITH sv_legacy_bbox_expand 1 and FL_ITEM (mins -= '15 15 1'; maxs += '15 15 1')
+ // it's round so should have a symmetrical bbox, same height as pickup items so it can't be jumped over in any physics
+ setsize(e, '-24 -24 -24', '24 24 24');
+ e.damageforcescale = autocvar_g_keepawayball_damageforcescale;
+ e.takedamage = DAMAGE_YES;
+ e.event_damage = ka_DamageEvent;
+ e.damagedbycontents = true;
+ IL_PUSH(g_damagedbycontents, e);
+ set_movetype(e, MOVETYPE_BOUNCE);
+ e.glow_color = autocvar_g_keepawayball_trail_color;
+ e.glow_trail = true;
+ e.flags = FL_ITEM;
+ IL_PUSH(g_items, e);
+ e.pushable = true;
+ settouch(e, ka_TouchEvent);
+ e.owner = NULL;
+ IL_PUSH(g_kaballs, e);
+ navigation_dynamicgoal_init(e, false);
+
+ ka_RespawnBall(e);
+
+ ++i;
+ }
+ while (i < KA_BALL_COUNT);
}
void ka_Handler_CheckBall(entity this)
{
if(time < game_starttime)
{
- if (ka_ball)
- ka_RemoveBall();
+ if (!IL_EMPTY(g_kaballs))
+ ka_RemoveBalls();
}
else
{
- if (!ka_ball)
- ka_SpawnBall();
+ if (IL_EMPTY(g_kaballs))
+ ka_SpawnBalls(); // ;)
}
this.nextthink = time;
}
-void ka_Initialize() // run at the start of a match, initiates game mode
-{
- ka_Handler = new(ka_Handler);
- setthink(ka_Handler, ka_Handler_CheckBall);
- ka_Handler.nextthink = time;
-}
-
// ================
// Bot player logic
void havocbot_goalrating_ball(entity this, float ratingscale, vector org)
{
- entity ball_owner;
- ball_owner = ka_ball.owner;
-
- if (ball_owner == this)
- return;
+ entity ball = NULL, ball_carried = NULL;
- if (ball_owner)
- navigation_routerating(this, ball_owner, ratingscale, 2000);
- else
- navigation_routerating(this, ka_ball, ratingscale, 2000);
+ // stops at last ball, prefers ball without carrier
+ IL_EACH(g_kaballs, it.owner != this,
+ {
+ if(it.owner)
+ ball_carried = it.owner;
+ else
+ ball = it;
+ });
+
+ if(ball)
+ navigation_routerating(this, ball, ratingscale, 2000);
+ else if(ball_carried)
+ navigation_routerating(this, ball_carried, ratingscale, 2000);
}
void havocbot_role_ka_carrier(entity this)
return true; // you deceptive little bugger ;3 This needs to be true in order for this function to even count.
}
+MUTATOR_HOOKFUNCTION(ka, Scores_CountFragsRemaining)
+{
+ // announce remaining frags, but only when timed scoring is off
+ return !autocvar_g_keepaway_score_timepoints;
+}
+
MUTATOR_HOOKFUNCTION(ka, PlayerPreThink)
{
entity player = M_ARGV(0, entity);
- // clear the item used for the ball in keepaway
- player.items &= ~IT_KEY1;
-
// if the player has the ball, make sure they have the item for it (Used for HUD primarily)
- if(player.ballcarried)
- player.items |= IT_KEY1;
+ STAT(OBJECTIVE_STATUS, player) = BITSET(STAT(OBJECTIVE_STATUS, player), KA_CARRYING, player.ballcarried != NULL);
}
MUTATOR_HOOKFUNCTION(ka, PlayerUseKey)
}
}
-MUTATOR_HOOKFUNCTION(ka, Damage_Calculate) // for changing damage and force values that are applied to players in g_damage.qc
+MUTATOR_HOOKFUNCTION(ka, Damage_Calculate) // for changing damage and force values that are applied to players
{
entity frag_attacker = M_ARGV(1, entity);
entity frag_target = M_ARGV(2, entity);
- float frag_damage = M_ARGV(4, float);
- vector frag_force = M_ARGV(6, vector);
+
+ // as a gamemode rule, only apply scaling to player versus player combat
+ if(!IS_PLAYER(frag_attacker) || !IS_PLAYER(frag_target))
+ return;
if(frag_attacker.ballcarried) // if the attacker is a ballcarrier
{
if(frag_target == frag_attacker) // damage done to yourself
{
- frag_damage *= autocvar_g_keepaway_ballcarrier_selfdamage;
- frag_force *= autocvar_g_keepaway_ballcarrier_selfforce;
+ M_ARGV(4, float) *= autocvar_g_keepaway_ballcarrier_selfdamage;
+ M_ARGV(6, vector) *= autocvar_g_keepaway_ballcarrier_selfforce;
}
- else // damage done to noncarriers
+ else // damage done to other ballcarriers
{
- frag_damage *= autocvar_g_keepaway_ballcarrier_damage;
- frag_force *= autocvar_g_keepaway_ballcarrier_force;
+ M_ARGV(4, float) *= autocvar_g_keepaway_ballcarrier_damage;
+ M_ARGV(6, vector) *= autocvar_g_keepaway_ballcarrier_force;
}
}
- else if (!frag_target.ballcarried) // if the target is a noncarrier
+ else // if the attacker is a noncarrier
{
if(frag_target == frag_attacker) // damage done to yourself
{
- frag_damage *= autocvar_g_keepaway_noncarrier_selfdamage;
- frag_force *= autocvar_g_keepaway_noncarrier_selfforce;
+ M_ARGV(4, float) *= autocvar_g_keepaway_noncarrier_selfdamage;
+ M_ARGV(6, vector) *= autocvar_g_keepaway_noncarrier_selfforce;
}
else // damage done to other noncarriers
{
- frag_damage *= autocvar_g_keepaway_noncarrier_damage;
- frag_force *= autocvar_g_keepaway_noncarrier_force;
+ M_ARGV(4, float) *= autocvar_g_keepaway_noncarrier_damage;
+ M_ARGV(6, vector) *= autocvar_g_keepaway_noncarrier_force;
}
}
-
- M_ARGV(4, float) = frag_damage;
- M_ARGV(6, vector) = frag_force;
}
MUTATOR_HOOKFUNCTION(ka, ClientDisconnect)
entity targ = M_ARGV(1, entity);
// if neither player has ball then don't attack unless the ball is on the ground
- if(!targ.ballcarried && !bot.ballcarried && ka_ball.owner)
+ bool have_held_ball = false;
+ IL_EACH(g_kaballs, it.owner,
+ {
+ have_held_ball = true;
+ break;
+ });
+ if(!targ.ballcarried && !bot.ballcarried && have_held_ball)
return true;
}