An in-depth explanation of the entities in Egg War
Reading this, you should be familiar with naming player entities, !targets, and User I/O. If not...
• The player's entity (type: 'player') can be given a name like any other using AddOutput to change the targetname key on the entity.
• Using !activator as an output target will send it to whatever entity started the chain. If a player walks into a trigger and four entities later one targets !activator, it is sent back to the player.
• Using !self will cause an entity to send an output to itself, useful for when you don't want to give it a name or the name will be changing.
• Every entity has FireUser1(2/3/4) inputs and OnUser1(2/3/4) outputs. When an entity recives FireUser it sends the corresponding OnUser output, that's all.
________________________________________________
Part 1: Dynamic Eggs
Simple stuff first. I wanted the eggs hovering on the capture point look the same as the ones brought to it, so I needed to dynamically set them according to which of the 18 eggs was captured.
Each flag has the following outputs, the InValue Parameter being the same as the skin number used for the egg. The outputs are sent to all of the team's cases, but only one will exist.
Each team has a stack of 12 logic_case and 11 point_template. Only the first case exists when the game starts, the rest are removed into the template. There are cases set up for each possible skin value, and OnUser1 is used to make the egg visible and spawn the next logic_case regardless of which InValue is sent.
When the template spawns the next logic_case, it Kills both itself and the previous logic_case.
________________________________________________
Part 2: Tiny People and Unique Naming
This will get really complicated, so hopefully starting simple and building up each layer of complexity should make it followable.
The Shrinking
Basics
Shrinking the player is the easiest thing since Valve added it themselves for, obviously, the halloween shrinking spells. There are two ways to get it done though:
• SetModelScale, the same input that prop_dynamic has, and will change the size of the player. The practical range is from 0.22 (below, the camera can clip through the floor) up to somewhere around up to 3.76, above which errors flood the console. You can set it to 0 and be infinitely small (invisible) without error, but it looks awful.
• Condition 75, is used for the actual spell and can be applied for a specific duration with trigger_add_tf_player_condition. Scale used is 0.5, it forces thirdperson, and your head grows big.
Problems
Both of these have problems for what I wanted to do, however. When you make someone larger using SetModelScale they will simply get stuck if they are near anything their new bounding box would clip into, even as innocuous as uneven ground. This is resolved by condition 75, which has unstuck code that nudges the player into free space (or if none is found, kills them).
However, the condition also forces thirdperson which is completely unacceptable for running through tiny tunnels, so it can't be used alone. (SetForcedTauntCam can't return you to first person, either)
Solution
Thus, SetModelScale has what we want for going small, and Condition 75 has what we want for expanding to normal, it was just a matter of figuring out how to use them as such.
Using a trigger_add_tf_player_condition set to 75 with a duration of 0 worked for this as it would instantaneously and imperceptibly shrink and expand the player, thus utilizing the unstuck code we wanted. Shrink the player with a direct input, expand them with a trigger_ATPC.
Where and When?
I have the shrink occurring in one location as players pass through it, but the expand needs to happen everywhere at certain times. To determine who should be expanded (and thus controlling when) the trigger_ATPC needs to be filtered by player name.
(wildcards work in filters!)
Using that, only players with a name beginning with expand will be affected. But I can't just have a giant trigger covering the whole map, because filters are only checked when an entity enters the trigger, so they would never be re-checked after spawning.
To resolve this, the trigger_ATPC is a 64 unit thick brush that covers the entire length and width of the map, parented to a door that moves the height of the map. It sweeps across the map once every second, expanding any eligible players it finds.
How long? Duration and timing outputs
So far, it is pretty simple to set up since you could just use delayed outputs on the shrinking trigger to name someone expand and have their time run out. Of course, I always want more and simple is never an option. I wanted a modifiable duration because I wanted to give a small time bonus for capturing an egg while you are still tiny. Thus delayed outputs are no longer feasible.
Since I only want the bonus for tiny people, it also means we need three names instead of two: normal players, tiny players, tiny players who are out of time and are tagged for expansion. The reason for the interim "to be expanded" name instead of going directly from tiny to normal is that the trigger_ATPC would continually be unsticking players as they walk around resulting in annoying jitters. They need to be hit once and then filtered out.
For adjustable time, I need to use a logic_timer which can be started, stopped, and arbitrarily extended without modifying the base time period.
At the capture point I have a func_flagdetectionzone to catch egg runners, but this entity itself can't be filtered, so players are checked manually by a filter via inputs.
So if the player is both tiny AND Blu team then they "pass" the filter and OnPass will be fired. Matching setup for Red. FireUser4 will be explained later on, for now just know it is what actually increases the time.
Player Outputs
So how do I link a logic_timer to the player and juggle three names? Now the fun starts...
The player is given the following User outputs via AddOutput:
Output
|
Target
|
Input
|
Parameter
OnUser1|namer_button_P1|PressIn
|
OnUser2|!self|AddOutput|targetname tiny_P1
OnUser2|namer_timer_P1|Enable
|
OnUser3|!self|AddOutput|targetname normal_P1
OnUser3|namer_timer_P1|Disable
|
OnUser4|namer_timer_P1|AddToTimer|15
No screenshot because they don't exist in Hammer except as input parameters on other entities (which we will get to) and I wanted to keep it simple to read.
OnUser1 and the associated button will be explained soon.
OnUser2 is used when shrinking, assigning the player a tiny name and turning their timer on.
The shrinking trigger is pretty straightforward:
A bonus to this is that Enabling a timer that is already Enabled resets it, so tiny players jumping into the shrink vortex are refreshed to full time without any extra effort.
OnUser3 is for when the player returns to a normal state, which can occur by either running out of time, or death (in which case the timer needs to be stopped).
The first is done with a trigger_multiple identical to the trigger_ATPC and moving along with it (for some reason, the ATPC won't send OnStartTouch despite having it.
Death is handled with trigger_multiple that covers the entire map. When a player dies they stop touching it and thus:
OnUser4 is pretty self-explanatory. It extends the timer duration as mentioned earlier.
Finally, the timer itself is what sets the expansion name.
Unique Player Naming
Ok, so that is how all the shrinking logic works, but how do we actually give players those outputs and keep everyone separate from each other?
Things that must be considered:
• Each player must have a unique name and matching set of entities.
• It must be global because names persist through death, teamchange (including spectator), and class change.
• If someone leaves the server their name is freed for reuse, but we need to know which one it is.
• The system must maintain integrity throughout rapid player joining and leaving.
Mechanical Logic - a relic of the golden era lives again
Back in the days before HL2, all entities could do is trigger another, no various outputs or numerous logic entities. Instead you would do stuff like an env_beam that randomly strikes a selection of func_button targets because there was no logic_case.
This is based off that concept because it greatly simplifies what would otherwise be an incredibly complex logic system [edit: turns out not quite, see caveat below and later post]. But simple doesn't always mean conservative entity use...
33 func_button, 32 logic_relay, 32 logic_timer, 1 env_laser and an info_target for it.
A player "enters" the system when they spawn, touching a trigger_multiple. It uses OnTrigger with a 0.5 second reset delay rather than OnStartTouch because we want to discretely handle each player one at a time (this is why eggwar has a short setup time, to let the system iterate a full 32 players before they leave the spawn).
The trigger attempts to fire every relay at once but only one will be Enabled. The trigger also starts disabled, but is enabled by the logic_auto after 1 second because the system needs a moment to be initialized.
We also only want this trigger to work on new players that have not been assigned a name, so we filter it to players with blank targetnames. (Who knew blank name filtering worked? Something I learned out of this)
The relays are what assign initial names and the previously discussed outputs to the player, with P# for 1-32. Note that they all start disabled.
In addition to what we covered about player outputs, the relay disables itself and tells the laser to turn on. The laser's target is at the other end of the row of buttons, but the difference between env_beam and env_laser is that beams go through objects and lasers will stop at the first solid object. This is the key to the whole system.
The buttons are set to Toggle and Damage Activates, and move upward equal distance to their height. When the laser activates it will damage the first button in the way, enabling the relay below, turning off the laser, and moving the button out of the line of fire.
Now a different relay is enabled, and when the trigger activates for the next player, they will be assigned another name.
Leavers
If you have a habit of looking at server logs you may have seen the line Firing: (game_playerleave) whenever someone leaves the server. I suspect this is another relic left in the code from Goldsrc when all entity could do was trigger another one. What this means is, whatever a play leaves the server sends an "input-less fire" to any entities named game_playerleave. The is the same as if you use ent_fire without specifying an input. In both cases, Source sends the generic Use input.
This can be utilized through a func_button by having it Start Locked and the OnUseLocked output, one of the few things in the game that respond to a Use input. When the button is fired it will first disable the naming trigger to isolate the system from new players, disable all the relays, reset all the buttons to their Out position, and send FireUser1 to every player entity. As shown earlier, the players were given an OnUser1 that sends PressIn to their corresponding button. After a short delay the laser is turned on and the trigger is reenabled.
To put it more simply, what this does is cause every player to "check in" with the system and verify they are still on the server. Anyone who left is not around to send the input to their button, leaving it in line of fire for the laser to pick as the next available name.
Additionally, the logic_auto will Use this button at the start of the round to initialize the system and enable the first relay.
Visualizing the System
To help understand it, I have a video of the system in action. The green cubes are visual indicators of a relay being enabled, they are what I used for debugging when I was creating the system.
It starts with two buttons pressed in, and one relay enabled (first relay was used on me). Then I add 11 bots, kick 1, add 1, kick 2 at once, then add a bunch more.
https://www.youtube.com/watch?v=tfyDaP-kvHo
Alternative method using logic_branch
Check post #11 below for details on another way to do it which I figured out after the fact.
________________________________________________
Hopefully you made it through all that okay and learned something you can find useful someday.
The system could have a lot of uses, but the nuance of all the settings for the specific situations mostly prevent me from supplying any easy to use prefab.
Reading this, you should be familiar with naming player entities, !targets, and User I/O. If not...
• The player's entity (type: 'player') can be given a name like any other using AddOutput to change the targetname key on the entity.
• Using !activator as an output target will send it to whatever entity started the chain. If a player walks into a trigger and four entities later one targets !activator, it is sent back to the player.
• Using !self will cause an entity to send an output to itself, useful for when you don't want to give it a name or the name will be changing.
• Every entity has FireUser1(2/3/4) inputs and OnUser1(2/3/4) outputs. When an entity recives FireUser it sends the corresponding OnUser output, that's all.
________________________________________________
Part 1: Dynamic Eggs
Simple stuff first. I wanted the eggs hovering on the capture point look the same as the ones brought to it, so I needed to dynamically set them according to which of the 18 eggs was captured.
Each flag has the following outputs, the InValue Parameter being the same as the skin number used for the egg. The outputs are sent to all of the team's cases, but only one will exist.
Each team has a stack of 12 logic_case and 11 point_template. Only the first case exists when the game starts, the rest are removed into the template. There are cases set up for each possible skin value, and OnUser1 is used to make the egg visible and spawn the next logic_case regardless of which InValue is sent.
When the template spawns the next logic_case, it Kills both itself and the previous logic_case.
________________________________________________
Part 2: Tiny People and Unique Naming
This will get really complicated, so hopefully starting simple and building up each layer of complexity should make it followable.
The Shrinking
Basics
Shrinking the player is the easiest thing since Valve added it themselves for, obviously, the halloween shrinking spells. There are two ways to get it done though:
• SetModelScale, the same input that prop_dynamic has, and will change the size of the player. The practical range is from 0.22 (below, the camera can clip through the floor) up to somewhere around up to 3.76, above which errors flood the console. You can set it to 0 and be infinitely small (invisible) without error, but it looks awful.
• Condition 75, is used for the actual spell and can be applied for a specific duration with trigger_add_tf_player_condition. Scale used is 0.5, it forces thirdperson, and your head grows big.
Problems
Both of these have problems for what I wanted to do, however. When you make someone larger using SetModelScale they will simply get stuck if they are near anything their new bounding box would clip into, even as innocuous as uneven ground. This is resolved by condition 75, which has unstuck code that nudges the player into free space (or if none is found, kills them).
However, the condition also forces thirdperson which is completely unacceptable for running through tiny tunnels, so it can't be used alone. (SetForcedTauntCam can't return you to first person, either)
Solution
Thus, SetModelScale has what we want for going small, and Condition 75 has what we want for expanding to normal, it was just a matter of figuring out how to use them as such.
Using a trigger_add_tf_player_condition set to 75 with a duration of 0 worked for this as it would instantaneously and imperceptibly shrink and expand the player, thus utilizing the unstuck code we wanted. Shrink the player with a direct input, expand them with a trigger_ATPC.
Where and When?
I have the shrink occurring in one location as players pass through it, but the expand needs to happen everywhere at certain times. To determine who should be expanded (and thus controlling when) the trigger_ATPC needs to be filtered by player name.
(wildcards work in filters!)
Using that, only players with a name beginning with expand will be affected. But I can't just have a giant trigger covering the whole map, because filters are only checked when an entity enters the trigger, so they would never be re-checked after spawning.
To resolve this, the trigger_ATPC is a 64 unit thick brush that covers the entire length and width of the map, parented to a door that moves the height of the map. It sweeps across the map once every second, expanding any eligible players it finds.
How long? Duration and timing outputs
So far, it is pretty simple to set up since you could just use delayed outputs on the shrinking trigger to name someone expand and have their time run out. Of course, I always want more and simple is never an option. I wanted a modifiable duration because I wanted to give a small time bonus for capturing an egg while you are still tiny. Thus delayed outputs are no longer feasible.
Since I only want the bonus for tiny people, it also means we need three names instead of two: normal players, tiny players, tiny players who are out of time and are tagged for expansion. The reason for the interim "to be expanded" name instead of going directly from tiny to normal is that the trigger_ATPC would continually be unsticking players as they walk around resulting in annoying jitters. They need to be hit once and then filtered out.
For adjustable time, I need to use a logic_timer which can be started, stopped, and arbitrarily extended without modifying the base time period.
At the capture point I have a func_flagdetectionzone to catch egg runners, but this entity itself can't be filtered, so players are checked manually by a filter via inputs.
So if the player is both tiny AND Blu team then they "pass" the filter and OnPass will be fired. Matching setup for Red. FireUser4 will be explained later on, for now just know it is what actually increases the time.
Player Outputs
So how do I link a logic_timer to the player and juggle three names? Now the fun starts...
The player is given the following User outputs via AddOutput:
OnUser1|namer_button_P1|PressIn
|
OnUser2|!self|AddOutput|targetname tiny_P1
OnUser2|namer_timer_P1|Enable
|
OnUser3|!self|AddOutput|targetname normal_P1
OnUser3|namer_timer_P1|Disable
|
OnUser4|namer_timer_P1|AddToTimer|15
No screenshot because they don't exist in Hammer except as input parameters on other entities (which we will get to) and I wanted to keep it simple to read.
OnUser1 and the associated button will be explained soon.
OnUser2 is used when shrinking, assigning the player a tiny name and turning their timer on.
The shrinking trigger is pretty straightforward:
A bonus to this is that Enabling a timer that is already Enabled resets it, so tiny players jumping into the shrink vortex are refreshed to full time without any extra effort.
OnUser3 is for when the player returns to a normal state, which can occur by either running out of time, or death (in which case the timer needs to be stopped).
The first is done with a trigger_multiple identical to the trigger_ATPC and moving along with it (for some reason, the ATPC won't send OnStartTouch despite having it.
Death is handled with trigger_multiple that covers the entire map. When a player dies they stop touching it and thus:
OnUser4 is pretty self-explanatory. It extends the timer duration as mentioned earlier.
Finally, the timer itself is what sets the expansion name.
Unique Player Naming
Ok, so that is how all the shrinking logic works, but how do we actually give players those outputs and keep everyone separate from each other?
Things that must be considered:
• Each player must have a unique name and matching set of entities.
• It must be global because names persist through death, teamchange (including spectator), and class change.
• If someone leaves the server their name is freed for reuse, but we need to know which one it is.
• The system must maintain integrity throughout rapid player joining and leaving.
Mechanical Logic - a relic of the golden era lives again
Back in the days before HL2, all entities could do is trigger another, no various outputs or numerous logic entities. Instead you would do stuff like an env_beam that randomly strikes a selection of func_button targets because there was no logic_case.
This is based off that concept because it greatly simplifies what would otherwise be an incredibly complex logic system [edit: turns out not quite, see caveat below and later post]. But simple doesn't always mean conservative entity use...
33 func_button, 32 logic_relay, 32 logic_timer, 1 env_laser and an info_target for it.
A player "enters" the system when they spawn, touching a trigger_multiple. It uses OnTrigger with a 0.5 second reset delay rather than OnStartTouch because we want to discretely handle each player one at a time (this is why eggwar has a short setup time, to let the system iterate a full 32 players before they leave the spawn).
The trigger attempts to fire every relay at once but only one will be Enabled. The trigger also starts disabled, but is enabled by the logic_auto after 1 second because the system needs a moment to be initialized.
We also only want this trigger to work on new players that have not been assigned a name, so we filter it to players with blank targetnames. (Who knew blank name filtering worked? Something I learned out of this)
The relays are what assign initial names and the previously discussed outputs to the player, with P# for 1-32. Note that they all start disabled.
In addition to what we covered about player outputs, the relay disables itself and tells the laser to turn on. The laser's target is at the other end of the row of buttons, but the difference between env_beam and env_laser is that beams go through objects and lasers will stop at the first solid object. This is the key to the whole system.
The buttons are set to Toggle and Damage Activates, and move upward equal distance to their height. When the laser activates it will damage the first button in the way, enabling the relay below, turning off the laser, and moving the button out of the line of fire.
Now a different relay is enabled, and when the trigger activates for the next player, they will be assigned another name.
Leavers
If you have a habit of looking at server logs you may have seen the line Firing: (game_playerleave) whenever someone leaves the server. I suspect this is another relic left in the code from Goldsrc when all entity could do was trigger another one. What this means is, whatever a play leaves the server sends an "input-less fire" to any entities named game_playerleave. The is the same as if you use ent_fire without specifying an input. In both cases, Source sends the generic Use input.
This can be utilized through a func_button by having it Start Locked and the OnUseLocked output, one of the few things in the game that respond to a Use input. When the button is fired it will first disable the naming trigger to isolate the system from new players, disable all the relays, reset all the buttons to their Out position, and send FireUser1 to every player entity. As shown earlier, the players were given an OnUser1 that sends PressIn to their corresponding button. After a short delay the laser is turned on and the trigger is reenabled.
To put it more simply, what this does is cause every player to "check in" with the system and verify they are still on the server. Anyone who left is not around to send the input to their button, leaving it in line of fire for the laser to pick as the next available name.
Additionally, the logic_auto will Use this button at the start of the round to initialize the system and enable the first relay.
Visualizing the System
To help understand it, I have a video of the system in action. The green cubes are visual indicators of a relay being enabled, they are what I used for debugging when I was creating the system.
It starts with two buttons pressed in, and one relay enabled (first relay was used on me). Then I add 11 bots, kick 1, add 1, kick 2 at once, then add a bunch more.
https://www.youtube.com/watch?v=tfyDaP-kvHo
Alternative method using logic_branch
Check post #11 below for details on another way to do it which I figured out after the fact.
________________________________________________
Hopefully you made it through all that okay and learned something you can find useful someday.
The system could have a lot of uses, but the nuance of all the settings for the specific situations mostly prevent me from supplying any easy to use prefab.
Last edited: