Changeset 25207
- Timestamp:
- Apr 8, 2021, 7:40:49 AM (3 years ago)
- 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 50 50 }; 51 51 52 // We have no dynamic state to save.53 Heal.prototype.Serialize = null;54 55 52 Heal.prototype.GetTimers = function() 56 53 { … … 98 95 { 99 96 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 104 100 let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); 105 101 if (!cmpOwnership || !IsOwnedByAllyOfPlayer(cmpOwnership.GetOwner(), target)) 106 102 return false; 107 103 108 // Verify that the target has the right class.109 104 let cmpIdentity = Engine.QueryInterface(target, IID_Identity); 110 105 if (!cmpIdentity) … … 130 125 131 126 /** 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 */ 131 Heal.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 */ 172 Heal.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 */ 204 Heal.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); 142 224 let targetState = cmpHealth.Increase(this.GetHealth()); 143 225 144 226 // Add experience. 145 let cmpLoot = Engine.QueryInterface(t arget, IID_Loot);227 let cmpLoot = Engine.QueryInterface(target, IID_Loot); 146 228 let cmpPromotion = Engine.QueryInterface(this.entity, IID_Promotion); 147 229 if (targetState !== undefined && cmpLoot && cmpPromotion) 148 {149 230 // Health healed times experience per health. 150 231 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 */ 259 Heal.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); 154 264 }; 155 265 -
ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js
r25206 r25207 2680 2680 "HEALING": { 2681 2681 "enter": function() { 2682 2683 2684 2685 2686 2687 2688 2682 2689 if (!this.CheckRange(this.order.data, IID_Heal)) 2683 2690 { 2684 this. SetNextState("APPROACHING");2691 this."); 2685 2692 return true; 2686 2693 } 2687 2694 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"); 2692 2698 return true; 2693 2699 } 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;2714 2700 2715 2701 this.FaceTowardsTarget(this.order.data.target); … … 2718 2704 2719 2705 "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 2728 2720 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"); 2759 2725 }, 2760 2726 }, … … 3411 3377 // For preventing increased action rate due to Stop orders or target death. 3412 3378 this.lastAttacked = undefined; 3413 this.lastHealed = undefined;3414 3379 3415 3380 this.formationAnimationVariant = undefined; -
ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Heal.js
r25087 r25207 6 6 Engine.LoadComponentScript("interfaces/Loot.js"); 7 7 Engine.LoadComponentScript("interfaces/Promotion.js"); 8 8 9 Engine.LoadComponentScript("interfaces/UnitAI.js"); 9 10 Engine.LoadComponentScript("Heal.js"); 11 10 12 11 13 const entity = 60; … … 13 15 const otherPlayer = 2; 14 16 17 18 19 20 15 21 let template = { 16 "Range": 20,22 "Range": , 17 23 "RangeOverlay": { 18 24 "LineTexture": "heal_overlay_range.png", 19 25 "LineTextureMask": "heal_overlay_range_mask.png", 20 "LineThickness": 0.3521 }, 22 "Health": 5,23 "Interval": 2000,26 "LineThickness": 27 }, 28 "Health": , 29 "Interval": , 24 30 "UnhealableClasses": { "_string": "Cavalry" }, 25 "HealableClasses": { "_string": "Support Infantry" } ,31 "HealableClasses": { "_string": "Support Infantry" } 26 32 }; 27 33 … … 35 41 36 42 AddMock(player, IID_Player, { 37 "IsAlly": ( ) => true43 "IsAlly": ( 38 44 }); 39 45 40 46 AddMock(otherPlayer, IID_Player, { 41 "IsAlly": ( ) => false47 "IsAlly": ( 42 48 }); 43 49 … … 81 87 }]); 82 88 83 // Test PerformHeal89 // Test 84 90 let target = 70; 85 86 91 AddMock(target, IID_Ownership, { 87 92 "GetOwner": () => player 88 93 }); 89 94 90 let targetClasses ;95 let targetClasses; 91 96 AddMock(target, IID_Identity, { 92 97 "GetClassesList": () => targetClasses 93 98 }); 94 99 100 95 101 let increased; 96 102 let unhealable = false; … … 102 108 return { "old": 600, "new": 600 + 5 + 100 }; 103 109 }, 104 "IsUnhealable": () => unhealable 105 }); 106 107 cmpHeal.PerformHeal(target); 110 "IsUnhealable": () => unhealable, 111 "IsInjured": () => true 112 }); 113 114 TS_ASSERT(cmpHeal.StartHealing(target)); 115 cmpTimer.OnUpdate({ "turnLength": 1 }); 108 116 TS_ASSERT(increased); 109 117 increased = false; 118 cmpTimer.OnUpdate({ "turnLength": 2.2 }); 119 TS_ASSERT(increased); 120 121 // Test we can't heal too quickly. 122 increased = false; 123 TS_ASSERT(cmpHeal.StartHealing(target)); 124 cmpTimer.OnUpdate({ "turnLength": 2 }); 125 TS_ASSERT(!increased); 126 127 // Test experience. 110 128 let looted; 111 129 AddMock(target, IID_Loot, { … … 124 142 125 143 increased = false; 126 cmpHeal.PerformHeal(target); 144 TS_ASSERT(cmpHeal.StartHealing(target)); 145 cmpTimer.OnUpdate({ "turnLength": 1 }); 127 146 TS_ASSERT(increased && looted && promoted); 128 147 … … 130 149 let updated; 131 150 AddMock(entity, IID_UnitAI, { 151 132 152 "UpdateRangeQueries": () => { 133 153 updated = true; … … 162 182 let otherTarget = 71; 163 183 AddMock(otherTarget, IID_Ownership, { 164 "GetOwner": () => player184 "GetOwner": () => layer 165 185 }); 166 186 167 187 AddMock(otherTarget, IID_Health, { 168 "IsUnhealable": () => false 169 }); 170 TS_ASSERT_UNEVAL_EQUALS(cmpHeal.CanHeal(otherTarget), false); 188 "IsUnhealable": () => false, 189 "IsInjured": () => true 190 }); 191 192 AddMock(otherTarget, IID_Identity, { 193 "GetClassesList": () => ["Infantry"] 194 }); 195 TS_ASSERT(!cmpHeal.CanHeal(otherTarget)); 196 197 // Test we stop healing when finished. 198 increased = false; 199 AddMock(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 }); 209 TS_ASSERT(cmpHeal.StartHealing(target)); 210 cmpTimer.OnUpdate({ "turnLength": 2.2 }); 211 TS_ASSERT(increased); 212 213 increased = false; 214 AddMock(target, IID_Health, { 215 "GetMaxHitpoints": () => 700, 216 "Increase": amount => { 217 increased = true; 218 TS_ASSERT(false); 219 }, 220 "IsUnhealable": () => false, 221 "IsInjured": () => false 222 }); 223 cmpTimer.OnUpdate({ "turnLength": 2.2 }); 224 TS_ASSERT(!increased);
Note:
See TracChangeset
for help on using the changeset viewer.