How TF2 Party was Made

BigfootBeto

Party Time 2.0!
aa
Jun 8, 2016
496
847
TF2 Party: April Fools 2020

Note: This is very lengthy for what it is, and rushed to just get the main ideas I wanted to get across. It's late and I will go back and add screenshots later too.

BONUS!!!!! 1 ONE Free Copy of the official version alpha 12 VMF file!!!! WOWOW omgm such a good deal. Only while suppiles lasts! Speical offer for 72hr jammerino!!2​

Obviously don't go about rehashing the map into other versions, but it will probably serve useful when digging through the logic, and use it as a learning tool. I kept a consistent naming convention and some entities even have notes! This logic section of this text doesn't focus on the exact details of the logic, but rather some important tips and tricks I learned when wrangling it all. Feel free to ask any questions or if there is something I should elaborate on.

History & Inspiration

3 years in the making? Really? Why yes, but what if I said that TF2 Party actually started its humble existence as “Seimann Says,” which is exactly as it sounds, a map where the gimmick is follow the instructions of Simon Says or die complete with voicelines by WillFromAfar aka “We are in the beam guy.” This project started immediately after the 2017 TF2Maps April Fools Gameday. The layout was nothing more than a simple area with hazards related to the gimmick.

Originally, the map would be arena mode such that you just play the game until everyone dies. However, since one of the commands was to change class, I had to move the gamemode away from arena to something like koth. In hindsight, I probably could have kept an arena layout, but faked the gamemode. Nevertheless, some commands such as “crouch” or “rocket jump” relied heavily on a flat layout because the method I was using to detect crouching was just a trigger_hurt that extended barely enough off the ground such that in you weren’t crouching you would die. Not the most elegant solution by any means. What this meant was, I now needed a super flat koth layout. The map was put onto the backburner for the next few months after developing the initial Seimann Says logic and commands.

Fast forward to March 2018 with the next April Fools looming, there was a newfound drive to complete the map. Inspired by the general trend set by popular April Fools maps, a successful AF map can simply be a Valve map, but with simplified geometry (see: rockpaperglitter, countless Viaduct, Badwater, and 2Fort alterations). The mindset I was following was, “even though it’s a joke map, people still want to play TF2.” In other words, whatever the gimmick is, people should still be able to shoot other players and have TF2 gameplay. I ended up picking Lakeside for the layout, since the entire layout can be simplified into 2 levels for the highground and lowground. Then, to somewhat justify the flatness, it was an easy decision to give this reskin a soho theme. I wasn’t too focused on detailing, but once everything was compiled and ready, I was playing the map alone and there definitely felt like there was something missing.

I decided the gimmick needed more commands because the few I had became very repetitive, and every time you had to switch classes everyone would be sent back to spawn which was no fun. Sticking with the theme of a recess game, I figured red light green light would make for some fun commands telling everyone to stop and go. Some background for the implementation, each command for Siemann is a logic_relay that contains everything relevant for it, and these relays are chosen by a series of logic_case with pickRandomShuffle. Then there was a logic_branch for handling an “opposite day” mode reversing the voicelines. As a result, my initial thought was to simply add in red light green light as a new mode because it would be dumb to have people stop and be unable to move until the logic_case randomly decided to let people go again out of all possible commands. One thing led to another, and the idea of separating this mode into a minigame, which would enable the possibility of multiple minigames, et voila, TF2 Party was born.

Fun Fact! TF2 Party a1 was saved on March 16th, 2018, and a2 was saved on March 24th, 2018. Alpha 2 already had the first 10 minigames, and a5 of the map was almost submitted to the 2018 April Fools Contest. However, at the last minute, literally everything broke, and minigames were being spawned incorrectly. It would have been a disaster of a map. It would be another 2 years before the map actually goes public, and 2019 would be spent completely reworking the system for switching between minigames and the “overworld.”

Some “Game Design” Goals

I had some clearly defined gameplay goals in mind for TF2 Party from the beginning. The most prominent being a mantra that slowly evolved into, “even though it’s a joke map, people still want to play TF2.” I don’t exactly remember if I heard this from someone, but regardless, it was the phrase I focused on the most when developing TF2 Party. What It means is the gimmicks can’t remove the gameplay so much from the class-based shooting that the players are playing an entirely different game. It’s an April Fools Game Day after all, where the maps must be a joke of some kind. Whether the map inherently tries to be funny, or the emergent gameplay leads to funny moments. TF2 Party was definitely leaning towards the latter. The only times I would consider TF2 Party being explicitly funny is the instruction messages for the minigames, but even those are a cheap laugh. I put all the eggs in the one basket for people having fun and enjoying trying to figure out the minigames.

