Tactical Breach Wizards

Tactical Breach Wizards

Tactical Breach Wizards Workshop
Upload your own missions and find missions made by others.
Hacky way to control perks, unit allegiances and more in user levels
When messing around with the editor, I looked at the file format and started to wonder whether there aren't more options than what the editor provides. And there are, although a little hacky. Some of those were already mentioned by others (e.g. the few player-made missions where you fight Liv, who is not normally placeable). But I think I found more, although it makes the job of making levels much harder, as the editor doesn't preserve them when resaving. However, I've tested that it is still possible to upload these (both of the items I uploaded use these, at least on 2024-09-12-2-Barberry). The capabilities that are possible with this are:
  • Enabling all perks of a character
  • Changing unit allegiances
  • Units starting with Unsteady, Sedated and more

Warning
Since we are venturing into undocumented and unsupported territory, we should acknowledge, that any use of these techniques might end up being broken by some future patch (possibly intentionally or even unintentionally). Anyone, that uses them, should expect that they might stop working (or it might cause the levels to be unloadable entirely). I would also recommend putting this similar warning to any mission where you would use these if having them function is required for the mission to be playable.

Also, everything I write here comes just from my exploration of the functionality and it is possible I missed some options or interactions. Imagine any absolute statements about how things are or aren't to be surrounded by plenty of "maybes", "seems" and "I thinks", which are excluded to streamline the text of this post (which is already taking me too long to compose).

And if any developer ends up reading this and not being happy about it, I understand that you might not like this and I will take it down (or just remove this post yourself, which I presume you can). Although it would be nice to have some of these (the ones that do not break the game too much) officially supported by the editor, since the game already supports some way to read them from level file.


Level format structure and inspiration sources
Each level is a separate .lvl file in a folder named after the mission (= level pack) in:
%USERPROFILE%\AppData\LocalLow\Suspicious Developments\Tactical Breach Wizards\SaveData\LevelEditor
Inside these .lvl files are several named "elements" with name-value properties on separate lines wrapped in opening and closing "tags" (the structure looks HTML/XML-like at first glance, so I use that terminology, even though it is not that format at all). For example, at the start of each level is an element that looks like this:
<LevelHeader> type = Level name = Tutorial description = timestamp = 133710161235899739 </LevelHeader>
Later down the file are also "elements" for every floor tile, wall tile, unit or other objects. For example, this is the code for Rion:
<prefab:Druid> # Person # Wizard # Prefab instanceID = 35 transformPosition = -2:0:0 transformRotation = 1:0:0:0 #(0.0, 0.0, 0.0) </prefab:Druid>
Anything after # is just a comment and is ignored.
Some properties are "lists" that can hold multiple values and uses a syntax with square brackets, e.g. the list of objectives:
Objective[] = ClearEnemiesObjective Objective[] = SealAllDoorsObjective

