How do I change spawn locations when a team wins a certain amount of wins?

Grassen

L1: Registered
Feb 15, 2024
29
0
So recently i've started to play around with math_counter entity to add to a map. What came to mind was, let's say red team wins 3 rounds the map changes spawnpoints and if blue wins a round it removes a "counter" to the score, making it so red does not progress to the next area of the map. How would I go about recreating this?
(I've attached screenshots to see what I have so far)
 

Attachments

  • math2.png
    math2.png
    335.2 KB · Views: 12
  • math3.png
    math3.png
    329.2 KB · Views: 13
  • math4.png
    math4.png
    323 KB · Views: 10
  • math5.png
    math5.png
    353.7 KB · Views: 11
  • math6.png
    math6.png
    334 KB · Views: 10
  • math1.png
    math1.png
    328.9 KB · Views: 12
Solution
You're welcome ^^

The triggers and filters look good but... I can barely see the "has input" icon on the filter_red; maybe you are killing it? A remnant of old logic? It would explain why OnEndTouchAll triggers when a blue player dies.

Some notes:
  • The trigger should start disabled and be re-enabled only when the team_round_timer triggers its OnSetupFinished. This way the counter doesn't decrease if everyone dies during setup.
  • There's no need to RoundWin since the gamemode already does that when the Survivors are wiped out. You can simply just decrease the town mood.

Box Of Paper

L3: Member
Jul 15, 2019
135
159
If you really want to use a math_counter you could turn it into a preserved entity by changing its classname (https://developer.valvesoftware.com/wiki/S_PreserveEnts) and then check the value at the start of every round to do what you need to do...

On a new round the original math_counter will be respawned while the preserved one still lingers; leading to two copies with the same targetname. You can workaround this with a logic_auto but there are problems there too and this time in the hammer editor itself.

For a working prefab you can check out the "Preserved Entities" section of the Box Of Features I made a while ago at https://tf2maps.net/downloads/prefabs-box-of-features.10818/.

But with VScript this is no longer needed.

I'll paste here the script and attach a demo .vmf to play around with.
C:
// Setting the "town_mood" value
::town_mood <- "town_mood" in getroottable() ? ::town_mood : 0

// Defining this entity's methods
function Increase() {
    if (::town_mood < 3) ::town_mood++
    ClientPrint(null, 3, "\x07FF3F3FRed\x07EEFFAA wins! Increasing the score to \x07FFFFFF"+::town_mood)
}

function Decrease() {
    if (::town_mood > 0) ::town_mood--
    ClientPrint(null, 3, "\x0799CCFFBlue\x07EEFFAA wins! Decreasing the score to \x07FFFFFF"+::town_mood)
}

// Checking this round's "town_mood"
switch (::town_mood) {

    case 3:
        for (local spawn; spawn = Entities.FindByName(spawn, "spawn_town_*");) spawn.AcceptInput("Disable", "", null, null)
        for (local spawn; spawn = Entities.FindByName(spawn, "spawn_otherside_*");) spawn.AcceptInput("Enable", "", null, null)
        self.AcceptInput("FireUser1", "", null, null)
        break

    default:
        for (local spawn; spawn = Entities.FindByName(spawn, "spawn_town_*");) spawn.AcceptInput("Enable", "", null, null)
        for (local spawn; spawn = Entities.FindByName(spawn, "spawn_otherside_*");) spawn.AcceptInput("Disable", "", null, null)
        self.AcceptInput("FireUser2", "", null, null)

}

// Debug print
ClientPrint(null, 3, "----- ---- --- -- -")
ClientPrint(null, 3, "\x07EEFFAACurrent town mood: \x07FFFFFF"+::town_mood)
ClientPrint(null, 3, "\x07EEFFAAWin as \x07FF3F3FRed\x07EEFFAA to increase the value or win as \x0799CCFFBlue\x07EEFFAA to decrease it!")
ClientPrint(null, 3, "----- ---- --- -- -")

Basically: we are using the VScript "Root table" (::town_mood is stored in it) which is preserved between rounds.

To use the VScript, go to C:\Program Files (x86)\Steam\steamapps\common\Team Fortress 2\tf\scripts\vscripts (or wherever you setup your TF2, create the folders if needed) and create a town_mood.nut file; then open it with any text editor and paste the above script.

Then in hammer you have to place a logic_script and set its Entity Scripts property to town_mood.nut; then set its name to something like town_mood.

That's it! You may now OnRoundWin town_mood RunScriptCode Increase() to update the town's mood!
I wasn't sure of what you were planning so I just went with a simple and generic solution.
Update the VScript as you wish, and if you need help, ask away!

You may hook your outputs to the FireUser1 and FireUser2 to just work within hammer or you may go crazy and just do everything with VScript.

Just make sure you remember to pack the script into your .bsp when you want to share the map! It's a tad annoying but I think it's worth it.
I personally use VIDE (https://www.tophattwaffle.com/packing-custom-content-using-vide-in-steampipe/) but I heard good things about CompilePal.

PS... why didn't I just hook the spawn enabling and disabling to the FireUsers? Simply because it doesn't work otherwise! VScript has this really nice quirk that it runs BEFORE player spawning but AFTER every entity has loaded, thus we can immediately disable the spawns we don't want.

town_mood_thumbnail.png
 

Attachments

  • town_mood_vscript.zip
    1.3 MB · Views: 10
Last edited:

Grassen

L1: Registered
Feb 15, 2024
29
0
This is really beautiful! I never thought that vscript can be used in such way. I'll be sure to ask further questions once i've played around with it. As for my plan for this map, its essentially a zombie infection map based off the town of Silent Hill. Everytime red wins the town becomes "enraged" when red wins 3 consecutive times, the area changes to favor zombies a bit more. Likewise, if zombies win before red reaches those 3 wins, it removes one point from the 3 leaving the 2. Also when red reaches 3 points it resets back to 0

Edit: i have zero idea with vscript as you may know just started with this. :')
 
Last edited:

Grassen

L1: Registered
Feb 15, 2024
29
0
This is really beautiful! I never thought that vscript can be used in such way. I'll be sure to ask further questions once i've played around with it. As for my plan for this map, its essentially a zombie infection map based off the town of Silent Hill. Everytime red wins the town becomes "enraged" when red wins 3 consecutive times, the area changes to favor zombies a bit more. Likewise, if zombies win before red reaches those 3 wins, it removes one point from the 3 leaving the 2. Also when red reaches 3 points it resets back to 0

Edit: i have zero idea with vscript as you may know just started with this. :')


So this is beyond me but how would i make it so when it switches to the next area a turns on or off a fog controller entity and reset back to 0 when red reaches 3 on your vscript .nut file
 

Box Of Paper

L3: Member
Jul 15, 2019
135
159
To turn ON a fog_controller named town_fog you can have it "accept an input" with Entities.FindByName(null, "town_fog").AcceptInput("TurnOn", "", null, null).

Or you could hook onto the FireUser1 we setup in the script: add the OnUser1 town_fog TurnOn to the entity named town_mood (that has the town_mood.nut VScript) and it should work just as well.

To turn OFF the fog you can do the same but use TurnOff instead of TurnOn.

To reset the counter you can simply ::town_mood = 0 when the round starts with a value of 3.

So something like
C:
switch (::town_mood) {

    case 3:
        // ...
        Entities.FindByName(null, "town_fog").AcceptInput("TurnOn", "", null, null)
        ::town_mood = 0
        break

    default:
        // ...
        Entities.FindByName(null, "town_fog").AcceptInput("TurnOff", "", null, null)

}
should do the trick.

When the round starts with a ::town_mood at 3: VScript runs case 3 which will TurnOn the fog and reset the ::town_mood to 0.
Thus ::town_mood should be at most 1 when the next round starts and VScript will TurnOff the fog.

If you want to know more about VScript functions you can check out the wiki page https://developer.valvesoftware.com/wiki/VScript; or if you want to learn the "Squirrel" language you may use the documentation http://www.squirrel-lang.org/squirreldoc/reference/language.html.
 

Grassen

L1: Registered
Feb 15, 2024
29
0
Brilliant, i'll test this right away. I will also take a look at the vscript document to see how I can make these edits autonomously. Can't thank you enough for this
 

Grassen

L1: Registered
Feb 15, 2024
29
0
To turn ON a fog_controller named town_fog you can have it "accept an input" with Entities.FindByName(null, "town_fog").AcceptInput("TurnOn", "", null, null).

Or you could hook onto the FireUser1 we setup in the script: add the OnUser1 town_fog TurnOn to the entity named town_mood (that has the town_mood.nut VScript) and it should work just as well.

To turn OFF the fog you can do the same but use TurnOff instead of TurnOn.

To reset the counter you can simply ::town_mood = 0 when the round starts with a value of 3.

So something like
C:
switch (::town_mood) {

    case 3:
        // ...
        Entities.FindByName(null, "town_fog").AcceptInput("TurnOn", "", null, null)
        ::town_mood = 0
        break

    default:
        // ...
        Entities.FindByName(null, "town_fog").AcceptInput("TurnOff", "", null, null)

}
should do the trick.

When the round starts with a ::town_mood at 3: VScript runs case 3 which will TurnOn the fog and reset the ::town_mood to 0.
Thus ::town_mood should be at most 1 when the next round starts and VScript will TurnOff the fog.

If you want to know more about VScript functions you can check out the wiki page https://developer.valvesoftware.com/wiki/VScript; or if you want to learn the "Squirrel" language you may use the documentation http://www.squirrel-lang.org/squirreldoc/reference/language.html.


So something that've noticed when importing the settings/nut file onto my map:
  • When blue wins by killing all the red team, the "Blue wins! Decreasing score to /" debug text does not show, nor appear to affect the score despite red owning more than 0 points. Also it doesn't seem to trigger the game_round_win entity for blue. (My workaround is putting a trigger_multiple in the whole playable area when OnEndTouchAll for red.)
  • Using your info_player_teamspawn entities from the vmf file for blue and red placed in their respective area. Once red reaches 3, the red still spawn on the town section instead of otherside
  • When Red reaches its 3 points the zombies spawn on the otherside just fine but red remains on the town spawn

Would you be open to message me on discord to discuss more details and troubleshoot?
 

Box Of Paper

L3: Member
Jul 15, 2019
135
159
That's because the "town_mood" can only change its value with the Increase() and Descrease() methods.

In the .vmf I shared these are called by touching the two triggers and there's nothing that checks for a "team wipe".

To check for a team wipe, you can indeed use the good old trick of putting an OnEndTouchAll around the whole map.
But again, this is no longer needed thanks to VScript.

Here's how you can do it with "Events":

C:
// Check for a team wipe of the Survivors
if (!("town_mood_events" in getroottable())) {
    // We must register the event only once and not every round
    ::town_mood_events <- {}

    // Handle a player's death to check if every Survivor is dead
    ::town_mood_events.OnGameEvent_player_death <- function(info) {

        // If the dead player wasn't a Survivor then we don't need to check anything
        local victim = EntIndexToHScript(info.victim_entindex)
        if (victim.GetTeam() != 2) return

        // Do nothing if another Survivor is alive
        for (local player; player = Entities.FindByClassname(player, "player");) {
            // The event is fired before the victim is actually recognized as dead, so skip if the player is the victim
            if (player != victim && player.GetTeam() == 2 && player.IsAlive()) return
        }

        // If every Survivor is dead we'll end the round
        Entities.FindByName(null, "game_round_win_blue").AcceptInput("RoundWin", "", null, null)
    }

    __CollectGameEventCallbacks(::town_mood_events)
}
For a list of every event check out https://wiki.alliedmods.net/Team_Fortress_2_Events, but do note that only "server" events can be listened to.
To check out the events in game as they fire you can open the console and set net_showevents 2)

You're gonna have to deal with some edge cases tho, like: what happens when there's only one player and they change team?
To keep it simple I didn't deal with these cases, so to apologize I'll give you a few more tools that could help you make a zombie map:

Want to disable team changes AND give the zombies their zombie voice lines?
C:
    // Disable a player's ability to change team
    ::town_mood_events.OnGameEvent_player_spawn <- function(info) {
        local player = GetPlayerFromUserID(info.userid)
        player.SetNextChangeTeamTime(player.GetTeam() == 0 ? 0 : 1e39)
        if (player.GetTeam() == 3) {
            // Needs a little delay or the attribute won't be applied
            EntFireByHandle(player, "RunScriptCode", "self.AddCustomAttribute(`zombiezombiezombiezombie`, 1, -1)", 0, null, null)
        }
    }
Add this before the __CollectGameEventCallbacks(::town_mood_events).
For a list of every attribute check out https://wiki.teamfortress.com/wiki/List_of_item_attributes, do note that attributes are either for the players or for the weapons, and that some weapon attributes can be given to the player, it is mostly hard-coded so you'll have to go with trial-and-error and some hope/luck.
For more info check out https://developer.valvesoftware.com/wiki/Team_Fortress_2/Scripting/Script_Functions#CEconEntity.
To test the zombie voice lines you can use the "Battle Cry" (C+2) voice line on classes like the Scout or Solider.

Want to force players to select a certain team?
C:
// Set convars
Convars.SetValue("mp_teams_unbalance_limit", 0)
Convars.SetValue("mp_humans_must_join_team", "red")
To check which convars can be updated read the Team Fortress 2/tf/cfg/vscript_convar_allowlist.txt in your files; for a description of what they do you can use the wiki https://developer.valvesoftware.com/wiki/List_of_Team_Fortress_2_console_commands_and_variables; for more info read https://developer.valvesoftware.com/wiki/Team_Fortress_2/Scripting/Script_Functions#Convars.

And there's so much more that can be done! I didn't even touch the NetProps subject. But I'm pretty sure I went overboard and should just stop at these.

As for the teamspawns not working: I'm not sure about what the problem could be since it works for me.
Do the spawnpoints' names match the ones in the script? They should be spawn_town_red, spawn_town_blue, spawn_otherside_red and spawn_otherside_blue.

And sorry, but I'm not a Discord-kinda-guy so I'll pass.

Hope this helps!
 

Grassen

L1: Registered
Feb 15, 2024
29
0
No problem for the discord part, Now for the teamspawns:
I linked the images of all spawn which follow your exact lettering for the spawnpoints. The spawns were ripped directly from your map. (same for the game_round_win and logic_script entity.

I've also taken it upon myself to make a compilation of the bugs in a video format you can view since its far better than explaining on images and text sometimes. (Do note that the zombie game is the Zombie Infection gamemode released in the 2023 scream fortress update and not the other zombie fortress gamemode.) Another thing to note is in the video, If present a bug that includes glitches from the zombie infection vscript gamemode, It is to just show what got affected by implementing your code
View: https://youtu.be/Vk9KXMRrSWw
 

Attachments

  • spawner2.png
    spawner2.png
    440.9 KB · Views: 18
  • spawner4.png
    spawner4.png
    598.1 KB · Views: 14
  • spawner3.png
    spawner3.png
    587.6 KB · Views: 10
  • spawner1.png
    spawner1.png
    539.7 KB · Views: 11

Box Of Paper

L3: Member
Jul 15, 2019
135
159
Oh my, didn't expect a video (cool, also cool map).
Also didn't expect that you were already using another VScript, that complicates things.

I've snooped around the gamemode's scripts (https://tf2maps.net/downloads/zombie-infection-vscript-gamemode.17383/) a bit and I'd say that we got a pretty common VScript conflict thingy: ClearGameEventCallbacks().

And yup, my OnGameEvent_player_death which is only run once gets cleared by the gamemode.
At 00:20 there was no debug print too; it was simply a victory because of the gamemode.

For the glitch at 01:20 there might be a chance that you have 2 team_round_timer and one of them has a setup time of 60 seconds.
I've tested this in the zi_example bundled with the VScript by copying the team_round_timer and setting the setup time to 60 seconds; and it did trigger the teamplay_setup_finished event twice which is what turns the random players into zombies.
The A pose at 02:00 is probably also caused by this since it also happend to me sometimes.
To check for the entities in your map you can "Map" > "Entity Report..." from the header.

As for the team spawns not working and the script not running anymore at the beggining of a round: I don't know.
That particular spawn where everyone ends up looks suspicious but that's all I can say.

This is a pickle...
You should probably try to setup the logic first in the zi_example like I did, and only when everything works you should try it in your map.
You should be able to just leave the script as I first posted it to have the ::town_mood value and the prints; but for the "on team wipe" part you should (*) just modify the code in the VScripts of the gamemode (check out functions.nut at line 303 where the game_round_win is created, just add a ::town_mood-- or try to Entities.FindByName(null, "town_mood").AcceptInput("RunScriptCode", "Decrease()", null, null)).

(*) A little note: the download page says "Permission to modify optional", I'd like to think that adding a single line is allowed but you never know.
 
Last edited:

Grassen

L1: Registered
Feb 15, 2024
29
0
  • Ok, so the team_rounder_timer thing is resolved, I realized there was another one present ontop a misplaced logic_auto.
  • The teamspawners seem to share the same settings and flags. So i have no clue.
  • The event caller thing, to check who's dead, i can always keep it on a large trigger area to detect if read is all dead.
I'm trying to opt for a much more simpler fix for resetting the counter back to zero. I fear that my lacking in coding could royally screw my edits. I do have a fresh copy of the zi_ vscript incase I fail however, IF its possible, maybe a fix that can be performed in hammer. Simply because if i need to debug myself, I would have no clue where to modify and not many people I know have knowledge of vscript

{ local _hGameWin = SpawnEntityFromTable( "game_round_win", ::town_mood-- { win_reason = "0", force_map_reset = "1", TeamNum = "3", // TF_TEAM_BLUE switch_teams = "0" } ); // the zombies have won the round. ::bGameStarted <- false; EntFireByHandle ( _hGameWin, "RoundWin", "", 0, null, null ); }
In another note, Where would i place the ::town_mood and the Entities.FindByName(null, "town_mood").AcceptInput("RunScriptCode", "Decrease()", null, null)).


Edit: is it possible to decrease the count by certain amount, if so, when the map area switches i can make the trigger area brush around the area so when the match ends, it can Decrease() by 3 so it can go back to 0 on its own

Edit 2: Hi, so i realized something great. When red reaches the 3 point mark and get teleported to the new area, i've had a trigger area in that area where when red died it runs the Decrease() command 3 times to set it back and so far its actually working! Now the other important matter is red spawning jumbled up in a single spawnpoint. Now they are spawning in blue spawnpoints for some reason(in the blue spawn in town not the blue otherside spawns). ive checked what was wrong with the last time this happened, and it was bc that particular spawnpoint wasn't raised at least one hammer unit off the ground. I checked these spawns and they are raised properly now its odd
 

Attachments

  • teamspawner.png
    teamspawner.png
    396.9 KB · Views: 9
Last edited:

Box Of Paper

L3: Member
Jul 15, 2019
135
159
To decrease/increase by a certain amount:
C:
function Increase(amt = 1) {
    ::town_mood += amt
    if (::town_mood > 3) ::town_mood = 3
    ClientPrint(null, 3, "\x07FF3F3FSurvivors\x07EEFFAA win! The Zombies will remember this... (Town mood level: "+::town_mood+")")
}

function Decrease(amt = 1) {
    ::town_mood -= amt
    if (::town_mood < 0) ::town_mood = 0
    ClientPrint(null, 3, "\x0799CCFFZombies\x07EEFFAA win! Fresh meat! (Town mood level: "+::town_mood+")")
}

Then you just Decrease(3).



The script you wrote wouldn't work, it should be:
C:
// ...
::town_mood--;
local _hGameWin = SpawnEntityFromTable( "game_round_win",
{
// ...



Use this console command to check if your spawns are obstructed: sv_cheats 1; map_showspawnpoints;
1739821043018.png
Red means "obstructed" and green means "ok".
A spawnpoint at ground level is considered obstructed.
 

Grassen

L1: Registered
Feb 15, 2024
29
0
Ok so what do I do if the spawnpoints are obstructed, im checking right now and i see that the some spawns are red but the entire spawnpoint of red_otherside_spawn are all red. What causes this?

edit: im bout to test everything up to this point and i'll let you know how things play out!

Edit 2: So everything works as intended and the only thing missing is the team checker when all of red is gone, blue scores. This time around can we try to do the trigger region method so I don't waste too much for your time in this post?
(I've tested it so far and everytime blue zombies eliminate red, it doesn't seem to to lower the counter except when me(blue zombie) killbinds then it sets to zero. The code is not the problem since it adds and subtracts properly and even teleports red when reaching 3. Ive tried to do OnEndTouchAll with red filter so it could only count red players but it doesn't seem to only when I die. (I also checked the NPC and Client flags just to test).
 
Last edited:

Grassen

L1: Registered
Feb 15, 2024
29
0
And also thank you so much, i'm going to credit you onto the map when this gets released too

edit: referring back to the post about this is the trigger region that isn't pick up red when they are still alive. It instead give blue the win despite red being alive. Wondering if its my filter_team entities? (i've screenshotted the team_round_timer as well just incase this could be the cause)
 

Attachments

  • town_1.png
    town_1.png
    496.4 KB · Views: 14
  • town_2.png
    town_2.png
    504.8 KB · Views: 12
  • town_3.png
    town_3.png
    235.8 KB · Views: 19
  • town_4.png
    town_4.png
    376 KB · Views: 12
  • town_5.png
    town_5.png
    375 KB · Views: 10
Last edited:

Box Of Paper

L3: Member
Jul 15, 2019
135
159
You're welcome ^^

The triggers and filters look good but... I can barely see the "has input" icon on the filter_red; maybe you are killing it? A remnant of old logic? It would explain why OnEndTouchAll triggers when a blue player dies.

Some notes:
  • The trigger should start disabled and be re-enabled only when the team_round_timer triggers its OnSetupFinished. This way the counter doesn't decrease if everyone dies during setup.
  • There's no need to RoundWin since the gamemode already does that when the Survivors are wiped out. You can simply just decrease the town mood.
 
Solution

Grassen

L1: Registered
Feb 15, 2024
29
0
Thank you for everything, I've setup everything and i works as intended. Give yourself pats in the back for this! Lastly, may I have your steam profile to link into credits?