From: Mario Date: Sat, 18 Apr 2020 14:00:26 +0000 (+1000) Subject: Implement teamplay-based variant of keepaway X-Git-Url: https://git.rm.cloudns.org/?a=commitdiff_plain;h=6c9238ad8aaf48ab28624d27c2e8fb881bd39043;p=xonotic%2Fxonotic-data.pk3dir.git Implement teamplay-based variant of keepaway --- diff --git a/gamemodes-client.cfg b/gamemodes-client.cfg index c43b9d1d3f..a24a321706 100644 --- a/gamemodes-client.cfg +++ b/gamemodes-client.cfg @@ -32,6 +32,7 @@ alias cl_hook_gamestart_ka alias cl_hook_gamestart_ft alias cl_hook_gamestart_inv alias cl_hook_gamestart_duel +alias cl_hook_gamestart_tka alias cl_hook_gameend "rpn /cl_matchcount dup load 1 + =" // increase match count every time a game ends alias cl_hook_shutdown alias cl_hook_activeweapon diff --git a/gamemodes-server.cfg b/gamemodes-server.cfg index b1631b2333..67919f8899 100644 --- a/gamemodes-server.cfg +++ b/gamemodes-server.cfg @@ -29,6 +29,7 @@ alias sv_hook_gamestart_ka alias sv_hook_gamestart_ft alias sv_hook_gamestart_inv alias sv_hook_gamestart_duel +alias sv_hook_gamestart_tka // there is currently no hook for when the match is restarted // see sv_hook_readyrestart for previous uses of this hook //alias sv_hook_gamerestart @@ -58,6 +59,7 @@ alias sv_vote_gametype_hook_ons alias sv_vote_gametype_hook_rc alias sv_vote_gametype_hook_tdm alias sv_vote_gametype_hook_duel +alias sv_vote_gametype_hook_tka // Example preset to allow 1v1ctf to be used for the gametype voting screen. // Aliases can have max 31 chars so the gametype can have max 9 chars. @@ -208,6 +210,13 @@ set g_duel_respawn_delay_large_count 0 set g_duel_respawn_delay_max 0 set g_duel_respawn_waves 0 set g_duel_weapon_stay 0 +set g_tka_respawn_delay_small 0 +set g_tka_respawn_delay_small_count 0 +set g_tka_respawn_delay_large 0 +set g_tka_respawn_delay_large_count 0 +set g_tka_respawn_delay_max 0 +set g_tka_respawn_waves 0 +set g_tka_weapon_stay 0 // ========= @@ -551,3 +560,34 @@ set g_duel 0 "Duel: frag the opponent more in a one versus one arena battle" //set g_duel_warmup 180 "Have a short warmup period before beginning the actual duel" set g_duel_with_powerups 0 "Enable powerups to spawn in the duel gamemode" set g_duel_not_dm_maps 0 "when this is set, DM maps will NOT be listed in duel" + +// =============== +// team keepaway +// =============== +set g_tka 0 "another game mode which focuses around a ball" +set g_tka_on_dm_maps 0 "when this is set, all DM and KA maps automatically support TKA" +set g_tka_teams 2 "how many teams are in team keepaway (set by mapinfo)" +set g_tka_team_spawns 0 "when 1, players spawn from the team spawnpoints of the map, if any" +set g_tka_teams_override 0 "how many teams are in team keepaway" +set g_tka_point_limit -1 "TKA point limit overriding the mapinfo specified one (use 0 to play without limit, and -1 to use the mapinfo's limit)" +set g_tka_point_leadlimit -1 "TKA point lead limit overriding the mapinfo specified one (use 0 to play without limit, and -1 to use the mapinfo's limit)" +set g_tka_score_team 1 "allow points to be awarded to teammates for any kill when the ball is in your team's possession" +set g_tka_score_bckill 1 "points for killing the ball barrier (Ball Carrier Kill)" +set g_tka_score_killac 1 "points for kills while holding the ball (Kill As Carrier)" +set g_tka_score_timeinterval 1 "amount of time it takes between intervals for timepoints to be added to the score" +set g_tka_score_timepoints 0 "points to add to score per timeinterval, 0 for no points" +set g_tka_ballcarrier_effects 8 "Add together the numbers you want: EF_ADDITIVE (32) / EF_NODEPTHTEST (8192) / EF_DIMLIGHT (8)" +set g_tka_ballcarrier_highspeed 1 "speed multiplier done to the person holding the ball (recommended when used with some mutators)" +set g_tka_ballcarrier_damage 1 "damage multiplier while holding the ball" +set g_tka_ballcarrier_force 1 "force multiplier while holding the ball" +set g_tka_ballcarrier_selfdamage 1 "self damage multiplier while holding the ball" +set g_tka_ballcarrier_selfforce 1 "self force multiplier while holding the ball" +set g_tka_noncarrier_warn 1 "warn players when they kill without holding the ball" +set g_tka_noncarrier_damage 1 "damage done to other players if both you and they don't have the ball" +set g_tka_noncarrier_force 1 "force done to other players if both you and they don't have the ball" +set g_tka_noncarrier_selfdamage 1 "self damage if you don't have the ball" +set g_tka_noncarrier_selfforce 1 "self force if you don't have the ball" +set g_tkaball_effects 0 "Add together the numbers you want: EF_ADDITIVE (32) / EF_NODEPTHTEST (8192) / EF_DIMLIGHT (8)" +set g_tkaball_trail_color 254 "particle trail color from player/ball" +set g_tkaball_damageforcescale 3 "Scale of force which is applied to the ball by weapons/explosions/etc" +set g_tkaball_respawntime 10 "if no one picks up the ball, how long to wait until the ball respawns" diff --git a/gfx/hud/default/tka_taken_blue.tga b/gfx/hud/default/tka_taken_blue.tga new file mode 100644 index 0000000000..67ed3f5197 Binary files /dev/null and b/gfx/hud/default/tka_taken_blue.tga differ diff --git a/gfx/hud/default/tka_taken_pink.tga b/gfx/hud/default/tka_taken_pink.tga new file mode 100644 index 0000000000..1263eac10d Binary files /dev/null and b/gfx/hud/default/tka_taken_pink.tga differ diff --git a/gfx/hud/default/tka_taken_red.tga b/gfx/hud/default/tka_taken_red.tga new file mode 100644 index 0000000000..eb7fc58fee Binary files /dev/null and b/gfx/hud/default/tka_taken_red.tga differ diff --git a/gfx/hud/default/tka_taken_yellow.tga b/gfx/hud/default/tka_taken_yellow.tga new file mode 100644 index 0000000000..06f5b5ace6 Binary files /dev/null and b/gfx/hud/default/tka_taken_yellow.tga differ diff --git a/gfx/hud/luma/tka_taken_blue.tga b/gfx/hud/luma/tka_taken_blue.tga new file mode 100644 index 0000000000..0d517b02ef Binary files /dev/null and b/gfx/hud/luma/tka_taken_blue.tga differ diff --git a/gfx/hud/luma/tka_taken_pink.tga b/gfx/hud/luma/tka_taken_pink.tga new file mode 100644 index 0000000000..954f3bd960 Binary files /dev/null and b/gfx/hud/luma/tka_taken_pink.tga differ diff --git a/gfx/hud/luma/tka_taken_red.tga b/gfx/hud/luma/tka_taken_red.tga new file mode 100644 index 0000000000..d540cc53d0 Binary files /dev/null and b/gfx/hud/luma/tka_taken_red.tga differ diff --git a/gfx/hud/luma/tka_taken_yellow.tga b/gfx/hud/luma/tka_taken_yellow.tga new file mode 100644 index 0000000000..7f0da092cf Binary files /dev/null and b/gfx/hud/luma/tka_taken_yellow.tga differ diff --git a/gfx/menu/luma/gametype_tka.tga b/gfx/menu/luma/gametype_tka.tga new file mode 100644 index 0000000000..3731ab3c1a Binary files /dev/null and b/gfx/menu/luma/gametype_tka.tga differ diff --git a/gfx/menu/luminos/gametype_tka.tga b/gfx/menu/luminos/gametype_tka.tga new file mode 100644 index 0000000000..21e472c011 Binary files /dev/null and b/gfx/menu/luminos/gametype_tka.tga differ diff --git a/gfx/menu/wickedx/gametype_tka.tga b/gfx/menu/wickedx/gametype_tka.tga new file mode 100644 index 0000000000..21e472c011 Binary files /dev/null and b/gfx/menu/wickedx/gametype_tka.tga differ diff --git a/gfx/menu/xaw/gametype_tka.tga b/gfx/menu/xaw/gametype_tka.tga new file mode 100644 index 0000000000..c65ffba712 Binary files /dev/null and b/gfx/menu/xaw/gametype_tka.tga differ diff --git a/qcsrc/common/gamemodes/gamemode/_mod.inc b/qcsrc/common/gamemodes/gamemode/_mod.inc index a33ec87a01..922442320c 100644 --- a/qcsrc/common/gamemodes/gamemode/_mod.inc +++ b/qcsrc/common/gamemodes/gamemode/_mod.inc @@ -16,3 +16,4 @@ #include #include #include +#include diff --git a/qcsrc/common/gamemodes/gamemode/_mod.qh b/qcsrc/common/gamemodes/gamemode/_mod.qh index ffd71d59d3..01224714f1 100644 --- a/qcsrc/common/gamemodes/gamemode/_mod.qh +++ b/qcsrc/common/gamemodes/gamemode/_mod.qh @@ -16,3 +16,4 @@ #include #include #include +#include diff --git a/qcsrc/common/gamemodes/gamemode/tka/_mod.inc b/qcsrc/common/gamemodes/gamemode/tka/_mod.inc new file mode 100644 index 0000000000..6a33efdcf3 --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/tka/_mod.inc @@ -0,0 +1,8 @@ +// generated file; do not modify +#include +#ifdef CSQC + #include +#endif +#ifdef SVQC + #include +#endif diff --git a/qcsrc/common/gamemodes/gamemode/tka/_mod.qh b/qcsrc/common/gamemodes/gamemode/tka/_mod.qh new file mode 100644 index 0000000000..e35dee6a8e --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/tka/_mod.qh @@ -0,0 +1,8 @@ +// generated file; do not modify +#include +#ifdef CSQC + #include +#endif +#ifdef SVQC + #include +#endif diff --git a/qcsrc/common/gamemodes/gamemode/tka/cl_tka.qc b/qcsrc/common/gamemodes/gamemode/tka/cl_tka.qc new file mode 100644 index 0000000000..98ce0e05f9 --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/tka/cl_tka.qc @@ -0,0 +1,54 @@ +#include "cl_tka.qh" + +#include + +// Keepaway HUD mod icon +int tkaball_prevstatus; // last remembered status +float tkaball_statuschange_time; // time when the status changed + +// we don't need to reset for team keepaway since it immediately +// autocorrects prevstatus as to if the player has the ball or not + +void HUD_Mod_TeamKeepaway(vector pos, vector mySize) +{ + mod_active = 1; // team keepaway should always show the mod HUD + + float tkaball_alpha = blink(0.85, 0.15, 5); + + int stat_items = STAT(TKA_BALLSTATUS); + int tkaball = (stat_items & TKA_BALL_CARRYING); + + if(tkaball != tkaball_prevstatus) + { + tkaball_statuschange_time = time; + tkaball_prevstatus = tkaball; + } + + vector tkaball_pos, tkaball_size; + + if(mySize.x > mySize.y) { + tkaball_pos = pos + eX * 0.25 * mySize.x; + tkaball_size = vec2(0.5 * mySize.x, mySize.y); + } else { + tkaball_pos = pos + eY * 0.25 * mySize.y; + tkaball_size = vec2(mySize.x, 0.5 * mySize.y); + } + + float tkaball_statuschange_elapsedtime = time - tkaball_statuschange_time; + float f = bound(0, tkaball_statuschange_elapsedtime*2, 1); + + if(tkaball_prevstatus && f < 1) + drawpic_aspect_skin_expanding(tkaball_pos, "keepawayball_carrying", tkaball_size, '1 1 1', panel_fg_alpha * tkaball_alpha, DRAWFLAG_NORMAL, f); + + if(stat_items & TKA_BALL_CARRYING) // TODO: unique team based icon while carrying + drawpic_aspect_skin(pos, "keepawayball_carrying", vec2(mySize.x, mySize.y), '1 1 1', panel_fg_alpha * tkaball_alpha * f, DRAWFLAG_NORMAL); + else if(stat_items & TKA_BALL_TAKEN_RED) + drawpic_aspect_skin(pos, "tka_taken_red", vec2(mySize.x, mySize.y), '1 1 1', panel_fg_alpha * tkaball_alpha * f, DRAWFLAG_NORMAL); + else if(stat_items & TKA_BALL_TAKEN_RED) + drawpic_aspect_skin(pos, "tka_taken_blue", vec2(mySize.x, mySize.y), '1 1 1', panel_fg_alpha * tkaball_alpha * f, DRAWFLAG_NORMAL); + else if(stat_items & TKA_BALL_TAKEN_RED) + drawpic_aspect_skin(pos, "tka_taken_yellow", vec2(mySize.x, mySize.y), '1 1 1', panel_fg_alpha * tkaball_alpha * f, DRAWFLAG_NORMAL); + else if(stat_items & TKA_BALL_TAKEN_RED) + drawpic_aspect_skin(pos, "tka_taken_pink", vec2(mySize.x, mySize.y), '1 1 1', panel_fg_alpha * tkaball_alpha * f, DRAWFLAG_NORMAL); + +} diff --git a/qcsrc/common/gamemodes/gamemode/tka/cl_tka.qh b/qcsrc/common/gamemodes/gamemode/tka/cl_tka.qh new file mode 100644 index 0000000000..d062456a9d --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/tka/cl_tka.qh @@ -0,0 +1,3 @@ +#pragma once + +void HUD_Mod_TeamKeepaway(vector pos, vector mySize); diff --git a/qcsrc/common/gamemodes/gamemode/tka/sv_tka.qc b/qcsrc/common/gamemodes/gamemode/tka/sv_tka.qc new file mode 100644 index 0000000000..8337cef670 --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/tka/sv_tka.qc @@ -0,0 +1,524 @@ +#include "sv_tka.qh" + +#include + +.entity ballcarried; + +int autocvar_g_tka_ballcarrier_effects; +float autocvar_g_tka_ballcarrier_damage; +float autocvar_g_tka_ballcarrier_force; +float autocvar_g_tka_ballcarrier_highspeed; +float autocvar_g_tka_ballcarrier_selfdamage; +float autocvar_g_tka_ballcarrier_selfforce; +float autocvar_g_tka_noncarrier_damage; +float autocvar_g_tka_noncarrier_force; +float autocvar_g_tka_noncarrier_selfdamage; +float autocvar_g_tka_noncarrier_selfforce; +bool autocvar_g_tka_noncarrier_warn; +int autocvar_g_tka_score_bckill; +int autocvar_g_tka_score_killac; +bool autocvar_g_tka_score_team; +int autocvar_g_tka_score_timepoints; +float autocvar_g_tka_score_timeinterval; +float autocvar_g_tkaball_damageforcescale; +int autocvar_g_tkaball_effects; +float autocvar_g_tkaball_respawntime; +int autocvar_g_tkaball_trail_color; + +bool tka_ballcarrier_waypointsprite_visible_for_player(entity this, entity player, entity view) // runs on waypoints which are attached to ballcarriers, updates once per frame +{ + if(view.ballcarried) + if(IS_SPEC(player)) + return false; // we don't want spectators of the ballcarrier to see the attached waypoint on the top of their screen + + // TODO: Make the ballcarrier lack a waypointsprite whenever they have the invisibility powerup + + return true; +} + +void tka_EventLog(string mode, entity actor) // use an alias for easy changing and quick editing later +{ + if(autocvar_sv_eventlog) + GameLogEcho(strcat(":tka:", mode, ((actor != NULL) ? (strcat(":", ftos(actor.team), ":", ftos(actor.playerid))) : ""))); +} + +void tka_TouchEvent(entity this, entity toucher); +void tka_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; + } + + makevectors(this.angles); + set_movetype(this, MOVETYPE_BOUNCE); + this.velocity = '0 0 200'; + this.angles = '0 0 0'; + this.effects = autocvar_g_tkaball_effects; + settouch(this, tka_TouchEvent); + setthink(this, tka_RespawnBall); + this.nextthink = time + autocvar_g_tkaball_respawntime; + navigation_dynamicgoal_set(this, NULL); + + Send_Effect(EFFECT_ELECTRO_COMBO, oldballorigin, '0 0 0', 1); + Send_Effect(EFFECT_ELECTRO_COMBO, this.origin, '0 0 0', 1); + + WaypointSprite_Spawn(WP_KaBall, 0, 0, this, '0 0 64', NULL, this.team, this, waypointsprite_attachedforcarrier, false, RADARICON_FLAGCARRIER); + WaypointSprite_Ping(this.waypointsprite_attachedforcarrier); + + sound(this, CH_TRIGGER, SND_KA_RESPAWN, VOL_BASE, ATTEN_NONE); // ATTEN_NONE (it's a sound intended to be heard anywhere) +} + +void tka_TimeScoring(entity this) +{ + if(this.owner.ballcarried) + { // add points for holding the ball after a certain amount of time + if(autocvar_g_tka_score_timepoints) + GameRules_scoring_add_team(this.owner, SCORE, autocvar_g_tka_score_timepoints); + + GameRules_scoring_add(this.owner, TKA_BCTIME, (autocvar_g_tka_score_timeinterval / 1)); // interval is divided by 1 so that time always shows "seconds" + this.nextthink = time + autocvar_g_tka_score_timeinterval; + } +} + +void tka_TouchEvent(entity this, entity toucher) // runs any time that the ball comes in contact with something +{ + if (!this || game_stopped) + return; + + if(trace_dphitq3surfaceflags & Q3SURFACEFLAG_NOIMPACT) + { // The ball fell off the map, respawn it since players can't get to it + tka_RespawnBall(this); + return; + } + if(IS_DEAD(toucher)) { return; } + if(STAT(FROZEN, toucher)) { return; } + if (!IS_PLAYER(toucher)) + { // The ball just touched an object, most likely the world + Send_Effect(EFFECT_BALL_SPARKS, this.origin, '0 0 0', 1); + sound(this, CH_TRIGGER, SND_KA_TOUCH, VOL_BASE, ATTEN_NORM); + return; + } + else if(this.wait > time) { return; } + + // attach the ball to the player + this.owner = toucher; + toucher.ballcarried = this; + GameRules_scoring_vip(toucher, true); + setattachment(this, toucher, ""); + setorigin(this, '0 0 0'); + + // make the ball invisible/unable to do anything/set up time scoring + this.velocity = '0 0 0'; + set_movetype(this, MOVETYPE_NONE); + this.effects |= EF_NODRAW; + settouch(this, func_null); + setthink(this, tka_TimeScoring); + this.nextthink = time + autocvar_g_tka_score_timeinterval; + this.takedamage = DAMAGE_NO; + navigation_dynamicgoal_unset(this); + + // apply effects to player + toucher.glow_color = autocvar_g_tkaball_trail_color; + toucher.glow_trail = true; + toucher.effects |= autocvar_g_tka_ballcarrier_effects; + + // messages and sounds + tka_EventLog("pickup", toucher); + Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_KEEPAWAY_PICKUP, toucher.netname); + Send_Notification(NOTIF_ALL_EXCEPT, toucher, MSG_CENTER, CENTER_KEEPAWAY_PICKUP, toucher.netname); + Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_KEEPAWAY_PICKUP_SELF); + sound(this.owner, CH_TRIGGER, SND_KA_PICKEDUP, VOL_BASE, ATTEN_NONE); // ATTEN_NONE (it's a sound intended to be heard anywhere) + + // scoring + GameRules_scoring_add(toucher, TKA_PICKUPS, 1); + + // waypoints + WaypointSprite_AttachCarrier(WP_Null, toucher, RADARICON_FLAGCARRIER); + toucher.waypointsprite_attachedforcarrier.colormod = colormapPaletteColor(toucher.team - 1, 0); + toucher.waypointsprite_attachedforcarrier.waypointsprite_visible_for_player = tka_ballcarrier_waypointsprite_visible_for_player; + WaypointSprite_UpdateRule(toucher.waypointsprite_attachedforcarrier, toucher.team, SPRITERULE_TEAMPLAY); + if(toucher.team == NUM_TEAM_1) + WaypointSprite_UpdateSprites(toucher.waypointsprite_attachedforcarrier, WP_TkaBallCarrierRed, WP_KaBallCarrier, WP_TkaBallCarrierRed); + else if(toucher.team == NUM_TEAM_2) + WaypointSprite_UpdateSprites(toucher.waypointsprite_attachedforcarrier, WP_TkaBallCarrierBlue, WP_KaBallCarrier, WP_TkaBallCarrierBlue); + else if(toucher.team == NUM_TEAM_3) + WaypointSprite_UpdateSprites(toucher.waypointsprite_attachedforcarrier, WP_TkaBallCarrierYellow, WP_KaBallCarrier, WP_TkaBallCarrierYellow); + else if(toucher.team == NUM_TEAM_4) + WaypointSprite_UpdateSprites(toucher.waypointsprite_attachedforcarrier, WP_TkaBallCarrierPink, WP_KaBallCarrier, WP_TkaBallCarrierPink); + WaypointSprite_Ping(toucher.waypointsprite_attachedforcarrier); + WaypointSprite_Kill(this.waypointsprite_attachedforcarrier); +} + +void tka_PlayerReset(entity plyr) +{ + plyr.ballcarried = NULL; + GameRules_scoring_vip(plyr, false); + WaypointSprite_Kill(plyr.waypointsprite_attachedforcarrier); + + // reset the player effects + plyr.glow_trail = false; + plyr.effects &= ~autocvar_g_tka_ballcarrier_effects; +} + +void tka_DropEvent(entity plyr) // runs any time that a player is supposed to lose the ball +{ + entity ball; + ball = plyr.ballcarried; + + if(!ball) { return; } + + // reset the ball + setattachment(ball, NULL, ""); + set_movetype(ball, MOVETYPE_BOUNCE); + ball.wait = time + 1; + settouch(ball, tka_TouchEvent); + setthink(ball, tka_RespawnBall); + ball.nextthink = time + autocvar_g_tkaball_respawntime; + ball.takedamage = DAMAGE_YES; + ball.effects &= ~EF_NODRAW; + setorigin(ball, plyr.origin + '0 0 10'); + ball.velocity = '0 0 200' + '0 100 0'*crandom() + '100 0 0'*crandom(); + ball.owner = NULL; + navigation_dynamicgoal_set(ball, plyr); + + // messages and sounds + tka_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); + sound(NULL, CH_TRIGGER, SND_KA_DROPPED, VOL_BASE, ATTEN_NONE); // ATTEN_NONE (it's a sound intended to be heard anywhere) + + // waypoints + WaypointSprite_Spawn(WP_KaBall, 0, 0, ball, '0 0 64', NULL, ball.team, ball, waypointsprite_attachedforcarrier, false, RADARICON_FLAGCARRIER); + WaypointSprite_UpdateRule(ball.waypointsprite_attachedforcarrier, 0, SPRITERULE_DEFAULT); + WaypointSprite_Ping(ball.waypointsprite_attachedforcarrier); + + tka_PlayerReset(plyr); +} + +.bool pushable; + +MODEL(TKA_BALL, "models/orbs/orbblue.md3"); + +void tka_RemoveBall() +{ + entity plyr = tka_ball.owner; + if (plyr) // it was attached + tka_PlayerReset(plyr); + else + WaypointSprite_DetachCarrier(tka_ball); + delete(tka_ball); + tka_ball = NULL; +} + +void tka_SpawnBall() +{ + entity e = new(keepawayball); + setmodel(e, MDL_TKA_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_tkaball_damageforcescale; + e.takedamage = DAMAGE_YES; + e.solid = SOLID_TRIGGER; + set_movetype(e, MOVETYPE_BOUNCE); + e.glow_color = autocvar_g_tkaball_trail_color; + e.glow_trail = true; + e.flags = FL_ITEM; + IL_PUSH(g_items, e); + e.pushable = true; + settouch(e, tka_TouchEvent); + e.owner = NULL; + tka_ball = e; + navigation_dynamicgoal_init(tka_ball, false); + + InitializeEntity(e, tka_RespawnBall, INITPRIO_SETLOCATION); // is this the right priority? Neh, I have no idea.. Well-- it works! So. +} + +void tka_Handler_CheckBall(entity this) +{ + if(time < game_starttime) + { + if (tka_ball) + tka_RemoveBall(); + } + else + { + if (!tka_ball) + tka_SpawnBall(); + } + + this.nextthink = time; +} + +void tka_DelayedInit(entity this) // run at the start of a match, initiates game mode +{ + tka_Handler = new(tka_Handler); + setthink(tka_Handler, tka_Handler_CheckBall); + tka_Handler.nextthink = time; +} + + +// ================ +// Bot player logic +// ================ + +void havocbot_goalrating_tkaball(entity this, float ratingscale, vector org) +{ + entity ball_owner = tka_ball.owner; + + if (ball_owner == this || SAME_TEAM(ball_owner, this)) // TODO: defend ball carrier? + return; + + if (ball_owner) + navigation_routerating(this, ball_owner, ratingscale, 2000); + else + navigation_routerating(this, tka_ball, ratingscale, 2000); +} + +void havocbot_role_tka_carrier(entity this) +{ + if (IS_DEAD(this)) + return; + + if (navigation_goalrating_timeout(this)) + { + navigation_goalrating_start(this); + havocbot_goalrating_items(this, 10000, this.origin, 10000); + havocbot_goalrating_enemyplayers(this, 10000, this.origin, 10000); + havocbot_goalrating_waypoints(this, 1, this.origin, 3000); + navigation_goalrating_end(this); + + navigation_goalrating_timeout_set(this); + } + + if (!this.ballcarried) + { + this.havocbot_role = havocbot_role_tka_collector; + navigation_goalrating_timeout_expire(this, 2); + } +} + +void havocbot_role_tka_collector(entity this) +{ + if (IS_DEAD(this)) + return; + + if (navigation_goalrating_timeout(this)) + { + navigation_goalrating_start(this); + havocbot_goalrating_items(this, 10000, this.origin, 10000); + havocbot_goalrating_enemyplayers(this, 500, this.origin, 10000); + havocbot_goalrating_tkaball(this, 8000, this.origin); + navigation_goalrating_end(this); + + navigation_goalrating_timeout_set(this); + } + + if (this.ballcarried) + { + this.havocbot_role = havocbot_role_tka_carrier; + navigation_goalrating_timeout_expire(this, 2); + } +} + + +// ============== +// Hook Functions +// ============== + +MUTATOR_HOOKFUNCTION(tka, PlayerDies) +{ + entity frag_attacker = M_ARGV(1, entity); + entity frag_target = M_ARGV(2, entity); + + if(frag_attacker != frag_target && IS_PLAYER(frag_attacker) && DIFF_TEAM(frag_attacker, frag_target)) + { + if(frag_target.ballcarried) { // add to amount of times killing carrier + GameRules_scoring_add(frag_attacker, TKA_CARRIERKILLS, 1); + if(autocvar_g_tka_score_bckill) // add bckills to the score + GameRules_scoring_add_team(frag_attacker, SCORE, autocvar_g_tka_score_bckill); + } + else if(!frag_attacker.ballcarried && !(autocvar_g_tka_score_team && SAME_TEAM(tka_ball.owner, frag_attacker))) + { + if(autocvar_g_tka_noncarrier_warn) + Send_Notification(NOTIF_ONE_ONLY, frag_attacker, MSG_CENTER, CENTER_KEEPAWAY_WARN); + } + + if(frag_attacker.ballcarried || (autocvar_g_tka_score_team && SAME_TEAM(tka_ball.owner, frag_attacker))) // add to amount of kills while ballcarrier (or if team scoring is enabled) + GameRules_scoring_add_team(frag_attacker, SCORE, autocvar_g_tka_score_killac); + } + + if(frag_target.ballcarried) { tka_DropEvent(frag_target); } // a player with the ball has died, drop it +} + +MUTATOR_HOOKFUNCTION(tka, GiveFragsForKill) +{ + M_ARGV(2, float) = 0; // no frags counted in keepaway + return true; // you deceptive little bugger ;3 This needs to be true in order for this function to even count. +} + +MUTATOR_HOOKFUNCTION(tka, Scores_CountFragsRemaining) +{ + // announce remaining frags, but only when timed scoring is off + return !autocvar_g_tka_score_timepoints; +} + +MUTATOR_HOOKFUNCTION(tka, PlayerPreThink) +{ + entity player = M_ARGV(0, entity); + + // clear the item used for the ball in keepaway + STAT(TKA_BALLSTATUS, player) &= ~(TKA_BALL_TAKEN_RED | TKA_BALL_TAKEN_BLUE | TKA_BALL_TAKEN_YELLOW | TKA_BALL_TAKEN_PINK | TKA_BALL_CARRYING | TKA_BALL_DROPPED); + + // if the player has the ball, make sure they have the item for it (Used for HUD primarily) + if(player.ballcarried) + STAT(TKA_BALLSTATUS, player) |= TKA_BALL_CARRYING; + + if(!tka_ball.owner) + STAT(TKA_BALLSTATUS, player) |= TKA_BALL_DROPPED; + else + { + // TODO: teamless carrier? + switch(tka_ball.owner.team) + { + case NUM_TEAM_1: STAT(TKA_BALLSTATUS, player) |= TKA_BALL_TAKEN_RED; break; + case NUM_TEAM_2: STAT(TKA_BALLSTATUS, player) |= TKA_BALL_TAKEN_BLUE; break; + case NUM_TEAM_3: STAT(TKA_BALLSTATUS, player) |= TKA_BALL_TAKEN_YELLOW; break; + case NUM_TEAM_4: STAT(TKA_BALLSTATUS, player) |= TKA_BALL_TAKEN_PINK; break; + } + } +} + +MUTATOR_HOOKFUNCTION(tka, PlayerUseKey) +{ + entity player = M_ARGV(0, entity); + + if(MUTATOR_RETURNVALUE == 0) + if(player.ballcarried) + { + tka_DropEvent(player); + return true; + } +} + +MUTATOR_HOOKFUNCTION(tka, Damage_Calculate) // for changing damage and force values that are applied to players in g_damage.qc +{ + 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); + + if(frag_attacker.ballcarried) // if the attacker is a ballcarrier + { + if(frag_target == frag_attacker) // damage done to yourself + { + frag_damage *= autocvar_g_tka_ballcarrier_selfdamage; + frag_force *= autocvar_g_tka_ballcarrier_selfforce; + } + else // damage done to noncarriers + { + frag_damage *= autocvar_g_tka_ballcarrier_damage; + frag_force *= autocvar_g_tka_ballcarrier_force; + } + } + else if (IS_PLAYER(frag_attacker) && !frag_target.ballcarried) // if the target is a noncarrier + { + if(frag_target == frag_attacker) // damage done to yourself + { + frag_damage *= autocvar_g_tka_noncarrier_selfdamage; + frag_force *= autocvar_g_tka_noncarrier_selfforce; + } + else // damage done to other noncarriers + { + frag_damage *= autocvar_g_tka_noncarrier_damage; + frag_force *= autocvar_g_tka_noncarrier_force; + } + } + + M_ARGV(4, float) = frag_damage; + M_ARGV(6, vector) = frag_force; +} + +MUTATOR_HOOKFUNCTION(tka, ClientDisconnect) +{ + entity player = M_ARGV(0, entity); + + if(player.ballcarried) { tka_DropEvent(player); } // a player with the ball has left the match, drop it +} + +MUTATOR_HOOKFUNCTION(tka, MakePlayerObserver) +{ + entity player = M_ARGV(0, entity); + + if(player.ballcarried) { tka_DropEvent(player); } // a player with the ball has left the match, drop it +} + +MUTATOR_HOOKFUNCTION(tka, PlayerPowerups) +{ + entity player = M_ARGV(0, entity); + + // In the future this hook is supposed to allow me to do some extra stuff with waypointsprites and invisibility powerup + // So bare with me until I can fix a certain bug with tka_ballcarrier_waypointsprite_visible_for_player() + + player.effects &= ~autocvar_g_tka_ballcarrier_effects; + + if(player.ballcarried) + player.effects |= autocvar_g_tka_ballcarrier_effects; +} + + +MUTATOR_HOOKFUNCTION(tka, PlayerPhysics_UpdateStats) +{ + entity player = M_ARGV(0, entity); + // these automatically reset, no need to worry + + if(player.ballcarried) + STAT(MOVEVARS_HIGHSPEED, player) *= autocvar_g_tka_ballcarrier_highspeed; +} + +MUTATOR_HOOKFUNCTION(tka, BotShouldAttack) +{ + entity bot = M_ARGV(0, entity); + 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 && tka_ball.owner && !(autocvar_g_tka_score_team && SAME_TEAM(tka_ball.owner, bot))) + return true; +} + +MUTATOR_HOOKFUNCTION(tka, HavocBot_ChooseRole) +{ + entity bot = M_ARGV(0, entity); + + if (bot.ballcarried) + bot.havocbot_role = havocbot_role_tka_carrier; + else + bot.havocbot_role = havocbot_role_tka_collector; + return true; +} + +MUTATOR_HOOKFUNCTION(tka, DropSpecialItems) +{ + entity frag_target = M_ARGV(0, entity); + + if(frag_target.ballcarried) + tka_DropEvent(frag_target); +} + +MUTATOR_HOOKFUNCTION(tka, SpectateCopy) +{ + entity spectatee = M_ARGV(0, entity); + entity client = M_ARGV(1, entity); + + STAT(TKA_BALLSTATUS, client) = STAT(TKA_BALLSTATUS, spectatee); +} + +MUTATOR_HOOKFUNCTION(tka, TeamBalance_CheckAllowedTeams, CBC_ORDER_EXCLUSIVE) +{ + M_ARGV(0, float) = tka_teams; + return true; +} diff --git a/qcsrc/common/gamemodes/gamemode/tka/sv_tka.qh b/qcsrc/common/gamemodes/gamemode/tka/sv_tka.qh new file mode 100644 index 0000000000..7b7236f9f8 --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/tka/sv_tka.qh @@ -0,0 +1,45 @@ +#pragma once + +#include +int autocvar_g_tka_point_limit; +int autocvar_g_tka_point_leadlimit; +bool autocvar_g_tka_team_spawns; +void tka_DelayedInit(entity this); + +int tka_teams; +//int autocvar_g_tka_teams; +int autocvar_g_tka_teams_override; + +REGISTER_MUTATOR(tka, false) +{ + MUTATOR_STATIC(); + MUTATOR_ONADD + { + GameRules_teams(true); + GameRules_spawning_teams(autocvar_g_tka_team_spawns); + GameRules_limit_score(autocvar_g_tka_point_limit); + GameRules_limit_lead(autocvar_g_tka_point_leadlimit); + + tka_teams = autocvar_g_tka_teams_override; + if(tka_teams < 2) + tka_teams = cvar("g_tka_teams"); // read the cvar directly as it gets written earlier in the same frame + tka_teams = BITS(bound(2, tka_teams, 4)); + GameRules_scoring(tka_teams, SFL_SORT_PRIO_PRIMARY, SFL_SORT_PRIO_PRIMARY, { + field(SP_TKA_PICKUPS, "pickups", 0); + field(SP_TKA_CARRIERKILLS, "bckills", 0); + field(SP_TKA_BCTIME, "bctime", SFL_SORT_PRIO_SECONDARY); + }); + + InitializeEntity(NULL, tka_DelayedInit, INITPRIO_GAMETYPE); + } + return false; +} + + +entity tka_ball; +entity tka_Handler; + +void(entity this) havocbot_role_tka_carrier; +void(entity this) havocbot_role_tka_collector; + +void tka_DropEvent(entity plyr); diff --git a/qcsrc/common/gamemodes/gamemode/tka/tka.qc b/qcsrc/common/gamemodes/gamemode/tka/tka.qc new file mode 100644 index 0000000000..e0e6033c08 --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/tka/tka.qc @@ -0,0 +1 @@ +#include "tka.qh" diff --git a/qcsrc/common/gamemodes/gamemode/tka/tka.qh b/qcsrc/common/gamemodes/gamemode/tka/tka.qh new file mode 100644 index 0000000000..55743d40eb --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/tka/tka.qh @@ -0,0 +1,8 @@ +#pragma once + +const int TKA_BALL_TAKEN_RED = 1; +const int TKA_BALL_TAKEN_BLUE = 2; +const int TKA_BALL_TAKEN_YELLOW = 3; +const int TKA_BALL_TAKEN_PINK = 4; +const int TKA_BALL_CARRYING = 8; +const int TKA_BALL_DROPPED = 12; diff --git a/qcsrc/common/mapinfo.qh b/qcsrc/common/mapinfo.qh index b669ba1f7b..a2c3ef3f2b 100644 --- a/qcsrc/common/mapinfo.qh +++ b/qcsrc/common/mapinfo.qh @@ -607,6 +607,53 @@ ENDCLASS(Duel) REGISTER_GAMETYPE(DUEL, NEW(Duel)); #define g_duel IS_GAMETYPE(DUEL) +#ifdef CSQC +void HUD_Mod_TeamKeepaway(vector pos, vector mySize); +#endif +CLASS(TeamKeepaway, Gametype) + INIT(TeamKeepaway) + { + this.gametype_init(this, _("Team Keepaway"),"tka","g_tka",true,true,"","timelimit=15 pointlimit=50 teams=2 leadlimit=0",_("Keep the ball in your team's possession to get points for kills")); + } + METHOD(TeamKeepaway, m_parse_mapinfo, bool(string k, string v)) + { + if (!k) { + cvar_set("g_tka_teams", cvar_defstring("g_tka_teams")); + return true; + } + switch (k) { + case "teams": + cvar_set("g_tka_teams", v); + return true; + } + return false; + } + METHOD(TeamKeepaway, m_isAlwaysSupported, bool(Gametype this, int spawnpoints, float diameter)) + { + if(spawnpoints >= 8 && diameter > 4096) + return true; + return false; + } + METHOD(TeamKeepaway, m_isForcedSupported, bool(Gametype this)) + { + if(cvar("g_tka_on_dm_maps")) + { + // if this is set, all DM and KA maps support TKA too + if(!(MapInfo_Map_supportedGametypes & this.m_flags) && ((MapInfo_Map_supportedGametypes & MAPINFO_TYPE_DEATHMATCH.m_flags) || (MapInfo_Map_supportedGametypes & MAPINFO_TYPE_KEEPAWAY.m_flags))) + return true; // TODO: references another gametype (alternatively, we could check which gamemodes are always enabled and append this if any are supported) + } + return false; + } + METHOD(TeamKeepaway, m_setTeams, void(string sa)) + { + cvar_set("g_tka_teams", sa); + } +#ifdef CSQC + ATTRIB(TeamKeepaway, m_modicons, void(vector pos, vector mySize), HUD_Mod_TeamKeepaway); +#endif +ENDCLASS(TeamKeepaway) +REGISTER_GAMETYPE(TEAMKEEPAWAY, NEW(TeamKeepaway)); + const int MAPINFO_FEATURE_WEAPONS = 1; // not defined for instagib-only maps const int MAPINFO_FEATURE_VEHICLES = 2; const int MAPINFO_FEATURE_TURRETS = 4; diff --git a/qcsrc/common/mutators/mutator/waypoints/all.inc b/qcsrc/common/mutators/mutator/waypoints/all.inc index c8c4db546a..a97577f999 100644 --- a/qcsrc/common/mutators/mutator/waypoints/all.inc +++ b/qcsrc/common/mutators/mutator/waypoints/all.inc @@ -44,6 +44,10 @@ REGISTER_WAYPOINT(KeyCarrierPink, _("Key carrier"), "kh_pink_carrying", '0 1 1', REGISTER_WAYPOINT(KaBall, _("Ball"), "notify_ballpickedup", '0 1 1', 1); REGISTER_WAYPOINT(KaBallCarrier, _("Ball carrier"), "keepawayball_carrying", '1 0 0', 1); +REGISTER_WAYPOINT(TkaBallCarrierRed, _("Ball carrier"), "tka_taken_red", '0 1 1', 1); +REGISTER_WAYPOINT(TkaBallCarrierBlue, _("Ball carrier"), "tka_taken_blue", '0 1 1', 1); +REGISTER_WAYPOINT(TkaBallCarrierYellow, _("Ball carrier"), "tka_taken_yellow", '0 1 1', 1); +REGISTER_WAYPOINT(TkaBallCarrierPink, _("Ball carrier"), "tka_taken_pink", '0 1 1', 1); REGISTER_WAYPOINT(NbBall, _("Ball"), "", '0.91 0.85 0.62', 1); REGISTER_WAYPOINT(NbGoal, _("Goal"), "", '1 0.5 0', 1); diff --git a/qcsrc/common/scores.qh b/qcsrc/common/scores.qh index 377a780770..96108462ed 100644 --- a/qcsrc/common/scores.qh +++ b/qcsrc/common/scores.qh @@ -84,6 +84,10 @@ REGISTER_SP(NEXBALL_FAULTS); REGISTER_SP(ONS_TAKES); REGISTER_SP(ONS_CAPS); + +REGISTER_SP(TKA_PICKUPS); +REGISTER_SP(TKA_BCTIME); +REGISTER_SP(TKA_CARRIERKILLS); #endif diff --git a/qcsrc/common/stats.qh b/qcsrc/common/stats.qh index c77ca16be2..99c6d67633 100644 --- a/qcsrc/common/stats.qh +++ b/qcsrc/common/stats.qh @@ -125,6 +125,7 @@ REGISTER_STAT(ITEMSTIME, int, autocvar_sv_itemstime) REGISTER_STAT(KILL_TIME, float) REGISTER_STAT(VEIL_ORB, float) REGISTER_STAT(VEIL_ORB_ALPHA, float) +REGISTER_STAT(TKA_BALLSTATUS, int) #ifdef SVQC float autocvar_sv_showfps = 5; diff --git a/qcsrc/server/g_world.qc b/qcsrc/server/g_world.qc index fce1f942d5..1bcbef99c2 100644 --- a/qcsrc/server/g_world.qc +++ b/qcsrc/server/g_world.qc @@ -289,6 +289,9 @@ void cvar_changes_init() BADCVAR("g_tdm"); BADCVAR("g_tdm_on_dm_maps"); BADCVAR("g_tdm_teams"); + BADCVAR("g_tka"); + BADCVAR("g_tka_on_dm_maps"); + BADCVAR("g_tka_teams"); BADCVAR("g_vip"); BADCVAR("leadlimit"); BADCVAR("nextmap");