The campaing mission files are also accessible and are in:
{path to steam}\steamapps\common\Tactical Breach Wizards\Tactical Breach Wizards_Data\StreamingAssets\Levels
We can already open these mission in the editor and clone them, but not everything survives and taking a look into the actual files is useful for figuring out how some things are done. Most of the objects that you can find only there however do not work in editor. There is already a post from DemoCorn that touches on some of these limitations.
(If you are feeling adventurous, it is possible to edit the campaign missions and then launch them from the campaing mission/dream select. You can even add levels and components that are prohibited in editor do work. You however cannot use the editor for that. Also, you won't be able to post these to Workshop, as the levels downloaded from it have the same restrictions as the editor. The only option would then be to instruct players to tamper with their game files as well... which seems like a bad idea.)

But the source for all the new capabilities I wish to describe here is from the following: When playing a level, specifically after making any action, the game creates a new file "Move state X.gsf" or "Turn state X.gsf" in:
%USERPROFILE%\AppData\LocalLow\Suspicious Developments\Tactical Breach Wizards
Specifically, it looks like the serialization of the state of the game before the action happened. It might be how the rewind buffer is implemented or it is there for reporting purposes, not really sure. But the point is that these serialized game states use the same format as the levels, but there are a lot more properties for the different objects than what is exposed by editor or campaign levels. A lot of these properties don't work when used in the level directly (health, mana, actionPoints, stability), but there are others that do (perk, Condition, allegiance). The manipulation that this post describes is using these propeties on objects in the level file format. And these properties are the ones that editor removes on any save (most likely because when serializing the level, it is not allowed to output them).
(I didn't really try all the properties, so there might still be some room for exploration, although based on the names, there is a lot properties that affect campaign progression, achievement tracking or they encode some internal state, e.g. "artPrefabName". Even with the properties I did mess around with, I managed to get the game into some invalid states where the game half-loaded a level. I think I might have crashed the game too. So proceed with caution... and don't report such bugs/crashes to devs, they are your faults... and mine I guess, but mostly yours.)


Old capabilities
Just for completeness, I will mention the few changes I found useful when taking inspiration from the mission files only. All of these are retained when saving the file from an editor:
  • Tips - a list of lines that are shown as tips in the ESC menu; pretty much the only place where you can have a piece of custom text shown to the player besides the mission description, although it's pretty hidden
  • LevelManager->disableAutoWall - can be used to make levels that are not surrounded by walls or when you want to mix a corridor floor tile in the middle of a level without a wall around it; you will however have to your own walls (or Anti-Walls) everywhere
  • LevelManager->ignoreActionsDifficulty and LevelManager->ignoreReinforcementDifficulty - for puzzle-esque levels where you need to have a predictable setup; unfortunately, there is no way to counteract the bonus health option
  • Enemy door can spawn units that are not allowed when placed directly (mostly just useful to spawn Liv)


New capabilities
Everything that we will be doing here is adding additional name-value properties to the objects in a level. Any manual change of this kind gets removed after opening and saving the file in the in-game editor. Just opening and launching the file works fine. You can also make a change in a level that is currently loaded and your changes will be loaded after pressing "Restart level".

Perks
The perks that are selected for a specific hero are serialized as a list of "perk[]" values on the unit itself. And when you include them in the level directly, they do apply to the unit. The game doesn't really expect that and if the player doesn't have the perk enabled, there are parts of the UI that do not correctly reflect, that the unit has the perk. You also do not get to reallocate the perk points if you have the perk already. And I found no way to disable a perk...
Because of that, I always ended up just enabling all perks for all the characters, since the player would most likely be able to do that anyway if I ended up enabling a bunch of them. And since the UI might already be confusing for the player, as they will almost certainly have no idea what is happening, it is beter to just do the optimal choice for them automatically.

The following are the complete perk lists for all 5 characters:
<prefab:Navy Seer> # Zan perk[] = seerOverwatch perk[] = UnlockTimeBoost perk[] = UnlockFalseProphet perk[] = PredictiveShotManaGain perk[] = PredictiveShotRefresh perk[] = FalseProphetHealth perk[] = FalseProphetInteract perk[] = SupportingFire perk[] = PredictiveShotDamage perk[] = ArcaneBurstDamage perk[] = FalseProphetAttack perk[] = InfiniteSupportingFire perk[] = TimeBoostBigTime </prefab:Navy Seer> <prefab:Melee Wizard> # Jen perk[] = UnlockChainShock perk[] = UnlockGaleGrenade perk[] = UnlockBroomBreach perk[] = ChainShockForce perk[] = ChainShockTargets perk[] = WitchShotGrantsMove perk[] = InfiniteRefreshingJolt perk[] = GaleGrenadeUses perk[] = ChainShockSuperchain perk[] = ChainShockConduits perk[] = WitchBoltMelee perk[] = BroomPush perk[] = GaleStorm perk[] = GaleSecondWind </prefab:Melee Wizard> <prefab:Necromedic> # Banks perk[] = UnlockGhostShot perk[] = UnlockTransference perk[] = GhostSkullDamage perk[] = TransferenceGenius perk[] = DeathsDoorRange perk[] = SedativeGrenadeUnsteady perk[] = GhostSkullWidth perk[] = DeathsFloor perk[] = UnlockDeathsFloor perk[] = TransferenceUnsteady perk[] = DeathsDoorUses perk[] = SedativeGrenadePotency perk[] = GhostSkullDizzy perk[] = SedativeGrenadeShards </prefab:Necromedic> <prefab:Riot Priest> # Dall perk[] = CenserSlamDamage perk[] = ChargeExtraKnockback perk[] = ChargeRampage perk[] = ChargeRampageInfinite perk[] = ChargeSelective perk[] = SwapConfusion perk[] = SwapIsFreeOnFriends perk[] = SwapWithObjects perk[] = SwapWithoutLOS perk[] = ThrowCoverHeight perk[] = ThrowCoverRetrieve </prefab:Riot Priest> <prefab:Druid> # Rion perk[] = UnlockCrowdGrenade perk[] = PullBrittle perk[] = PullFriends perk[] = PullGround perk[] = BiteSedative perk[] = SporeKnockback perk[] = SporeIntelligent perk[] = SporeUses perk[] = DartGasmask </prefab:Druid>
Also, in case you would want the final innate perk for Zan (+3 knockback), put this in the LevelManager element:
<config:LevelManager> singleLevelPerk[] = FinaleSeerKnockback </config:LevelManager>
(Unfortunately, the singleLevelPerk property cannot be used to enable the other perks. This is also the only change out of these that is retained when re-saving in editor.)

Because the perk list is specified for the unit specifically, if you have multiples of the same wizard, you have to repeat the same list for both. Or not, if you would want to them to behave differently... I haven't found a use case for that.

Note that when you look at the perks, there are perks that use the naming convention "Unlock{ability}". That seems to be how the game implements that Zan and Jen do not start with all of their abilities unlocked in the first campaign levels. I was initially ignoring them, but it is necessary to include them for some of the other perks to work correctly. Specifically, in my case it was Banks's Spectral Skull damage upgrade (GhostSkullDamage) that was not being applied unless UnlockGhostShot was present earlier in the list. I am not really sure why. As mentioned, I just ended up unlocking all perks so I didn't explore this further...


Unit allegiances
There are three teams/allegiances in the game:
  • NonCombatant - anything that is not meant to attack, most commonly seen with disabled turrets (and Rion in dog-form)
  • Wizards - your team and any allies you may have (e.g. friendly Traffic Warlock)
  • Enemies - any enemy
And it is possible to alter the allegiance of a unit in a file by specifying one of these values like this:
<prefab:Reactor Medic> allegiance = Wizards </prefab:Reactor Medic>
Changing enemies into allies works fairly well and should be the most common use-case. You can even make a friendly medic or a friendly Shepherd (he protects anyone that shares his allegiance).

Changing the allegiance of wizards kind of works (except for Rion for some reason), but the game is not expecting that -- even though a wizard is an enemy, they are still controlled by the player and abilities may or may not consider the altered teammates (or the new allies of those teammates) as allies or enemies. Enemy wizards are also not considered as enemies for the purpose of the "Knock out all Enemies" objective. Generally, it felt like it is necessary to test each interaction to see how it works, which is not really a good thing to use in a level.

The third neutral allegiance NonCombatant turns out to have some non-intuitive behaviour. The main property of this group is that any unit of that allegiance is not considered hostile by the other groups and is thus not targeted by AI. But it doesn't actually stop the unit from engaging in combat, so if you just take a unit that normally attacks you and change it to NonCombatant, it will look friendly but will still attack you... but it will attack units with allegiance=Enemies as well. You can also have wizards with allegiance=NonCombatant, which leads to less perceived inconsistencies than when using Enemies, although it might still be confusing (as there is no indication for the player to see the difference).

If you take a level and change all enemies to NonCombatant, they will still be hostile towards the team, but it gives wizards the ability to move arbitrarily within one turn, because (technically) there are no enemies.

Unfortunately, because the units that spawn from doors do not have a corresponding "element" and are mentioned only by name, it is not possible to adjust allegiance of spawning unit in a user-level (which is likely why official missions use something called Allegiance Switcher instead).

When messing around with different values, it turns out that it is possible to set the allegiance using numbers as well (0=NonCombatant, 1=Wizards, 2=Enemies), so I tried to use other numbers as well... and it does work, but only until you Rewind for the first time, at which point it reverts to the original allegiance. So I would advise against using that.

(Continued in next post...)
Last edited by PacifistMime; 6 Oct, 2024 @ 3:00pm
< >
Showing 1-4 of 4 comments
PacifistMime 2 6 Oct, 2024 @ 2:58pm 
(...continuation from the main post)

Conditions
Conditions are the most versatile out of these techniques. Condition seems to be a generic name of any type of effect that is applied to a unit that usually appears in the right panel when hovering over a unit, for example Unsteady, Sedated, Stunned or even GasMask or Fireproof. (But not Stability or Armour unfortunately, those have special properties that aren't modifiable.)
Adding a Condition {name} can be done by using these two lines:
Condition[] = {name} {name}Count = 1
E.g. to make a unit 5 Unsteady, 2 Sedated, 3 Stunned and Fireproof, we would use:
Condition[] = Unsteady UnsteadyCount = 5 Condition[] = Sedated SedatedCount = 2 Condition[] = Stunned StunnedCount = 3 Condition[] = Fireproof FireproofCount = 1
The Count specifies a number that is associated with the Condition, which can mean different things for different Conditions (magnitude for Unsteady, turn count for Stunned). It is required to specify it even for Conditions that are just binary on/off such as Fireproof.

Similarly to how units are named in the file, not all conditions use the same name in-game and in-file. Here are all Conditions with their in-file names that I found (but there may be more):
  • Confused - applied by Banks's Revive on enemy and Rabid Bite; treat all enemis as hostile, on/off
  • Stunned - applied by Stun Barrels (and Stun Coffins); unit cannot act for {Count} turns
  • Globbed = Brittle - applied by Rion; unit takes {Count} bonus damage and counts down on hit
  • Sedated - applied by Banks; unit takes {Count} damage at the end of turn
  • Unsteady - applied by Banks; unit increases any induced knockback by {Count}
  • Fireproof - Bori's innate Condition; only affects Mildfire, on-off; the main attack of controllable wizard-Bori still goes through
  • Gasmask - innate Condition of some enemies; prevents Sedated, Unsteady, removed after losing {Count} health; you can actually increase the starting Gasmask count as the specified Count is added to the innate value
  • Shielded - applied by Shield Generator; temporary shield acting as {Count} bonus health, gets removed at the end of turn
  • Off - applied to a turret by disabling it; forces the unit to be allegiance=NonCombatant and it cannot do anything (works on wizards as well)
  • Retaliation = Reactive - Shepherd's innate Condition; unit protects anyone of the same allegiance, the unit doesn't act on its own; on/off; seems to only work on enemies with normal "guns", does not work on wizards
  • SelfishRetaliation = Prickly - Dream Boss Zan's innate Condition; unit protects itself after it is damaged/targeted, the unit still acts on its own, on/off; seems to have same limitations as Retaliation
  • Big - Siege Cleric's innate Condition; prevents defenestrations, on/off; interestingly, you can still throw a Big unit off of the map if there is just an edge to nothingness (when Autowall is disabled)
  • Mildfire - applied by Bori; doing anything hurts, on/off
  • Inorganic - Turret's innate Condition; prevents Unsteady, Sedated, cannot be targeted by Rabid Bite, possibly more, on/off
  • Poisoned - the effect that Zan has in that one level; movement is limited and take 1 damage per turn, but unlike Sedated, it doesn't apply the last tick of damage if the objective is finished; on/off
The following Conditions don't do anything (or at least not the thing that they claim to be doing). They might only work with certain characters or are there only to force a description to show up in the right panel.
  • ChannelingMildfire - Bori's innate Condition, says that knocking him out removes all Mildfire; giving it to another unit and knocking them out doesn't clear Mildfire
  • DeathClone = Liv and Let die - Liv's innate Condition, counts down the amount of lives she has, giving it to another unit unfortunately does allow them to cheat death
  • AttacksRemaining - Liv's innate Condition, counts down how many times she can attack; giving it to another unit does not allow them to act out of order and giving it to a wizard doesn't change how many actions they have
  • Invincible - applied by Chapel Absolvers, prevents any damage; it doesn't seem to persist without an Absolver (but you can the effect disappear at the start of the level)
  • Neutralised - applied by Neutralisers, should prevent from using magical abilities; it doesn't seem to persist without a Neutraliser
And here are two Conditions, that are impossible to catch in the .gsf files, but I did discover them so I figured I should share them, since I did use them in one of my missions:
  • ManaDrain - applied by Chapel Cleansers; removes all mana of the unit and disappears (so it never appears in .gsf file, but kind of appears in in-game event log), on/off; useful when you need to force a wizard to start with zero mana regardless of difficulty
  • Cushioned = Padding - never used in the game as far as I know; reduces the damage incurred by being knocked into a wall/object by {Count}

I am sure that there are plenty of combinations of conditions that can lead to interesting levels, you are free to experiment, I will just mention two cases I found useful:

Giving a wizard Off makes it that they cannot ever be controlled by a player, but they still count as a wizard for purposes of Green zone objective and others. However, it also force the wizard to allegiance=NonCombatant, which e.g. causes them to be damaged by Intelligent Spore Bomb. Instead, you could just use Stunned with a ridiculously high value (or high enough for all cases).

Inorganic is useful to put on living enemies, as it can be thought of as an unremovable Gasmask. It also prevents the unit from being targeted by Rabid Bite, which is useful if you would have a map without any allegiance=Enemies, which allows Rion to use Rabid Bite infinitely.

The one thing that I used a lot (and might not be a great idea since it's quite obtuse) is 3 Sedated + Poisoned. On Normal nad Hard difficulties (with no Banks to get rid of it), it will kill any wizard at the end of first turn... unless the objectives are done, then it does only 3 damage and unless you took any other point of damage, you win. You can use this along with some objective that requires everybody to survive to essentially create the objective of "Finish in 1 turn", which the game doesn't normally have. Technically, you would get the same thing even without Poisoned, but then game would allow you to end the first turn without completing the objective, dooming yourself in turn 2. I am not sure if it is good to use, as the behaviour of Poisoned is not very intuitive and requires an explanation. Directly having an objective "Finish in X turns" would be better (currently, there is only a Confidence goal that does that).

You can use Shielded on a wizard to give them temporary bonus health on first turn only, useful when combined with the previous setup for 1 turn levels.

Lastly, I will also mention, that {name}Count can be negative and it does counteract innate conditions. On/off conditions can be disabled this way, e.g. Fireproof from Bori, Big from Siege Cleric, even Retaliation from Shepherd (turning him into a normal enemy). But when trying to counteract Gasmask, any negative value is treated as just -1, so only 1 Gasmask can me removed. Interestingly, you can even disable ChannelingMildfire on Bori. A negative value can be used even when the condition is not an innate one, e.g. setting -2 Unsteady (essentially making it 2 Stability) or -2 Sedated (healing 2 at the of a turn). However, these negative values (sometimes) do not survive a rewind, similarly to out-of-bounds allegiance, so they are not very useful. And I would advise against negative Counts in general (even for counter-acting of innate Conditions), as if there is anything that could be disabled/fixed from this post, it would be this.

Practical considerations
As mentioned, editor doesn't really like any of these modifications. It can open the level and launch it just fine, but after you save it, all of your manual interventions will be gone. Thankfully, uploading to Workshop doesn't resave any level, so your levels should be publishable. However, if you want to use these techniques, you will either have to:
  1. Edit the level in text file only, without editor
  2. Reapply any changes after every save (or just before testing or uploading it)
  3. Automate the process somehow
I went with option 3, I wrote a small Powershell script that takes the level file and another file .inj and "injects" the modifications in that file into the appropriate "elements" in the level file. If you take a look at the files of one of my missions (e.g. Rion's Favorite Dall), you can see those injection files there, because the game uploads the entire folder.
I still have to launch the script after every save, but that's much easier than option 2 (I didn't trust myself to have it do it automatically out of fear of cutting a file in half somehow).

I am willing to share my script, but you should be careful around it. It is a piece of code, with a potential to be dangerous, so unless you yourself can check it, it seems like a bad idea to just run it. Again, do not run this unless you know it is safe. Here is the code:
https://pastebin.com/1pqwQup1

Other maybe useful properties of the Editor
When uploading a mission to Workshop, the game takes a screenshot of the start position of currently selected level and uses that as the thumnail in Workshop. And as far as I could tell, there is no way to change this image after the upload through Steam. To get better framing in the picture, you could change the default camera of the level, but then anyone playing the game will start with a wonky camera. However, the screenshot is taken before the mission is actually uploaded, so what you can do, is change the camera to the better shot, click the Submit to Workshop button and then go into the folder structure and restore the camera to its previous state. And if you want an even more freedom, you can just add a new unplayable level with the scene you want to use as thumbnail, have the game capture that level with the best framing and then just go into the mission folder and delete the unwanted mission.

There are two objectives where everybody has to reach green zone: first, where everybody has to be alive, and second, where everybody except Banks can die, as long as they die in the green zone. These two actually have additional difference -- if your wizard gets defenestrated, you cannot complete the first objective, but you can complete the second. Not sure what is happening there, I though this was the objective used in that one level with Liv, but it isn't. I considered using it for my Golf Dall level, where you would be supposed to throw Dall through a convoluted Golberg machine-esque level into a hole... but decided against it, because this differentiation is not communicated properly to the player. And it is also most certainly a bug that might get fixed.
Last edited by PacifistMime; 25 Oct, 2024 @ 4:56pm
LupinSamurai 5 9 Oct, 2024 @ 7:18am 
I appreciate this write-up! I did some looking into the level code as well (I needed to disable Auto-Wall for one of my levels), and I was trying to figure out the Perk thing as well; I figured out the singlelevelPerk[] for Zan, and I have a text document that has all of the perk names + their in-game respective perk. I also looked into your level's code when you posted the Banks Level, and the option to guarantee a specific perk (or all perks) was something I was looking into but couldn't figure out.

I'm glad that you have shared this info with others, and I look forward to what other levels can be created with this tool in our toolbelt. I do hope that more edits to the editor come that allow for perks to be set manually; as it is right now, I think a lot of interesting levels are left on the cutting room floor because we can't remove perks; it makes characters like Dall and Banks w/Genius Grant way too powerful for many custom levels as is.
Last edited by LupinSamurai; 9 Oct, 2024 @ 7:18am
PacifistMime 2 25 Oct, 2024 @ 5:00pm 
I managed to get the script, so here it is. Again, a disclaimer: do not just run a piece of code without checking what it does. Specifically, if you are unsure of how to even run it, that might be a good indicator that you maybe shouldn't...

Here is the code:
https://pastebin.com/1pqwQup1

I ended up upgrading it a little with the -Continuous switch which allows it to run in the background and almost immediately react to. Should be a little more user-friendly this way. But use it at your own peril, as there is a small chance that the script could 'inject' into a partially saved file, causing the rest of the file to be lost. For that purpose, I also added automatic backups of 1 minute and 10 minute old versions, just in case.
Seraguith 29 Oct, 2024 @ 3:08am 
How about making a Guide for this? This looks great, I think it counts as a Guide.
< >
Showing 1-4 of 4 comments
Per page: 1530 50