Another goal was that every minigame had to encourage teamwork, and no one person could ruin the minigame for their entire team. Whether that be by competition with the enemy team, or cooperation and coordination. You can see the different styles of gameplay by comparing Red Light Green Light versus Pick-a-Door. In RLGL, people just run for it and disregard their teammates. However, for Pick-a-Door, everyone is suddenly obsessed with selfless acts, and the players sacrifice themselves willingly at the doors to be the first team to cross to the other side. Although the actions of the players are incredibly similar, the interactions with their teammates is completely different.


The Special Delivery Version

So, the OG gamemode of TF2 Party was actually special delivery. And this is where I must give a super huge thank you to everyone involved during the April Fools playtesting because the map went through a complete transformation as a result. The reasoning behind the SD gamemode went simply back to the matra of, “even though it’s a joke map, people still want to play TF2.” I figured, oh hey, people can play special delivery and then delivery the australium to get a minigame. My initial vision being where the australium is like a Mario Party Star, and then you get the minigame after someone collects it. There was a bit of extra functionality built-in, because upon capturing the australium, the capper would be given 3 options, “All Minigames, Simple Minigames, and Complex Minigames.” These were 3 unique minigame wheels with different minigames on them. The goal of the game being to be the first team to win 5 minigames.

What I discovered very quickly and surprisingly during the very first playtest, people didn’t care at all for the special delivery part of the map, and everyone just wanted to play the minigames. Nobody was playing SD, and the teams were just helping each other to cap the australium as fast as possible. It was at this time that the mantra became completely wrong, and I had to pivot entirely. Bad news, It was March 21st, and I had less than a week and a half to completely redo the “overworld” area.

Luckily, I was saved by my modular logic setup, and I could relatively easily swap out the triple minigames wheels for 1 minigames wheel and 2 “move spaces” wheels that were used for moving the carts. The crunch time wasn’t nearly as crushing as it could have been, but it was still definitely stressful having to scrap an entire SD layout to invent something entirely new.



The Entity Logic

So you want to know what it takes to make your very own TF2 Party? Well the main premise is simple. Store each minigame in their own point_template, then just have a system that can cycle between the different minigames and handle moving players around. This way everything is modular, and new minigames can be plugged in incredibly quickly. When working on the logic for the map, a major component of the solutions is saving edicts. As a result, some complex workarounds had to be made where an otherwise easy by edict wasting solution was possible. Because (supposedly) despite everything being stored in templates, the templates are only created on map load, and the limit of 2048 edicts still applies. I never actually tested this to confirm because having 5 copies of “Leaked” didn’t overflow edits, and I actually ran out of brushsides before then (“Leaked” having the most entities of any minigame).

Fun Fact! The entdata for the map is 245.7%!
Code:
entdata               [variable]      966188/393216   (245.7%) VERY FULL!



The Minigames Wheel

Ah, the pièce de résistance. The secret to the minigames wheel lies in the fact it is indeed physics based an a func_physbox. There is a motor that starts it spinning and then another physbox with a phys_hinge for the stopper/chooser thing. At the tip of this stopper is a tiny func_brush, and then each “slot” of the minigames wheel actually has a trigger filtered for this func_brush. If it’s touching, then that number gets set to the current value of the wheel in a math_counter (Each slot has an associated number ID). The reason why the wheel is behind a gate is because it is spawned and killed as needed, and the texture of the wheel is swapped using func_brushes parented to the wheel. There is only one physics wheel template, and the only thing that changes between the minigames wheel and the move spaces wheel is the func_brush texture “overlay.”

