Changeset 25207

Timestamp:
Apr 8, 2021, 7:40:49 AM (3 years ago)
Author:
Freagarach
Message:

Heal using Heal instead of UnitAI.

Moves the healing logic from UnitAI to Heal.
Makes it easier for modders to change healing behaviour, e.g. letting structures heal (instead of using an aura).

Differential revision: D2680
Comments by: @Stan, @wraitii

Location:
ps/trunk/binaries/data/mods/public/simulation/components
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • ps/trunk/binaries/data/mods/public/simulation/components/Heal.js

    r23863 r25207  
    5050};
    5151
    52 // We have no dynamic state to save.
    53 Heal.prototype.Serialize = null;
    54 
    5552Heal.prototype.GetTimers = function()
    5653{
     
    9895{
    9996    let cmpHealth = Engine.QueryInterface(target, IID_Health);
    100     if (!cmpHealth || cmpHealth.IsUnhealable())
    101         return false;
    102 
    103     // Verify that the target is owned by an ally or the player self.
     97    if (!cmpHealth || cmpHealth.IsUnhealable() || !cmpHealth.IsInjured())
     98        return false;
     99
    104100    let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
    105101    if (!cmpOwnership || !IsOwnedByAllyOfPlayer(cmpOwnership.GetOwner(), target))
    106102        return false;
    107103
    108     // Verify that the target has the right class.
    109104    let cmpIdentity = Engine.QueryInterface(target, IID_Identity);
    110105    if (!cmpIdentity)
     
    130125
    131126/**
    132  * Heal the target entity. This should only be called after a successful range
    133  * check, and should only be called after GetTimers().repeat msec has passed
    134  * since the last call to PerformHeal.
    135  */
    136 Heal.prototype.PerformHeal = function(target)
    137 {
    138     let cmpHealth = Engine.QueryInterface(target, IID_Health);
    139     if (!cmpHealth)
    140         return;
    141 
     127 * @param {number} target - The target to heal.
     128 * @param {number} callerIID - The IID to notify on specific events.
     129 * @return {boolean} - Whether we started healing.
     130 */
     131Heal.prototype.StartHealing = function(target, callerIID)
     132{
     133    if (this.target)
     134        this.StopHealing();
     135
     136    if (!this.CanHeal(target))
     137        return false;
     138
     139    let timings = this.GetTimers();
     140    let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
     141
     142    // If the repeat time since the last heal hasn't elapsed,
     143    // delay the action to avoid healing too fast.
     144    let prepare = timings.prepare;
     145    if (this.lastHealed)
     146    {
     147        let repeatLeft = this.lastHealed + timings.repeat - cmpTimer.GetTime();
     148        prepare = Math.max(prepare, repeatLeft);
     149    }
     150
     151    let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
     152    if (cmpVisual)
     153    {
     154        cmpVisual.SelectAnimation("heal", false, 1.0);
     155        cmpVisual.SetAnimationSyncRepeat(timings.repeat);
     156        cmpVisual.SetAnimationSyncOffset(prepare);
     157    }
     158
     159    // If using a non-default prepare time, re-sync the animation when the timer runs.
     160    this.resyncAnimation = prepare != timings.prepare;
     161    this.target = target;
     162    this.callerIID = callerIID;
     163    this.timer = cmpTimer.SetInterval(this.entity, IID_Heal, "PerformHeal", prepare, timings.repeat, null);
     164
     165    return true;
     166};
     167
     168/**
     169 * @param {string} reason - The reason why we stopped healing. Currently implemented are:
     170 *  "outOfRange", "targetInvalidated".
     171 */
     172Heal.prototype.StopHealing = function(reason)
     173{
     174    if (this.timer)
     175    {
     176        let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
     177        cmpTimer.CancelTimer(this.timer);
     178        delete this.timer;
     179    }
     180
     181    let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
     182    if (cmpVisual)
     183        cmpVisual.SelectAnimation("idle", false, 1.0);
     184
     185    delete this.target;
     186
     187    // The callerIID component may start healing again,
     188    // replacing the callerIID, hence save that.
     189    let callerIID = this.callerIID;
     190    delete this.callerIID;
     191
     192    if (reason && callerIID)
     193    {
     194        let component = Engine.QueryInterface(this.entity, callerIID);
     195        if (component)
     196            component.ProcessMessage(reason, null);
     197    }
     198};
     199
     200/**
     201 * Heal our target entity.
     202 * @params - data and lateness are unused.
     203 */
     204Heal.prototype.PerformHeal = function(data, lateness)
     205{
     206    if (!this.CanHeal(this.target))
     207    {
     208        this.StopHealing("TargetInvalidated");
     209        return;
     210    }
     211    if (!this.IsTargetInRange(this.target))
     212    {
     213        this.StopHealing("OutOfRange");
     214        return;
     215    }
     216
     217    // ToDo: Enable entities to keep facing a target.
     218    Engine.QueryInterface(this.entity, IID_UnitAI)?.FaceTowardsTarget(this.target);
     219
     220    let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
     221    this.lastHealed = cmpTimer.GetTime() - lateness;
     222
     223    let cmpHealth = Engine.QueryInterface(this.target, IID_Health);
    142224    let targetState = cmpHealth.Increase(this.GetHealth());
    143225
    144226    // Add experience.
    145     let cmpLoot = Engine.QueryInterface(target, IID_Loot);
     227    let cmpLoot = Engine.QueryInterface(target, IID_Loot);
    146228    let cmpPromotion = Engine.QueryInterface(this.entity, IID_Promotion);
    147229    if (targetState !== undefined && cmpLoot && cmpPromotion)
    148     {
    149230        // Health healed times experience per health.
    150231        cmpPromotion.IncreaseXp((targetState.new - targetState.old) / cmpHealth.GetMaxHitpoints() * cmpLoot.GetXp());
    151     }
    152     // TODO we need a sound file
    153 //  PlaySound("heal_impact", this.entity);
     232
     233    // TODO we need a sound file.
     234    // PlaySound("heal_impact", this.entity);
     235
     236    if (!cmpHealth.IsInjured())
     237    {
     238        this.StopHealing("TargetInvalidated");
     239        return;
     240    }
     241
     242    if (this.resyncAnimation)
     243    {
     244        let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
     245        if (cmpVisual)
     246        {
     247            let repeat = this.GetTimers().repeat;
     248            cmpVisual.SetAnimationSyncRepeat(repeat);
     249            cmpVisual.SetAnimationSyncOffset(repeat);
     250        }
     251        delete this.resyncAnimation;
     252    }
     253};
     254
     255/**
     256 * @param {number} - The entity ID of the target to check.
     257 * @return {boolean} - Whether this entity is in range of its target.
     258 */
     259Heal.prototype.IsTargetInRange = function(target)
     260{
     261    let range = this.GetRange();
     262    let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
     263    return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, range.max, false);
    154264};
    155265
  • ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js

    r25206 r25207  
    26802680            "HEALING": {
    26812681                "enter": function() {
     2682
     2683
     2684
     2685
     2686
     2687
     2688
    26822689                    if (!this.CheckRange(this.order.data, IID_Heal))
    26832690                    {
    2684                         this.SetNextState("APPROACHING");
     2691                        this.");
    26852692                        return true;
    26862693                    }
    26872694
    2688                     if (!this.TargetIsAlive(this.order.data.target) ||
    2689                         !this.CanHeal(this.order.data.target))
    2690                     {
    2691                         this.SetNextState("FINDINGNEWTARGET");
     2695                    if (!cmpHeal.StartHealing(this.order.data.target, IID_UnitAI))
     2696                    {
     2697                        this.ProcessMessage("TargetInvalidated");
    26922698                        return true;
    26932699                    }
    2694 
    2695                     let cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
    2696                     this.healTimers = cmpHeal.GetTimers();
    2697 
    2698                     // If the repeat time since the last heal hasn't elapsed,
    2699                     // delay the action to avoid healing too fast.
    2700                     var prepare = this.healTimers.prepare;
    2701                     if (this.lastHealed)
    2702                     {
    2703                         var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
    2704                         var repeatLeft = this.lastHealed + this.healTimers.repeat - cmpTimer.GetTime();
    2705                         prepare = Math.max(prepare, repeatLeft);
    2706                     }
    2707 
    2708                     this.SelectAnimation("heal");
    2709                     this.SetAnimationSync(prepare, this.healTimers.repeat);
    2710                     this.StartTimer(prepare, this.healTimers.repeat);
    2711 
    2712                     // If using a non-default prepare time, re-sync the animation when the timer runs.
    2713                     this.resyncAnimation = prepare != this.healTimers.prepare;
    27142700
    27152701                    this.FaceTowardsTarget(this.order.data.target);
     
    27182704
    27192705                "leave": function() {
    2720                     this.ResetAnimation();
    2721                     this.StopTimer();
    2722                 },
    2723 
    2724                 "Timer": function(msg) {
    2725                     let target = this.order.data.target;
    2726                     if (!this.TargetIsAlive(target) || !this.CanHeal(target))
    2727                     {
     2706                    let cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
     2707                    if (cmpHeal)
     2708                        cmpHeal.StopHealing();
     2709                },
     2710
     2711                "OutOfRange": function(msg) {
     2712                    if (this.ShouldChaseTargetedEntity(this.order.data.target, this.order.data.force))
     2713                    {
     2714                        if (this.CanPack())
     2715                            this.PushOrderFront("Pack", { "force": true });
     2716                        else
     2717                            this.SetNextState("APPROACHING");
     2718                    }
     2719                    else
    27282720                        this.SetNextState("FINDINGNEWTARGET");
    2729                         return;
    2730                     }
    2731                     if (!this.CheckRange(this.order.data, IID_Heal))
    2732                     {
    2733                         if (this.ShouldChaseTargetedEntity(target, this.order.data.force))
    2734                         {
    2735                             if (this.CanPack())
    2736                             {
    2737                                 this.PushOrderFront("Pack", { "force": true });
    2738                                 return;
    2739                             }
    2740                             this.SetNextState("HEAL.APPROACHING");
    2741                         }
    2742                         else
    2743                             this.SetNextState("FINDINGNEWTARGET");
    2744                         return;
    2745                     }
    2746 
    2747                     let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
    2748                     this.lastHealed = cmpTimer.GetTime() - msg.lateness;
    2749 
    2750                     this.FaceTowardsTarget(target);
    2751                     let cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
    2752                     cmpHeal.PerformHeal(target);
    2753 
    2754                     if (this.resyncAnimation)
    2755                     {
    2756                         this.SetAnimationSync(this.healTimers.repeat, this.healTimers.repeat);
    2757                         this.resyncAnimation = false;
    2758                     }
     2721                },
     2722
     2723                "TargetInvalidated": function(msg) {
     2724                    this.SetNextState("FINDINGNEWTARGET");
    27592725                },
    27602726            },
     
    34113377    // For preventing increased action rate due to Stop orders or target death.
    34123378    this.lastAttacked = undefined;
    3413     this.lastHealed = undefined;
    34143379
    34153380    this.formationAnimationVariant = undefined;
  • ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Heal.js

    r25087 r25207  
    66Engine.LoadComponentScript("interfaces/Loot.js");
    77Engine.LoadComponentScript("interfaces/Promotion.js");
     8
    89Engine.LoadComponentScript("interfaces/UnitAI.js");
    910Engine.LoadComponentScript("Heal.js");
     11
    1012
    1113const entity = 60;
     
    1315const otherPlayer = 2;
    1416
     17
     18
     19
     20
    1521let template = {
    16     "Range": 20,
     22    "Range": ,
    1723    "RangeOverlay": {
    1824        "LineTexture": "heal_overlay_range.png",
    1925        "LineTextureMask": "heal_overlay_range_mask.png",
    20         "LineThickness": 0.35
    21     },
    22     "Health": 5,
    23     "Interval": 2000,
     26        "LineThickness":
     27    },
     28    "Health": ,
     29    "Interval": ,
    2430    "UnhealableClasses": { "_string": "Cavalry" },
    25     "HealableClasses": { "_string": "Support Infantry" },
     31    "HealableClasses": { "_string": "Support Infantry" }
    2632};
    2733
     
    3541
    3642AddMock(player, IID_Player, {
    37     "IsAlly": () => true
     43    "IsAlly": (
    3844});
    3945
    4046AddMock(otherPlayer, IID_Player, {
    41     "IsAlly": () => false
     47    "IsAlly": (
    4248});
    4349
     
    8187}]);
    8288
    83 // Test PerformHeal
     89// Test
    8490let target = 70;
    85 
    8691AddMock(target, IID_Ownership, {
    8792    "GetOwner": () => player
    8893});
    8994
    90 let targetClasses;
     95let targetClasses;
    9196AddMock(target, IID_Identity, {
    9297    "GetClassesList": () => targetClasses
    9398});
    9499
     100
    95101let increased;
    96102let unhealable = false;
     
    102108        return { "old": 600, "new": 600 + 5 + 100 };
    103109    },
    104     "IsUnhealable": () => unhealable
    105 });
    106 
    107 cmpHeal.PerformHeal(target);
     110    "IsUnhealable": () => unhealable,
     111    "IsInjured": () => true
     112});
     113
     114TS_ASSERT(cmpHeal.StartHealing(target));
     115cmpTimer.OnUpdate({ "turnLength": 1 });
    108116TS_ASSERT(increased);
    109 
     117increased = false;
     118cmpTimer.OnUpdate({ "turnLength": 2.2 });
     119TS_ASSERT(increased);
     120
     121// Test we can't heal too quickly.
     122increased = false;
     123TS_ASSERT(cmpHeal.StartHealing(target));
     124cmpTimer.OnUpdate({ "turnLength": 2 });
     125TS_ASSERT(!increased);
     126
     127// Test experience.
    110128let looted;
    111129AddMock(target, IID_Loot, {
     
    124142
    125143increased = false;
    126 cmpHeal.PerformHeal(target);
     144TS_ASSERT(cmpHeal.StartHealing(target));
     145cmpTimer.OnUpdate({ "turnLength": 1 });
    127146TS_ASSERT(increased && looted && promoted);
    128147
     
    130149let updated;
    131150AddMock(entity, IID_UnitAI, {
     151
    132152    "UpdateRangeQueries": () => {
    133153        updated = true;
     
    162182let otherTarget = 71;
    163183AddMock(otherTarget, IID_Ownership, {
    164     "GetOwner": () => player
     184    "GetOwner": () => layer
    165185});
    166186
    167187AddMock(otherTarget, IID_Health, {
    168     "IsUnhealable": () => false
    169 });
    170 TS_ASSERT_UNEVAL_EQUALS(cmpHeal.CanHeal(otherTarget), false);
     188    "IsUnhealable": () => false,
     189    "IsInjured": () => true
     190});
     191
     192AddMock(otherTarget, IID_Identity, {
     193    "GetClassesList": () => ["Infantry"]
     194});
     195TS_ASSERT(!cmpHeal.CanHeal(otherTarget));
     196
     197// Test we stop healing when finished.
     198increased = false;
     199AddMock(target, IID_Health, {
     200    "GetMaxHitpoints": () => 700,
     201    "Increase": amount => {
     202        increased = true;
     203        TS_ASSERT_EQUALS(amount, 5 + 100);
     204        return { "old": 600, "new": 600 + 5 + 100 };
     205    },
     206    "IsUnhealable": () => false,
     207    "IsInjured": () => true
     208});
     209TS_ASSERT(cmpHeal.StartHealing(target));
     210cmpTimer.OnUpdate({ "turnLength": 2.2 });
     211TS_ASSERT(increased);
     212
     213increased = false;
     214AddMock(target, IID_Health, {
     215    "GetMaxHitpoints": () => 700,
     216    "Increase": amount => {
     217        increased = true;
     218        TS_ASSERT(false);
     219    },
     220    "IsUnhealable": () => false,
     221    "IsInjured": () => false
     222});
     223cmpTimer.OnUpdate({ "turnLength": 2.2 });
     224TS_ASSERT(!increased);
Note: See TracChangeset for help on using the changeset viewer.