From: TimePath Date: Sun, 21 Aug 2016 04:44:45 +0000 (+1000) Subject: Merge branch 'master' into TimePath/modules X-Git-Tag: xonotic-v0.8.2~688^2~6 X-Git-Url: https://git.rm.cloudns.org/?a=commitdiff_plain;h=4ee2807b2d8f808928ef14b3e814945b3edb4350;p=xonotic%2Fxonotic-data.pk3dir.git Merge branch 'master' into TimePath/modules # Conflicts: # qcsrc/common/gamemodes/gamemode/onslaught/onslaught.qc # qcsrc/common/mutators/mutator/campcheck/campcheck.qc # qcsrc/server/mutators/mutator/gamemode_domination.qh --- 4ee2807b2d8f808928ef14b3e814945b3edb4350 diff --cc qcsrc/common/gamemodes/gamemode/onslaught/sv_onslaught.qc index 9551e9d85,000000000..f57625607 mode 100644,000000..100644 --- a/qcsrc/common/gamemodes/gamemode/onslaught/sv_onslaught.qc +++ b/qcsrc/common/gamemodes/gamemode/onslaught/sv_onslaught.qc @@@ -1,2128 -1,0 +1,2128 @@@ +#include "sv_controlpoint.qh" +#include "sv_generator.qh" + +bool g_onslaught; + +float autocvar_g_onslaught_teleport_wait; +bool autocvar_g_onslaught_spawn_at_controlpoints; +bool autocvar_g_onslaught_spawn_at_generator; +float autocvar_g_onslaught_cp_proxydecap; +float autocvar_g_onslaught_cp_proxydecap_distance = 512; +float autocvar_g_onslaught_cp_proxydecap_dps = 100; +float autocvar_g_onslaught_spawn_at_controlpoints_chance = 0.5; +float autocvar_g_onslaught_spawn_at_controlpoints_random; +float autocvar_g_onslaught_spawn_at_generator_chance; +float autocvar_g_onslaught_spawn_at_generator_random; +float autocvar_g_onslaught_cp_buildhealth; +float autocvar_g_onslaught_cp_buildtime; +float autocvar_g_onslaught_cp_health; +float autocvar_g_onslaught_cp_regen; +float autocvar_g_onslaught_gen_health; +float autocvar_g_onslaught_shield_force = 100; +float autocvar_g_onslaught_allow_vehicle_touch; +float autocvar_g_onslaught_round_timelimit; +float autocvar_g_onslaught_warmup; +float autocvar_g_onslaught_teleport_radius; +float autocvar_g_onslaught_spawn_choose; +float autocvar_g_onslaught_click_radius; + +void FixSize(entity e); + +// ======================= +// CaptureShield Functions +// ======================= + +bool ons_CaptureShield_Customize(entity this, entity client) +{ + entity e = WaypointSprite_getviewentity(client); + + if(!this.enemy.isshielded && (ons_ControlPoint_Attackable(this.enemy, e.team) > 0 || this.enemy.classname != "onslaught_controlpoint")) { return false; } + if(SAME_TEAM(this, e)) { return false; } + + return true; +} + +void ons_CaptureShield_Touch(entity this, entity toucher) +{ + if(!this.enemy.isshielded && (ons_ControlPoint_Attackable(this.enemy, toucher.team) > 0 || this.enemy.classname != "onslaught_controlpoint")) { return; } + if(!IS_PLAYER(toucher)) { return; } + if(SAME_TEAM(toucher, this)) { return; } + + vector mymid = (this.absmin + this.absmax) * 0.5; + vector theirmid = (toucher.absmin + toucher.absmax) * 0.5; + + Damage(toucher, this, this, 0, DEATH_HURTTRIGGER.m_id, mymid, normalize(theirmid - mymid) * ons_captureshield_force); + + if(IS_REAL_CLIENT(toucher)) + { + play2(toucher, SND(ONS_DAMAGEBLOCKEDBYSHIELD)); + + if(this.enemy.classname == "onslaught_generator") + Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_ONS_GENERATOR_SHIELDED); + else + Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_ONS_CONTROLPOINT_SHIELDED); + } +} + +void ons_CaptureShield_Reset(entity this) +{ + this.colormap = this.enemy.colormap; + this.team = this.enemy.team; +} + +void ons_CaptureShield_Spawn(entity generator, bool is_generator) +{ + entity shield = new(ons_captureshield); + + shield.enemy = generator; + shield.team = generator.team; + shield.colormap = generator.colormap; + shield.reset = ons_CaptureShield_Reset; + settouch(shield, ons_CaptureShield_Touch); + setcefc(shield, ons_CaptureShield_Customize); + shield.effects = EF_ADDITIVE; + set_movetype(shield, MOVETYPE_NOCLIP); + shield.solid = SOLID_TRIGGER; + shield.avelocity = '7 0 11'; + shield.scale = 1; + shield.model = ((is_generator) ? "models/onslaught/generator_shield.md3" : "models/onslaught/controlpoint_shield.md3"); + + precache_model(shield.model); + setorigin(shield, generator.origin); + _setmodel(shield, shield.model); + setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs); +} + + +// ========== +// Junk Pile +// ========== + +void setmodel_fixsize(entity e, Model m) +{ + setmodel(e, m); + FixSize(e); +} + +void onslaught_updatelinks() +{ + entity l; + // first check if the game has ended + LOG_DEBUG("--- updatelinks ---"); + // mark generators as being shielded and networked + for(l = ons_worldgeneratorlist; l; l = l.ons_worldgeneratornext) + { + if (l.iscaptured) + LOG_DEBUG(etos(l), " (generator) belongs to team ", ftos(l.team)); + else + LOG_DEBUG(etos(l), " (generator) is destroyed"); + l.islinked = l.iscaptured; + l.isshielded = l.iscaptured; + l.sprite.SendFlags |= 16; + } + // mark points as shielded and not networked + for(l = ons_worldcplist; l; l = l.ons_worldcpnext) + { + l.islinked = false; + l.isshielded = true; + int i; + for(i = 0; i < 17; ++i) { l.isgenneighbor[i] = false; l.iscpneighbor[i] = false; } + LOG_DEBUG(etos(l), " (point) belongs to team ", ftos(l.team)); + l.sprite.SendFlags |= 16; + } + // flow power outward from the generators through the network + bool stop = false; + while (!stop) + { + stop = true; + for(l = ons_worldlinklist; l; l = l.ons_worldlinknext) + { + // if both points are captured by the same team, and only one of + // them is powered, mark the other one as powered as well + if (l.enemy.iscaptured && l.goalentity.iscaptured) + if (l.enemy.islinked != l.goalentity.islinked) + if(SAME_TEAM(l.enemy, l.goalentity)) + { + if (!l.goalentity.islinked) + { + stop = false; + l.goalentity.islinked = true; + LOG_DEBUG(etos(l), " (link) is marking ", etos(l.goalentity), " (point) because its team matches ", etos(l.enemy), " (point)"); + } + else if (!l.enemy.islinked) + { + stop = false; + l.enemy.islinked = true; + LOG_DEBUG(etos(l), " (link) is marking ", etos(l.enemy), " (point) because its team matches ", etos(l.goalentity), " (point)"); + } + } + } + } + // now that we know which points are powered we can mark their neighbors + // as unshielded if team differs + for(l = ons_worldlinklist; l; l = l.ons_worldlinknext) + { + if (l.goalentity.islinked) + { + if(DIFF_TEAM(l.goalentity, l.enemy)) + { + LOG_DEBUG(etos(l), " (link) is unshielding ", etos(l.enemy), " (point) because its team does not match ", etos(l.goalentity), " (point)"); + l.enemy.isshielded = false; + } + if(l.goalentity.classname == "onslaught_generator") + l.enemy.isgenneighbor[l.goalentity.team] = true; + else + l.enemy.iscpneighbor[l.goalentity.team] = true; + } + if (l.enemy.islinked) + { + if(DIFF_TEAM(l.goalentity, l.enemy)) + { + LOG_DEBUG(etos(l), " (link) is unshielding ", etos(l.goalentity), " (point) because its team does not match ", etos(l.enemy), " (point)"); + l.goalentity.isshielded = false; + } + if(l.enemy.classname == "onslaught_generator") + l.goalentity.isgenneighbor[l.enemy.team] = true; + else + l.goalentity.iscpneighbor[l.enemy.team] = true; + } + } + // now update the generators + for(l = ons_worldgeneratorlist; l; l = l.ons_worldgeneratornext) + { + if (l.isshielded) + { + LOG_DEBUG(etos(l), " (generator) is shielded"); + l.takedamage = DAMAGE_NO; + l.bot_attack = false; + } + else + { + LOG_DEBUG(etos(l), " (generator) is not shielded"); + l.takedamage = DAMAGE_AIM; + l.bot_attack = true; + } + + ons_Generator_UpdateSprite(l); + } + // now update the takedamage and alpha variables on control point icons + for(l = ons_worldcplist; l; l = l.ons_worldcpnext) + { + if (l.isshielded) + { + LOG_DEBUG(etos(l), " (point) is shielded"); + if (l.goalentity) + { + l.goalentity.takedamage = DAMAGE_NO; + l.goalentity.bot_attack = false; + } + } + else + { + LOG_DEBUG(etos(l), " (point) is not shielded"); + if (l.goalentity) + { + l.goalentity.takedamage = DAMAGE_AIM; + l.goalentity.bot_attack = true; + } + } + ons_ControlPoint_UpdateSprite(l); + } + FOREACH_ENTITY_CLASS("ons_captureshield", true, + { + it.team = it.enemy.team; + it.colormap = it.enemy.colormap; + }); +} + + +// =================== +// Main Link Functions +// =================== + +bool ons_Link_Send(entity this, entity to, int sendflags) +{ + WriteHeader(MSG_ENTITY, ENT_CLIENT_RADARLINK); + WriteByte(MSG_ENTITY, sendflags); + if(sendflags & 1) + { + WriteCoord(MSG_ENTITY, this.goalentity.origin_x); + WriteCoord(MSG_ENTITY, this.goalentity.origin_y); + WriteCoord(MSG_ENTITY, this.goalentity.origin_z); + } + if(sendflags & 2) + { + WriteCoord(MSG_ENTITY, this.enemy.origin_x); + WriteCoord(MSG_ENTITY, this.enemy.origin_y); + WriteCoord(MSG_ENTITY, this.enemy.origin_z); + } + if(sendflags & 4) + { + WriteByte(MSG_ENTITY, this.clientcolors); // which is goalentity's color + enemy's color * 16 + } + return true; +} + +void ons_Link_CheckUpdate(entity this) +{ + // TODO check if the two sides have moved (currently they won't move anyway) + float cc = 0, cc1 = 0, cc2 = 0; + + if(this.goalentity.islinked || this.goalentity.iscaptured) { cc1 = (this.goalentity.team - 1) * 0x01; } + if(this.enemy.islinked || this.enemy.iscaptured) { cc2 = (this.enemy.team - 1) * 0x10; } + + cc = cc1 + cc2; + + if(cc != this.clientcolors) + { + this.clientcolors = cc; + this.SendFlags |= 4; + } + + this.nextthink = time; +} + +void ons_DelayedLinkSetup(entity this) +{ + this.goalentity = find(NULL, targetname, this.target); + this.enemy = find(NULL, targetname, this.target2); + if(!this.goalentity) { objerror(this, "can not find target\n"); } + if(!this.enemy) { objerror(this, "can not find target2\n"); } + + LOG_DEBUG(etos(this.goalentity), " linked with ", etos(this.enemy)); + this.SendFlags |= 3; + setthink(this, ons_Link_CheckUpdate); + this.nextthink = time; +} + + +// ============================= +// Main Control Point Functions +// ============================= + +int ons_ControlPoint_CanBeLinked(entity cp, int teamnumber) +{ + if(cp.isgenneighbor[teamnumber]) { return 2; } + if(cp.iscpneighbor[teamnumber]) { return 1; } + + return 0; +} + +int ons_ControlPoint_Attackable(entity cp, int teamnumber) + // -2: SAME TEAM, attackable by enemy! + // -1: SAME TEAM! + // 0: off limits + // 1: attack it + // 2: touch it + // 3: attack it (HIGH PRIO) + // 4: touch it (HIGH PRIO) +{ + int a; + + if(cp.isshielded) + { + return 0; + } + else if(cp.goalentity) + { + // if there's already an icon built, nothing happens + if(cp.team == teamnumber) + { + a = ons_ControlPoint_CanBeLinked(cp, teamnumber); + if(a) // attackable by enemy? + return -2; // EMERGENCY! + return -1; + } + // we know it can be linked, so no need to check + // but... + a = ons_ControlPoint_CanBeLinked(cp, teamnumber); + if(a == 2) // near our generator? + return 3; // EMERGENCY! + return 1; + } + else + { + // free point + if(ons_ControlPoint_CanBeLinked(cp, teamnumber)) + { + a = ons_ControlPoint_CanBeLinked(cp, teamnumber); // why was this here NUM_TEAM_1 + NUM_TEAM_2 - t + if(a == 2) + return 4; // GET THIS ONE NOW! + else + return 2; // TOUCH ME + } + } + return 0; +} + +void ons_ControlPoint_Icon_Damage(entity this, entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force) +{ + if(damage <= 0) { return; } + + if (this.owner.isshielded) + { + // this is protected by a shield, so ignore the damage + if (time > this.pain_finished) + if (IS_PLAYER(attacker)) + { + play2(attacker, SND(ONS_DAMAGEBLOCKEDBYSHIELD)); + this.pain_finished = time + 1; + attacker.typehitsound += 1; // play both sounds (shield is way too quiet) + } + + return; + } + + if(IS_PLAYER(attacker)) + if(time - ons_notification_time[this.team] > 10) + { + play2team(this.team, SND(ONS_CONTROLPOINT_UNDERATTACK)); + ons_notification_time[this.team] = time; + } + + this.health = this.health - damage; + if(this.owner.iscaptured) + WaypointSprite_UpdateHealth(this.owner.sprite, this.health); + else + WaypointSprite_UpdateBuildFinished(this.owner.sprite, time + (this.max_health - this.health) / (this.count / ONS_CP_THINKRATE)); + this.pain_finished = time + 1; + // particles on every hit + pointparticles(EFFECT_SPARKS, hitloc, force*-1, 1); + //sound on every hit + if (random() < 0.5) + sound(this, CH_TRIGGER, SND_ONS_HIT1, VOL_BASE+0.3, ATTEN_NORM); + else + sound(this, CH_TRIGGER, SND_ONS_HIT2, VOL_BASE+0.3, ATTEN_NORM); + + if (this.health < 0) + { + sound(this, CH_TRIGGER, SND_GRENADE_IMPACT, VOL_BASE, ATTEN_NORM); + pointparticles(EFFECT_ROCKET_EXPLODE, this.origin, '0 0 0', 1); + Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(this.team, INFO_ONSLAUGHT_CPDESTROYED), this.owner.message, attacker.netname); + + PlayerScore_Add(attacker, SP_ONS_TAKES, 1); + PlayerScore_Add(attacker, SP_SCORE, 10); + + this.owner.goalentity = NULL; + this.owner.islinked = false; + this.owner.iscaptured = false; + this.owner.team = 0; + this.owner.colormap = 1024; + + WaypointSprite_UpdateMaxHealth(this.owner.sprite, 0); + + onslaught_updatelinks(); + + // Use targets now (somebody make sure this is in the right place..) + SUB_UseTargets(this.owner, this, NULL); + + this.owner.waslinked = this.owner.islinked; + if(this.owner.model != "models/onslaught/controlpoint_pad.md3") + setmodel_fixsize(this.owner, MDL_ONS_CP_PAD1); + //setsize(this, '-32 -32 0', '32 32 8'); + + delete(this); + } + + this.SendFlags |= CPSF_STATUS; +} + +void ons_ControlPoint_Icon_Think(entity this) +{ + this.nextthink = time + ONS_CP_THINKRATE; + + if(autocvar_g_onslaught_cp_proxydecap) + { + int _enemy_count = 0; + int _friendly_count = 0; + + FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), { + if(vdist(it.origin - this.origin, <, autocvar_g_onslaught_cp_proxydecap_distance)) + { + if(SAME_TEAM(it, this)) + ++_friendly_count; + else + ++_enemy_count; + } + }); + + _friendly_count = _friendly_count * (autocvar_g_onslaught_cp_proxydecap_dps * ONS_CP_THINKRATE); + _enemy_count = _enemy_count * (autocvar_g_onslaught_cp_proxydecap_dps * ONS_CP_THINKRATE); + + this.health = bound(0, this.health + (_friendly_count - _enemy_count), this.max_health); + this.SendFlags |= CPSF_STATUS; + if(this.health <= 0) + { + ons_ControlPoint_Icon_Damage(this, this, this, 1, 0, this.origin, '0 0 0'); + return; + } + } + + if (time > this.pain_finished + 5) + { + if(this.health < this.max_health) + { + this.health = this.health + this.count; + if (this.health >= this.max_health) + this.health = this.max_health; + WaypointSprite_UpdateHealth(this.owner.sprite, this.health); + } + } + + if(this.owner.islinked != this.owner.waslinked) + { + // unteam the spawnpoint if needed + int t = this.owner.team; + if(!this.owner.islinked) + this.owner.team = 0; + + SUB_UseTargets(this.owner, this, NULL); + + this.owner.team = t; + + this.owner.waslinked = this.owner.islinked; + } + + // damaged fx + if(random() < 0.6 - this.health / this.max_health) + { + Send_Effect(EFFECT_ELECTRIC_SPARKS, this.origin + randompos('-10 -10 -20', '10 10 20'), '0 0 0', 1); + + if(random() > 0.8) + sound(this, CH_PAIN, SND_ONS_SPARK1, VOL_BASE, ATTEN_NORM); + else if (random() > 0.5) + sound(this, CH_PAIN, SND_ONS_SPARK2, VOL_BASE, ATTEN_NORM); + } +} + +void ons_ControlPoint_Icon_BuildThink(entity this) +{ + int a; + + this.nextthink = time + ONS_CP_THINKRATE; + + // only do this if there is power + a = ons_ControlPoint_CanBeLinked(this.owner, this.owner.team); + if(!a) + return; + + this.health = this.health + this.count; + + this.SendFlags |= CPSF_STATUS; + + if (this.health >= this.max_health) + { + this.health = this.max_health; + this.count = autocvar_g_onslaught_cp_regen * ONS_CP_THINKRATE; // slow repair rate from now on + setthink(this, ons_ControlPoint_Icon_Think); + sound(this, CH_TRIGGER, SND_ONS_CONTROLPOINT_BUILT, VOL_BASE, ATTEN_NORM); + this.owner.iscaptured = true; + this.solid = SOLID_BBOX; + + Send_Effect(EFFECT_CAP(this.owner.team), this.owner.origin, '0 0 0', 1); + + WaypointSprite_UpdateMaxHealth(this.owner.sprite, this.max_health); + WaypointSprite_UpdateHealth(this.owner.sprite, this.health); + + if(IS_PLAYER(this.owner.ons_toucher)) + { + Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_ONSLAUGHT_CAPTURE, this.owner.ons_toucher.netname, this.owner.message); + Send_Notification(NOTIF_ALL_EXCEPT, this.owner.ons_toucher, MSG_CENTER, APP_TEAM_NUM(this.owner.ons_toucher.team, CENTER_ONS_CAPTURE), this.owner.message); + Send_Notification(NOTIF_ONE, this.owner.ons_toucher, MSG_CENTER, CENTER_ONS_CAPTURE, this.owner.message); + PlayerScore_Add(this.owner.ons_toucher, SP_ONS_CAPS, 1); + PlayerTeamScore_AddScore(this.owner.ons_toucher, 10); + } + + this.owner.ons_toucher = NULL; + + onslaught_updatelinks(); + + // Use targets now (somebody make sure this is in the right place..) + SUB_UseTargets(this.owner, this, NULL); + + this.SendFlags |= CPSF_SETUP; + } + if(this.owner.model != MDL_ONS_CP_PAD2.model_str()) + setmodel_fixsize(this.owner, MDL_ONS_CP_PAD2); + + if(random() < 0.9 - this.health / this.max_health) + Send_Effect(EFFECT_RAGE, this.origin + 10 * randomvec(), '0 0 -1', 1); +} + +void onslaught_controlpoint_icon_link(entity e, void(entity this) spawnproc); + +void ons_ControlPoint_Icon_Spawn(entity cp, entity player) +{ + entity e = new(onslaught_controlpoint_icon); + + setsize(e, CPICON_MIN, CPICON_MAX); + setorigin(e, cp.origin + CPICON_OFFSET); + + e.owner = cp; + e.max_health = autocvar_g_onslaught_cp_health; + e.health = autocvar_g_onslaught_cp_buildhealth; + e.solid = SOLID_NOT; + e.takedamage = DAMAGE_AIM; + e.bot_attack = true; + e.event_damage = ons_ControlPoint_Icon_Damage; + e.team = player.team; + e.colormap = 1024 + (e.team - 1) * 17; + e.count = (e.max_health - e.health) * ONS_CP_THINKRATE / autocvar_g_onslaught_cp_buildtime; // how long it takes to build + + sound(e, CH_TRIGGER, SND_ONS_CONTROLPOINT_BUILD, VOL_BASE, ATTEN_NORM); + + cp.goalentity = e; + cp.team = e.team; + cp.colormap = e.colormap; + + Send_Effect(EFFECT_FLAG_TOUCH(player.team), e.origin, '0 0 0', 1); + + WaypointSprite_UpdateBuildFinished(cp.sprite, time + (e.max_health - e.health) / (e.count / ONS_CP_THINKRATE)); + WaypointSprite_UpdateRule(cp.sprite,cp.team,SPRITERULE_TEAMPLAY); + cp.sprite.SendFlags |= 16; + + onslaught_controlpoint_icon_link(e, ons_ControlPoint_Icon_BuildThink); +} + +entity ons_ControlPoint_Waypoint(entity e) +{ + if(e.team) + { + int a = ons_ControlPoint_Attackable(e, e.team); + + if(a == -2) { return WP_OnsCPDefend; } // defend now + if(a == -1 || a == 1 || a == 2) { return WP_OnsCP; } // touch + if(a == 3 || a == 4) { return WP_OnsCPAttack; } // attack + } + else + return WP_OnsCP; + + return WP_Null; +} + +void ons_ControlPoint_UpdateSprite(entity e) +{ + entity s1 = ons_ControlPoint_Waypoint(e); + WaypointSprite_UpdateSprites(e.sprite, s1, s1, s1); + + bool sh; + sh = !(ons_ControlPoint_CanBeLinked(e, NUM_TEAM_1) || ons_ControlPoint_CanBeLinked(e, NUM_TEAM_2) || ons_ControlPoint_CanBeLinked(e, NUM_TEAM_3) || ons_ControlPoint_CanBeLinked(e, NUM_TEAM_4)); + + if(e.lastteam != e.team + 2 || e.lastshielded != sh || e.iscaptured != e.lastcaptured) + { + if(e.iscaptured) // don't mess up build bars! + { + if(sh) + { + WaypointSprite_UpdateMaxHealth(e.sprite, 0); + } + else + { + WaypointSprite_UpdateMaxHealth(e.sprite, e.goalentity.max_health); + WaypointSprite_UpdateHealth(e.sprite, e.goalentity.health); + } + } + if(e.lastshielded) + { + if(e.team) + WaypointSprite_UpdateTeamRadar(e.sprite, RADARICON_CONTROLPOINT, 0.5 * colormapPaletteColor(e.team - 1, false)); + else + WaypointSprite_UpdateTeamRadar(e.sprite, RADARICON_CONTROLPOINT, '0.5 0.5 0.5'); + } + else + { + if(e.team) + WaypointSprite_UpdateTeamRadar(e.sprite, RADARICON_CONTROLPOINT, colormapPaletteColor(e.team - 1, false)); + else + WaypointSprite_UpdateTeamRadar(e.sprite, RADARICON_CONTROLPOINT, '0.75 0.75 0.75'); + } + WaypointSprite_Ping(e.sprite); + + e.lastteam = e.team + 2; + e.lastshielded = sh; + e.lastcaptured = e.iscaptured; + } +} + +void ons_ControlPoint_Touch(entity this, entity toucher) +{ + int attackable; + + if(IS_VEHICLE(toucher) && toucher.owner) + if(autocvar_g_onslaught_allow_vehicle_touch) + toucher = toucher.owner; + else + return; + + if(!IS_PLAYER(toucher)) { return; } + if(STAT(FROZEN, toucher)) { return; } + if(IS_DEAD(toucher)) { return; } + + if ( SAME_TEAM(this,toucher) ) + if ( this.iscaptured ) + { + if(time <= toucher.teleport_antispam) + Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_ONS_TELEPORT_ANTISPAM, rint(toucher.teleport_antispam - time)); + else + Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_ONS_TELEPORT); + } + + attackable = ons_ControlPoint_Attackable(this, toucher.team); + if(attackable != 2 && attackable != 4) + return; + // we've verified that this player has a legitimate claim to this point, + // so start building the captured point icon (which only captures this + // point if it successfully builds without being destroyed first) + ons_ControlPoint_Icon_Spawn(this, toucher); + + this.ons_toucher = toucher; + + onslaught_updatelinks(); +} + +void ons_ControlPoint_Think(entity this) +{ + this.nextthink = time + ONS_CP_THINKRATE; + CSQCMODEL_AUTOUPDATE(this); +} + +void ons_ControlPoint_Reset(entity this) +{ + if(this.goalentity) + delete(this.goalentity); + + this.goalentity = NULL; + this.team = 0; + this.colormap = 1024; + this.iscaptured = false; + this.islinked = false; + this.isshielded = true; + setthink(this, ons_ControlPoint_Think); + this.ons_toucher = NULL; + this.nextthink = time + ONS_CP_THINKRATE; + setmodel_fixsize(this, MDL_ONS_CP_PAD1); + + WaypointSprite_UpdateMaxHealth(this.sprite, 0); + WaypointSprite_UpdateRule(this.sprite,this.team,SPRITERULE_TEAMPLAY); + + onslaught_updatelinks(); + + SUB_UseTargets(this, this, NULL); // to reset the structures, playerspawns etc. + + CSQCMODEL_AUTOUPDATE(this); +} + +void ons_DelayedControlPoint_Setup(entity this) +{ + onslaught_updatelinks(); + + // captureshield setup + ons_CaptureShield_Spawn(this, false); + + CSQCMODEL_AUTOINIT(this); +} + +void ons_ControlPoint_Setup(entity cp) +{ + // main setup + cp.ons_worldcpnext = ons_worldcplist; // link control point into ons_worldcplist + ons_worldcplist = cp; + + cp.netname = "Control point"; + cp.team = 0; + cp.solid = SOLID_BBOX; + set_movetype(cp, MOVETYPE_NONE); + settouch(cp, ons_ControlPoint_Touch); + setthink(cp, ons_ControlPoint_Think); + cp.nextthink = time + ONS_CP_THINKRATE; + cp.reset = ons_ControlPoint_Reset; + cp.colormap = 1024; + cp.iscaptured = false; + cp.islinked = false; + cp.isshielded = true; + + if(cp.message == "") { cp.message = "a"; } + + // appearence + setmodel_fixsize(cp, MDL_ONS_CP_PAD1); + + // control point placement + if((cp.spawnflags & 1) || cp.noalign) // don't drop to floor, just stay at fixed location + { + cp.noalign = true; + set_movetype(cp, MOVETYPE_NONE); + } + else // drop to floor, automatically find a platform and set that as spawn origin + { + setorigin(cp, cp.origin + '0 0 20'); + cp.noalign = false; + droptofloor(cp); + set_movetype(cp, MOVETYPE_TOSS); + } + + // waypointsprites + WaypointSprite_SpawnFixed(WP_Null, cp.origin + CPGEN_WAYPOINT_OFFSET, cp, sprite, RADARICON_NONE); + WaypointSprite_UpdateRule(cp.sprite, cp.team, SPRITERULE_TEAMPLAY); + + InitializeEntity(cp, ons_DelayedControlPoint_Setup, INITPRIO_SETLOCATION); +} + + +// ========================= +// Main Generator Functions +// ========================= + +entity ons_Generator_Waypoint(entity e) +{ + if (e.isshielded) + return WP_OnsGenShielded; + return WP_OnsGen; +} + +void ons_Generator_UpdateSprite(entity e) +{ + entity s1 = ons_Generator_Waypoint(e); + WaypointSprite_UpdateSprites(e.sprite, s1, s1, s1); + + if(e.lastteam != e.team + 2 || e.lastshielded != e.isshielded) + { + e.lastteam = e.team + 2; + e.lastshielded = e.isshielded; + if(e.lastshielded) + { + if(e.team) + WaypointSprite_UpdateTeamRadar(e.sprite, RADARICON_GENERATOR, 0.5 * colormapPaletteColor(e.team - 1, false)); + else + WaypointSprite_UpdateTeamRadar(e.sprite, RADARICON_GENERATOR, '0.5 0.5 0.5'); + } + else + { + if(e.team) + WaypointSprite_UpdateTeamRadar(e.sprite, RADARICON_GENERATOR, colormapPaletteColor(e.team - 1, false)); + else + WaypointSprite_UpdateTeamRadar(e.sprite, RADARICON_GENERATOR, '0.75 0.75 0.75'); + } + WaypointSprite_Ping(e.sprite); + } +} + +void ons_GeneratorDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force) +{ + if(damage <= 0) { return; } + if(warmup_stage || gameover) { return; } + if(!round_handler_IsRoundStarted()) { return; } + + if (attacker != this) + { + if (this.isshielded) + { + // this is protected by a shield, so ignore the damage + if (time > this.pain_finished) + if (IS_PLAYER(attacker)) + { + play2(attacker, SND(ONS_DAMAGEBLOCKEDBYSHIELD)); + attacker.typehitsound += 1; + this.pain_finished = time + 1; + } + return; + } + if (time > this.pain_finished) + { + this.pain_finished = time + 10; + FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it) && SAME_TEAM(it, this), Send_Notification(NOTIF_ONE, it, MSG_CENTER, CENTER_GENERATOR_UNDERATTACK)); + play2team(this.team, SND(ONS_GENERATOR_UNDERATTACK)); + } + } + this.health = this.health - damage; + WaypointSprite_UpdateHealth(this.sprite, this.health); + // choose an animation frame based on health + this.frame = 10 * bound(0, (1 - this.health / this.max_health), 1); + // see if the generator is still functional, or dying + if (this.health > 0) + { + this.lasthealth = this.health; + } + else + { + if (attacker == this) + Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(this.team, INFO_ONSLAUGHT_GENDESTROYED_OVERTIME)); + else + { + Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(this.team, INFO_ONSLAUGHT_GENDESTROYED)); + PlayerScore_Add(attacker, SP_SCORE, 100); + } + this.iscaptured = false; + this.islinked = false; + this.isshielded = false; + this.takedamage = DAMAGE_NO; // can't be hurt anymore + this.event_damage = func_null; // won't do anything if hurt + this.count = 0; // reset counter + setthink(this, func_null); + this.nextthink = 0; + //this.think(); // do the first explosion now + + WaypointSprite_UpdateMaxHealth(this.sprite, 0); + WaypointSprite_Ping(this.sprite); + //WaypointSprite_Kill(this.sprite); // can't do this yet, code too poor + + onslaught_updatelinks(); + } + + // Throw some flaming gibs on damage, more damage = more chance for gib + if(random() < damage/220) + { + sound(this, CH_TRIGGER, SND_ROCKET_IMPACT, VOL_BASE, ATTEN_NORM); + } + else + { + // particles on every hit + Send_Effect(EFFECT_SPARKS, hitloc, force * -1, 1); + + //sound on every hit + if (random() < 0.5) + sound(this, CH_TRIGGER, SND_ONS_HIT1, VOL_BASE, ATTEN_NORM); + else + sound(this, CH_TRIGGER, SND_ONS_HIT2, VOL_BASE, ATTEN_NORM); + } + + this.SendFlags |= GSF_STATUS; +} + +void ons_GeneratorThink(entity this) +{ + this.nextthink = time + GEN_THINKRATE; + if (!gameover) + { + if(!this.isshielded && this.wait < time) + { + this.wait = time + 5; + FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it), { + if(SAME_TEAM(it, this)) + { + Send_Notification(NOTIF_ONE, it, MSG_CENTER, CENTER_ONS_NOTSHIELDED_TEAM); + soundto(MSG_ONE, it, CHAN_AUTO, SND(KH_ALARM), VOL_BASE, ATTEN_NONE); // FIXME: unique sound? + } + else + Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_TEAM_NUM(this.team, CENTER_ONS_NOTSHIELDED)); + }); + } + } +} + +void ons_GeneratorReset(entity this) +{ + this.team = this.team_saved; + this.lasthealth = this.max_health = this.health = autocvar_g_onslaught_gen_health; + this.takedamage = DAMAGE_AIM; + this.bot_attack = true; + this.iscaptured = true; + this.islinked = true; + this.isshielded = true; + this.event_damage = ons_GeneratorDamage; + setthink(this, ons_GeneratorThink); + this.nextthink = time + GEN_THINKRATE; + + Net_LinkEntity(this, false, 0, generator_send); + + this.SendFlags = GSF_SETUP; // just incase + this.SendFlags |= GSF_STATUS; + + WaypointSprite_UpdateMaxHealth(this.sprite, this.max_health); + WaypointSprite_UpdateHealth(this.sprite, this.health); + WaypointSprite_UpdateRule(this.sprite,this.team,SPRITERULE_TEAMPLAY); + + onslaught_updatelinks(); +} + +void ons_DelayedGeneratorSetup(entity this) +{ + // bot waypoints + waypoint_spawnforitem_force(this, this.origin); + this.nearestwaypointtimeout = 0; // activate waypointing again + this.bot_basewaypoint = this.nearestwaypoint; + + // captureshield setup + ons_CaptureShield_Spawn(this, true); + + onslaught_updatelinks(); + + Net_LinkEntity(this, false, 0, generator_send); +} + + +void onslaught_generator_touch(entity this, entity toucher) +{ + if ( IS_PLAYER(toucher) ) + if ( SAME_TEAM(this,toucher) ) + if ( this.iscaptured ) + { + Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_ONS_TELEPORT); + } +} + +void ons_GeneratorSetup(entity gen) // called when spawning a generator entity on the map as a spawnfunc +{ + // declarations + int teamnumber = gen.team; + + // main setup + gen.ons_worldgeneratornext = ons_worldgeneratorlist; // link generator into ons_worldgeneratorlist + ons_worldgeneratorlist = gen; + + gen.netname = sprintf("%s generator", Team_ColoredFullName(teamnumber)); + gen.classname = "onslaught_generator"; + gen.solid = SOLID_BBOX; + gen.team_saved = teamnumber; + set_movetype(gen, MOVETYPE_NONE); + gen.lasthealth = gen.max_health = gen.health = autocvar_g_onslaught_gen_health; + gen.takedamage = DAMAGE_AIM; + gen.bot_attack = true; + gen.event_damage = ons_GeneratorDamage; + gen.reset = ons_GeneratorReset; + setthink(gen, ons_GeneratorThink); + gen.nextthink = time + GEN_THINKRATE; + gen.iscaptured = true; + gen.islinked = true; + gen.isshielded = true; + settouch(gen, onslaught_generator_touch); + + // appearence + // model handled by CSQC + setsize(gen, GENERATOR_MIN, GENERATOR_MAX); + setorigin(gen, (gen.origin + CPGEN_SPAWN_OFFSET)); + gen.colormap = 1024 + (teamnumber - 1) * 17; + + // generator placement + droptofloor(gen); + + // waypointsprites + WaypointSprite_SpawnFixed(WP_Null, gen.origin + CPGEN_WAYPOINT_OFFSET, gen, sprite, RADARICON_NONE); + WaypointSprite_UpdateRule(gen.sprite, gen.team, SPRITERULE_TEAMPLAY); + WaypointSprite_UpdateMaxHealth(gen.sprite, gen.max_health); + WaypointSprite_UpdateHealth(gen.sprite, gen.health); + + InitializeEntity(gen, ons_DelayedGeneratorSetup, INITPRIO_SETLOCATION); +} + + +// =============== +// Round Handler +// =============== + +int total_generators; +void Onslaught_count_generators() +{ + entity e; + total_generators = redowned = blueowned = yellowowned = pinkowned = 0; + for(e = ons_worldgeneratorlist; e; e = e.ons_worldgeneratornext) + { + ++total_generators; + redowned += (e.team == NUM_TEAM_1 && e.health > 0); + blueowned += (e.team == NUM_TEAM_2 && e.health > 0); + yellowowned += (e.team == NUM_TEAM_3 && e.health > 0); + pinkowned += (e.team == NUM_TEAM_4 && e.health > 0); + } +} + +int Onslaught_GetWinnerTeam() +{ + int winner_team = 0; + if(redowned > 0) + winner_team = NUM_TEAM_1; + if(blueowned > 0) + { + if(winner_team) return 0; + winner_team = NUM_TEAM_2; + } + if(yellowowned > 0) + { + if(winner_team) return 0; + winner_team = NUM_TEAM_3; + } + if(pinkowned > 0) + { + if(winner_team) return 0; + winner_team = NUM_TEAM_4; + } + if(winner_team) + return winner_team; + return -1; // no generators left? +} + +void nades_Clear(entity e); + +#define ONS_OWNED_GENERATORS() ((redowned > 0) + (blueowned > 0) + (yellowowned > 0) + (pinkowned > 0)) +#define ONS_OWNED_GENERATORS_OK() (ONS_OWNED_GENERATORS() > 1) +bool Onslaught_CheckWinner() +{ + if ((autocvar_timelimit && time > game_starttime + autocvar_timelimit * 60) || (round_handler_GetEndTime() > 0 && round_handler_GetEndTime() - time <= 0)) + { + ons_stalemate = true; + + if (!wpforenemy_announced) + { + Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_OVERTIME_CONTROLPOINT); + sound(NULL, CH_INFO, SND_ONS_GENERATOR_DECAY, VOL_BASE, ATTEN_NONE); + + wpforenemy_announced = true; + } + + entity tmp_entity; // temporary entity + float d; + for(tmp_entity = ons_worldgeneratorlist; tmp_entity; tmp_entity = tmp_entity.ons_worldgeneratornext) if(time >= tmp_entity.ons_overtime_damagedelay) + { + // tmp_entity.max_health / 300 gives 5 minutes of overtime. + // control points reduce the overtime duration. + d = 1; + entity e; + for(e = ons_worldcplist; e; e = e.ons_worldcpnext) + { + if(DIFF_TEAM(e, tmp_entity)) + if(e.islinked) + d = d + 1; + } + + if(autocvar_g_campaign && autocvar__campaign_testrun) + d = d * tmp_entity.max_health; + else + d = d * tmp_entity.max_health / max(30, 60 * autocvar_timelimit_suddendeath); + + Damage(tmp_entity, tmp_entity, tmp_entity, d, DEATH_HURTTRIGGER.m_id, tmp_entity.origin, '0 0 0'); + + tmp_entity.sprite.SendFlags |= 16; + + tmp_entity.ons_overtime_damagedelay = time + 1; + } + } + else { wpforenemy_announced = false; ons_stalemate = false; } + + Onslaught_count_generators(); + + if(ONS_OWNED_GENERATORS_OK()) + return 0; + + int winner_team = Onslaught_GetWinnerTeam(); + + if(winner_team > 0) + { + Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, APP_TEAM_NUM(winner_team, CENTER_ROUND_TEAM_WIN)); + Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(winner_team, INFO_ROUND_TEAM_WIN)); + TeamScore_AddToTeam(winner_team, ST_ONS_CAPS, +1); + } + else if(winner_team == -1) + { + Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_ROUND_TIED); + Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_ROUND_TIED); + } + + ons_stalemate = false; + + play2all(SND(CTF_CAPTURE(winner_team))); + + round_handler_Init(7, autocvar_g_onslaught_warmup, autocvar_g_onslaught_round_timelimit); + + FOREACH_CLIENT(IS_PLAYER(it), { - it.ons_roundlost = true; ++ STAT(ROUNDLOST, it) = true; + it.player_blocked = true; + + nades_Clear(it); + }); + + return 1; +} + +bool Onslaught_CheckPlayers() +{ + return 1; +} + +void Onslaught_RoundStart() +{ + entity tmp_entity; + FOREACH_CLIENT(IS_PLAYER(it), it.player_blocked = false); + + for(tmp_entity = ons_worldcplist; tmp_entity; tmp_entity = tmp_entity.ons_worldcpnext) + tmp_entity.sprite.SendFlags |= 16; + + for(tmp_entity = ons_worldgeneratorlist; tmp_entity; tmp_entity = tmp_entity.ons_worldgeneratornext) + tmp_entity.sprite.SendFlags |= 16; +} + + +// ================ +// Bot player logic +// ================ + +// NOTE: LEGACY CODE, needs to be re-written! + +void havocbot_goalrating_ons_offenseitems(entity this, float ratingscale, vector org, float sradius) +{ + bool needarmor = false, needweapons = false; + + // Needs armor/health? + if(this.health<100) + needarmor = true; + + // Needs weapons? + int c = 0; + FOREACH(Weapons, it != WEP_Null, { + if(this.weapons & (it.m_wepset)) + if(++c >= 4) + break; + }); + + if(c<4) + needweapons = true; + + if(!needweapons && !needarmor) + return; + + LOG_DEBUG(this.netname, " needs weapons ", ftos(needweapons)); + LOG_DEBUG(this.netname, " needs armor ", ftos(needarmor)); + + // See what is around + FOREACH_ENTITY_FLOAT(bot_pickup, true, + { + // gather health and armor only + if (it.solid) + if ( ((it.health || it.armorvalue) && needarmor) || (it.weapons && needweapons ) ) + if (vdist(it.origin - org, <, sradius)) + { + int t = it.bot_pickupevalfunc(this, it); + if (t > 0) + navigation_routerating(this, it, t * ratingscale, 500); + } + }); +} + +void havocbot_role_ons_setrole(entity this, int role) +{ + LOG_DEBUG(this.netname," switched to "); + switch(role) + { + case HAVOCBOT_ONS_ROLE_DEFENSE: + LOG_DEBUG("defense"); + this.havocbot_role = havocbot_role_ons_defense; + this.havocbot_role_flags = HAVOCBOT_ONS_ROLE_DEFENSE; + this.havocbot_role_timeout = 0; + break; + case HAVOCBOT_ONS_ROLE_ASSISTANT: + LOG_DEBUG("assistant"); + this.havocbot_role = havocbot_role_ons_assistant; + this.havocbot_role_flags = HAVOCBOT_ONS_ROLE_ASSISTANT; + this.havocbot_role_timeout = 0; + break; + case HAVOCBOT_ONS_ROLE_OFFENSE: + LOG_DEBUG("offense"); + this.havocbot_role = havocbot_role_ons_offense; + this.havocbot_role_flags = HAVOCBOT_ONS_ROLE_OFFENSE; + this.havocbot_role_timeout = 0; + break; + } + LOG_DEBUG(""); +} + +void havocbot_goalrating_ons_controlpoints_attack(entity this, float ratingscale) +{ + entity cp, cp1, cp2, best, wp; + float radius, bestvalue; + int c; + bool found; + + // Filter control points + for(cp2 = ons_worldcplist; cp2; cp2 = cp2.ons_worldcpnext) + { + cp2.wpcost = c = 0; + cp2.wpconsidered = false; + + if(cp2.isshielded) + continue; + + // Ignore owned controlpoints + if(!(cp2.isgenneighbor[this.team] || cp2.iscpneighbor[this.team])) + continue; + + // Count team mates interested in this control point + // (easier and cleaner than keeping counters per cp and teams) + FOREACH_CLIENT(IS_PLAYER(it), { + if(SAME_TEAM(it, this)) + if(it.havocbot_role_flags & HAVOCBOT_ONS_ROLE_OFFENSE) + if(it.havocbot_ons_target == cp2) + ++c; + }); + + // NOTE: probably decrease the cost of attackable control points + cp2.wpcost = c; + cp2.wpconsidered = true; + } + + // We'll consider only the best case + bestvalue = 99999999999; + cp = NULL; + for(cp1 = ons_worldcplist; cp1; cp1 = cp1.ons_worldcpnext) + { + if (!cp1.wpconsidered) + continue; + + if(cp1.wpcost this.havocbot_role_timeout) + { + havocbot_ons_reset_role(this); + return; + } + + if(this.havocbot_attack_time>time) + return; + + if (this.bot_strategytime < time) + { + navigation_goalrating_start(this); + havocbot_goalrating_enemyplayers(this, 20000, this.origin, 650); + if(!havocbot_goalrating_ons_generator_attack(this, 20000)) + havocbot_goalrating_ons_controlpoints_attack(this, 20000); + havocbot_goalrating_ons_offenseitems(this, 10000, this.origin, 10000); + navigation_goalrating_end(this); + + this.bot_strategytime = time + autocvar_bot_ai_strategyinterval; + } +} + +void havocbot_role_ons_assistant(entity this) +{ + havocbot_ons_reset_role(this); +} + +void havocbot_role_ons_defense(entity this) +{ + havocbot_ons_reset_role(this); +} + +void havocbot_ons_reset_role(entity this) +{ + if(IS_DEAD(this)) + return; + + this.havocbot_ons_target = NULL; + + // TODO: Defend control points or generator if necessary + + havocbot_role_ons_setrole(this, HAVOCBOT_ONS_ROLE_OFFENSE); +} + + +/* + * Find control point or generator owned by the same team self which is nearest to pos + * if max_dist is positive, only control points within this range will be considered + */ +entity ons_Nearest_ControlPoint(entity this, vector pos, float max_dist) +{ + entity closest_target = NULL; + FOREACH_ENTITY_CLASS("onslaught_controlpoint", true, + { + if(SAME_TEAM(it, this)) + if(it.iscaptured) + if(max_dist <= 0 || vdist(it.origin - pos, <=, max_dist)) + if(vlen2(it.origin - pos) <= vlen2(closest_target.origin - pos) || closest_target == NULL) + closest_target = it; + }); + FOREACH_ENTITY_CLASS("onslaught_generator", true, + { + if(SAME_TEAM(it, this)) + if(max_dist <= 0 || vdist(it.origin - pos, <, max_dist)) + if(vlen2(it.origin - pos) <= vlen2(closest_target.origin - pos) || closest_target == NULL) + closest_target = it; + }); + + return closest_target; +} + +/* + * Find control point or generator owned by the same team self which is nearest to pos + * if max_dist is positive, only control points within this range will be considered + * This function only check distances on the XY plane, disregarding Z + */ +entity ons_Nearest_ControlPoint_2D(entity this, vector pos, float max_dist) +{ + entity closest_target = NULL; + vector delta; + float smallest_distance = 0, distance; + + FOREACH_ENTITY_CLASS("onslaught_controlpoint", true, + { + delta = it.origin - pos; + delta_z = 0; + distance = vlen(delta); + + if(SAME_TEAM(it, this)) + if(it.iscaptured) + if(max_dist <= 0 || distance <= max_dist) + if(closest_target == NULL || distance <= smallest_distance ) + { + closest_target = it; + smallest_distance = distance; + } + }); + FOREACH_ENTITY_CLASS("onslaught_generator", true, + { + delta = it.origin - pos; + delta_z = 0; + distance = vlen(delta); + + if(SAME_TEAM(it, this)) + if(max_dist <= 0 || distance <= max_dist) + if(closest_target == NULL || distance <= smallest_distance ) + { + closest_target = it; + smallest_distance = distance; + } + }); + + return closest_target; +} +/** + * find the number of control points and generators in the same team as this + */ +int ons_Count_SelfControlPoints(entity this) +{ + int n = 0; + FOREACH_ENTITY_CLASS("onslaught_controlpoint", true, + { + if(SAME_TEAM(it, this)) + if(it.iscaptured) + n++; + }); + FOREACH_ENTITY_CLASS("onslaught_generator", true, + { + if(SAME_TEAM(it, this)) + n++; + }); + return n; +} + +/** + * Teleport player to a random position near tele_target + * if tele_effects is true, teleport sound+particles are created + * return false on failure + */ +bool ons_Teleport(entity player, entity tele_target, float range, bool tele_effects) +{ + if ( !tele_target ) + return false; + + int i; + vector loc; + float theta; + // narrow the range for each iteration to increase chances that a spawnpoint + // can be found even if there's little room around the control point + float iteration_scale = 1; + for(i = 0; i < 16; ++i) + { + iteration_scale -= i / 16; + theta = random() * 2 * M_PI; + loc_y = sin(theta); + loc_x = cos(theta); + loc_z = 0; + loc *= random() * range * iteration_scale; + + loc += tele_target.origin + '0 0 128' * iteration_scale; + + tracebox(loc, STAT(PL_MIN, NULL), STAT(PL_MAX, NULL), loc, MOVE_NORMAL, player); + if(trace_fraction == 1.0 && !trace_startsolid) + { + traceline(tele_target.origin, loc, MOVE_NOMONSTERS, tele_target); // double check to make sure we're not spawning outside the NULL + if(trace_fraction == 1.0 && !trace_startsolid) + { + if ( tele_effects ) + { + Send_Effect(EFFECT_TELEPORT, player.origin, '0 0 0', 1); + sound (player, CH_TRIGGER, SND_TELEPORT, VOL_BASE, ATTEN_NORM); + } + setorigin(player, loc); + player.angles = '0 1 0' * ( theta * RAD2DEG + 180 ); + makevectors(player.angles); + player.fixangle = true; + player.teleport_antispam = time + autocvar_g_onslaught_teleport_wait; + + if ( tele_effects ) + Send_Effect(EFFECT_TELEPORT, player.origin + v_forward * 32, '0 0 0', 1); + return true; + } + } + } + + return false; +} + +// ============== +// Hook Functions +// ============== + +MUTATOR_HOOKFUNCTION(ons, reset_map_global) +{ + FOREACH_CLIENT(IS_PLAYER(it), { - it.ons_roundlost = false; ++ STAT(ROUNDLOST, it) = false; + it.ons_deathloc = '0 0 0'; + PutClientInServer(it); + }); + return false; +} + +MUTATOR_HOOKFUNCTION(ons, ClientDisconnect) +{ + entity player = M_ARGV(0, entity); + + player.ons_deathloc = '0 0 0'; +} + +MUTATOR_HOOKFUNCTION(ons, MakePlayerObserver) +{ + entity player = M_ARGV(0, entity); + + player.ons_deathloc = '0 0 0'; +} + +MUTATOR_HOOKFUNCTION(ons, PlayerSpawn) +{ + entity player = M_ARGV(0, entity); + + if(!round_handler_IsRoundStarted()) + { + player.player_blocked = true; + return false; + } + + entity l; + for(l = ons_worldgeneratorlist; l; l = l.ons_worldgeneratornext) + { + l.sprite.SendFlags |= 16; + } + for(l = ons_worldcplist; l; l = l.ons_worldcpnext) + { + l.sprite.SendFlags |= 16; + } + + if(ons_stalemate) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_OVERTIME_CONTROLPOINT); } + + if ( autocvar_g_onslaught_spawn_choose ) + if ( player.ons_spawn_by ) + if ( ons_Teleport(player,player.ons_spawn_by,autocvar_g_onslaught_teleport_radius,false) ) + { + player.ons_spawn_by = NULL; + return false; + } + + if(autocvar_g_onslaught_spawn_at_controlpoints) + if(random() <= autocvar_g_onslaught_spawn_at_controlpoints_chance) + { + float random_target = autocvar_g_onslaught_spawn_at_controlpoints_random; + entity tmp_entity, closest_target = NULL; + vector spawn_loc = player.ons_deathloc; + + // new joining player or round reset, don't bother checking + if(spawn_loc == '0 0 0') { return false; } + + if(random_target) { RandomSelection_Init(); } + + for(tmp_entity = ons_worldcplist; tmp_entity; tmp_entity = tmp_entity.ons_worldcpnext) + { + if(SAME_TEAM(tmp_entity, player)) + if(random_target) + RandomSelection_Add(tmp_entity, 0, string_null, 1, 1); + else if(vlen2(tmp_entity.origin - spawn_loc) <= vlen2(closest_target.origin - spawn_loc) || closest_target == NULL) + closest_target = tmp_entity; + } + + if(random_target) { closest_target = RandomSelection_chosen_ent; } + + if(closest_target) + { + float i; + vector loc; + float iteration_scale = 1; + for(i = 0; i < 10; ++i) + { + iteration_scale -= i / 10; + loc = closest_target.origin + '0 0 96' * iteration_scale; + loc += ('0 1 0' * random()) * 128 * iteration_scale; + tracebox(loc, STAT(PL_MIN, NULL), STAT(PL_MAX, NULL), loc, MOVE_NORMAL, player); + if(trace_fraction == 1.0 && !trace_startsolid) + { + traceline(closest_target.origin, loc, MOVE_NOMONSTERS, closest_target); // double check to make sure we're not spawning outside the NULL + if(trace_fraction == 1.0 && !trace_startsolid) + { + setorigin(player, loc); + player.angles = normalize(loc - closest_target.origin) * RAD2DEG; + return false; + } + } + } + } + } + + if(autocvar_g_onslaught_spawn_at_generator) + if(random() <= autocvar_g_onslaught_spawn_at_generator_chance) + { + float random_target = autocvar_g_onslaught_spawn_at_generator_random; + entity tmp_entity, closest_target = NULL; + vector spawn_loc = player.ons_deathloc; + + // new joining player or round reset, don't bother checking + if(spawn_loc == '0 0 0') { return false; } + + if(random_target) { RandomSelection_Init(); } + + for(tmp_entity = ons_worldgeneratorlist; tmp_entity; tmp_entity = tmp_entity.ons_worldgeneratornext) + { + if(random_target) + RandomSelection_Add(tmp_entity, 0, string_null, 1, 1); + else + { + if(SAME_TEAM(tmp_entity, player)) + if(vlen2(tmp_entity.origin - spawn_loc) <= vlen2(closest_target.origin - spawn_loc) || closest_target == NULL) + closest_target = tmp_entity; + } + } + + if(random_target) { closest_target = RandomSelection_chosen_ent; } + + if(closest_target) + { + float i; + vector loc; + float iteration_scale = 1; + for(i = 0; i < 10; ++i) + { + iteration_scale -= i / 10; + loc = closest_target.origin + '0 0 128' * iteration_scale; + loc += ('0 1 0' * random()) * 256 * iteration_scale; + tracebox(loc, STAT(PL_MIN, NULL), STAT(PL_MAX, NULL), loc, MOVE_NORMAL, player); + if(trace_fraction == 1.0 && !trace_startsolid) + { + traceline(closest_target.origin, loc, MOVE_NOMONSTERS, closest_target); // double check to make sure we're not spawning outside the NULL + if(trace_fraction == 1.0 && !trace_startsolid) + { + setorigin(player, loc); + player.angles = normalize(loc - closest_target.origin) * RAD2DEG; + return false; + } + } + } + } + } + + return false; +} + +MUTATOR_HOOKFUNCTION(ons, PlayerDies) +{ + entity frag_target = M_ARGV(2, entity); + + frag_target.ons_deathloc = frag_target.origin; + entity l; + for(l = ons_worldgeneratorlist; l; l = l.ons_worldgeneratornext) + { + l.sprite.SendFlags |= 16; + } + for(l = ons_worldcplist; l; l = l.ons_worldcpnext) + { + l.sprite.SendFlags |= 16; + } + + if ( autocvar_g_onslaught_spawn_choose ) + if ( ons_Count_SelfControlPoints(frag_target) > 1 ) + stuffcmd(frag_target, "qc_cmd_cl hud clickradar\n"); + + return false; +} + +MUTATOR_HOOKFUNCTION(ons, MonsterMove) +{ + entity mon = M_ARGV(0, entity); + + entity e = find(NULL, targetname, mon.target); + if (e != NULL) + mon.team = e.team; +} + +void ons_MonsterSpawn_Delayed(entity this) +{ + entity own = this.owner; + + if(!own) { delete(this); return; } + + if(own.targetname) + { + entity e = find(NULL, target, own.targetname); + if(e != NULL) + { + own.team = e.team; + + own.use(own, e, NULL); + } + } + + delete(this); +} + +MUTATOR_HOOKFUNCTION(ons, MonsterSpawn) +{ + entity mon = M_ARGV(0, entity); + + entity e = spawn(); + e.owner = mon; + InitializeEntity(e, ons_MonsterSpawn_Delayed, INITPRIO_FINDTARGET); +} + +void ons_TurretSpawn_Delayed(entity this) +{ + entity own = this.owner; + + if(!own) { delete(this); return; } + + if(own.targetname) + { + entity e = find(NULL, target, own.targetname); + if(e != NULL) + { + own.team = e.team; + own.active = ACTIVE_NOT; + + own.use(own, e, NULL); + } + } + + delete(this); +} + +MUTATOR_HOOKFUNCTION(ons, TurretSpawn) +{ + entity turret = M_ARGV(0, entity); + + entity e = spawn(); + e.owner = turret; + InitializeEntity(e, ons_TurretSpawn_Delayed, INITPRIO_FINDTARGET); + + return false; +} + +MUTATOR_HOOKFUNCTION(ons, HavocBot_ChooseRole) +{ + entity bot = M_ARGV(0, entity); + + havocbot_ons_reset_role(bot); + return true; +} + +MUTATOR_HOOKFUNCTION(ons, GetTeamCount) +{ + // onslaught is special + for(entity tmp_entity = ons_worldgeneratorlist; tmp_entity; tmp_entity = tmp_entity.ons_worldgeneratornext) + { + switch(tmp_entity.team) + { + case NUM_TEAM_1: c1 = 0; break; + case NUM_TEAM_2: c2 = 0; break; + case NUM_TEAM_3: c3 = 0; break; + case NUM_TEAM_4: c4 = 0; break; + } + } + + return true; +} + +MUTATOR_HOOKFUNCTION(ons, SpectateCopy) +{ + entity spectatee = M_ARGV(0, entity); + entity client = M_ARGV(1, entity); + - client.ons_roundlost = spectatee.ons_roundlost; // make spectators see it too ++ STAT(ROUNDLOST, client) = STAT(ROUNDLOST, spectatee); // make spectators see it too +} + +MUTATOR_HOOKFUNCTION(ons, SV_ParseClientCommand) +{ + if(MUTATOR_RETURNVALUE) // command was already handled? + return false; + + entity player = M_ARGV(0, entity); + string cmd_name = M_ARGV(1, string); + int cmd_argc = M_ARGV(2, int); + + if ( cmd_name == "ons_spawn" ) + { + vector pos = player.origin; + if(cmd_argc > 1) + pos_x = stof(argv(1)); + if(cmd_argc > 2) + pos_y = stof(argv(2)); + if(cmd_argc > 3) + pos_z = stof(argv(3)); + + if ( IS_PLAYER(player) ) + { + if ( !STAT(FROZEN, player) ) + { + entity source_point = ons_Nearest_ControlPoint(player, player.origin, autocvar_g_onslaught_teleport_radius); + + if ( !source_point && player.health > 0 ) + { + sprint(player, "\nYou need to be next to a control point\n"); + return true; + } + + + entity closest_target = ons_Nearest_ControlPoint_2D(player, pos, autocvar_g_onslaught_click_radius); + + if ( closest_target == NULL ) + { + sprint(player, "\nNo control point found\n"); + return true; + } + + if ( player.health <= 0 ) + { + player.ons_spawn_by = closest_target; + player.respawn_flags = player.respawn_flags | RESPAWN_FORCE; + } + else + { + if ( source_point == closest_target ) + { + sprint(player, "\nTeleporting to the same point\n"); + return true; + } + + if ( !ons_Teleport(player,closest_target,autocvar_g_onslaught_teleport_radius,true) ) + sprint(player, "\nUnable to teleport there\n"); + } + + return true; + } + + sprint(player, "\nNo teleportation for you\n"); + } + + return true; + } + return false; +} + +MUTATOR_HOOKFUNCTION(ons, PlayerUseKey) +{ + if(MUTATOR_RETURNVALUE || gameover) { return false; } + + entity player = M_ARGV(0, entity); + + if((time > player.teleport_antispam) && (!IS_DEAD(player)) && !player.vehicle) + { + entity source_point = ons_Nearest_ControlPoint(player, player.origin, autocvar_g_onslaught_teleport_radius); + if ( source_point ) + { + stuffcmd(player, "qc_cmd_cl hud clickradar\n"); + return true; + } + } +} + +MUTATOR_HOOKFUNCTION(ons, PlayHitsound) +{ + entity frag_victim = M_ARGV(0, entity); + + return (frag_victim.classname == "onslaught_generator" && !frag_victim.isshielded) + || (frag_victim.classname == "onslaught_controlpoint_icon" && !frag_victim.owner.isshielded); +} + +MUTATOR_HOOKFUNCTION(ons, SendWaypoint) +{ + entity wp = M_ARGV(0, entity); + entity to = M_ARGV(1, entity); + int sf = M_ARGV(2, int); + int wp_flag = M_ARGV(3, int); + + if(sf & 16) + { + if(wp.owner.classname == "onslaught_controlpoint") + { + entity wp_owner = wp.owner; + entity e = WaypointSprite_getviewentity(to); + if(SAME_TEAM(e, wp_owner) && wp_owner.goalentity.health >= wp_owner.goalentity.max_health) { wp_flag |= 2; } + if(!ons_ControlPoint_Attackable(wp_owner, e.team)) { wp_flag |= 2; } + } + if(wp.owner.classname == "onslaught_generator") + { + entity wp_owner = wp.owner; + if(wp_owner.isshielded && wp_owner.health >= wp_owner.max_health) { wp_flag |= 2; } + if(wp_owner.health <= 0) { wp_flag |= 2; } + } + } + + M_ARGV(3, int) = wp_flag; +} + +MUTATOR_HOOKFUNCTION(ons, TurretValidateTarget) +{ + entity turret_target = M_ARGV(1, entity); + + if(substring(turret_target.classname, 0, 10) == "onslaught_") // don't attack onslaught targets, that's the player's job! + { + M_ARGV(3, float) = -3; + return true; + } + + return false; +} + +MUTATOR_HOOKFUNCTION(ons, TurretThink) +{ + entity turret = M_ARGV(0, entity); + + // ONS uses somewhat backwards linking. + if(turret.target) + { + entity e = find(NULL, targetname, turret.target); + if (e != NULL) + turret.team = e.team; + } + + if(turret.team != turret.tur_head.team) + turret_respawn(turret); +} + + +// ========== +// Spawnfuncs +// ========== + +/*QUAKED spawnfunc_onslaught_link (0 .5 .8) (-16 -16 -16) (16 16 16) + Link between control points. + + This entity targets two different spawnfunc_onslaught_controlpoint or spawnfunc_onslaught_generator entities, and suppresses shielding on both if they are owned by different teams. + +keys: +"target" - first control point. +"target2" - second control point. + */ +spawnfunc(onslaught_link) +{ + if(!g_onslaught) { delete(this); return; } + + if (this.target == "" || this.target2 == "") + objerror(this, "target and target2 must be set\n"); + + this.ons_worldlinknext = ons_worldlinklist; // link into ons_worldlinklist + ons_worldlinklist = this; + + InitializeEntity(this, ons_DelayedLinkSetup, INITPRIO_FINDTARGET); + Net_LinkEntity(this, false, 0, ons_Link_Send); +} + +/*QUAKED spawnfunc_onslaught_controlpoint (0 .5 .8) (-32 -32 0) (32 32 128) + Control point. Be sure to give this enough clearance so that the shootable part has room to exist + + This should link to an spawnfunc_onslaught_controlpoint entity or spawnfunc_onslaught_generator entity. + +keys: +"targetname" - name that spawnfunc_onslaught_link entities will use to target this. +"target" - target any entities that are tied to this control point, such as vehicles and buildable structure entities. +"message" - name of this control point (should reflect the location in the map, such as "center bridge", "north tower", etc) + */ + +spawnfunc(onslaught_controlpoint) +{ + if(!g_onslaught) { delete(this); return; } + + ons_ControlPoint_Setup(this); +} + +/*QUAKED spawnfunc_onslaught_generator (0 .5 .8) (-32 -32 -24) (32 32 64) + Base generator. + + spawnfunc_onslaught_link entities can target this. + +keys: +"team" - team that owns this generator (5 = red, 14 = blue, etc), MUST BE SET. +"targetname" - name that spawnfunc_onslaught_link entities will use to target this. + */ +spawnfunc(onslaught_generator) +{ + if(!g_onslaught) { delete(this); return; } + if(!this.team) { objerror(this, "team must be set"); } + + ons_GeneratorSetup(this); +} + +// scoreboard setup +void ons_ScoreRules() +{ + CheckAllowedTeams(NULL); + int teams = 0; + if(c1 >= 0) teams |= BIT(0); + if(c2 >= 0) teams |= BIT(1); + if(c3 >= 0) teams |= BIT(2); + if(c4 >= 0) teams |= BIT(3); + ScoreRules_basics(teams, SFL_SORT_PRIO_PRIMARY, 0, true); + ScoreInfo_SetLabel_TeamScore (ST_ONS_CAPS, "destroyed", SFL_SORT_PRIO_PRIMARY); + ScoreInfo_SetLabel_PlayerScore(SP_ONS_CAPS, "caps", SFL_SORT_PRIO_SECONDARY); + ScoreInfo_SetLabel_PlayerScore(SP_ONS_TAKES, "takes", 0); + ScoreRules_basics_end(); +} + +void ons_DelayedInit(entity this) // Do this check with a delay so we can wait for teams to be set up +{ + ons_ScoreRules(); + + round_handler_Spawn(Onslaught_CheckPlayers, Onslaught_CheckWinner, Onslaught_RoundStart); + round_handler_Init(5, autocvar_g_onslaught_warmup, autocvar_g_onslaught_round_timelimit); +} + +void ons_Initialize() +{ + g_onslaught = true; + ons_captureshield_force = autocvar_g_onslaught_shield_force; + + InitializeEntity(NULL, ons_DelayedInit, INITPRIO_GAMETYPE); +} diff --cc qcsrc/common/gamemodes/gamemode/onslaught/sv_onslaught.qh index f70ba3c55,000000000..70a5c8b6b mode 100644,000000..100644 --- a/qcsrc/common/gamemodes/gamemode/onslaught/sv_onslaught.qh +++ b/qcsrc/common/gamemodes/gamemode/onslaught/sv_onslaught.qh @@@ -1,119 -1,0 +1,117 @@@ +#pragma once + +float autocvar_g_onslaught_point_limit; +void ons_Initialize(); + +REGISTER_MUTATOR(ons, false) +{ + MUTATOR_ONADD + { + if (time > 1) // game loads at time 1 + error("This is a game type and it cannot be added at runtime."); + ons_Initialize(); + + ActivateTeamplay(); + SetLimits(autocvar_g_onslaught_point_limit, autocvar_leadlimit_override, autocvar_timelimit_override, -1); + have_team_spawns = -1; // request team spawns + } + + MUTATOR_ONROLLBACK_OR_REMOVE + { + // we actually cannot roll back ons_Initialize here + // BUT: we don't need to! If this gets called, adding always + // succeeds. + } + + MUTATOR_ONREMOVE + { + LOG_INFO("This is a game type and it cannot be removed at runtime."); + return -1; + } + + return false; +} + +.entity ons_toucher; // player who touched the control point + +// control point / generator constants +const float ONS_CP_THINKRATE = 0.2; +const float GEN_THINKRATE = 1; +#define CPGEN_SPAWN_OFFSET ('0 0 1' * (PL_MAX_CONST.z - 13)) +const vector CPGEN_WAYPOINT_OFFSET = ('0 0 128'); +const vector CPICON_OFFSET = ('0 0 96'); + +// list of generators on the map +entity ons_worldgeneratorlist; +.entity ons_worldgeneratornext; +.entity ons_stalegeneratornext; + +// list of control points on the map +entity ons_worldcplist; +.entity ons_worldcpnext; +.entity ons_stalecpnext; + +// list of links on the map +entity ons_worldlinklist; +.entity ons_worldlinknext; +.entity ons_stalelinknext; + +// definitions +.entity sprite; +.string target2; +.int iscaptured; +.int islinked; +.int isshielded; +.float lasthealth; +.int lastteam; +.int lastshielded; +.int lastcaptured; + +.bool waslinked; + +bool ons_stalemate; + +.float teleport_antispam; + - .bool ons_roundlost = _STAT(ROUNDLOST); - +// waypoint sprites +.entity bot_basewaypoint; // generator waypointsprite + +.bool isgenneighbor[17]; +.bool iscpneighbor[17]; +float ons_notification_time[17]; + +.float ons_overtime_damagedelay; + +.vector ons_deathloc; + +.entity ons_spawn_by; + +// declarations for functions used outside gamemode_onslaught.qc +void ons_Generator_UpdateSprite(entity e); +void ons_ControlPoint_UpdateSprite(entity e); +bool ons_ControlPoint_Attackable(entity cp, int teamnumber); + +// CaptureShield: Prevent capturing or destroying control point/generator if it is not available yet +float ons_captureshield_force; // push force of the shield + +// bot player logic +const int HAVOCBOT_ONS_ROLE_NONE = 0; +const int HAVOCBOT_ONS_ROLE_DEFENSE = 2; +const int HAVOCBOT_ONS_ROLE_ASSISTANT = 4; +const int HAVOCBOT_ONS_ROLE_OFFENSE = 8; + +.entity havocbot_ons_target; + +.int havocbot_role_flags; +.float havocbot_attack_time; + +void havocbot_role_ons_defense(entity this); +void havocbot_role_ons_offense(entity this); +void havocbot_role_ons_assistant(entity this); + +void havocbot_ons_reset_role(entity this); +void havocbot_goalrating_items(entity this, float ratingscale, vector org, float sradius); +void havocbot_goalrating_enemyplayers(entity this, float ratingscale, vector org, float sradius); + +// score rule declarations +const int ST_ONS_CAPS = 1; diff --cc qcsrc/common/mutators/mutator/campcheck/sv_campcheck.qc index be7d3a2e2,000000000..c4d2e0302 mode 100644,000000..100644 --- a/qcsrc/common/mutators/mutator/campcheck/sv_campcheck.qc +++ b/qcsrc/common/mutators/mutator/campcheck/sv_campcheck.qc @@@ -1,91 -1,0 +1,92 @@@ +#include "sv_campcheck.qh" + +float autocvar_g_campcheck_damage; +float autocvar_g_campcheck_distance; +float autocvar_g_campcheck_interval; + +REGISTER_MUTATOR(campcheck, cvar("g_campcheck")); + +.float campcheck_nextcheck; +.float campcheck_traveled_distance; + +MUTATOR_HOOKFUNCTION(campcheck, PlayerDies) +{ + entity frag_target = M_ARGV(2, entity); + + Kill_Notification(NOTIF_ONE, frag_target, MSG_CENTER, CPID_CAMPCHECK); +} + +MUTATOR_HOOKFUNCTION(campcheck, PlayerDamage_Calculate) +{ + entity frag_attacker = M_ARGV(1, entity); + entity frag_target = M_ARGV(2, entity); + + if(IS_PLAYER(frag_target)) + if(IS_PLAYER(frag_attacker)) + if(frag_attacker != frag_target) + { + frag_target.campcheck_traveled_distance = autocvar_g_campcheck_distance; + frag_attacker.campcheck_traveled_distance = autocvar_g_campcheck_distance; + } +} + +MUTATOR_HOOKFUNCTION(campcheck, PlayerPreThink) +{ + entity player = M_ARGV(0, entity); + + if(!gameover) + if(!warmup_stage) // don't consider it camping during warmup? + if(time >= game_starttime) + if(IS_PLAYER(player)) + if(IS_REAL_CLIENT(player)) // bots may camp, but that's no reason to constantly kill them + if(!IS_DEAD(player)) ++ if(!forbidWeaponUse(player)) + if(!STAT(FROZEN, player)) + if(!PHYS_INPUT_BUTTON_CHAT(player)) + if(autocvar_g_campcheck_interval) + { + vector dist; + + // calculate player movement (in 2 dimensions only, so jumping on one spot doesn't count as movement) + dist = player.prevorigin - player.origin; + dist.z = 0; + player.campcheck_traveled_distance += fabs(vlen(dist)); + + if((autocvar_g_campaign && !campaign_bots_may_start) || (time < game_starttime) || (round_handler_IsActive() && !round_handler_IsRoundStarted())) + { + player.campcheck_nextcheck = time + autocvar_g_campcheck_interval * 2; + player.campcheck_traveled_distance = 0; + } + + if(time > player.campcheck_nextcheck) + { + if(player.campcheck_traveled_distance < autocvar_g_campcheck_distance) + { + Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_CAMPCHECK); + if(player.vehicle) + Damage(player.vehicle, NULL, NULL, autocvar_g_campcheck_damage * 2, DEATH_CAMP.m_id, player.vehicle.origin, '0 0 0'); + else + Damage(player, NULL, NULL, bound(0, autocvar_g_campcheck_damage, player.health + player.armorvalue * autocvar_g_balance_armor_blockpercent + 5), DEATH_CAMP.m_id, player.origin, '0 0 0'); + } + player.campcheck_nextcheck = time + autocvar_g_campcheck_interval; + player.campcheck_traveled_distance = 0; + } + + return; + } + + player.campcheck_nextcheck = time + autocvar_g_campcheck_interval; // one of the above checks failed, so keep the timer up to date +} + +MUTATOR_HOOKFUNCTION(campcheck, PlayerSpawn) +{ + entity player = M_ARGV(0, entity); + + player.campcheck_nextcheck = time + autocvar_g_campcheck_interval * 2; + player.campcheck_traveled_distance = 0; +} + +MUTATOR_HOOKFUNCTION(campcheck, BuildMutatorsString) +{ + M_ARGV(0, string) = strcat(M_ARGV(0, string), ":CampCheck"); +} diff --cc qcsrc/common/mutators/mutator/itemstime/itemstime.qc index 0c348b624,000000000..3fe9de6d7 mode 100644,000000..100644 --- a/qcsrc/common/mutators/mutator/itemstime/itemstime.qc +++ b/qcsrc/common/mutators/mutator/itemstime/itemstime.qc @@@ -1,434 -1,0 +1,434 @@@ +#include "itemstime.qh" + +REGISTER_MUTATOR(itemstime, true); + +REGISTER_NET_TEMP(itemstime) + +#ifdef SVQC +void IT_Write(entity e, int i, float f) { + if (!IS_REAL_CLIENT(e)) return; + msg_entity = e; + WriteHeader(MSG_ONE, itemstime); + WriteByte(MSG_ONE, i); + WriteFloat(MSG_ONE, f); +} +#endif + +#ifdef CSQC +// reserve one more spot for superweapons time +float ItemsTime_time[Items_MAX + 1]; +float ItemsTime_availableTime[Items_MAX + 1]; +NET_HANDLE(itemstime, bool isNew) +{ + int i = ReadByte(); + float f = ReadFloat(); + return = true; + ItemsTime_time[i] = f; +} +#endif + +#ifdef CSQC +void Item_ItemsTime_Init() +{ + FOREACH(Items, true, LAMBDA( + ItemsTime_time[it.m_id] = -1; + )); + ItemsTime_time[Items_MAX] = -1; +} + +STATIC_INIT(ItemsTime_Init) { + Item_ItemsTime_Init(); +} + +int autocvar_hud_panel_itemstime = 2; +float autocvar_hud_panel_itemstime_dynamicsize = 1; +float autocvar_hud_panel_itemstime_ratio = 2; +int autocvar_hud_panel_itemstime_iconalign; +bool autocvar_hud_panel_itemstime_progressbar = 0; +float autocvar_hud_panel_itemstime_progressbar_maxtime = 30; +string autocvar_hud_panel_itemstime_progressbar_name = "progressbar"; +float autocvar_hud_panel_itemstime_progressbar_reduced; +bool autocvar_hud_panel_itemstime_hidespawned = 1; +bool autocvar_hud_panel_itemstime_hidelarge = false; +int autocvar_hud_panel_itemstime_text = 1; +#define hud_panel_itemstime_hidelarge autocvar_hud_panel_itemstime_hidelarge +#else +#define hud_panel_itemstime_hidelarge false +#endif + +bool Item_ItemsTime_SpectatorOnly(GameItem it) +{ + return (false + || it == ITEM_ArmorMega || (it == ITEM_ArmorLarge && !hud_panel_itemstime_hidelarge) + || it == ITEM_HealthMega || (it == ITEM_HealthLarge && !hud_panel_itemstime_hidelarge) + ); +} + +bool Item_ItemsTime_Allow(GameItem it) +{ + return (false + || it.instanceOfPowerup + || Item_ItemsTime_SpectatorOnly(it) + ); +} + +#ifdef SVQC + +// reserve one more spot for superweapons time +float it_times[Items_MAX + 1]; + +void Item_ItemsTime_Init() +{ + FOREACH(Items, Item_ItemsTime_Allow(it), LAMBDA( + it_times[it.m_id] = -1; + )); + it_times[Items_MAX] = -1; +} + +STATIC_INIT(ItemsTime_Init) { + // items time + Item_ItemsTime_Init(); +} + +void Item_ItemsTime_ResetTimes() +{ + FOREACH(Items, Item_ItemsTime_Allow(it), LAMBDA( + it_times[it.m_id] = (it_times[it.m_id] == -1) ? -1 : 0; + )); + it_times[Items_MAX] = (it_times[Items_MAX] == -1) ? -1 : 0; +} + +void Item_ItemsTime_ResetTimesForPlayer(entity e) +{ + FOREACH(Items, Item_ItemsTime_Allow(it), LAMBDA( + IT_Write(e, it.m_id, (it_times[it.m_id] == -1) ? -1 : 0); + )); + IT_Write(e, Items_MAX, (it_times[Items_MAX] == -1) ? -1 : 0); +} + +void Item_ItemsTime_SetTimesForPlayer(entity e) +{ + FOREACH(Items, Item_ItemsTime_Allow(it), LAMBDA( + IT_Write(e, it.m_id, it_times[it.m_id]); + )); + IT_Write(e, Items_MAX, it_times[Items_MAX]); +} + +void Item_ItemsTime_SetTime(entity e, float t) +{ + if (!autocvar_sv_itemstime) + return; + + GameItem item = e.itemdef; + if (item.instanceOfGameItem) + { + if (!item.instanceOfWeaponPickup) + it_times[item.m_id] = t; + else if (e.weapons & WEPSET_SUPERWEAPONS) + it_times[Items_MAX] = t; + } +} + +void Item_ItemsTime_SetTimesForAllPlayers() +{ + FOREACH_CLIENT(IS_REAL_CLIENT(it) && (warmup_stage || !IS_PLAYER(it) || autocvar_sv_itemstime == 2), LAMBDA(Item_ItemsTime_SetTimesForPlayer(it))); +} + +float Item_ItemsTime_UpdateTime(entity e, float t) +{ + bool isavailable = (t == 0); + FOREACH_ENTITY_FLOAT(pure_data, false, + { + if(!(it.itemdef == e.itemdef || ((e.weapons & WEPSET_SUPERWEAPONS) && (it.weapons & WEPSET_SUPERWEAPONS) && clienttype(it) == CLIENTTYPE_NOTACLIENT))) + continue; + if (e == it) continue; + if (it.scheduledrespawntime <= time) + isavailable = true; + else if (t == 0 || it.scheduledrespawntime < t) + t = it.scheduledrespawntime; + }); + if (isavailable) + t = -t; // let know the client there's another available item + return t; +} + +MUTATOR_HOOKFUNCTION(itemstime, reset_map_global) +{ + Item_ItemsTime_ResetTimes(); + // ALL the times need to be reset before .reset()ing each item + // since Item_Reset schedules respawn of superweapons and powerups + FOREACH_ENTITY_FLOAT(pure_data, false, + { + if(IS_CLIENT(it)) + continue; + if (it.reset) Item_ItemsTime_SetTime(it, 0); + }); + Item_ItemsTime_SetTimesForAllPlayers(); +} + +MUTATOR_HOOKFUNCTION(itemstime, MakePlayerObserver) +{ + entity player = M_ARGV(0, entity); + + Item_ItemsTime_SetTimesForPlayer(player); +} + +MUTATOR_HOOKFUNCTION(itemstime, ClientConnect, CBC_ORDER_LAST) +{ + entity player = M_ARGV(0, entity); + + if(IS_PLAYER(player)) + { + // client became player on connection skipping putObserverInServer step + if (IS_REAL_CLIENT(player)) + if (warmup_stage || autocvar_sv_itemstime == 2) + Item_ItemsTime_SetTimesForPlayer(player); + } +} + +MUTATOR_HOOKFUNCTION(itemstime, PlayerSpawn) +{ + if (warmup_stage || autocvar_sv_itemstime == 2) return; + entity player = M_ARGV(0, entity); + + Item_ItemsTime_ResetTimesForPlayer(player); +} + +#endif + +#ifdef CSQC + +void DrawItemsTimeItem(vector myPos, vector mySize, float ar, string item_icon, float item_time, bool item_available, float item_availableTime) +{ + float t = 0; + vector color = '0 0 0'; + float picalpha; + + if (autocvar_hud_panel_itemstime_hidespawned == 2) + picalpha = 1; + else if (item_available) + { + float BLINK_FACTOR = 0.15; + float BLINK_BASE = 0.85; + float BLINK_FREQ = 5; + picalpha = BLINK_BASE + BLINK_FACTOR * cos(time * BLINK_FREQ); + } + else + picalpha = 0.5; + t = floor(item_time - time + 0.999); + if (t < 5) + color = '0.7 0 0'; + else if (t < 10) + color = '0.7 0.7 0'; + else + color = '1 1 1'; + + vector picpos, numpos; + if (autocvar_hud_panel_itemstime_iconalign) + { + numpos = myPos; + picpos = myPos + eX * (ar - 1) * mySize_y; + } + else + { + numpos = myPos + eX * mySize_y; + picpos = myPos; + } + + if (t > 0 && autocvar_hud_panel_itemstime_progressbar) + { + vector p_pos, p_size; + if (autocvar_hud_panel_itemstime_progressbar_reduced) + { + p_pos = numpos; + p_size = eX * ((ar - 1)/ar) * mySize_x + eY * mySize_y; + } + else + { + p_pos = myPos; + p_size = mySize; + } + HUD_Panel_DrawProgressBar(p_pos, p_size, autocvar_hud_panel_itemstime_progressbar_name, t/autocvar_hud_panel_itemstime_progressbar_maxtime, 0, autocvar_hud_panel_itemstime_iconalign, color, autocvar_hud_progressbar_alpha * panel_fg_alpha, DRAWFLAG_NORMAL); + } + + if(autocvar_hud_panel_itemstime_text) + { + if(t > 0) + drawstring_aspect(numpos, ftos(t), eX * ((ar - 1)/ar) * mySize_x + eY * mySize_y, color, panel_fg_alpha, DRAWFLAG_NORMAL); + else if(precache_pic("gfx/hud/default/checkmark")) // COMPAT: check if this image exists, as 0.8.1 clients lack it + drawpic_aspect_skin(numpos, "checkmark", eX * (ar - 1) * mySize_y + eY * mySize_y, '1 1 1', panel_fg_alpha * picalpha, DRAWFLAG_NORMAL); + else // legacy code, if the image is missing just center the icon + picpos.x = myPos.x + mySize.x / 2 - mySize.y / 2; + } + if (item_availableTime) + drawpic_aspect_skin_expanding(picpos, item_icon, '1 1 0' * mySize_y, '1 1 1', panel_fg_alpha * picalpha, DRAWFLAG_NORMAL, item_availableTime); + drawpic_aspect_skin(picpos, item_icon, '1 1 0' * mySize_y, '1 1 1', panel_fg_alpha * picalpha, DRAWFLAG_NORMAL); +} + +void HUD_ItemsTime() +{ + if (!autocvar__hud_configure) + { + if (!( + (autocvar_hud_panel_itemstime == 1 && spectatee_status != 0) + || (autocvar_hud_panel_itemstime == 2 && (spectatee_status != 0 || warmup_stage || STAT(ITEMSTIME) == 2)) + )) { return; } + } + else + { + ItemsTime_time[ITEM_ArmorMega.m_id] = time + 0; + ItemsTime_time[ITEM_HealthMega.m_id] = time + 8; + ItemsTime_time[ITEM_Strength.m_id] = time + 0; + ItemsTime_time[ITEM_Shield.m_id] = time + 4; + } + + int count = 0; + if (autocvar_hud_panel_itemstime_hidespawned == 1) + { + FOREACH(Items, Item_ItemsTime_Allow(it), LAMBDA( + count += (ItemsTime_time[it.m_id] > time || -ItemsTime_time[it.m_id] > time); + )); + count += (ItemsTime_time[Items_MAX] > time || -ItemsTime_time[Items_MAX] > time); + } + else if (autocvar_hud_panel_itemstime_hidespawned == 2) + { + FOREACH(Items, Item_ItemsTime_Allow(it), LAMBDA( + count += (ItemsTime_time[it.m_id] > time); + )); + count += (ItemsTime_time[Items_MAX] > time); + } + else + { + FOREACH(Items, Item_ItemsTime_Allow(it), LAMBDA( + count += (ItemsTime_time[it.m_id] != -1); + )); + count += (ItemsTime_time[Items_MAX] != -1); + } + if (count == 0) + return; + + HUD_Panel_UpdateCvars(); + + vector pos, mySize; + pos = panel_pos; + mySize = panel_size; + + if (panel_bg_padding) + { + pos += '1 1 0' * panel_bg_padding; + mySize -= '2 2 0' * panel_bg_padding; + } + + float rows, columns; + float ar = max(2, autocvar_hud_panel_itemstime_ratio) + 1; + rows = HUD_GetRowCount(count, mySize, ar); + columns = ceil(count/rows); + + vector itemstime_size = eX * mySize.x*(1/columns) + eY * mySize.y*(1/rows); + + vector offset = '0 0 0'; + float newSize; + if (autocvar_hud_panel_itemstime_dynamicsize) + { + if (autocvar__hud_configure) - if (menu_enabled != 2) ++ if (hud_configure_menu_open != 2) + HUD_Panel_DrawBg(1); // also draw the bg of the entire panel + + // reduce panel to avoid spacing items + if (itemstime_size.x / itemstime_size.y < ar) + { + newSize = rows * itemstime_size.x / ar; + pos.y += (mySize.y - newSize) / 2; + mySize.y = newSize; + itemstime_size.y = mySize.y / rows; + } + else + { + newSize = columns * itemstime_size.y * ar; + pos.x += (mySize.x - newSize) / 2; + mySize.x = newSize; + itemstime_size.x = mySize.x / columns; + } + panel_pos = pos - '1 1 0' * panel_bg_padding; + panel_size = mySize + '2 2 0' * panel_bg_padding; + } + else + { + if (itemstime_size.x/itemstime_size.y > ar) + { + newSize = ar * itemstime_size.y; + offset.x = itemstime_size.x - newSize; + pos.x += offset.x/2; + itemstime_size.x = newSize; + } + else + { + newSize = 1/ar * itemstime_size.x; + offset.y = itemstime_size.y - newSize; + pos.y += offset.y/2; + itemstime_size.y = newSize; + } + } + + HUD_Scale_Enable(); + HUD_Panel_DrawBg(1); + + float row = 0, column = 0; + bool item_available; + int id = 0; + string icon = ""; + FOREACH(Items, Item_ItemsTime_Allow(it) && ItemsTime_time[it.m_id] != -1, LAMBDA( + id = it.m_id; + icon = it.m_icon; + +LABEL(iteration) + float item_time = ItemsTime_time[id]; + if (item_time < -1) + { + item_available = true; + item_time = -item_time; + } + else + item_available = (item_time <= time); + + if (ItemsTime_time[id] >= 0) + { + if (time <= ItemsTime_time[id]) + ItemsTime_availableTime[id] = 0; + else if (ItemsTime_availableTime[id] == 0) + ItemsTime_availableTime[id] = time; + } + else if (ItemsTime_availableTime[id] == 0) + ItemsTime_availableTime[id] = time; + + float f = (time - ItemsTime_availableTime[id]) * 2; + f = (f > 1) ? 0 : bound(0, f, 1); + + if (autocvar_hud_panel_itemstime_hidespawned == 1) + if (!(ItemsTime_time[id] > time || -ItemsTime_time[id] > time)) + continue; + + if (autocvar_hud_panel_itemstime_hidespawned == 2) + if (!(ItemsTime_time[id] > time)) + continue; + + DrawItemsTimeItem(pos + eX * column * (itemstime_size.x + offset.x) + eY * row * (itemstime_size.y + offset.y), itemstime_size, ar, icon, item_time, item_available, f); + ++row; + if (row >= rows) + { + row = 0; + column = column + 1; + } + if(id == Items_MAX) // can happen only in the last fake iteration + break; + )); + // add another fake iteration for superweapons time + if(id < Items_MAX && ItemsTime_time[Items_MAX] != -1) + { + id = Items_MAX; + icon = "superweapons"; + goto iteration; + } +} + +#endif diff --cc qcsrc/common/mutators/mutator/spawn_near_teammate/sv_spawn_near_teammate.qc index 4c57d0189,000000000..fb14f27db mode 100644,000000..100644 --- a/qcsrc/common/mutators/mutator/spawn_near_teammate/sv_spawn_near_teammate.qc +++ b/qcsrc/common/mutators/mutator/spawn_near_teammate/sv_spawn_near_teammate.qc @@@ -1,187 -1,0 +1,188 @@@ +#include "sv_spawn_near_teammate.qh" + +float autocvar_g_spawn_near_teammate_distance; +int autocvar_g_spawn_near_teammate_ignore_spawnpoint; +float autocvar_g_spawn_near_teammate_ignore_spawnpoint_delay; +float autocvar_g_spawn_near_teammate_ignore_spawnpoint_delay_death; +int autocvar_g_spawn_near_teammate_ignore_spawnpoint_check_health; +bool autocvar_g_spawn_near_teammate_ignore_spawnpoint_closetodeath; + +REGISTER_MUTATOR(spawn_near_teammate, cvar("g_spawn_near_teammate")); + +.entity msnt_lookat; + +.float msnt_timer; +.vector msnt_deathloc; + +.float cvar_cl_spawn_near_teammate; + +MUTATOR_HOOKFUNCTION(spawn_near_teammate, Spawn_Score) +{ + entity player = M_ARGV(0, entity); + entity spawn_spot = M_ARGV(1, entity); + vector spawn_score = M_ARGV(2, vector); + + if(autocvar_g_spawn_near_teammate_ignore_spawnpoint == 1 || (autocvar_g_spawn_near_teammate_ignore_spawnpoint == 2 && player.cvar_cl_spawn_near_teammate)) + return; + + spawn_spot.msnt_lookat = NULL; + + if(!teamplay) + return; + + RandomSelection_Init(); + FOREACH_CLIENT(IS_PLAYER(it) && it != player && SAME_TEAM(it, player) && !IS_DEAD(it), LAMBDA( + if(vdist(spawn_spot.origin - it.origin, >, autocvar_g_spawn_near_teammate_distance)) + continue; + if(vdist(spawn_spot.origin - it.origin, <, 48)) + continue; + if(!checkpvs(spawn_spot.origin, it)) + continue; + RandomSelection_Add(it, 0, string_null, 1, 1); + )); + + if(RandomSelection_chosen_ent) + { + spawn_spot.msnt_lookat = RandomSelection_chosen_ent; + spawn_score.x += SPAWN_PRIO_NEAR_TEAMMATE_FOUND; + } + else if(player.team == spawn_spot.team) + spawn_score.x += SPAWN_PRIO_NEAR_TEAMMATE_SAMETEAM; // prefer same team, if we can't find a spawn near teammate + + M_ARGV(2, vector) = spawn_score; +} + +MUTATOR_HOOKFUNCTION(spawn_near_teammate, PlayerSpawn) +{ + if(!teamplay) { return; } + entity player = M_ARGV(0, entity); + entity spawn_spot = M_ARGV(1, entity); + + int num_red = 0, num_blue = 0, num_yellow = 0, num_pink = 0; + FOREACH_CLIENT(IS_PLAYER(it), + { + switch(it.team) + { + case NUM_TEAM_1: ++num_red; break; + case NUM_TEAM_2: ++num_blue; break; + case NUM_TEAM_3: ++num_yellow; break; + case NUM_TEAM_4: ++num_pink; break; + } + }); + + if(num_red == 1 || num_blue == 1 || num_yellow == 1 || num_pink == 1) + return; // at least 1 team has only 1 player, let's not give the bigger team too much of an advantage! + + // Note: when entering this, fixangle is already set. + if(autocvar_g_spawn_near_teammate_ignore_spawnpoint == 1 || (autocvar_g_spawn_near_teammate_ignore_spawnpoint == 2 && player.cvar_cl_spawn_near_teammate)) + { + if(autocvar_g_spawn_near_teammate_ignore_spawnpoint_delay_death) + player.msnt_timer = time + autocvar_g_spawn_near_teammate_ignore_spawnpoint_delay_death; + + entity best_mate = NULL; + vector best_spot = '0 0 0'; + float pc = 0, best_dist = 0, dist = 0; + FOREACH_CLIENT(IS_PLAYER(it), LAMBDA( + if((autocvar_g_spawn_near_teammate_ignore_spawnpoint_check_health >= 0 && it.health >= autocvar_g_balance_health_regenstable) || autocvar_g_spawn_near_teammate_ignore_spawnpoint_check_health == 0) + if(!IS_DEAD(it)) + if(it.msnt_timer < time) + if(SAME_TEAM(player, it)) + if(time > it.spawnshieldtime) // spawn shielding ++ if(!forbidWeaponUse(it)) + if(STAT(FROZEN, it) == 0) + if(it != player) + { + tracebox(it.origin, STAT(PL_MIN, NULL), STAT(PL_MAX, NULL), it.origin - '0 0 100', MOVE_NOMONSTERS, it); + if(trace_fraction != 1.0) + if(!(trace_dphitq3surfaceflags & Q3SURFACEFLAG_SKY)) + { + pc = pointcontents(trace_endpos + '0 0 1'); + if(pc == CONTENT_EMPTY) + { + if(vdist(it.velocity, >, 5)) + fixedmakevectors(vectoangles(it.velocity)); + else + fixedmakevectors(it.angles); + + for(pc = 0; pc < 4; ++pc) // test 4 diffrent spots close to mate + { + switch(pc) + { + case 0: + tracebox(it.origin , STAT(PL_MIN, NULL), STAT(PL_MAX, NULL), it.origin + v_right * 128, MOVE_NOMONSTERS, it); + break; + case 1: + tracebox(it.origin , STAT(PL_MIN, NULL), STAT(PL_MAX, NULL), it.origin - v_right * 128 , MOVE_NOMONSTERS, it); + break; + case 2: + tracebox(it.origin , STAT(PL_MIN, NULL), STAT(PL_MAX, NULL), it.origin + v_right * 128 - v_forward * 64, MOVE_NOMONSTERS, it); + break; + case 3: + tracebox(it.origin , STAT(PL_MIN, NULL), STAT(PL_MAX, NULL), it.origin - v_right * 128 - v_forward * 64, MOVE_NOMONSTERS, it); + break; + //case 4: + //tracebox(it.origin , STAT(PL_MIN, NULL), STAT(PL_MAX, NULL), it.origin - v_forward * 128, MOVE_NOMONSTERS, it); + //break; + } + + if(trace_fraction == 1.0) + { + traceline(trace_endpos + '0 0 4', trace_endpos - '0 0 100', MOVE_NOMONSTERS, it); + if(trace_fraction != 1.0) + { + if(autocvar_g_spawn_near_teammate_ignore_spawnpoint_closetodeath) + { + dist = vlen(trace_endpos - player.msnt_deathloc); + if(dist < best_dist || best_dist == 0) + { + best_dist = dist; + best_spot = trace_endpos; + best_mate = it; + } + } + else + { + setorigin(player, trace_endpos); + player.angles = it.angles; + player.angles_z = 0; // never spawn tilted even if the spot says to + it.msnt_timer = time + autocvar_g_spawn_near_teammate_ignore_spawnpoint_delay; + return; + } + } + } + } + } + } + } + )); + + if(autocvar_g_spawn_near_teammate_ignore_spawnpoint_closetodeath) + if(best_dist) + { + setorigin(player, best_spot); + player.angles = best_mate.angles; + player.angles_z = 0; // never spawn tilted even if the spot says to + best_mate.msnt_timer = time + autocvar_g_spawn_near_teammate_ignore_spawnpoint_delay; + } + } + else if(spawn_spot.msnt_lookat) + { + player.angles = vectoangles(spawn_spot.msnt_lookat.origin - player.origin); + player.angles_x = -player.angles.x; + player.angles_z = 0; // never spawn tilted even if the spot says to + /* + sprint(player, "You should be looking at ", spawn_spot.msnt_lookat.netname, "^7.\n"); + sprint(player, "distance: ", vtos(spawn_spot.msnt_lookat.origin - player.origin), "\n"); + sprint(player, "angles: ", vtos(player.angles), "\n"); + */ + } +} + +MUTATOR_HOOKFUNCTION(spawn_near_teammate, PlayerDies) +{ + entity frag_target = M_ARGV(0, entity); + + frag_target.msnt_deathloc = frag_target.origin; +} + +REPLICATE(cvar_cl_spawn_near_teammate, bool, "cl_spawn_near_teammate"); diff --cc qcsrc/server/mutators/mutator/gamemode_domination.qh index eb2379ca2,9a0a1262f..e9eb2c2d9 --- a/qcsrc/server/mutators/mutator/gamemode_domination.qh +++ b/qcsrc/server/mutators/mutator/gamemode_domination.qh @@@ -2,60 -2,4 +2,62 @@@ #include "../gamemode.qh" +#define autocvar_g_domination_point_limit cvar("g_domination_point_limit") +bool autocvar_g_domination_roundbased; +int autocvar_g_domination_roundbased_point_limit; +int autocvar_g_domination_point_leadlimit; + +void dom_Initialize(); + +REGISTER_MUTATOR(dom, false) +{ + MUTATOR_ONADD + { + if (time > 1) // game loads at time 1 + error("This is a game type and it cannot be added at runtime."); + dom_Initialize(); + + int fraglimit_override = autocvar_g_domination_point_limit; + if (autocvar_g_domination_roundbased && autocvar_g_domination_roundbased_point_limit) + fraglimit_override = autocvar_g_domination_roundbased_point_limit; + + ActivateTeamplay(); + SetLimits(fraglimit_override, autocvar_g_domination_point_leadlimit, autocvar_timelimit_override, -1); + have_team_spawns = -1; // request team spawns + } + + MUTATOR_ONREMOVE + { + LOG_INFO("This is a game type and it cannot be removed at runtime."); + return -1; + } + + return 0; +} + +// score rule declarations +const float ST_DOM_TICKS = 1; +const float ST_DOM_CAPS = 1; + +// pps: points per second +.float dom_total_pps = _STAT(DOM_TOTAL_PPS); +.float dom_pps_red = _STAT(DOM_PPS_RED); +.float dom_pps_blue = _STAT(DOM_PPS_BLUE); +.float dom_pps_yellow = _STAT(DOM_PPS_YELLOW); +.float dom_pps_pink = _STAT(DOM_PPS_PINK); +float total_pps; +float pps_red; +float pps_blue; +float pps_yellow; +float pps_pink; + +// capture declarations +.float enemy_playerid; +.entity sprite; +.float captime; + +// misc globals +float domination_roundbased; +float domination_teams; ++ + void AnimateDomPoint(entity this); diff --cc qcsrc/server/player.qc index 5316a85b5,000000000..f2ceecf9e mode 100644,000000..100644 --- a/qcsrc/server/player.qc +++ b/qcsrc/server/player.qc @@@ -1,962 -1,0 +1,964 @@@ +#include "player.qh" + +#include "bot/api.qh" +#include "cheats.qh" +#include "g_damage.qh" +#include "g_subs.qh" +#include "miscfunctions.qh" +#include "portals.qh" +#include "teamplay.qh" +#include "weapons/throwing.qh" +#include "command/common.qh" +#include "../common/state.qh" +#include "../common/anim.qh" +#include "../common/animdecide.qh" +#include "../common/csqcmodel_settings.qh" +#include "../common/deathtypes/all.qh" +#include "../common/triggers/subs.qh" +#include "../common/playerstats.qh" +#include "../lib/csqcmodel/sv_model.qh" + +#include "../common/minigames/sv_minigames.qh" + +#include "../common/physics/player.qh" +#include "../common/effects/qc/all.qh" +#include "../common/mutators/mutator/waypoints/waypointsprites.qh" +#include "../common/triggers/include.qh" + +#include "weapons/weaponstats.qh" + +#include "../common/animdecide.qh" + +void Drop_Special_Items(entity player) +{ + // called when the player has become stuck or frozen + // so objective items aren't stuck with the player + + MUTATOR_CALLHOOK(DropSpecialItems, player); +} + +void CopyBody_Think(entity this) +{ + if(this.CopyBody_nextthink && time > this.CopyBody_nextthink) + { + this.CopyBody_think(this); + if(wasfreed(this)) + return; + this.CopyBody_nextthink = this.nextthink; + this.CopyBody_think = getthink(this); + setthink(this, CopyBody_Think); + } + CSQCMODEL_AUTOUPDATE(this); + this.nextthink = time; +} +void CopyBody(entity this, float keepvelocity) +{ + if (this.effects & EF_NODRAW) + return; + entity clone = new(body); + clone.enemy = this; + clone.lip = this.lip; + clone.colormap = this.colormap; + clone.iscreature = this.iscreature; + clone.teleportable = this.teleportable; + clone.damagedbycontents = this.damagedbycontents; + clone.angles = this.angles; + clone.v_angle = this.v_angle; + clone.avelocity = this.avelocity; + clone.damageforcescale = this.damageforcescale; + clone.effects = this.effects; + clone.glowmod = this.glowmod; + clone.event_damage = this.event_damage; + clone.anim_state = this.anim_state; + clone.anim_time = this.anim_time; + clone.anim_lower_action = this.anim_lower_action; + clone.anim_lower_time = this.anim_lower_time; + clone.anim_upper_action = this.anim_upper_action; + clone.anim_upper_time = this.anim_upper_time; + clone.anim_implicit_state = this.anim_implicit_state; + clone.anim_implicit_time = this.anim_implicit_time; + clone.anim_lower_implicit_action = this.anim_lower_implicit_action; + clone.anim_lower_implicit_time = this.anim_lower_implicit_time; + clone.anim_upper_implicit_action = this.anim_upper_implicit_action; + clone.anim_upper_implicit_time = this.anim_upper_implicit_time; + clone.dphitcontentsmask = this.dphitcontentsmask; + clone.death_time = this.death_time; + clone.pain_finished = this.pain_finished; + clone.health = this.health; + clone.armorvalue = this.armorvalue; + clone.armortype = this.armortype; + clone.model = this.model; + clone.modelindex = this.modelindex; + clone.skin = this.skin; + clone.species = this.species; + clone.move_qcphysics = false; // don't run gamecode logic on clones, too many + set_movetype(clone, this.move_movetype); + clone.solid = this.solid; + clone.ballistics_density = this.ballistics_density; + clone.takedamage = this.takedamage; + setcefc(clone, getcefc(this)); + clone.uncustomizeentityforclient = this.uncustomizeentityforclient; + clone.uncustomizeentityforclient_set = this.uncustomizeentityforclient_set; + if (keepvelocity == 1) + clone.velocity = this.velocity; + clone.oldvelocity = clone.velocity; + clone.alpha = this.alpha; + clone.fade_time = this.fade_time; + clone.fade_rate = this.fade_rate; + //clone.weapon = this.weapon; + setorigin(clone, this.origin); + setsize(clone, this.mins, this.maxs); + clone.prevorigin = this.origin; + clone.reset = SUB_Remove; + clone._ps = this._ps; + + Drag_MoveDrag(this, clone); + + if(clone.colormap <= maxclients && clone.colormap > 0) + clone.colormap = 1024 + this.clientcolors; + + CSQCMODEL_AUTOINIT(clone); + clone.CopyBody_nextthink = this.nextthink; + clone.CopyBody_think = getthink(this); + clone.nextthink = time; + setthink(clone, CopyBody_Think); + // "bake" the current animation frame for clones (they don't get clientside animation) + animdecide_load_if_needed(clone); + animdecide_setframes(clone, false, frame, frame1time, frame2, frame2time); ++ ++ MUTATOR_CALLHOOK(CopyBody, this, clone, keepvelocity); +} + +void player_setupanimsformodel(entity this) +{ + // load animation info + animdecide_load_if_needed(this); + animdecide_setstate(this, 0, false); +} + +void player_anim(entity this) +{ + int deadbits = (this.anim_state & (ANIMSTATE_DEAD1 | ANIMSTATE_DEAD2)); + if(IS_DEAD(this)) { + if (!deadbits) { + // Decide on which death animation to use. + if(random() < 0.5) + deadbits = ANIMSTATE_DEAD1; + else + deadbits = ANIMSTATE_DEAD2; + } + } else { + // Clear a previous death animation. + deadbits = 0; + } + int animbits = deadbits; + if(STAT(FROZEN, this)) + animbits |= ANIMSTATE_FROZEN; + if(this.move_movetype == MOVETYPE_FOLLOW) + animbits |= ANIMSTATE_FOLLOW; + if(this.crouch) + animbits |= ANIMSTATE_DUCK; + animdecide_setstate(this, animbits, false); + animdecide_setimplicitstate(this, IS_ONGROUND(this)); +} + +void PlayerCorpseDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force) +{ + float take, save; + vector v; + Violence_GibSplash_At(hitloc, force, 2, bound(0, damage, 200) / 16, this, attacker); + + // damage resistance (ignore most of the damage from a bullet or similar) + damage = max(damage - 5, 1); + + v = healtharmor_applydamage(this.armorvalue, autocvar_g_balance_armor_blockpercent, deathtype, damage); + take = v.x; + save = v.y; + + if(sound_allowed(MSG_BROADCAST, attacker)) + { + if (save > 10) + sound (this, CH_SHOTS, SND_ARMORIMPACT, VOL_BASE, ATTEN_NORM); + else if (take > 30) + sound (this, CH_SHOTS, SND_BODYIMPACT2, VOL_BASE, ATTEN_NORM); + else if (take > 10) + sound (this, CH_SHOTS, SND_BODYIMPACT1, VOL_BASE, ATTEN_NORM); + } + + if (take > 50) + Violence_GibSplash_At(hitloc, force * -0.1, 3, 1, this, attacker); + if (take > 100) + Violence_GibSplash_At(hitloc, force * -0.2, 3, 1, this, attacker); + + this.armorvalue = this.armorvalue - save; + this.health = this.health - take; + // pause regeneration for 5 seconds + this.pauseregen_finished = max(this.pauseregen_finished, time + autocvar_g_balance_pause_health_regen); + + this.dmg_save = this.dmg_save + save;//max(save - 10, 0); + this.dmg_take = this.dmg_take + take;//max(take - 10, 0); + this.dmg_inflictor = inflictor; + + if (this.health <= -autocvar_sv_gibhealth && this.alpha >= 0) + { + // don't use any animations as a gib + this.frame = 0; + // view just above the floor + this.view_ofs = '0 0 4'; + + Violence_GibSplash(this, 1, 1, attacker); + this.alpha = -1; + this.solid = SOLID_NOT; // restore later + this.takedamage = DAMAGE_NO; // restore later + this.damagedbycontents = false; + } +} + +void calculate_player_respawn_time(entity this) +{ + if(g_ca) + return; + + float gametype_setting_tmp; + float sdelay_max = GAMETYPE_DEFAULTED_SETTING(respawn_delay_max); + float sdelay_small = GAMETYPE_DEFAULTED_SETTING(respawn_delay_small); + float sdelay_large = GAMETYPE_DEFAULTED_SETTING(respawn_delay_large); + float sdelay_small_count = GAMETYPE_DEFAULTED_SETTING(respawn_delay_small_count); + float sdelay_large_count = GAMETYPE_DEFAULTED_SETTING(respawn_delay_large_count); + float waves = GAMETYPE_DEFAULTED_SETTING(respawn_waves); + + float pcount = 1; // Include myself whether or not team is already set right and I'm a "player". + if (teamplay) + { + FOREACH_CLIENT(IS_PLAYER(it) && it != this, LAMBDA( + if(it.team == this.team) + ++pcount; + )); + if (sdelay_small_count == 0) + sdelay_small_count = 1; + if (sdelay_large_count == 0) + sdelay_large_count = 1; + } + else + { + FOREACH_CLIENT(IS_PLAYER(it) && it != this, LAMBDA( + ++pcount; + )); + if (sdelay_small_count == 0) + { + if (g_cts) + { + // Players play independently. No point in requiring enemies. + sdelay_small_count = 1; + } + else + { + // Players play AGAINST each other. Enemies required. + sdelay_small_count = 2; + } + } + if (sdelay_large_count == 0) + { + if (g_cts) + { + // Players play independently. No point in requiring enemies. + sdelay_large_count = 1; + } + else + { + // Players play AGAINST each other. Enemies required. + sdelay_large_count = 2; + } + } + } + + float sdelay; + + if (pcount <= sdelay_small_count) + sdelay = sdelay_small; + else if (pcount >= sdelay_large_count) + sdelay = sdelay_large; + else // NOTE: this case implies sdelay_large_count > sdelay_small_count. + sdelay = sdelay_small + (sdelay_large - sdelay_small) * (pcount - sdelay_small_count) / (sdelay_large_count - sdelay_small_count); + + if(waves) + this.respawn_time = ceil((time + sdelay) / waves) * waves; + else + this.respawn_time = time + sdelay; + + if(sdelay < sdelay_max) + this.respawn_time_max = time + sdelay_max; + else + this.respawn_time_max = this.respawn_time; + + if((sdelay + waves >= 5.0) && (this.respawn_time - time > 1.75)) + this.respawn_countdown = 10; // first number to count down from is 10 + else + this.respawn_countdown = -1; // do not count down + + if(autocvar_g_forced_respawn) + this.respawn_flags = this.respawn_flags | RESPAWN_FORCE; +} + +void PlayerDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force) +{ + float take, save, dh, da; + vector v; + float valid_damage_for_weaponstats; + float excess; + + dh = max(this.health, 0); + da = max(this.armorvalue, 0); + + if(!DEATH_ISSPECIAL(deathtype)) + { + damage *= sqrt(bound(1.0, this.cvar_cl_handicap, 100.0)); + if(this != attacker) + damage /= sqrt(bound(1.0, attacker.cvar_cl_handicap, 100.0)); + } + + if(DEATH_ISWEAPON(deathtype, WEP_TUBA)) + { + // tuba causes blood to come out of the ears + vector ear1, ear2; + vector d; + float f; + ear1 = this.origin; + ear1_z += 0.125 * this.view_ofs.z + 0.875 * this.maxs.z; // 7/8 + ear2 = ear1; + makevectors(this.angles); + ear1 += v_right * -10; + ear2 += v_right * +10; + d = inflictor.origin - this.origin; + if (d) + f = (d * v_right) / vlen(d); // this is cos of angle of d and v_right! + else + f = 0; // Assum ecenter. + force = v_right * vlen(force); + Violence_GibSplash_At(ear1, force * -1, 2, bound(0, damage, 25) / 2 * (0.5 - 0.5 * f), this, attacker); + Violence_GibSplash_At(ear2, force, 2, bound(0, damage, 25) / 2 * (0.5 + 0.5 * f), this, attacker); + if(f > 0) + { + hitloc = ear1; + force = force * -1; + } + else + { + hitloc = ear2; + // force is already good + } + } + else + Violence_GibSplash_At(hitloc, force, 2, bound(0, damage, 200) / 16, this, attacker); + + + v = healtharmor_applydamage(this.armorvalue, autocvar_g_balance_armor_blockpercent, deathtype, damage); + take = v.x; + save = v.y; + + if(attacker == this) + { + // don't reset pushltime for this damage as it may be an attempt to + // escape a lava pit or similar + //this.pushltime = 0; + this.istypefrag = 0; + } + else if(IS_PLAYER(attacker)) + { + this.pusher = attacker; + this.pushltime = time + autocvar_g_maxpushtime; + this.istypefrag = PHYS_INPUT_BUTTON_CHAT(this); + } + else if(time < this.pushltime) + { + attacker = this.pusher; + this.pushltime = max(this.pushltime, time + 0.6); + } + else + { + this.pushltime = 0; + this.istypefrag = 0; + } + + if (time < this.spawnshieldtime && autocvar_g_spawnshield_blockdamage < 1) + { + vector v = healtharmor_applydamage(this.armorvalue, max(0, autocvar_g_spawnshield_blockdamage), deathtype, damage); + take = v.x; + save = v.y; + } + + MUTATOR_CALLHOOK(PlayerDamage_SplitHealthArmor, inflictor, attacker, this, force, take, save, deathtype, damage); + take = bound(0, M_ARGV(4, float), this.health); + save = bound(0, M_ARGV(5, float), this.armorvalue); + excess = max(0, damage - take - save); + + if(sound_allowed(MSG_BROADCAST, attacker)) + { + if (save > 10) + sound (this, CH_SHOTS, SND_ARMORIMPACT, VOL_BASE, ATTEN_NORM); + else if (take > 30) + sound (this, CH_SHOTS, SND_BODYIMPACT2, VOL_BASE, ATTEN_NORM); + else if (take > 10) + sound (this, CH_SHOTS, SND_BODYIMPACT1, VOL_BASE, ATTEN_NORM); // FIXME possibly remove them? + } + + if (take > 50) + Violence_GibSplash_At(hitloc, force * -0.1, 3, 1, this, attacker); + if (take > 100) + Violence_GibSplash_At(hitloc, force * -0.2, 3, 1, this, attacker); + + if (time >= this.spawnshieldtime || autocvar_g_spawnshield_blockdamage < 1) + { + if (!(this.flags & FL_GODMODE)) + { + this.armorvalue = this.armorvalue - save; + this.health = this.health - take; + // pause regeneration for 5 seconds + if(take) + this.pauseregen_finished = max(this.pauseregen_finished, time + autocvar_g_balance_pause_health_regen); + + if (time > this.pain_finished) //Don't switch pain sequences like crazy + { + this.pain_finished = time + 0.5; //Supajoe + + if(autocvar_sv_gentle < 1) { + if(this.classname != "body") // pain anim is BORKED on our ZYMs, FIXME remove this once we have good models + { + if (!this.animstate_override) + { + if (random() > 0.5) + animdecide_setaction(this, ANIMACTION_PAIN1, true); + else + animdecide_setaction(this, ANIMACTION_PAIN2, true); + } + } + + if(sound_allowed(MSG_BROADCAST, attacker)) + if((this.health < 2 * WEP_CVAR_PRI(blaster, damage) * autocvar_g_balance_selfdamagepercent + 1) || !(DEATH_WEAPONOF(deathtype).spawnflags & WEP_FLAG_CANCLIMB) || attacker != this) // WEAPONTODO: create separate limit for pain notification with laser + if(this.health > 1) + // exclude pain sounds for laserjumps as long as you aren't REALLY low on health and would die of the next two + { + if(deathtype == DEATH_FALL.m_id) + PlayerSound(this, playersound_fall, CH_PAIN, VOL_BASE, VOICETYPE_PLAYERSOUND); + else if(this.health > 75) + PlayerSound(this, playersound_pain100, CH_PAIN, VOL_BASE, VOICETYPE_PLAYERSOUND); + else if(this.health > 50) + PlayerSound(this, playersound_pain75, CH_PAIN, VOL_BASE, VOICETYPE_PLAYERSOUND); + else if(this.health > 25) + PlayerSound(this, playersound_pain50, CH_PAIN, VOL_BASE, VOICETYPE_PLAYERSOUND); + else + PlayerSound(this, playersound_pain25, CH_PAIN, VOL_BASE, VOICETYPE_PLAYERSOUND); + } + } + } + + // throw off bot aim temporarily + float shake; + if(IS_BOT_CLIENT(this) && this.health >= 1) + { + shake = damage * 5 / (bound(0,skill,100) + 1); + this.v_angle_x = this.v_angle.x + (random() * 2 - 1) * shake; + this.v_angle_y = this.v_angle.y + (random() * 2 - 1) * shake; + this.v_angle_x = bound(-90, this.v_angle.x, 90); + } + } + else + this.max_armorvalue += (save + take); + } + this.dmg_save = this.dmg_save + save;//max(save - 10, 0); + this.dmg_take = this.dmg_take + take;//max(take - 10, 0); + this.dmg_inflictor = inflictor; + + if (this != attacker) { + float realdmg = damage - excess; + if (IS_PLAYER(attacker)) { + PlayerScore_Add(attacker, SP_DMG, realdmg); + } + if (IS_PLAYER(this)) { + PlayerScore_Add(this, SP_DMGTAKEN, realdmg); + } + } + + bool abot = (IS_BOT_CLIENT(attacker)); + bool vbot = (IS_BOT_CLIENT(this)); + + valid_damage_for_weaponstats = 0; + Weapon awep = WEP_Null; + + if(vbot || IS_REAL_CLIENT(this)) + if(abot || IS_REAL_CLIENT(attacker)) + if(attacker && this != attacker) + if(DIFF_TEAM(this, attacker)) + { + if(DEATH_ISSPECIAL(deathtype)) + awep = PS(attacker).m_weapon; + else + awep = DEATH_WEAPONOF(deathtype); + valid_damage_for_weaponstats = 1; + } + + dh = dh - max(this.health, 0); + da = da - max(this.armorvalue, 0); + if(valid_damage_for_weaponstats) + { + WeaponStats_LogDamage(awep.m_id, abot, PS(this).m_weapon.m_id, vbot, dh + da); + } + if (dh + da) + { + MUTATOR_CALLHOOK(PlayerDamaged, attacker, this, dh, da, hitloc, deathtype); + } + + if (this.health < 1) + { + float defer_ClientKill_Now_TeamChange; + defer_ClientKill_Now_TeamChange = false; + + if(this.alivetime) + { + PS_GR_P_ADDVAL(this, PLAYERSTATS_ALIVETIME, time - this.alivetime); + this.alivetime = 0; + } + + if(valid_damage_for_weaponstats) + WeaponStats_LogKill(awep.m_id, abot, PS(this).m_weapon.m_id, vbot); + + if(autocvar_sv_gentle < 1) + if(sound_allowed(MSG_BROADCAST, attacker)) + { + if(deathtype == DEATH_DROWN.m_id) + PlayerSound(this, playersound_drown, CH_PAIN, VOL_BASE, VOICETYPE_PLAYERSOUND); + else + PlayerSound(this, playersound_death, CH_PAIN, VOL_BASE, VOICETYPE_PLAYERSOUND); + } + + // get rid of kill indicator + if(this.killindicator) + { + delete(this.killindicator); + this.killindicator = NULL; + if(this.killindicator_teamchange) + defer_ClientKill_Now_TeamChange = true; + + if(this.classname == "body") + if(deathtype == DEATH_KILL.m_id) + { + // for the lemmings fans, a small harmless explosion + Send_Effect(EFFECT_ROCKET_EXPLODE, this.origin, '0 0 0', 1); + } + } + + // print an obituary message + if(this.classname != "body") + Obituary (attacker, inflictor, this, deathtype); + + // increment frag counter for used weapon type + Weapon w = DEATH_WEAPONOF(deathtype); + if(w != WEP_Null) + if(accuracy_isgooddamage(attacker, this)) + attacker.accuracy.(accuracy_frags[w.m_id-1]) += 1; + + MUTATOR_CALLHOOK(PlayerDies, inflictor, attacker, this, deathtype, damage); + excess = M_ARGV(4, float); + + Weapon wep = PS(this).m_weapon; + /*for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot) + { + .entity weaponentity = weaponentities[slot]; + wep.wr_playerdeath(wep, this, weaponentity); + }*/ + .entity weaponentity = weaponentities[0]; // TODO: unhardcode + wep.wr_playerdeath(wep, this, weaponentity); + + RemoveGrapplingHook(this); + + Portal_ClearAllLater(this); + + this.fixangle = true; + + if(defer_ClientKill_Now_TeamChange) + ClientKill_Now_TeamChange(this); // can turn player into spectator + + // player could have been miraculously resuscitated ;) + // e.g. players in freezetag get frozen, they don't really die + if(this.health >= 1 || !(IS_PLAYER(this) || this.classname == "body")) + return; + + // when we get here, player actually dies + + Unfreeze(this); // remove any icy remains + this.health = 0; // Unfreeze resets health, so we need to set it back + + // clear waypoints + WaypointSprite_PlayerDead(this); + // throw a weapon + SpawnThrownWeapon(this, this.origin + (this.mins + this.maxs) * 0.5, PS(this).m_switchweapon.m_id); + + // become fully visible + this.alpha = default_player_alpha; + // make the corpse upright (not tilted) + this.angles_x = 0; + this.angles_z = 0; + // don't spin + this.avelocity = '0 0 0'; + // view from the floor + this.view_ofs = '0 0 -8'; + // toss the corpse + set_movetype(this, MOVETYPE_TOSS); + // shootable corpse + this.solid = SOLID_CORPSE; + this.ballistics_density = autocvar_g_ballistics_density_corpse; + // don't stick to the floor + UNSET_ONGROUND(this); + // dying animation + this.deadflag = DEAD_DYING; + + // when to allow respawn + calculate_player_respawn_time(this); + + this.death_time = time; + if (random() < 0.5) + animdecide_setstate(this, this.anim_state | ANIMSTATE_DEAD1, true); + else + animdecide_setstate(this, this.anim_state | ANIMSTATE_DEAD2, true); + if (this.maxs.z > 5) + { + this.maxs_z = 5; + setsize(this, this.mins, this.maxs); + } + // set damage function to corpse damage + this.event_damage = PlayerCorpseDamage; + // call the corpse damage function just in case it wants to gib + this.event_damage(this, inflictor, attacker, excess, deathtype, hitloc, force); + + // set up to fade out later + SUB_SetFade (this, time + 6 + random (), 1); + // reset body think wrapper broken by SUB_SetFade + if(this.classname == "body" && getthink(this) != CopyBody_Think) { + this.CopyBody_think = getthink(this); + this.CopyBody_nextthink = this.nextthink; + setthink(this, CopyBody_Think); + this.nextthink = time; + } + + if(autocvar_sv_gentle > 0 || autocvar_ekg || this.classname == "body") { + // remove corpse + // clones don't run any animation code any more, so we must gib them when they die :( + PlayerCorpseDamage(this, inflictor, attacker, autocvar_sv_gibhealth+1.0, deathtype, hitloc, force); + } + + // reset fields the weapons may use just in case + FOREACH(Weapons, it != WEP_Null, LAMBDA( + it.wr_resetplayer(it, this); + for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot) + { + ATTACK_FINISHED_FOR(this, it.m_id, slot) = 0; + } + )); + } +} + +void MoveToTeam(entity client, int team_colour, int type) +{ + int lockteams_backup = lockteams; // backup any team lock + lockteams = 0; // disable locked teams + TeamchangeFrags(client); // move the players frags + SetPlayerColors(client, team_colour - 1); // set the players colour + Damage(client, client, client, 100000, DEATH_AUTOTEAMCHANGE.m_id, client.origin, '0 0 0'); // kill the player + lockteams = lockteams_backup; // restore the team lock + LogTeamchange(client.playerid, client.team, type); +} + +/** print(), but only print if the server is not local */ +void dedicated_print(string input) +{ + if (server_is_dedicated) print(input); +} + +/** + * message "": do not say, just test flood control + * return value: + * 1 = accept + * 0 = reject + * -1 = fake accept + */ +int Say(entity source, int teamsay, entity privatesay, string msgin, bool floodcontrol) +{ + if (!teamsay && !privatesay) if (substring(msgin, 0, 1) == " ") + msgin = substring(msgin, 1, -1); // work around DP say bug (say_team does not have this!) + + msgin = formatmessage(source, msgin); + + string colorstr; + if (!IS_PLAYER(source)) + colorstr = "^0"; // black for spectators + else if(teamplay) + colorstr = Team_ColorCode(source.team); + else + { + colorstr = ""; + teamsay = false; + } + + if(intermission_running) + teamsay = false; + + if (!source) { + colorstr = ""; + teamsay = false; + } + + if(msgin != "") + msgin = trigger_magicear_processmessage_forallears(source, teamsay, privatesay, msgin); + + /* + * using bprint solves this... me stupid + // how can we prevent the message from appearing in a listen server? + // for now, just give "say" back and only handle say_team + if(!teamsay) + { + clientcommand(source, strcat("say ", msgin)); + return; + } + */ + + string namestr = ""; + if (source) + namestr = autocvar_g_chat_teamcolors ? playername(source) : source.netname; + + string colorprefix = (strdecolorize(namestr) == namestr) ? "^3" : "^7"; + + string msgstr, cmsgstr; + string privatemsgprefix = string_null; + int privatemsgprefixlen = 0; + if (msgin == "") { + msgstr = cmsgstr = ""; + } else { + if(privatesay) + { + msgstr = strcat("\{1}\{13}* ", colorprefix, namestr, "^3 tells you: ^7"); + privatemsgprefixlen = strlen(msgstr); + msgstr = strcat(msgstr, msgin); + cmsgstr = strcat(colorstr, colorprefix, namestr, "^3 tells you:\n^7", msgin); + if(autocvar_g_chat_teamcolors) + privatemsgprefix = strcat("\{1}\{13}* ^3You tell ", playername(privatesay), ": ^7"); + else + privatemsgprefix = strcat("\{1}\{13}* ^3You tell ", privatesay.netname, ": ^7"); + } + else if(teamsay) + { + if(strstrofs(msgin, "/me", 0) >= 0) + { + //msgin = strreplace("/me", "", msgin); + //msgin = substring(msgin, 3, strlen(msgin)); + msgin = strreplace("/me", strcat(colorstr, "(", colorprefix, namestr, colorstr, ")^7"), msgin); + msgstr = strcat("\{1}\{13}^4* ", "^7", msgin); + } + else + msgstr = strcat("\{1}\{13}", colorstr, "(", colorprefix, namestr, colorstr, ") ^7", msgin); + cmsgstr = strcat(colorstr, "(", colorprefix, namestr, colorstr, ")\n^7", msgin); + } + else + { + if(strstrofs(msgin, "/me", 0) >= 0) + { + //msgin = strreplace("/me", "", msgin); + //msgin = substring(msgin, 3, strlen(msgin)); + msgin = strreplace("/me", strcat(colorprefix, namestr), msgin); + msgstr = strcat("\{1}^4* ", "^7", msgin); + } + else { + msgstr = "\{1}"; + msgstr = strcat(msgstr, (namestr != "") ? strcat(colorprefix, namestr, "^7: ") : "^7"); + msgstr = strcat(msgstr, msgin); + } + cmsgstr = ""; + } + msgstr = strcat(strreplace("\n", " ", msgstr), "\n"); // newlines only are good for centerprint + } + + string fullmsgstr = msgstr; + string fullcmsgstr = cmsgstr; + + // FLOOD CONTROL + int flood = 0; + var .float flood_field = floodcontrol_chat; + if(floodcontrol) + { + float flood_spl; + float flood_burst; + float flood_lmax; + float lines; + if(privatesay) + { + flood_spl = autocvar_g_chat_flood_spl_tell; + flood_burst = autocvar_g_chat_flood_burst_tell; + flood_lmax = autocvar_g_chat_flood_lmax_tell; + flood_field = floodcontrol_chattell; + } + else if(teamsay) + { + flood_spl = autocvar_g_chat_flood_spl_team; + flood_burst = autocvar_g_chat_flood_burst_team; + flood_lmax = autocvar_g_chat_flood_lmax_team; + flood_field = floodcontrol_chatteam; + } + else + { + flood_spl = autocvar_g_chat_flood_spl; + flood_burst = autocvar_g_chat_flood_burst; + flood_lmax = autocvar_g_chat_flood_lmax; + flood_field = floodcontrol_chat; + } + flood_burst = max(0, flood_burst - 1); + // to match explanation in default.cfg, a value of 3 must allow three-line bursts and not four! + + // do flood control for the default line size + if(msgstr != "") + { + getWrappedLine_remaining = msgstr; + msgstr = ""; + lines = 0; + while(getWrappedLine_remaining && (!flood_lmax || lines <= flood_lmax)) + { + msgstr = strcat(msgstr, " ", getWrappedLineLen(82.4289758859709, strlennocol)); // perl averagewidth.pl < gfx/vera-sans.width + ++lines; + } + msgstr = substring(msgstr, 1, strlen(msgstr) - 1); + + if(getWrappedLine_remaining != "") + { + msgstr = strcat(msgstr, "\n"); + flood = 2; + } + + if (time >= source.(flood_field)) + { + source.(flood_field) = max(time - flood_burst * flood_spl, source.(flood_field)) + lines * flood_spl; + } + else + { + flood = 1; + msgstr = fullmsgstr; + } + } + else + { + if (time >= source.(flood_field)) + source.(flood_field) = max(time - flood_burst * flood_spl, source.(flood_field)) + flood_spl; + else + flood = 1; + } + + if (timeout_status == TIMEOUT_ACTIVE) // when game is paused, no flood protection + source.(flood_field) = flood = 0; + } + + string sourcemsgstr, sourcecmsgstr; + if(flood == 2) // cannot happen for empty msgstr + { + if(autocvar_g_chat_flood_notify_flooder) + { + sourcemsgstr = strcat(msgstr, "\n^3FLOOD CONTROL: ^7message too long, trimmed\n"); + sourcecmsgstr = ""; + } + else + { + sourcemsgstr = fullmsgstr; + sourcecmsgstr = fullcmsgstr; + } + cmsgstr = ""; + } + else + { + sourcemsgstr = msgstr; + sourcecmsgstr = cmsgstr; + } + + if (!privatesay && source && !IS_PLAYER(source)) + { + if (!intermission_running) + if(teamsay || (autocvar_g_chat_nospectators == 1) || (autocvar_g_chat_nospectators == 2 && !(warmup_stage || gameover))) + teamsay = -1; // spectators + } + + if(flood) + LOG_INFO("NOTE: ", playername(source), "^7 is flooding.\n"); + + // build sourcemsgstr by cutting off a prefix and replacing it by the other one + if(privatesay) + sourcemsgstr = strcat(privatemsgprefix, substring(sourcemsgstr, privatemsgprefixlen, -1)); + + int ret; + if(source.muted) + { + // always fake the message + ret = -1; + } + else if(flood == 1) + { + if (autocvar_g_chat_flood_notify_flooder) + { + sprint(source, strcat("^3FLOOD CONTROL: ^7wait ^1", ftos(source.(flood_field) - time), "^3 seconds\n")); + ret = 0; + } + else + ret = -1; + } + else + { + ret = 1; + } + + if(sourcemsgstr != "" && ret != 0) + { + if(ret < 0) // faked message, because the player is muted + { + sprint(source, sourcemsgstr); + if(sourcecmsgstr != "" && !privatesay) + centerprint(source, sourcecmsgstr); + } + else if(privatesay) // private message, between 2 people only + { + sprint(source, sourcemsgstr); + sprint(privatesay, msgstr); + if (!autocvar_g_chat_tellprivacy) { dedicated_print(msgstr); } // send to server console too if "tellprivacy" is disabled + if(cmsgstr != "") + centerprint(privatesay, cmsgstr); + } + else if ( teamsay && source.active_minigame ) + { + sprint(source, sourcemsgstr); + dedicated_print(msgstr); // send to server console too + FOREACH_CLIENT(IS_REAL_CLIENT(it) && it != source && it.active_minigame == source.active_minigame, sprint(it, msgstr)); + } + else if(teamsay > 0) // team message, only sent to team mates + { + sprint(source, sourcemsgstr); + dedicated_print(msgstr); // send to server console too + if(sourcecmsgstr != "") + centerprint(source, sourcecmsgstr); + FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it) && it != source && it.team == source.team, { + sprint(it, msgstr); + if(cmsgstr != "") + centerprint(it, cmsgstr); + }); + } + else if(teamsay < 0) // spectator message, only sent to spectators + { + sprint(source, sourcemsgstr); + dedicated_print(msgstr); // send to server console too + FOREACH_CLIENT(!IS_PLAYER(it) && IS_REAL_CLIENT(it) && it != source, sprint(it, msgstr)); + } + else + { + if (source) { + sprint(source, sourcemsgstr); + dedicated_print(msgstr); // send to server console too + MX_Say(strcat(playername(source), "^7: ", msgin)); + } + FOREACH_CLIENT(IS_REAL_CLIENT(it) && it != source, sprint(it, msgstr)); + } + } + + return ret; +} diff --cc qcsrc/server/player.qh index b5f8ca07c,000000000..3e2b86016 mode 100644,000000..100644 --- a/qcsrc/server/player.qh +++ b/qcsrc/server/player.qh @@@ -1,39 -1,0 +1,41 @@@ +#pragma once + +.entity pusher; +.float pushltime; +.float istypefrag; + +.float CopyBody_nextthink; +.void(entity this) CopyBody_think; +void CopyBody_Think(entity this); +void CopyBody(entity this, float keepvelocity); + ++void dedicated_print(string input); ++ +void player_setupanimsformodel(entity this); + +void player_anim(entity this); + +void PlayerCorpseDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force); + +// g__str: +// If 0, default is used. +// If <0, 0 is used. +// Otherwise, g_str (default value) is used. +// For consistency, negative values there are mapped to zero too. +#define GAMETYPE_DEFAULTED_SETTING(str) \ + ((gametype_setting_tmp = cvar(strcat("g_", GetGametype(), "_" #str))), \ + (gametype_setting_tmp < 0) ? 0 \ + : (gametype_setting_tmp == 0 || autocvar_g_respawn_delay_forced) ? max(0, autocvar_g_##str) \ + : gametype_setting_tmp) + +void calculate_player_respawn_time(entity this); + +void ClientKill_Now_TeamChange(entity this); + +void MoveToTeam(entity client, float team_colour, float type); + +void PlayerDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force); + +/** to be used by `prvm_edictset server playernumber muted 1` */ +.float muted; +int Say(entity source, float teamsay, entity privatesay, string msgin, float floodcontrol);