From: Mario Date: Tue, 23 May 2023 08:49:45 +0000 (+0000) Subject: Add Team Keepaway, a teamplay variant of Keepaway where points are awarded for each... X-Git-Tag: xonotic-v0.8.6~56^2~4^2 X-Git-Url: https://git.xonotic.org/?a=commitdiff_plain;ds=sidebyside;h=a110ef05cf7a66dd38de4dd3b20e4ef29200c1bc;p=xonotic%2Fxonotic-data.pk3dir.git Add Team Keepaway, a teamplay variant of Keepaway where points are awarded for each frag to the team in possession of the ball --- diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f2334ef66..5d850785d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -75,7 +75,7 @@ test_sv_game: - wget -nv -O data/maps/stormkeep.waypoints https://gitlab.com/xonotic/xonotic-maps.pk3dir/raw/master/maps/stormkeep.waypoints - wget -nv -O data/maps/stormkeep.waypoints.cache https://gitlab.com/xonotic/xonotic-maps.pk3dir/raw/master/maps/stormkeep.waypoints.cache - - EXPECT=837de85eaf3bb3f9e853fa3de5b3b4af + - EXPECT=e7c012825007ed251efa4e52dfbb592e - HASH=$(${ENGINE} +timestamps 1 +exec serverbench.cfg | tee /dev/stderr | sed -e 's,^\[[^]]*\] ,,' diff --git a/gamemodes-client.cfg b/gamemodes-client.cfg index c0562cfb8..c63bba5a7 100644 --- a/gamemodes-client.cfg +++ b/gamemodes-client.cfg @@ -34,6 +34,7 @@ alias cl_hook_gamestart_inv alias cl_hook_gamestart_duel alias cl_hook_gamestart_mayhem alias cl_hook_gamestart_tmayhem +alias cl_hook_gamestart_tka alias cl_hook_gamestart_surv alias cl_hook_gameend alias cl_hook_shutdown diff --git a/gamemodes-server.cfg b/gamemodes-server.cfg index 23c97924e..140993fca 100644 --- a/gamemodes-server.cfg +++ b/gamemodes-server.cfg @@ -31,6 +31,7 @@ alias sv_hook_gamestart_inv alias sv_hook_gamestart_duel alias sv_hook_gamestart_mayhem alias sv_hook_gamestart_tmayhem +alias sv_hook_gamestart_tka alias sv_hook_gamestart_surv // there is currently no hook for when the match is restarted // see sv_hook_readyrestart for previous uses of this hook @@ -63,6 +64,7 @@ alias sv_vote_gametype_hook_tdm alias sv_vote_gametype_hook_duel alias sv_vote_gametype_hook_mayhem alias sv_vote_gametype_hook_tmayhem +alias sv_vote_gametype_hook_tka alias sv_vote_gametype_hook_surv // Example preset to allow 1v1ctf to be used for the gametype voting screen. @@ -228,6 +230,13 @@ set g_tmayhem_respawn_delay_large_count 0 set g_tmayhem_respawn_delay_max 0 set g_tmayhem_respawn_waves 0 set g_tmayhem_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 set g_surv_respawn_delay_small 0 set g_surv_respawn_delay_small_count 0 set g_surv_respawn_delay_large 0 @@ -646,6 +655,38 @@ set g_tmayhem_teams 2 "how many teams are in team mayhem (set by mapinfo)" set g_tmayhem_team_spawns 0 "when 1, players spawn from the team spawnpoints of the map, if any" set g_tmayhem_teams_override 0 "how many teams are in team mayhem" +// =============== +// team keepaway +// =============== +set g_tka 0 "another game mode which focuses around a ball" +set g_tka_on_ka_maps 1 "when this is set, all KA maps automatically support TKA" +set g_tka_on_tdm_maps 0 "when this is set, all TDM 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" + // ========== // survival // ========== diff --git a/gfx/hud/default/tka_taken_blue.tga b/gfx/hud/default/tka_taken_blue.tga new file mode 100644 index 000000000..67ed3f519 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 000000000..1263eac10 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 000000000..eb7fc58fe 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 000000000..06f5b5ace 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 000000000..0d517b02e 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 000000000..954f3bd96 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 000000000..d540cc53d 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 000000000..7f0da092c 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 000000000..b61fc0867 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 000000000..b4adb1859 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 000000000..74d422de3 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 000000000..d8e1ba5fd 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 12a5510c3..75b1ea001 100644 --- a/qcsrc/common/gamemodes/gamemode/_mod.inc +++ b/qcsrc/common/gamemodes/gamemode/_mod.inc @@ -18,4 +18,5 @@ #include #include #include +#include #include diff --git a/qcsrc/common/gamemodes/gamemode/_mod.qh b/qcsrc/common/gamemodes/gamemode/_mod.qh index a3208d4a6..776a88d25 100644 --- a/qcsrc/common/gamemodes/gamemode/_mod.qh +++ b/qcsrc/common/gamemodes/gamemode/_mod.qh @@ -18,4 +18,5 @@ #include #include #include +#include #include diff --git a/qcsrc/common/gamemodes/gamemode/keepaway/sv_keepaway.qc b/qcsrc/common/gamemodes/gamemode/keepaway/sv_keepaway.qc index 8b024dbc7..d42703fb2 100644 --- a/qcsrc/common/gamemodes/gamemode/keepaway/sv_keepaway.qc +++ b/qcsrc/common/gamemodes/gamemode/keepaway/sv_keepaway.qc @@ -431,7 +431,7 @@ MUTATOR_HOOKFUNCTION(ka, PlayerUseKey) } } -MUTATOR_HOOKFUNCTION(ka, Damage_Calculate) // for changing damage and force values that are applied to players in 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); @@ -447,13 +447,13 @@ MUTATOR_HOOKFUNCTION(ka, Damage_Calculate) // for changing damage and force valu 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 { M_ARGV(4, float) *= autocvar_g_keepaway_ballcarrier_damage; M_ARGV(6, vector) *= autocvar_g_keepaway_ballcarrier_force; } } - else // if the target is a noncarrier + else // if the attacker is a noncarrier { if(frag_target == frag_attacker) // damage done to yourself { diff --git a/qcsrc/common/gamemodes/gamemode/tka/_mod.inc b/qcsrc/common/gamemodes/gamemode/tka/_mod.inc new file mode 100644 index 000000000..6a33efdcf --- /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 000000000..e35dee6a8 --- /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 000000000..e3be464b0 --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/tka/cl_tka.qc @@ -0,0 +1,54 @@ +#include "cl_tka.qh" + +#include +#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_BLUE) + 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_YELLOW) + 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_PINK) + 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 000000000..d062456a9 --- /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 000000000..0028baf65 --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/tka/sv_tka.qc @@ -0,0 +1,584 @@ +#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(toucher.ballcarried) { 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 player) +{ + player.ballcarried = NULL; + GameRules_scoring_vip(player, false); + WaypointSprite_Kill(player.waypointsprite_attachedforcarrier); + + // reset the player effects + player.glow_trail = false; + player.effects &= ~autocvar_g_tka_ballcarrier_effects; +} + +void tka_DropEvent(entity player) // runs any time that a player is supposed to lose the ball +{ + entity ball; + ball = player.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, player.origin + '0 0 10'); + ball.velocity = '0 0 200' + '0 100 0'*crandom() + '100 0 0'*crandom(); + ball.owner = NULL; + navigation_dynamicgoal_set(ball, player); + + // messages and sounds + tka_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_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(player); +} + +.bool pushable; + +MODEL(TKA_BALL, "models/orbs/orbblue.md3"); + +void tka_RemoveBall(entity ball) +{ + entity player = ball.owner; + if (player) // it was attached + tka_PlayerReset(player); + else + WaypointSprite_DetachCarrier(ball); + delete(ball); +} + +void tka_RemoveBalls() +{ + IL_EACH(g_tkaballs, true, + { + tka_RemoveBall(it); + }); +} + +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; + IL_PUSH(g_tkaballs, e); + navigation_dynamicgoal_init(e, false); + + InitializeEntity(e, tka_RespawnBall, INITPRIO_SETLOCATION); // is this the right priority? Neh, I have no idea.. Well-- it works! So. +} + +void tka_SpawnBalls(int ballcount) +{ + int realballcount = max(1, ballcount); // never allow less than 1 ball to spawn + for(int j = 0; j < realballcount; ++j) + { + tka_SpawnBall(); + } +} + +void tka_Handler_CheckBall(entity this) +{ + if(time < game_starttime) + { + if (!IL_EMPTY(g_tkaballs)) + tka_RemoveBalls(); + } + else + { + if (IL_EMPTY(g_tkaballs)) + tka_SpawnBalls(TKA_BALL_COUNT); + } + + this.nextthink = time; +} + +void tka_DelayedInit(entity this) // run at the start of a match, initiates game mode +{ + g_tkaballs = IL_NEW(); + 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 = NULL, ball_carried = NULL; + + // stops at last ball, prefers ball without carrier + IL_EACH(g_tkaballs, it.owner != this && DIFF_TEAM(ball.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_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)) + { + bool team_has_ball = false; + IL_EACH(g_tkaballs, it.owner != frag_attacker && SAME_TEAM(it.owner, frag_attacker), + { + team_has_ball = true; + break; + }); + 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 && team_has_ball)) + { + 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 && team_has_ball)) // 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) = 0; + + // 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; + + IL_EACH(g_tkaballs, true, + { + if(!it.owner) + STAT(TKA_BALLSTATUS, player) |= TKA_BALL_DROPPED; + else + { + // TODO: teamless carrier? + switch(it.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 +{ + 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_tka_ballcarrier_selfdamage; + frag_force *= autocvar_g_tka_ballcarrier_selfforce; + } + else // damage done to other ballcarriers + { + frag_damage *= autocvar_g_tka_ballcarrier_damage; + frag_force *= autocvar_g_tka_ballcarrier_force; + } + } + else // if the attacker 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 + bool have_held_ball = false, team_has_ball = false; + IL_EACH(g_tkaballs, it.owner, + { + have_held_ball = true; + if(SAME_TEAM(bot, it.owner)) + team_has_ball = true; + }); + + if(!targ.ballcarried && !bot.ballcarried && have_held_ball && !(autocvar_g_tka_score_team && team_has_ball)) + 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; +} + +void tka_Initialize() +{ + 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); +} 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 000000000..b4bb35323 --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/tka/sv_tka.qh @@ -0,0 +1,36 @@ +#pragma once + +#include +int autocvar_g_tka_point_limit; +int autocvar_g_tka_point_leadlimit; +bool autocvar_g_tka_team_spawns; +void tka_Initialize(); + +int tka_teams; +//int autocvar_g_tka_teams; +int autocvar_g_tka_teams_override; + +IntrusiveList g_tkaballs; +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_Initialize(); + } + return false; +} + +const int TKA_BALL_COUNT = 1; + +entity tka_Handler; + +void(entity this) havocbot_role_tka_carrier; +void(entity this) havocbot_role_tka_collector; + +void tka_DropEvent(entity player); diff --git a/qcsrc/common/gamemodes/gamemode/tka/tka.qc b/qcsrc/common/gamemodes/gamemode/tka/tka.qc new file mode 100644 index 000000000..e0e6033c0 --- /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 000000000..c6de0eefd --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/tka/tka.qh @@ -0,0 +1,65 @@ +#pragma once + +#include +#include +#include +#if defined(CSQC) + #include +#endif + +CLASS(TeamKeepaway, Gametype) + INIT(TeamKeepaway) + { + this.gametype_init(this, _("Team Keepaway"),"tka","g_tka",GAMETYPE_FLAG_TEAMPLAY | GAMETYPE_FLAG_USEPOINTS,"","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_ka_maps")) + { + // if this is set, all KA maps support TKA too + if(!(MapInfo_Map_supportedGametypes & this.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) + } + if(cvar("g_tka_on_tdm_maps")) + { + // if this is set, all TDM maps support TKA too + if(!(MapInfo_Map_supportedGametypes & this.m_flags) && (MapInfo_Map_supportedGametypes & MAPINFO_TYPE_TEAM_DEATHMATCH.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(TEAM_KEEPAWAY, NEW(TeamKeepaway)); + +const int TKA_BALL_TAKEN_RED = BIT(0); +const int TKA_BALL_TAKEN_BLUE = BIT(1); +const int TKA_BALL_TAKEN_YELLOW = BIT(2); +const int TKA_BALL_TAKEN_PINK = BIT(3); +const int TKA_BALL_CARRYING = BIT(4); +const int TKA_BALL_DROPPED = BIT(5); diff --git a/qcsrc/common/mutators/mutator/waypoints/all.inc b/qcsrc/common/mutators/mutator/waypoints/all.inc index aa0ca250f..98b1f4d3c 100644 --- a/qcsrc/common/mutators/mutator/waypoints/all.inc +++ b/qcsrc/common/mutators/mutator/waypoints/all.inc @@ -47,6 +47,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(LmsLeader, _("Leader"), "", '0 1 1', 1); diff --git a/qcsrc/common/scores.qh b/qcsrc/common/scores.qh index b33f334ee..8a01893f1 100644 --- a/qcsrc/common/scores.qh +++ b/qcsrc/common/scores.qh @@ -60,6 +60,10 @@ REGISTER_SP(NEXBALL_FAULTS); REGISTER_SP(ONS_CAPS); REGISTER_SP(ONS_TAKES); +REGISTER_SP(TKA_PICKUPS); +REGISTER_SP(TKA_BCTIME); +REGISTER_SP(TKA_CARRIERKILLS); + REGISTER_SP(SURV_SURVIVALS); REGISTER_SP(SURV_HUNTS); diff --git a/qcsrc/common/stats.qh b/qcsrc/common/stats.qh index c093b6282..96a136cad 100644 --- a/qcsrc/common/stats.qh +++ b/qcsrc/common/stats.qh @@ -132,6 +132,7 @@ REGISTER_STAT(ROUNDLOST, int) REGISTER_STAT(CAPTURE_PROGRESS, float) REGISTER_STAT(ITEMSTIME, int, autocvar_sv_itemstime) REGISTER_STAT(KILL_TIME, float) +REGISTER_STAT(TKA_BALLSTATUS, int) #ifdef SVQC float autocvar_sv_showfps = 0; diff --git a/qcsrc/menu/xonotic/util.qc b/qcsrc/menu/xonotic/util.qc index 393a976e9..c038d5234 100644 --- a/qcsrc/menu/xonotic/util.qc +++ b/qcsrc/menu/xonotic/util.qc @@ -665,6 +665,7 @@ float updateCompression() GAMETYPE(MAPINFO_TYPE_TEAM_MAYHEM) \ GAMETYPE(MAPINFO_TYPE_MAYHEM) \ GAMETYPE(MAPINFO_TYPE_KEEPAWAY) \ + GAMETYPE(MAPINFO_TYPE_TEAM_KEEPAWAY) \ GAMETYPE(MAPINFO_TYPE_KEYHUNT) \ GAMETYPE(MAPINFO_TYPE_LMS) \ GAMETYPE(MAPINFO_TYPE_DOMINATION) \ diff --git a/qcsrc/server/world.qc b/qcsrc/server/world.qc index 124ca5afe..4cfd068cd 100644 --- a/qcsrc/server/world.qc +++ b/qcsrc/server/world.qc @@ -304,6 +304,10 @@ void cvar_changes_init() BADCVAR("g_tdm"); BADCVAR("g_tdm_on_dm_maps"); BADCVAR("g_tdm_teams"); + BADCVAR("g_tka"); + BADCVAR("g_tka_on_ka_maps"); + BADCVAR("g_tka_on_tdm_maps"); + BADCVAR("g_tka_teams"); BADCVAR("g_tmayhem"); BADCVAR("g_tmayhem_teams"); BADCVAR("g_vip");