There is then a separate timer and counter that tracks how long the stopper has gone without changing trigger values, and if the timer runs out, that means the wheel stopped (most likely). There are some edge cases that need to be handled, the first being if the stopper gets stuck mid-air and doesn’t swing down. In which case, the timer just runs out and whatever value is stored in the math_counter gets used. The second case is for avoiding repeat minigames, and the setup is a lot more complicated. The system can be summarized by a sort of “cascade” effect of 5 math_counters. If the current minigame chosen is equal to the 5th math_counter, then check the 4th math_counter, and so forth. If all 5 were equal, then the wheel now knows there has been a repeat minigame, and it will enable/disable the phys_motor quickly to nudge the wheel slightly. This is all fine and dandy except when the stopper also gets stuck mid-air. This results in an infinite loop and a never-ending minigames wheel because it will just keep nudging the wheel since the stopper never changes the value of the current minigame (This led to soft-locked servers with 30 hr+ demo files that never switched map). The fix was simple though since I just added a math_counter to count a max number of nudges and then take whatever the current minigame value was if the max is reached.

Finally, after getting the math_counter to save the value of the current wheel position from 1 to 16, this number gets fed into a logic_case that will ForceSpawn the appropriate minigame. The rest is handled by the minigame’s point_template’s OnEntitySpawned outputs. The order of events is very important here or else multiple copies of the point_template would be spawned (fun fact: this was exactly an issue in very early alpha versions).

The Payload Carts

I figured I might explain that there is really nothing fancy happening to the payload carts. The setup is exactly the same as the ABS payload race prefab, but I just stripped away all the triggers related to pushing the cart. The carts moved on the track based on a math_counter that saves the number of spaces from the spaces wheel. Each path_track over a space has “OnPass !activator FireUser1” which causes the cart to subtract 1 from the math_counter. When the math_counter reaches 0, the cart gets the Stop input.

The funny glitching physics for the carts passing over spaces was entirely intentional, but was discovered by accident. I had sloppily placed down the path_tracks, and that was the side effect. However, originally the carts would just get stuck, so to fix it I modified the phys_constraint “Follow Teleport Distance” to 128. This way the func_tracktrain continues on and then prop_physics_override payload prop gets snapped to the train position.

Teleporting Players

I chose to use one trigger_teleport and then a unique info_teleport_destination for each minigame with a local destination landmark set so that players don’t all get teleported to exactly the same point and are instead moved relative to their positions within the transition trains. This is the sytem used for most minigames, but there were some minigames where I needed more precise control over exactly where people spawned. For Red Light Green Light and Jump Rope, I had to use respawn points and game_forcerespawn to get players where they needed to be since the teleporter does not separate players even when multiple destinations exist.

One issue I faced by only having one teleporter was the fact that trigger filters cannot be changed at runtime. “AddOutput target [teleport destination]” worked fine for setting the destination for each minigame, but the same did not work for setting the class filter for class-restricted minigames. The workaround I whipped up was using a targetname filter for “passed_filter” so that any player named “passed_filter” would get teleported. There was a separate trigger sweeping the train on a func_door because changing your class wouldn’t fire OnStartTouch if your were still inside the trigger. Depending on which minigame was loaded, this trigger would have an output AddOutputted for “OnStartTouch filter_[tf class] TestActivator.” If the corresponding filter was true, the !activator (the player) would be named “passed_filter.” And now there are only 2 teleporters (on for each team), one trigger_multple and one func_door that can handle teleporting players to each minigame and also filter for class if necessary. The old system involved having 32 spawnpoints for each minigame (32 * 10 minigames is 320 edicts!)

Difficulty Scaling

One of the challenges faced for the minigames was making sure they were still playable regardless of the number of players on the server because some minigames, such as Pick-a-Door are impossible without a full server of players. The solution is to scale the difficulty by counting the number of players in the map. This is accomplished by a set of triggers sweeping the train area before teleporting players to the minigame. For the minigames that use difficult scaling, they have a special logic_case that gets a number input from a trigger on the train transition area that adds 1 to a math_counter for OnStartTouch. This logic_case then has outputs for certain thresholds of number of players.

Red Light Green Light - 1

The challenge for this minigame was detecting whether individual players are moving or not. The solution involved parenting an env_entity_maker to each player, and since this was the first minigame the easiest way of doing this was to have 32 different triggers, one for each info_player_teamspawn. A better way to do this would probably be to spawn new ent makers with another ent maker and parent them to players with SpawnAtEntityOrigin. Regardless, these ent makers all spawn a point_template that contains a trigger. Thus, whenever the light turns red, these triggers are spawned at the players’ location, and if a player leaves the trigger they become named “jaywalker.” There is a separate trigger_hurt sweeping the area on a func_door filtered to the targetname “jaywalker.”

