azayrahmad Posted March 14, 2021 Report Share Posted March 14, 2021 So I'd like to create a Morale system mod... My plan is to make it to be some kind of Aura of units that influences other units in range. However I need it to be more dynamic, i.e. the effect strength would change according to unit's own morale strength. I decided to create separate component. Morale.js. There are two mechanisms in this component: (1) Morale level and how it affects unit performance. This is similar to how Health.js works. (2) Unit effect on other units in range based on its own morale level. This is intended to be similar to how Auras.js works. The first one already work, but the second one is not yet. I still don't understand why the effect is not applied yet. This is the repository on GitHub: https://github.com/azayrahmad/morale-system The problematic part is in simulation/components/Morale.js. Here are the snippet of the relevant parts: Init: Morale.prototype.Init = function() { this.affectedPlayers = []; ... this.CleanMoraleInfluence(); ... }; The CleanMoraleInfluence(), intended to be similar to Clean() function from Aura.js. The target here is to apply morale influence to any allied units in 10m range. (GetAllies() also includes player's own units right?) Morale.prototype.CleanMoraleInfluence = function() { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); //Remove Morale let targetUnitsClone = []; if (this.targetUnits) { targetUnitsClone = this.targetUnits.slice(); this.RemoveMoraleInfluence(this.targetUnits); } if (this.rangeQuery) cmpRangeManager.DestroyActiveQuery(this.rangeQuery); //Add Morale var cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); if (!cmpPlayer) cmpPlayer = QueryOwnerInterface(this.entity); if (!cmpPlayer || cmpPlayer.GetState() == "defeated") return; this.targetUnits = []; let affectedPlayers = cmpPlayer.GetAllies(); this.rangeQuery = cmpRangeManager.CreateActiveQuery( this.entity, 0, 10, affectedPlayers, IID_Identity, cmpRangeManager.GetEntityFlagMask("normal"), false ); cmpRangeManager.EnableActiveQuery(this.rangeQuery); } Here is the apply and remove morale influence functions. The intention is to apply the effect (add Morale regenrate by 1 per second) and remove it if no longer in range). Morale.prototype.ApplyMoraleInfluence = function(ents) { var cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager); cmpModifiersManager.AddModifiers( "MoraleSupport", { "Morale/RegenRate": [{ "affects": ["Unit"], "add": 1 }] }, ents ); } Morale.prototype.RemoveMoraleInfluence = function(ents) { if (!ents.length) return; var cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager); cmpModifiersManager.RemoveAllModifiers("MoraleSupport", ents); } And lastly OnRangeUpdate. This should run apply and remove functions when units are moving in/out of other units range right? Morale.prototype.OnRangeUpdate = function(msg) { if(this.rangeQuery) { this.ApplyMoraleInfluence(msg.added); this.RemoveMoraleInfluence(msg.removed); } } I have been looking at the code all day and still don't understand why does the effect not apply. In the repo I added template units that includes Morale which start at 50%. The intention is that every players unit within 10m range should have increased Morale, but they don't. If you want to test this with Aura and see how I intend this to work, you can put these snippet inside template_unit.xml: <Auras datatype="tokens"> morale_friendly </Auras> Any help would be extremely appreciated. Thank you! 1 Quote Link to comment Share on other sites More sharing options...
Freagarach Posted March 14, 2021 Report Share Posted March 14, 2021 Very, very nice! (I hope that vanilla will someday include something like this as well, since battles of the past were primarily not fought on strength but on morale.) The snippets look way cleaner than what I have implemented a long time ago in my own mod xD (Your link is not alive.) Perhaps a silly question, do you clean on ownership change? Since on init the owner is INVALID_PLAYER, which will have no allies, I guess. 1 Quote Link to comment Share on other sites More sharing options...
azayrahmad Posted March 15, 2021 Author Report Share Posted March 15, 2021 (edited) Hi @Freagarach, Sorry I forgot to check that the repo was private, already made public now: https://github.com/azayrahmad/0AD-morale-system I did clean on ownership change, but I put it without checking for INVALID_PLAYER, as Auras.js done as well: Morale.prototype.OnOwnershipChanged = function(msg) { this.CleanMoraleInfluence(); if (msg.to != INVALID_PLAYER) ... }; I put Warn code inside ApplyMoraleInfluence & RemoveMoraleInfluence to debug and they are both triggered during the game. However, I don't know a way to see what entities morale influence applied to, and I see that my units are not impacted by the defined effect (They don't have increased morale). Please let me know if you notice something wrong, and thanks for your help! Edited March 15, 2021 by azayrahmad change repo link Quote Link to comment Share on other sites More sharing options...
Freagarach Posted March 15, 2021 Report Share Posted March 15, 2021 Thanks Does this work with A24 or SVN? For debugging I suggest that you follow the whole chain, so indeed it starts with one entity that applies the morale influence, an entity entering its influence should receive the OnValueModification message, calls RecalculateValues which should do the stuff you want. Put some warn(uneval(<variable>)) there to see what is going on. You could also choose to do it backwards. Warn something in the function you want to be seen executed (ExecuteRegeneration), if it isn't, trace back what should call that (CheckRegenTimer) to see whether that is called. That said, I don't see anything really wrong in your code, but I haven't tested it yet either. If you don't figure it out with the above I will take a closer look! 1 Quote Link to comment Share on other sites More sharing options...
azayrahmad Posted March 15, 2021 Author Report Share Posted March 15, 2021 @Freagarach This works with Alpha 24, haven't tested it on SVN. I have tested the morale regeneration using Aura i.e. create aura that increase morale regeneration, and it works well, suggesting that the morale regeneration is not the issue. That's why I tried to copy some code from Auras.js to Morale.js, I don't know why the morale regeneration is not applied. I haven't traced the whole chain though, that is a good suggestion. I should have also check whether there's something wrong with ExecuteRegeneration. I will take a look at it in the weekend. Thanks! Quote Link to comment Share on other sites More sharing options...
azayrahmad Posted March 20, 2021 Author Report Share Posted March 20, 2021 (edited) Hi @Freagarach I finally found out why it didn't work.Applying the morale regeneration must be looped per entity. So e.g. ApplyMoraleInfluence must be: Morale.prototype.ApplyMoraleInfluence = function(ents) { var cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager); for (let ent of ents) { cmpModifiersManager.AddModifiers( "MoraleSupport", { "Morale/RegenRate": [{ "affects": ["Unit"], "add": this.GetMoraleLevel() }], }, ent ); } } Not sure why cmpModifiersManager cannot apply to all entities at once like in ApplyMorale, perhaps there's something to do with the format of entities retrieved from this function: let affectedPlayers = cmpPlayer.GetAllies(); Apparently cmpModifiersManager.AddModifiers cannot be applied to affectedPlayers all at once, need to be looped per entity. EDIT: Although after further tests, I see that the effect do not stack, i.e. the RegenRate would stay the same once modified, therefore multiple units in the same range do not have their morale increased exponentially. I already added True at the end of AddModifiers but it seems to change nothing. Like this: cmpModifiersManager.AddModifiers( "MoraleAllies", { "Morale/RegenRate": [{ "affects": ["Unit"], "add": this.GetMoraleLevel() }], }, ent, true, ); If you or anyone reading this know how to fix this, I would be very thankful. Thanks! Edited March 20, 2021 by azayrahmad Quote Link to comment Share on other sites More sharing options...
Freagarach Posted March 20, 2021 Report Share Posted March 20, 2021 "ApplyMoraleEffects" says it receives multiple entities, but according to what calls it, it merely uses one entity, namely "this.entity", whereas "ApplyMoraleInfluence" actually receives an array of entities. I'm pinging @wraitii on the stacking, for I would as well say this is correct. 1 Quote Link to comment Share on other sites More sharing options...
azayrahmad Posted March 21, 2021 Author Report Share Posted March 21, 2021 Hi @Freagarach, Yes, I realized that I had misnamed the variable, already fixed now. In addition to stackable problem above, I found that OnRangeUpdate does not trigger for other units if a unit is killed. So if unit A is applying effect to unit B in range, and then unit A is killed, unit B still has effect applied Debugging current code shown that the effect removal (RemoveMoraleInfluence) does not trigger for unit B. However if in this case unit B is killed instead, the removal is triggered (removing effect applied from unit A to unit B as unit B is no longer in range). Do you perhaps have any idea how to solve this? i.e. ensure that killing units also remove its effect? Thank you. Quote Link to comment Share on other sites More sharing options...
wraitii Posted March 22, 2021 Report Share Posted March 22, 2021 Pretty cool mod that @azayrahmad 19 hours ago, azayrahmad said: Do you perhaps have any idea how to solve this? i.e. ensure that killing units also remove its effect? You can listen for OnOwnershipChanged or onDestroy messages. ---- Quote Apparently cmpModifiersManager.AddModifiers cannot be applied to affectedPlayers all at once, need to be looped per entity. Modifiers apply to entities, and there is no way to pass an array of entities (for now anyways). However, a modifier applied to a "Player entity" is also applied to all the entities it owns, so that might be where you got confused. ---- Stackability is actually not implemented in the modifiers manager for now (see L11: // - support stacking modifiers (MultiKeyMap handles it but not this manager).) You can work around it by giving unique identifiers to your modifier, e.g.: cmpModifiersManager.AddModifiers( "MoraleAllies_" + ent, { "Morale/RegenRate": [{ "affects": ["Unit"], "add": this.GetMoraleLevel() }], }, ent ); That way each modifier is unique and it stacks in practice. 1 Quote Link to comment Share on other sites More sharing options...
azayrahmad Posted March 25, 2021 Author Report Share Posted March 25, 2021 Thanks @wraitii for your help! It turns out that OnRangeUpdate seems to not detect entity once it is destroyed, so the solution is to reverse how ApplyMoraleInfluence and RemoveMoraleInfluence work. Previously the unit applies aura to other units in range, now I reversed it so all units in range applies the effect to the unit instead. I haven't tested this thoroughly but so far it seems to be okay. OnDestroy seems to be what I just looking for. Perhaps for morale damage on death instead of mucking around in Health component. That stackability trick is neat. I wonder why Aura component do it like that, your comment explains it well, thank you. Quote Link to comment Share on other sites More sharing options...
Freagarach Posted March 25, 2021 Report Share Posted March 25, 2021 I find it strange that OnRangeUpdate doesn't detect dying entities, because it does with UnitAI (you can check that by warn(uneval(msg));), if you kill entity A in range of entity B, you will see entity B will receive a range update . But perhaps you meant that a dying entity does not remove its morale influence from nearby entities? Then you should indeed use OnDestroy (or OnOwnershipChanged, for that will also account for deserting entities). Quote Link to comment Share on other sites More sharing options...
Recommended Posts
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.