Monsters attacks do not inherit cooldowns; are used in alphabetical order
Describe the bug
I was messing around with the zombie breaker (mon_zombie_concentration_ender) from MoM for #75649 when I discovered something weird; even if I completely remove the cooldown on zombie_anti_concentration_spell, if I'm in melee it will only cast the spell very rarely, something like once every few minutes, maybe as often as every 30 seconds. Instead it just tries to grab, over and over and over. Rearranging the list of special attacks didn't help, so it's not a matter of it just preferrentially using the first special attack in its entry. The only way I could get it to consistently cast the spell was to remove the grab entirely.
This severely hampers our ability to create zombies that do anything unique and interesting.
Attach save file
N/A
Steps to reproduce
- Mess about with zombie breaker (and other zombies with special attacks) as described and let it whale on you
Expected behavior
Ideally there would be some way to manually weight a monster's special attacks so that some occur more than others, but failing that they should all occur at a more or less equal rate.
Screenshots
Spell with no cooldown, grab removed. It casts the spell over and over, as expected.
Spell with no cooldown, grab present. It only uses the spell every few minutes, maybe as often as once every 30 seconds. I even moved the grab to the end of list of special attacks just to see if it was prioritizing the first listed special attack, but nope. If it was just picking a random attack that's off cooldown, this should be 50/50 grabs and spells, right?
Versions and configuration
- OS: Windows
- OS Version: 10.0.19045.4651 (22H2)
- Game Version: b8dba37 [64-bit]
- Graphics Version: Tiles
- Game Language: English [en]
- Mods loaded: [ Dark Days Ahead [dda], Disable NPC Needs [no_npc_food], Portal Storms Ignore NPCs [personal_portal_storms], Slowdown Fungal Growth [no_fungal_growth], Mind Over Matter [mindovermatter], Bionic Slots [cbm_slots], Stats Through Kills [stats_through_kills] ]
Additional context
No response
Could you reproduce with debug mode - DF_MATTACK filter and paste the log messages? The base attack has a 10-turn cooldown, they shouldn't be able to chain them like that.
Here you go:
And here's what it looks like after it successfully casts the spell:
And just for good measure, I checked to see what happened if I manually add "cooldown": 10 to the breaker's grab. And yeah, it does now only occasionally grab and use the spell at about the intended cooldown:
It looks to me like it only triggered because I failed to break the grab, which meant the grab attack didn't fire, and all the other valid attacks were on cooldown.
I also tried moving "grab" to the bottom of the breaker's list of special attacks to see if that would change the order of evaluation, but it did not. I noticed that the attacks seem to be listed in alphabetical order, so I added barbed_tentacle and cut_throat to the end of the entry to check, and lo and behold:
Now it's constantly using its barbed_tentacle attack every turn, despite the fact that it has a 5-turn cooldown defined in the base attack.
CONCLUSION:
- The game evaluates special attacks in alphabetical order. If the attack with the earliest alphabetical order is off cooldown and valid, it will always be used.
- Cooldowns are not preserved from base attacks, defaulting any monster attack without an explicitly defined cooldown to a cooldown of 0.
In combination, this means that if a monster has a valid low-alphabetical attack without a defined cooldown, it will use that attack to the exclusion of all other attacks. A quick tests confirms that basic zombies also just try to to grab repeatedly. I'm pretty sure that right now the only reason any zombie does literally anything other than grab is that grab has a failure mode (grab is already established).
Even if the cooldowns were passed down correctly, this seems like incorrect behavior to me. It wouldn't matter most of the time, but it means that if a monster has two attacks with the same cooldown it will always do them in the same order every time, and if it has two special attacks with no cooldown it will only ever use one of them.
Oh and here's the .json for the final test version of the breaker with the extra attacks and such:
{ "id": "mon_zombie_concentration_ender", "copy-from": "mon_zombie_base", "type": "MONSTER", "name": { "str": "zombie breaker" }, "description": "An enormous hump takes up most of this zombie's back, fusing its head directly into its torso. It moves slowly but seems more aware than most of its kind, constantly looking around rather than staring blankly ahead. When you look directly at it, you feel a cold chill at the base of your skull.", "species": [ "ZOMBIE", "HUMAN", "PSI_NULL" ], "proportional": { "hp": 2 }, "symbol": "Z", "color": "dark_gray_white", "scents_tracked": [ "sc_human", "sc_fetid" ], "melee_skill": 4, "dodge": 1, "bleed_rate": 50, "vision_day": 35, "vision_night": 5, "emit_fields": [ { "emit_id": "emit_anti_psi", "delay": "1 s" } ], "grab_strength": 30, "special_attacks": [ { "id": "grab" }, { "id": "bite_humanoid", "cooldown": 5 }, { "id": "scratch", "cooldown": { "math": [ "5 + rand(13)" ] }, "damage_max_instance": [ { "damage_type": "bash", "amount": 6, "armor_multiplier": 0.8 } ], "hit_dmg_u": "%1$s strikes at your %2$s!", "hit_dmg_npc": "%1$s strikes at <npcname>!", "miss_msg_u": "%1$s strikes at you, but you dodge!", "miss_msg_npc": "%1$s tries to strike at <npcname>, but they dodge!", "no_dmg_msg_u": "%1$s strikes at your %2$s, but fails to penetrate armor.", "no_dmg_msg_npc": "%1$s tries to strike at <npcname>, but fails to penetrate armor." }, { "id": "zombie_anti_concentration_spell", "type": "spell", "spell_data": { "id": "null_break_concentration_spell" }, "cooldown": { "math": [ "7 + rand(19)" ] }, "monster_message": "%1$s looks at %3$s and the static on the edge of your vision flickers." },{ "id": "barbed_tentacle" }, { "id": "cut_throat" }
From a cursory I'm-at-work search I'd imagine #74139 moving the cooldown over to variable objects doesn't play nice with the mattack loading code. I'm not gonna be in a position to bisect it properly in the near future so pinging @Ramza13 - could this be related or was it broken before?
Note sure if this is the same or a different bug, I just noticed this:
The mi-go used three psionic powers and a physical attack all within one second.
Why isn't this listed as a release blocker?
@tenehea because 74139 that introduced the issue was merged after 0.H diverge, it never was in release candidate in the first place
So what, to fix it, all that is needed is to add cooldown to all grabbing attacks?
To all special attacks of every kind
No, the fix is to fix mattack loading.
what exactly should be fixed then?
Monster attacks not loading their cooldown from their base attack. It's probably a consequence of special_attacks supporting two different sorts of attack syntax and hence not living in generic factory properly - you can't make cooldown a mandatory member since old monattacks don't define a "cooldown" as such, and just slappin a cooldown into every monster definition just turns it into a contributor trap.
If what, i think Kevin told that it is pretty much desired for zeds to try to grab you as often as possible
https://discord.com/channels/598523535169945603/598523535169945607/1291521408890834954
A major bug is not a feature
...i realised point raised by Kevin is not related to this bug, my bad
Grabs are completely incidental in the context of this bug (beyond being the one relevant monster attack in the game, apperently). Every monster with an inherited -cooldown attack will use them too often.
Monster attacks not loading their cooldown from their base attack. It's probably a consequence of
special_attackssupporting two different sorts of attack syntax and hence not living in generic factory properly - you can't make cooldown a mandatory member since old monattacks don't define a "cooldown" as such, and just slappin a cooldown into every monster definition just turns it into a contributor trap.
I'm too stoopid to understand what this means and want to fix this so do you mind explaining a bit more? (apologies if what I've said below makes no sense)
Monster attacks not loading their cooldown from their base attack.
So are you saying special attacks are meant to use base attack cooldowns if unspecified or am I misunderstanding? I can't see anywhere in the docs or code where special attacks cooldown values would be based off of the base attack cooldown value, from both I'd expect it to default to 0.
you can't make cooldown a mandatory member since old monattacks don't define a "cooldown" as such, and just slappin a cooldown into every monster definition just turns it into a contributor trap.
Why are there even 2 coexisting formats of writing special attacks? Wouldn't making the old style special attacks be read through the same function by just checking against a unordered set of the old method ids make sense? So like
"special_attacks": [
[ "ACID", 10 ],
{ "type": "leap", "cooldown": 8, "max_range": 4 }
]
->
"special_attacks": [
{ "type": "ACID", "cooldown": 10 },
{ "type": "leap", "cooldown": 8, "max_range": 4 }
]
Is the old version 10 not the same as the new version 10 or something? Then you could just make cooldown mandatory and slap appropriate ones everywhere, it feels like they're a big enough deal to warrant fine tuning imo rather than just defaulting to anything.
How cooldown should work
Yes, the monster (m)attack should inherit everything not defined in the mtype from the base attack, that's a pretty core functionality of them - apperently core enough in my eyes that I didn't document it back when I wrote the docs. I wouldn't go with mandatorifying cooldown in the monster definition, personally, but it also wouldn't be a dealbreaker.
Why do we use two concurrent definitions in DDA vol. 3221
You'd have to ask 2016 Coolthulu for the definitive answer, my guess would be "let's not break 3rd party mods". The whole mattack system laid around mostly-unused and completely undocumented until 2021ish when I stumbled across it and started messing around for my beloved bugs, so it's a pretty weird part of the codebase.
Long-term I'd have liked legacy monattacks to atrophy away/get moved over to the JSON mattack system but there hasn't been a concerted push towards that.
I'd have to dig into the current loading code properly but spooling old monattacks off into a separate legacy_attack or similar load path would let you move the mattack_actor stuff over to the generic factory and hook up an implicit copy-from thing from the base attack definition.
Is there any issue written about moving away from hardcoded monattacks? Because i don't think we would be able to get rid of them completely, some are simply impossible to generify to their own json monattacks
Changed the title to more accurately describe the actual problems. Just to summarize:
- Monster attacks do not inherit cooldowns from the base definition, so even though e.g. grab has a default cooldown of 10, when you give a monster "grab" without explicitly specifying a cooldown in the monster definition, it will have a cooldown of 0 and be indefinitely spammable.
- Monster attacks are always evaluated in alphabetical order. This means that they will always use their attacks in a specific order, and if they happen to have a 0-cooldown attack the comes early in alphabetical order, they will just spam that attack over and over to the exclusion of doing anything else.
Is this going anywhere? It's severely intrusive and breaks most of the game
It's less easy to fix than it sounds unless I were going about it wrong
Hello, I believe someone has come up with a partial fix for this in https://github.com/Cataclysm-TLG/Cataclysm-TLG/pull/457
It compiles and runs on my machine, and juggernauts no longer spam cut_throat, but instead select valid moves randomly as intended.
This does not fix cooldown inheritance, but they're two separate issues so I think a partial fix is probably a good idea if yall want to port this.
Just to be clear, the order of evaluation for attacks is not an issue. Alphabetical, reverse alphabetical, fixed, randomized, it really doesn't matter. The issue here is cooldowns.
What this default cooldown should be, and where it is/should be defined? My understanding is that right now it is a zero, in which case monster spamming attack makes total sense
The default is here, in the definition for the grab attack:
https://github.com/CleverRaven/Cataclysm-DDA/blob/c6a738a6686797b705b38e30f9508b82bcc70e9f/data/json/monster_special_attacks/monster_attacks.json#L60-L63
My understanding was that the attacks defined in that file were all meant to be referenced in monster definitions and everything not explicitly changed by the monster definition should inherit from the default.
Just to be clear, the order of evaluation for attacks is not an issue. Alphabetical, reverse alphabetical, fixed, randomized, it really doesn't matter. The issue here is cooldowns.
Weren't both issues a problem, which was leading to things like juggernauts spamming cut_throat and rarely or never using their other attacks? Just fixing the cooldowns won't entirely fix that issue if there are any zero cooldown attacks.
They shouldn't have a zero cooldown attack unless it's intended that they use it every turn. At which point... that's exactly what it's doing.
Kevin sums up my opinion on this matter:
Behavior trees and all that is very firmly in the realm of suggestions and desired features. It is not something that exists right now, and it's not relevant to this bug report.
Happy 1 year anniversary 😅
@tenehea Please refrain from whatever your last comment was supposed to be. If you do not have anything useful to add in solving a confirmed issue you do not need to post. Posting when you do not have anything to contribute is just spam wasting the time of people who actually want to fix it.