In an older version of the map, the triggers on the players simply had “OnEndTouch !activator SetHealth 0” which worked fine until people figured out bonk or uber would cheese it.

Jump Rope - 2

Nothing too fancy with the jump rope itself, it is a func_phybox that is rotated with a phys_motor that changes speed on a relay as time goes on. I had more issues with the score tracker to get the right team to win counting the number of players alive. Apparently when OnEndTouchAll is triggered it eats any OnEndTouch outputs that were there.

Plinko - 3

The entire board is a func_brush, which stickies won’t stick to. Then it’s just simply a func_button with OnDamaged for detecting exploding stickies at the bottom. To make the stickies giant, there is a logic_timer that has “OnTimer tf_projectile_pipe_remote SetModelScale 3.”

Sniper Season - 4

Very simple, it’s just a couple of trigger_catapult to launch snipers into the air.

Pootis Spencer - 5

This one went under a large revision from the initial playtests to the final version seen on the gameday. In essence, the platforms are func_tanktrain looping around on path_track. Func_tanktrain lets players build buildings that move with the platforms. The dispensers get sent through to a trigger_hurt that just adds the score to a math_counter. Whichever team had the highest counter wins, and their heavy model gets “SetModelScale 5 3” so that it scales 5 times the size over the course of 3 seconds.

This minigame actually has difficulty scaling by making the platforms move faster when there are less players, since it would otherwise be easy to camp one or two dispensers at a time instead of 5 at a time.


Leaked - 6

Probably the most visually advanced minigame. The “leak” effect is accomplished simply by encasing the room in nodraw since viewing nodraw gives the same effect as a leak. In order to assign the “paint” and “block” roles, players walk through the gates and they get their targetname changed. Then, every single block is a set of 2 buttons and 2 brushes were the buttons are filtered for the targetnames. The block button is 1 hu in front of the paint button, so it is always the first one pressed, and when it is pressed the button is killed and the dev-block func_brush is enabled. There are 15 “leaks” to fill, so 30 actions in total, and each brush/button combo needed to be uniquely named. I used paste special to create them all initially. Every time an action gets completed, it adds 1 to a counter. When the “paint” counter reaches the max value of 15, then the compile relay is triggered and the minigame ends. To accomplish the fullbright effect, I am actually spawning a separate copy of the room where the func_brush had its minimum light level keyvalue set to 0. This room gets killed and then the lit version of the func_brush is enabled. This unlit version of the room is actually below the main room so that none of the lights there affect it since minimum light level doesn’t override any existing lights.

Leaked has difficulty scaling because it would be unreasonable for a team of 4 to hit every single block location in time. The scaling will automatically press some buttons complete and even close doors that block off sections of the area so people spend less time walking in empty areas.

I Spy - 7

Another minigame that was very simple on paper. The spikewalls are just a series of parented func_door so that they emerge out of the ground then start closing in. Then, in order to give everyone invisibility the whole time, there is a trigger_add_tf_player_condition with “Cloak / Invisibility” or condition 64.

People complained about the spikewall being an instant kill, so it is instead 2 trigger_hurts. One that does medium damage when you first start touching the wall, and then an instant-kill trigger_hurt that is further within the spikes. This is so that brushing against the spikes isn’t punished because it can be hard to tell where the trigger bounds actually start.

Pick-a-Door - 8

Another simple minigame to implement. A logic_case for each set of doors is used to randomly pick which one is safe. Something more creative was used when adding difficulty scaling where the finish button is moved forward by being parented to a func_movelinear and the logic_case has a SetPosition input to it. That way I didn’t need multiple copies of the button for each difficulty level.

Sentrygun Battle Royale - 9

The main premise for this minigame is a func_brush glas wall that gets removed on the timer ends. All the players get teleported relative upwards and away from the sentries. The rotating platform is a func_tanktrain on a tiny 4 path_track loop. In order to count the sentryguns to determine the winner, there are 2 trigger_multiple with a filter_activator_class set to “obj_sentrygun” that are parented to a func_door to sweep the area when the 2nd stage starts since enabling a trigger over existing sentries doesn’t trigger OnStartTouch.

Interestingly the challenge for this minigame was overriding the default minigame timer to allow for a 2-stage minigame for building and then waiting for the sentryguns to duke it out. I settled for killing the current timer and using a new one specifically for this minigame, then respawning the default timer again with ForceSpawn.

