The Alternator

Last week’s map hack post had a requirement that some of the triggers fire in a particular order. In that post, the ordering was achieved by applying a delay to one of the triggers, but this was acknowledged to be insufficient for the map to work in co-op. In order to get the ordering without the delay, we could have resorted to putting the entities in a specific order within the map, as seen in several previous hacks.

The obvious question is whether we can create a map hack that allows us to dictate the order that two events triggered on the same frame will occur, regardless of the order the entities occur in the map file. As usual, I will spend the majority of the time describing how to create a different map hack that appears to be interesting but lack much utility, then in a surprise twist ending reveal how it solves the original problem.

Today’s objective is an “alternator”: an entity system which has a single input targetname, but switches between firing two different target values when triggered. The system leans heavily on the reusable trigger_counter hack, so you may want to go back and familiarise yourself with that.

One new idea today is the use of movetarget_f as the classname for the entities we build. This function initialises path_corner entities, giving them a 16 unit cube bounding box. This lets us create an entity larger than just a point, but with two advantages over using a brush entity: since we still place a point entity it is easy to control its origin, and it doesn’t use a model precache slot. The latter is a nice saving that could be back-ported to several other hacks, but it’s the former which really matters today.

The reusable trigger_counter consisted of a shootable trigger, along with an entity which shot lightning at that trigger for instant, consistent damage of 30 hp a pop. Today we will be creating two such shootable triggers (with minor variations on the original) and a lightning shooter to strike each one. For efficiency, we will combine the two actions into a single entity! Each trigger will shoot lightning at the other trigger, meaning the whole thing takes 2 entities instead of 4.

Build a small box outside your map and build a point entity inside as follows

// make a shootable target
"classname" "movetarget_f"
"max_health" "60"
"think" "multi_wait"
"nextthink" "0.1"

// shoot lightning when triggered
"targetname" "input"
"use" "W_FireLightning"
"ammo_cells" "999999999999"

// fire targets when hurt but not killed
"th_pain" "SUB_UseTargets"
"target" "output1"

// reset when killed
"th_die" "multi_wait"

The first 4 keys create an entity which can be shot – as mentioned above movetarget_f gives the point entity a small bbox, then the think function makes that bbox solid and damagable. The next three keys create a lightning shooter using the infinite ammo trick – and because we used movetarget_f we have control of the origin of the point entity where lightning will emanate from .

The function in th_pain is the standard way to trigger a target. An important detail we rely on here is that only one of th_pain and th_die runs after the entity is damaged, depending on whether it hit 0 HP yet. It’s also worth noting this is a minor change to the behaviour of the reusable trigger_counter: that fired triggers on death and reset on use, but this fires on pain and resets on death instead. Also worth observing at this point that we gave the entity 60 HP, so that it takes exactly two lightning hits to kill.

Make a copy of this entity, placed within 8 units of the first so that the origin of the new entity is inside the bbox of the original (and vice-versa). This makes sure that lightning from the first entity hits the second and vice versa. Change the target to output2 then add the following two keys:

"armortype" "-1"
"armorvalue" "-30"

This negative armour causes the entity to take 30 more points of damage from the first attack it takes (then the armour burns off and has no further effect). When the input event triggers for the first time, the second entity hits the first entity for 30 hp, causing it to trigger output1, while the first entity hits the second entity for 60 hp, killing it and causing it to reset to full health without triggering output2.

So our entity setup reacts to the first attack by only triggering the first output, but what about the next time input is triggered? Well, there’s an important difference to the entities now, the first entity only has 30 HP left, but the second has 60. So when the second input comes, the first entity will die from the attack (and reset) but the second will survive with 30 HP and trigger output2. The two entities will always remain 30 hp out of sync with each other, so each round of attacks kills one and injures the other.

To test this, create two trigger_relay entities with targetname of output1 and output2, and give them distinct values in the message key. Also add a func_button with a target of input. As you repeatedly press the button, the two messages should alternate.

All that remains is to show how you can use this to solve the original problem posed: how to get two triggers to fire on the same frame in a specific order. This is simple to demonstrate with the test map you have made. Replace your button with a trigger_multiple with wait of 3 that targets input, then clone that trigger and exactly overlap the original with the copy. When you enter the triggers, input is fired twice in a row, triggering output1 followed by output2 in that order (you may need an engine that logs centreprint text to check that the order is correct). You can reorder the entities in the map any way you want, and output1 will always trigger first from the pair.

You can download an updated version of the statue map here, which uses today’s map hack to prevent the coop bug. Now players can fight over the active statue all they like without breaking the internal state of the system.


Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.