Treasure Hunt - 10

Definitely my favorite minigame logic-wise and probably the most complex. To spawn in random treasure locations, there is only one treasure template per team, and then many env_entity_makers scattered through the area with a template set to “temp_null.” A logic_case chooses which ent maker by changing its EntityTemplate with AddOutput, and then enables the appropriate X on the map. The minigame start relay just triggers all crate spawning ent makers to ForceSpawn, but if all but one is null, then only one crate will spawn.

The crate itself is a conglomerate of multiple entities. The primary entity being a func_physbox because it is the only entity that can be hit by melee, and also be non-solid to players at the same time. The crate prop itself is parented to a func_door. In order to simuluate the act of digging, and allowing multiple people digging to actually dig faster, the func_door works to incrementally lift the crate upwards by setting the parent, and then clearing the parent each time the door is opened. Every time a player hits the physbox, 1 gets added to the 3 math_counters that work as “dig levels.” Then, there is a 4 second timer that resets the value of these counters. If one of these counters reaches the max value before getting reset, then the func_door gets its speed changed. For example, if the box gets hit 30 time within 4 seconds, then the door moves at speed of 30 (compared to the starting speed of 8), and it really feels like all the people are working together to dig the treasure faster. In order to detect when the box has reached its max height, since the func_door is being parented/unparented, there is a separate trigger_once above the crate prop filtered for the crate_prop itself. When the crate touches this trigger the minigame ends.

Hammer Time - 11

This one was fun to make because the effect of the hammer sliding back and forth is completely faked and has nothing to do with the actual score of the teams. There are some shaking effects too that add to the dramatic effect and the way the losers are gibbed at the end is very satisfying. This has since been changed in the newer version of TF2 Party, but the “jump to gain points” on the pad is a complete lie as it’s only a trigger_multiple looking for OnStartTouch to add points to a counter. This means that the minigame is cheesed if you just quickly strafe on and off the pad, but it’s still fair since both teams can do it. The fixed version that actually checks for jumping players is actually 2 triggers. One above the pad that renames players to “player_jumped,” and the bottom trigger is filtered for that targetname. When a player correctly jumps on the pad, their targetname is then reset to “none,” so that they have to jump again.

Skydive Buddies - 12

One of the newer minigames added shortly before the deadline, this one was by far the least engaging minigame (probably seconded by Plinko). I have slightly revised the gameplay for the newer version, but the trick for this minigame remains the same. There are 3 func_doors, one for each axis. The 2 horizontal doors are opening and closing rapidly, and there is a logic_timer that sends PickRandom to a logic_branch to vary the speed of the doors. The 3rd door is vertical and moves at a constant speed. The rings are created by env_entity_maker with a ring template parented to the ends of these func_doors. As the vertical door moves down, the horizontal doors then randomly create locations for the rings to go. People are forced to melee and given the parachute condition. In order to seamlessly force people to melee without a button like on Treasure Hunt, I used a combination of player conditions triggers (Minify, Disciplinary Action, Melee Weapons Only).

Waterfallers - 13

Oh boy. This one has caused the most jank and headaches out of the all the minigames purely for the fact that the minigame relies of moving platforms vertically very fast. Source is not great at that and players can stuck very easily. I will skip all the failed attempts at fixing this and focus on the one that worked (mostly). Each platform is a func_door, and inside each func_door is a trigger_teleport 1 hu away from the edge. If a player gets stuck, then they will most likely touch this trigger, which moves you safely to the top. I wish I could use a different solution since this sometimes still doesn’t work, but it’s worked so far. When I attempted using func_tracktrain instead of doors, I was met with multiple issues regarding StartBackwards and StartForwards, so I gave up and settled for this less than elegant solution.

For actually handling the platforms dropping randomly, there is a logic_timer and logic_case for each platform case. These then get replaced by a new logic_case and timer when the second platform indication is enabled. A relay with delays then shortens the time interval for the logic_timer as the minigame progresses.
 

Attachments

  • af_tf2_party_a12.vmf
    8.8 MB · Views: 238

Punn Ames Ardum

L1: Registered
Jan 16, 2021
46
26
This is great for people like me just getting into the logic of tf2, thanks so much for the time you spent on this!