Ticket #999: #999-2012-02-29.patch
File #999-2012-02-29.patch, 45.2 KB (added by , 12 years ago) |
---|
-
0ad/binaries/data/mods/public/simulation/helpers/Commands.js
66 66 }); 67 67 break; 68 68 69 case "heal": 70 if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByAllyOfPlayer(player, cmd.target))) 71 { 72 // This check is for debugging only! 73 warn("Invalid command: heal target is not owned by an ally of or player "+player+" itself: "+uneval(cmd)); 74 } 75 76 // See UnitAI.CanHeal for target checks 77 var entities = FilterEntityList(cmd.entities, player, controlAllUnits); 78 GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) { 79 cmpUnitAI.Heal(cmd.target, cmd.queued); 80 }); 81 break; 82 69 83 case "repair": 70 84 // This covers both repairing damaged buildings, and constructing unfinished foundations 71 85 if (g_DebugCommands && !IsOwnedByAllyOfPlayer(player, cmd.target)) -
0ad/binaries/data/mods/public/simulation/components/GuiInterface.js
155 155 ret.hitpoints = cmpHealth.GetHitpoints(); 156 156 ret.maxHitpoints = cmpHealth.GetMaxHitpoints(); 157 157 ret.needsRepair = cmpHealth.IsRepairable() && (cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints()); 158 ret.needsHeal = !cmpHealth.IsUnhealable() && (cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints()); 158 159 } 159 160 160 161 var cmpAttack = Engine.QueryInterface(ent, IID_Attack); … … 264 265 ret.barterMarket = { "prices": cmpBarter.GetPrices() }; 265 266 } 266 267 268 // Abilities 269 var cmpHeal = Engine.QueryInterface(ent, IID_Heal); 270 // Check if we have abilities 271 if (cmpHeal || (cmpHeal && cmpPromotion)) 272 ret.Ability = []; 273 if (cmpHeal) 274 { 275 ret.Ability.Healer = { 276 "unhealableClasses": cmpHeal.GetUnhealableClasses(), 277 "healableClasses": cmpHeal.GetHealableClasses(), 278 }; 279 } 280 281 // TODO remove this; This is just used to test/demonstrate the extensibility 282 // of the Ability system 283 // promoteAbility (just for healers) 284 if (cmpPromotion && cmpHeal) 285 { 286 ret.Ability.Promote = true ; 287 } 288 267 289 var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); 268 290 ret.visibility = cmpRangeManager.GetLosVisibility(ent, player, false); 269 291 -
0ad/binaries/data/mods/public/simulation/components/interfaces/Heal.js
1 Engine.RegisterInterface("Heal"); -
0ad/binaries/data/mods/public/simulation/components/GarrisonHolder.js
308 308 var cmpHealth = Engine.QueryInterface(entity, IID_Health); 309 309 if (cmpHealth) 310 310 { 311 if (cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints()) 311 // We do not want to heal unhealable units 312 if (!cmpHealth.IsUnhealable() && cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints()) 312 313 cmpHealth.Increase(this.healRate); 313 314 } 314 315 } -
0ad/binaries/data/mods/public/simulation/components/Health.js
25 25 "<value a:help='Remain in the world with 0 health'>remain</value>" + 26 26 "</choice>" + 27 27 "</element>" + 28 "<element name=' Healable' a:help='Indicates that the entity canbe healed by healer units'>" +28 "<element name='Unhealable' a:help='Indicates that the entity can not be healed by healer units'>" + 29 29 "<data type='boolean'/>" + 30 30 "</element>" + 31 31 "<element name='Repairable' a:help='Indicates that the entity can be repaired by builder units'>" + … … 72 72 return (this.template.Repairable == "true"); 73 73 }; 74 74 75 Health.prototype.IsUnhealable = function() 76 { 77 return (this.template.Unhealable == "true"); 78 }; 79 75 80 Health.prototype.Kill = function() 76 81 { 77 82 this.Reduce(this.hitpoints); … … 131 136 { 132 137 // If we're already dead, don't allow resurrection 133 138 if (this.hitpoints == 0) 134 return ;139 return false; 135 140 136 141 var old = this.hitpoints; 137 142 this.hitpoints = Math.min(this.hitpoints + amount, this.GetMaxHitpoints()); 138 143 139 144 Engine.PostMessage(this.entity, MT_HealthChanged, { "from": old, "to": this.hitpoints }); 145 // We return the old and the actual hp 146 return { "old": old, "new": this.hitpoints}; 140 147 }; 141 148 142 149 //// Private functions //// -
0ad/binaries/data/mods/public/simulation/components/Loot.js
21 21 22 22 Loot.prototype.GetXp = function() 23 23 { 24 return this.template.xp;24 return +(this.template.xp || 0); 25 25 }; 26 26 27 27 Loot.prototype.GetResources = function() -
0ad/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js
5 5 Engine.LoadComponentScript("interfaces/DamageReceiver.js"); 6 6 Engine.LoadComponentScript("interfaces/Foundation.js"); 7 7 Engine.LoadComponentScript("interfaces/GarrisonHolder.js"); 8 Engine.LoadComponentScript("interfaces/Heal.js"); 8 9 Engine.LoadComponentScript("interfaces/Health.js"); 9 10 Engine.LoadComponentScript("interfaces/Promotion.js"); 10 11 Engine.LoadComponentScript("interfaces/RallyPoint.js"); … … 270 271 GetHitpoints: function() { return 50; }, 271 272 GetMaxHitpoints: function() { return 60; }, 272 273 IsRepairable: function() { return false; }, 274 IsUnhealable: function() { return false; }, 273 275 }); 274 276 275 277 AddMock(10, IID_Identity, { … … 303 305 hitpoints: 50, 304 306 maxHitpoints: 60, 305 307 needsRepair: false, 308 needsHeal: true, 306 309 buildEntities: ["test1", "test2"], 307 310 barterMarket: { 308 311 prices: { "buy": {"food":150}, "sell": {"food":25} }, -
0ad/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js
4 4 Engine.LoadComponentScript("interfaces/Attack.js"); 5 5 Engine.LoadComponentScript("interfaces/DamageReceiver.js"); 6 6 Engine.LoadComponentScript("interfaces/Formation.js"); 7 Engine.LoadComponentScript("interfaces/Heal.js"); 7 8 Engine.LoadComponentScript("interfaces/Health.js"); 8 9 Engine.LoadComponentScript("interfaces/ResourceSupply.js"); 9 10 Engine.LoadComponentScript("interfaces/Timer.js"); -
0ad/binaries/data/mods/public/simulation/components/UnitAI.js
283 283 this.FinishOrder(); 284 284 }, 285 285 286 "Order.Heal": function(msg) { 287 // Check the target is alive 288 if (!this.TargetIsAlive(this.order.data.target)) 289 { 290 this.FinishOrder(); 291 return; 292 } 293 294 // Check if the target is in range 295 if (this.CheckTargetRange(this.order.data.target, IID_Heal)) 296 { 297 this.StopMoving(); 298 this.SetNextState("INDIVIDUAL.HEAL.HEALING"); 299 return; 300 } 301 302 // If we can't reach the target, but are standing ground, 303 // then abandon this heal order 304 if (this.GetStance().respondStandGround && !this.order.data.force) 305 { 306 this.FinishOrder(); 307 return; 308 } 309 310 // Try to move within heal range 311 if (this.MoveToTargetRange(this.order.data.target, IID_Heal)) 312 { 313 // We've started walking to the given point 314 this.SetNextState("INDIVIDUAL.HEAL.APPROACHING"); 315 return; 316 } 317 318 // We can't reach the target, and can't move towards it, 319 // so abandon this heal order 320 this.FinishOrder(); 321 }, 322 286 323 "Order.Gather": function(msg) { 287 324 288 325 // If the target is still alive, we need to kill it first … … 402 439 cmpFormation.Disband(); 403 440 }, 404 441 442 "Order.Heal": function(msg) { 443 // TODO: see notes in Order.Attack 444 var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); 445 cmpFormation.CallMemberFunction("Heal", [msg.data.target, false]); 446 cmpFormation.Disband(); 447 }, 448 405 449 "Order.Repair": function(msg) { 406 450 // TODO: see notes in Order.Attack 407 451 var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); … … 540 584 // So we'll set a timer here and only report the idle event if we 541 585 // remain idle 542 586 this.StartTimer(1000); 587 588 // If a unit can heal and attack we first want to heal wounded units, 589 // so check if we are a healer and find whether there's anybody nearby to heal. 590 // If anyone approaches later it'll be handled via LosRangeUpdate.) 591 if (this.IsHealer() && this.FindNewHealTargets()) 592 return true; // (abort the FSM transition since we may have already switched state) 543 593 544 594 // If we entered the idle state we must have nothing better to do, 545 595 // so immediately check whether there's anybody nearby to attack. 546 596 // (If anyone approaches later, it'll be handled via LosRangeUpdate.) 547 597 if (this.FindNewTargets()) 548 598 return true; // (abort the FSM transition since we may have already switched state) 549 599 550 600 // Nobody to attack - stay in idle 551 601 return false; 552 602 }, … … 554 604 "leave": function() { 555 605 var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); 556 606 rangeMan.DisableActiveQuery(this.losRangeQuery); 607 if (this.losHealRangeQuery) 608 rangeMan.DisableActiveQuery(this.losHealRangeQuery); 557 609 558 610 this.StopTimer(); 559 611 … … 563 615 Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle }); 564 616 } 565 617 }, 566 618 567 619 "LosRangeUpdate": function(msg) { 620 if (this.IsHealer()) 621 { 622 // Start healing one of the newly-seen own or ally units (if any) 623 this.FindNewHealTargets(); 624 } 568 625 if (this.GetStance().targetVisibleEnemies) 569 626 { 570 627 // Start attacking one of the newly-seen enemy (if any) … … 995 1052 }, 996 1053 }, 997 1054 1055 "HEAL": { 1056 "EntityRenamed": function(msg) { 1057 if (this.order.data.target == msg.entity) 1058 this.order.data.target = msg.newentity; 1059 }, 1060 1061 "Attacked": function(msg) { 1062 // If we stand ground we will rather die than flee 1063 if (!this.GetStance().respondStandGround) 1064 this.Flee(msg.data.attacker, false); 1065 }, 1066 1067 "APPROACHING": { 1068 "enter": function () { 1069 this.SelectAnimation("move"); 1070 this.StartTimer(1000, 1000); 1071 }, 1072 1073 "leave": function() { 1074 this.StopTimer(); 1075 }, 1076 1077 "Timer": function(msg) { 1078 if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force)) 1079 { 1080 this.StopMoving(); 1081 this.FinishOrder(); 1082 1083 // Return to our original position 1084 if (this.GetStance().respondHoldGround) 1085 this.WalkToHeldPosition(); 1086 } 1087 }, 1088 1089 "MoveCompleted": function() { 1090 this.SetNextState("HEALING"); 1091 }, 1092 1093 "Attacked": function(msg) { 1094 // If we stand ground we will rather die than flee 1095 if (!this.GetStance().respondStandGround) 1096 this.Flee(msg.data.attacker, false); 1097 }, 1098 }, 1099 1100 "HEALING": { 1101 "enter": function() { 1102 var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal); 1103 this.healTimers = cmpHeal.GetTimers(); 1104 // this.SelectAnimation("heal", false, 1.0, "heal"); // TODO needs animation 1105 // this.SetAnimationSync(this.healTimers.prepare, this.healTimers.repeat); 1106 this.StartTimer(this.healTimers.prepare, this.healTimers.repeat); 1107 // TODO if .prepare is short, players can cheat by cycling heal/stop/heal 1108 // to beat the .repeat time; should enforce a minimum time 1109 // see comment in ATTACKING.enter 1110 this.FaceTowardsTarget(this.order.data.target); 1111 }, 1112 1113 "leave": function() { 1114 this.StopTimer(); 1115 }, 1116 1117 "Timer": function(msg) { 1118 var target = this.order.data.target; 1119 // Check the target is still alive and healable 1120 if (this.TargetIsAlive(target) && this.CanHeal(target)) 1121 { 1122 // Check if we can still reach the target 1123 if (this.CheckTargetRange(target, IID_Heal)) 1124 { 1125 this.FaceTowardsTarget(target); 1126 var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal); 1127 cmpHeal.PerformHeal(target); 1128 return; 1129 } 1130 // Can't reach it - try to chase after it 1131 if (this.ShouldChaseTargetedEntity(target, this.order.data.force)) 1132 { 1133 if (this.MoveToTargetRange(target, IID_Heal)) 1134 { 1135 this.SetNextState("HEAL.CHASING"); 1136 return; 1137 } 1138 } 1139 } 1140 // Can't reach it, healed to max hp or doesn't exist any more - give up 1141 if (this.FinishOrder()) 1142 return; 1143 1144 // Heal another one 1145 if (this.FindNewHealTargets()) 1146 return; 1147 1148 // Return to our original position 1149 if (this.GetStance().respondHoldGround) 1150 this.WalkToHeldPosition(); 1151 }, 1152 "Attacked": function(msg) { 1153 // If we stand ground we will rather die than flee 1154 if (!this.GetStance().respondStandGround) 1155 this.Flee(msg.data.attacker, false); 1156 }, 1157 }, 1158 "CHASING": { 1159 "enter": function () { 1160 this.SelectAnimation("move"); 1161 this.StartTimer(1000, 1000); 1162 }, 1163 1164 "leave": function () { 1165 this.StopTimer(); 1166 }, 1167 "Timer": function(msg) { 1168 if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force)) 1169 { 1170 this.StopMoving(); 1171 this.FinishOrder(); 1172 1173 // Return to our original position 1174 if (this.GetStance().respondHoldGround) 1175 this.WalkToHeldPosition(); 1176 } 1177 }, 1178 "MoveCompleted": function () { 1179 this.SetNextState("HEALING"); 1180 }, 1181 }, 1182 }, 1183 998 1184 // Returning to dropsite 999 1185 "RETURNRESOURCE": { 1000 1186 "APPROACHING": { … … 1422 1608 return (this.template.NaturalBehaviour ? true : false); 1423 1609 }; 1424 1610 1611 UnitAI.prototype.IsHealer = function() 1612 { 1613 return Engine.QueryInterface(this.entity, IID_Heal); 1614 }; 1615 1425 1616 UnitAI.prototype.IsIdle = function() 1426 1617 { 1427 1618 return this.isIdle; … … 1445 1636 UnitAI.prototype.OnOwnershipChanged = function(msg) 1446 1637 { 1447 1638 this.SetupRangeQuery(); 1639 if (this.IsHealer()) 1640 this.SetupHealRangeQuery(); 1448 1641 }; 1449 1642 1450 1643 UnitAI.prototype.OnDestroy = function() … … 1456 1649 var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); 1457 1650 if (this.losRangeQuery) 1458 1651 rangeMan.DestroyActiveQuery(this.losRangeQuery); 1652 if (this.losHealRangeQuery) 1653 rangeMan.DestroyActiveQuery(this.losHealRangeQuery); 1459 1654 }; 1460 1655 1461 1656 // Set up a range query for all enemy units within LOS range … … 1494 1689 rangeMan.EnableActiveQuery(this.losRangeQuery); 1495 1690 }; 1496 1691 1692 // Set up a range query for all own or ally units within LOS range 1693 // which can be healed. 1694 // This should be called whenever our ownership changes. 1695 UnitAI.prototype.SetupHealRangeQuery = function() 1696 { 1697 var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); 1698 var owner = cmpOwnership.GetOwner(); 1699 1700 var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); 1701 var playerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); 1702 1703 if (this.losHealRangeQuery) 1704 rangeMan.DestroyActiveQuery(this.losHealRangeQuery); 1705 1706 var players = [owner]; 1707 1708 if (owner != -1) 1709 { 1710 // If unit not just killed, get ally players via diplomacy 1711 var cmpPlayer = Engine.QueryInterface(playerMan.GetPlayerByID(owner), IID_Player); 1712 var numPlayers = playerMan.GetNumPlayers(); 1713 for (var i = 1; i < numPlayers; ++i) 1714 { 1715 // Exclude gaia and enemies 1716 if (cmpPlayer.IsAlly(i)) 1717 players.push(i); 1718 } 1719 } 1720 1721 var range = this.GetQueryRange(true); 1722 1723 this.losHealRangeQuery = rangeMan.CreateActiveQuery(this.entity, range.min, range.max, players, IID_Health); 1724 rangeMan.EnableActiveQuery(this.losHealRangeQuery); 1725 }; 1726 1497 1727 //// FSM linkage functions //// 1498 1728 1499 1729 UnitAI.prototype.SetNextState = function(state) … … 1715 1945 1716 1946 UnitAI.prototype.OnRangeUpdate = function(msg) 1717 1947 { 1718 if (msg.tag == this.losRangeQuery )1948 if (msg.tag == this.losRangeQuery || msg.tag == this.losHealRangeQuery) 1719 1949 UnitFsm.ProcessMessage(this, {"type": "LosRangeUpdate", "data": msg}); 1720 1950 }; 1721 1951 … … 1746 1976 }; 1747 1977 1748 1978 /** 1979 * Returns true if the target exists and the current hitpoints are at maximum. 1980 */ 1981 UnitAI.prototype.TargetIsAtMaxHitpoints = function(ent) 1982 { 1983 var cmpHealth = Engine.QueryInterface(ent, IID_Health); 1984 if (!cmpHealth) 1985 return false; 1986 1987 return (cmpHealth.GetHitpoints() == cmpHealth.GetMaxHitpoints()); 1988 }; 1989 1990 /** 1991 * Returns true if the target exists and is unhealable. 1992 */ 1993 UnitAI.prototype.TargetIsUnhealable = function(ent) 1994 { 1995 var cmpHealth = Engine.QueryInterface(ent, IID_Health); 1996 if (!cmpHealth) 1997 return false; 1998 1999 return cmpHealth.IsUnhealable(); 2000 }; 2001 2002 /** 1749 2003 * Returns true if the target exists and needs to be killed before 1750 2004 * beginning to gather resources from it. 1751 2005 */ … … 2236 2490 case "Flee": 2237 2491 case "LeaveFoundation": 2238 2492 case "Attack": 2493 case "Heal": 2239 2494 case "Gather": 2240 2495 case "ReturnResource": 2241 2496 case "Repair": … … 2359 2614 this.AddOrder("GatherNearPosition", { "type": type, "x": position[0], "z": position[1] }, queued); 2360 2615 } 2361 2616 2617 UnitAI.prototype.Heal = function(target, queued) 2618 { 2619 if (!this.CanHeal(target)) 2620 { 2621 this.WalkToTarget(target, queued); 2622 return; 2623 } 2624 2625 this.AddOrder("Heal", { "target": target, "force": true }, queued); 2626 }; 2627 2362 2628 UnitAI.prototype.ReturnResource = function(target, queued) 2363 2629 { 2364 2630 if (!this.CanReturnResource(target, true)) … … 2397 2663 this.stance = stance; 2398 2664 else 2399 2665 error("UnitAI: Setting to invalid stance '"+stance+"'"); 2400 } 2666 }; 2401 2667 2402 2668 UnitAI.prototype.SwitchToStance = function(stance) 2403 2669 { … … 2413 2679 2414 2680 // Reset the range query, since the range depends on stance 2415 2681 this.SetupRangeQuery(); 2416 } 2682 // Just if we are a healer 2683 // TODO maybe move those two to a SetupRangeQuerys() 2684 if (this.IsHealer()) 2685 this.SetupHealRangeQuery(); 2686 }; 2417 2687 2418 2688 /** 2419 2689 * Resets losRangeQuery, and if there are some targets in range that we can … … 2434 2704 return this.RespondToTargetedEntities(ents); 2435 2705 }; 2436 2706 2437 UnitAI.prototype.GetQueryRange = function() 2707 /** 2708 * Resets losHealRangeQuery, and if there are some targets in range that we can heal 2709 * then we start healing and this returns true; otherwise, returns false. 2710 */ 2711 UnitAI.prototype.FindNewHealTargets = function() 2438 2712 { 2713 if (!this.losHealRangeQuery) 2714 return false; 2715 2716 var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); 2717 var ents = rangeMan.ResetActiveQuery(this.losHealRangeQuery); 2718 2719 for each (var ent in ents) 2720 { 2721 if (this.CanHeal(ent)) 2722 { 2723 this.PushOrderFront("Heal", { "target": ent, "force": false }); 2724 return true; 2725 } 2726 } 2727 // We haven't found any target to heal 2728 return false; 2729 }; 2730 2731 UnitAI.prototype.GetQueryRange = function(healer) 2732 { 2439 2733 var ret = { "min": 0, "max": 0 }; 2440 if (this.GetStance().respondStandGround) 2734 // If we have a healer with passive stance we need to set some defaults (eg same as stand ground) 2735 if (this.GetStance().respondStandGround || (healer && !this.GetStance().respondStandGround && !this.GetStance().respondChase && !this.GetStance().respondHoldGround)) 2441 2736 { 2442 var cmpRanged = Engine.QueryInterface(this.entity, IID_Attack);2737 var cmpRanged = Engine.QueryInterface(this.entity, healer?IID_Heal:IID_Attack); 2443 2738 if (!cmpRanged) 2444 2739 return ret; 2445 var range = cmpRanged.GetRange(cmpRanged.GetBestAttack());2740 var range = healer?cmpRanged.GetRange():cmpRanged.GetRange(cmpRanged.GetBestAttack()); 2446 2741 ret.min = range.min; 2447 2742 ret.max = range.max; 2448 2743 } … … 2456 2751 } 2457 2752 else if (this.GetStance().respondHoldGround) 2458 2753 { 2459 var cmpRanged = Engine.QueryInterface(this.entity, IID_Attack);2754 var cmpRanged = Engine.QueryInterface(this.entity, healer?IID_Heal:IID_Attack); 2460 2755 if (!cmpRanged) 2461 2756 return ret; 2462 var range = cmpRanged.GetRange(cmpRanged.GetBestAttack());2757 var range = healer?cmpRanged.GetRange():cmpRanged.GetRange(cmpRanged.GetBestAttack()); 2463 2758 var cmpVision = Engine.QueryInterface(this.entity, IID_Vision); 2464 2759 if (!cmpVision) 2465 2760 return ret; … … 2555 2850 return true; 2556 2851 }; 2557 2852 2853 UnitAI.prototype.CanHeal = function(target) 2854 { 2855 // Formation controllers should always respond to commands 2856 // (then the individual units can make up their own minds) 2857 if (this.IsFormationController()) 2858 return true; 2859 2860 // Verify that we're able to respond to Heal commands 2861 var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal); 2862 if (!cmpHeal) 2863 return false; 2864 2865 // Verify that the target is alive 2866 if (!this.TargetIsAlive(target)) 2867 return false; 2868 2869 // Verify that the target is owned by the same player as the entity or of an ally 2870 var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); 2871 if (!cmpOwnership || !(IsOwnedByPlayer(cmpOwnership.GetOwner(), target) || IsOwnedByAllyOfPlayer(cmpOwnership.GetOwner(), target))) 2872 return false; 2873 2874 // Verify that the target is not unhealable 2875 if (this.TargetIsUnhealable(target)) 2876 { 2877 return false; 2878 } 2879 2880 // Verify that the target has no unhealable class 2881 // We could also use cmpIdentity.GetClassesList but this way is cleaner 2882 var cmpIdentity = Engine.QueryInterface(target, IID_Identity); 2883 if (!cmpIdentity) 2884 return false; 2885 for each (var unhealableClass in cmpHeal.GetUnhealableClasses()) 2886 { 2887 if (cmpIdentity.HasClass(unhealableClass) != -1) 2888 { 2889 return false; 2890 } 2891 } 2892 2893 // Verify that the target is a healable class 2894 // We could also use cmpIdentity.GetClassesList but this way is cleaner 2895 var healable = false; 2896 for each (var healableClass in cmpHeal.GetHealableClasses()) 2897 { 2898 if (cmpIdentity.HasClass(healableClass) != -1) 2899 { 2900 healable = true; 2901 } 2902 } 2903 if (!healable) 2904 return false; 2905 2906 // Check that the target is not at MaxHealth 2907 if (this.TargetIsAtMaxHitpoints(target)) 2908 return false; 2909 2910 return true; 2911 }; 2912 2558 2913 UnitAI.prototype.CanReturnResource = function(target, checkCarriedResource) 2559 2914 { 2560 2915 // Formation controllers should always respond to commands -
0ad/binaries/data/mods/public/simulation/components/Heal.js
1 function Heal() {} 2 3 Heal.prototype.Schema = 4 "<a:help>Controls the healing abilities of the unit.</a:help>" + 5 "<a:example>" + 6 "<Range>20</Range>" + 7 "<HP>5</HP>" + 8 "<Rate>2000</Rate>" + 9 "<UnhealableClasses datatype=\"tokens\">Cavalry</UnhealableClasses>" + 10 "<HealableClasses datatype=\"tokens\">Support Infantry</HealableClasses>" + 11 "</a:example>" + 12 "<element name='Range' a:help='Range (in metres) where healing is possible'>" + 13 "<ref name='nonNegativeDecimal'/>" + 14 "</element>" + 15 "<element name='HP' a:help='Hitpoints healed per Rate'>" + 16 "<ref name='nonNegativeDecimal'/>" + 17 "</element>" + 18 "<element name='Rate' a:help='A heal is performed every Rate ms'>" + 19 "<ref name='nonNegativeDecimal'/>" + 20 "</element>" + 21 "<element name='UnhealableClasses' a:help='If the target has any of these classes it can not be healed (even if it has a class from HealableClasses)'>" + 22 "<attribute name='datatype'>" + 23 "<value>tokens</value>" + 24 "</attribute>" + 25 "<text/>" + 26 "</element>" + 27 "<element name='HealableClasses' a:help='The target must have one of these classes to be healable'>" + 28 "<attribute name='datatype'>" + 29 "<value>tokens</value>" + 30 "</attribute>" + 31 "<text/>" + 32 "</element>"; 33 34 Heal.prototype.Init = function() 35 { 36 }; 37 38 Heal.prototype.Serialize = null; // we have no dynamic state to save 39 40 Heal.prototype.GetTimers = function() 41 { 42 var prepare = 1000; 43 var repeat = +(this.template.Rate || 1000); 44 return { "prepare": prepare, "repeat": repeat }; 45 }; 46 47 Heal.prototype.GetRange = function() 48 { 49 var max = +this.template.Range; 50 var min = 0; 51 return { "max": max, "min": min }; 52 }; 53 54 Heal.prototype.GetUnhealableClasses = function() 55 { 56 var classes = this.template.UnhealableClasses._string; 57 // If we have no unhealable classes defined classes is undefined 58 return classes?classes.split(/\s+/):""; 59 }; 60 61 Heal.prototype.GetHealableClasses = function() 62 { 63 var classes = this.template.HealableClasses._string; 64 return classes.split(/\s+/); 65 }; 66 67 /** 68 * Heal the target entity. This should only be called after a successful range 69 * check, and should only be called after GetTimers().repeat msec has passed 70 * since the last call to PerformHeal. 71 */ 72 Heal.prototype.PerformHeal = function(target) 73 { 74 this.CauseHeal({"target": target}); 75 }; 76 77 /** 78 * Heal target 79 */ 80 Heal.prototype.CauseHeal = function(data) 81 { 82 var cmpHealth = Engine.QueryInterface(data.target, IID_Health); 83 if (!cmpHealth) 84 return; 85 var targetState = cmpHealth.Increase(Math.max(0,this.template.HP)); 86 87 // Add XP 88 var cmpLoot = Engine.QueryInterface(data.target, IID_Loot); 89 var cmpPromotion = Engine.QueryInterface(this.entity, IID_Promotion); 90 if (targetState.old && targetState.new && cmpLoot && cmpPromotion) 91 { 92 // HP healed * XP per HP 93 cmpPromotion.IncreaseXp((targetState.new-targetState.old)*(cmpLoot.GetXp()/cmpHealth.GetMaxHitpoints())); 94 } 95 //TODO we need a sound file 96 // PlaySound("heal_impact", this.entity); 97 }; 98 99 Engine.RegisterComponentType(IID_Heal, "Heal", Heal); -
0ad/binaries/data/mods/public/simulation/templates/template_unit_support_healer.xml
5 5 <Pierce>2.0</Pierce> 6 6 <Crush>2.0</Crush> 7 7 </Armour> 8 <Auras>8 <!-- <Auras> 9 9 <Heal> 10 10 <Radius>20</Radius> 11 11 <Speed>2000</Speed> 12 12 </Heal> 13 </Auras> 13 </Auras>--> 14 <Heal> 15 <Range>30</Range> 16 <HP>5</HP> 17 <Rate>2000</Rate> 18 <UnhealableClasses datatype="tokens"/> 19 <HealableClasses datatype="tokens">Support Infantry Cavalry</HealableClasses> 20 </Heal> 14 21 <Cost> 15 22 <Resources> 16 23 <metal>120</metal> … … 23 30 <Identity> 24 31 <Classes datatype="tokens">Healer</Classes> 25 32 <GenericName>Healer</GenericName> 26 <Tooltip>Heal units within his Aura. (Not implemented yet)</Tooltip>33 <Tooltip>Heal units.</Tooltip> 27 34 </Identity> 35 <Promotion> 36 <RequiredXp>100</RequiredXp> 37 </Promotion> 28 38 <Sound> 29 39 <SoundGroups> 30 40 <select>voice/hellenes/civ/civ_male_select.xml</select> -
0ad/binaries/data/mods/public/simulation/templates/template_structure.xml
33 33 <Health> 34 34 <DeathType>corpse</DeathType> 35 35 <RegenRate>0</RegenRate> 36 < Healable>false</Healable>36 <Unhealable>true</Unhealable> 37 37 <Repairable>true</Repairable> 38 38 </Health> 39 39 <Identity> -
0ad/binaries/data/mods/public/simulation/templates/template_unit.xml
30 30 <DeathType>corpse</DeathType> 31 31 <Max>100</Max> 32 32 <RegenRate>0</RegenRate> 33 < Healable>true</Healable>33 <Unhealable>false</Unhealable> 34 34 <Repairable>false</Repairable> 35 35 </Health> 36 36 <Identity> -
0ad/binaries/data/mods/public/simulation/templates/template_unit_support_slave.xml
36 36 </Resources> 37 37 </Cost> 38 38 <Health> 39 < Healable>false</Healable>39 <Unhealable>true</Unhealable> 40 40 </Health> 41 41 <Identity> 42 42 <Classes datatype="tokens">Slave</Classes> -
0ad/binaries/data/mods/public/simulation/templates/units/pers_support_healer_b.xml
6 6 <SpecificName>Maguš Mada</SpecificName> 7 7 <History>Under both the Medes and later the Persian the tribe of the Magi or the Magians were the masters of religious and oral tradition, comparable to the Levites of the Bible. They were connected to Zoroastrianism, but likely tended to other Iranian cults as well. Aside from religious duties the Magians also functioned as the Great King's bureaucrats and kept his administration running.</History> 8 8 <Icon>units/pers_support_healer.png</Icon> 9 <Rank>Basic</Rank> 9 10 </Identity> 11 <Promotion> 12 <Entity>units/pers_support_healer_a</Entity> 13 </Promotion> 10 14 <VisualActor> 11 15 <Actor>units/persians/healer.xml</Actor> 12 16 </VisualActor> -
0ad/binaries/data/mods/public/simulation/templates/units/cart_support_healer_b.xml
6 6 <History>Tanit (also spelled TINITH, TINNIT, or TINT), chief goddess of Carthage, equivalent of Astarte. Although she seems to have had some connection with the heavens, she was also a mother goddess, and fertility symbols often accompany representations of her. She was probably the consort of Baal Hammon (or Amon), the chief god of Carthage, and was often given the attribute "face of Baal." Although Tanit did not appear at Carthage before the 5th century BC, she soon eclipsed the more established cult of Baal Hammon and, in the Carthaginian area at least, was frequently listed before him on the monuments. In the worship of Tanit and Baal Hammon, children, probably firstborn, were sacrificed. Ample evidence of the practice has been found west of Carthage in the precinct of Tanit, where a tofet (a sanctuary for the sacrifice of children) was discovered. Tanit was also worshipped on Malta, Sardinia, and in Spain. There is no other reason for giving the Carthaginians a priestess instead of a priest in 0 A.D., although Tanit was the most popular of their two main gods with the people. </History> 7 7 <Tooltip>Heal units within her aura. (Not implemented yet)</Tooltip> 8 8 <Icon>units/cart_support_healer.png</Icon> 9 <Rank>Basic</Rank> 9 10 </Identity> 11 <Promotion> 12 <Entity>units/cart_support_healer_a</Entity> 13 </Promotion> 10 14 <VisualActor> 11 15 <Actor>units/carthaginians/healer.xml</Actor> 12 16 </VisualActor> -
0ad/binaries/data/mods/public/simulation/templates/units/rome_support_healer_b.xml
6 6 <SpecificName>Pontifex Minoris</SpecificName> 7 7 <History>During the Republic, the position of priest was elevated and required a lot of responsibilities, which is why priests were by no means chosen randomly. The position of Pontifex Maximus, the high priest of the Roman religion, was occupied by such prominent figures as Julius Caesar, Marcus Aemilius Lepidus and Augustus.</History> 8 8 <Icon>units/rome_support_healer.png</Icon> 9 <Rank>Basic</Rank> 9 10 </Identity> 11 <Promotion> 12 <Entity>units/rome_support_healer_a</Entity> 13 </Promotion> 10 14 <VisualActor> 11 15 <Actor>units/romans/healer.xml</Actor> 12 16 </VisualActor> -
0ad/binaries/data/mods/public/simulation/templates/units/hele_support_healer_b.xml
5 5 <SpecificName>Hiereús</SpecificName> 6 6 <History>The art of medicine was widely practised in Classical Greece. Hippocrates was the first physician to separate religion and superstition from actual medicine, and many others followed his lead.</History> 7 7 <Icon>units/hele_support_healer.png</Icon> 8 <Rank>Basic</Rank> 8 9 </Identity> 10 <Promotion> 11 <Entity>units/hele_support_healer_a</Entity> 12 </Promotion> 9 13 <VisualActor> 10 14 <Actor>units/hellenes/healer.xml</Actor> 11 15 </VisualActor> -
0ad/binaries/data/mods/public/simulation/templates/units/celt_support_healer_b.xml
5 5 <SpecificName>Druides </SpecificName> 6 6 <History>A druid may be one of many different professions; priest, historian, lawyer, judges, teachers, philosophers, poets, composers, musicians, astronomers, prophets, councillors, high craftsmen like a blacksmith, the classes of the 'men of art', and sometimes kings, chieftains, or other politicians. Druids were very hierarchal, with classes and ranks based on the length of their education and what fields they practiced. They learned their trades through mnemonics by way of poetry and songs, as writing was rarely used by Celts outside of prayers on votive objects, or lists of names for migratory records.</History> 7 7 <Icon>units/celt_support_healer.png</Icon> 8 <Rank>Basic</Rank> 8 9 </Identity> 10 <Promotion> 11 <Entity>units/celt_support_healer_a</Entity> 12 </Promotion> 9 13 <VisualActor> 10 14 <Actor>units/celts/healer.xml</Actor> 11 15 </VisualActor> -
0ad/binaries/data/mods/public/simulation/templates/units/iber_support_healer_b.xml
5 5 <SpecificName>Sacerdotisa de Ataekina</SpecificName> 6 6 <History> To the best of our knowledge, only one 'temple'-like structure has been found on the Iberian Peninsula dating from the times and the Iberians worshiped their pantheon of gods at small home altars; however, a very special sculptured head and torso was found in a farmer's field around the turn of the 20th century of a personage who was obviously someone of great substance. As the two principal gods, of the many worshiped, were male Endovellikos and female Ataekina, we thought it would be nice to adopt The Lady of Elche as our priestess-healer representing Ataekina. We know from archelogy and the Romans that Ataekina was associated with spring, the changing of seasons, and nature in general. Ataekina also seems to have been associated with the cycle of birth-death-rebirth.</History> 7 7 <Icon>units/iber_support_healer.png</Icon> 8 <Rank>Basic</Rank> 8 9 </Identity> 10 <Promotion> 11 <Entity>units/iber_support_healer_a</Entity> 12 </Promotion> 9 13 <VisualActor> 10 14 <Actor>units/iberians/healer.xml</Actor> 11 15 </VisualActor> -
0ad/binaries/data/mods/public/simulation/templates/template_unit_mechanical.xml
7 7 <SinkAccel>2.0</SinkAccel> 8 8 </Decay> 9 9 <Health> 10 < Healable>false</Healable>10 <Unhealable>true</Unhealable> 11 11 <Repairable>true</Repairable> 12 12 </Health> 13 13 <Identity> -
0ad/binaries/data/mods/public/simulation/templates/template_unit_fauna_hunt_whale.xml
4 4 <Max>100</Max> 5 5 <DeathType>remain</DeathType> 6 6 <RegenRate>1</RegenRate> 7 < Healable>false</Healable>7 <Unhealable>true</Unhealable> 8 8 <Repairable>false</Repairable> 9 9 </Health> 10 10 <Identity> -
0ad/binaries/data/mods/public/gui/session/session.xml
725 725 </object> 726 726 </object> 727 727 728 <object name="unitAbilityPanel" 729 size="14 12 100% 100%" 730 > 731 <object size="0 0 100% 100%"> 732 <repeat count="24"> 733 <object name="unitAbilityButton[n]" hidden="true" style="iconButton" type="button" size="0 0 46 46" tooltip_style="sessionToolTipBottom"> 734 <object name="unitAbilityIcon[n]" type="image" ghost="true" size="3 3 43 43"/> 735 </object> 736 </repeat> 737 </object> 738 </object> 739 728 740 <object name="unitResearchPanel" 729 741 style="TranslucentPanelThinBorder" 730 742 size="0 100%-56 100% 100%" -
0ad/binaries/data/mods/public/gui/session/input.js
15 15 const ACTION_NONE = 0; 16 16 const ACTION_GARRISON = 1; 17 17 const ACTION_REPAIR = 2; 18 const ACTION_HEAL = 3; 18 19 var preSelectedAction = ACTION_NONE; 19 20 20 21 var INPUT_NORMAL = 0; … … 255 256 } 256 257 } 257 258 break; 259 case "heal": 260 // The check if the target is unhealable is done by targetState.needsHeal 261 if (isUnit(targetState) && targetState.needsHeal && (playerOwned || allyOwned)) 262 { 263 var unhealableClasses = entState.Ability.Healer.unhealableClasses; 264 for each (var unitClass in targetState.identity.classes) 265 { 266 if (unhealableClasses.indexOf(unitClass) != -1) 267 { 268 return {"possible": false}; 269 } 270 } 271 272 var healableClasses = entState.Ability.Healer.healableClasses; 273 for each (var unitClass in targetState.identity.classes) 274 { 275 if (healableClasses.indexOf(unitClass) != -1) 276 { 277 return {"possible": true}; 278 } 279 } 280 } 281 break; 258 282 case "gather": 259 283 if (targetState.resourceSupply && (playerOwned || gaiaOwned)) 260 284 { … … 350 374 else 351 375 return {"type": "none", "cursor": "action-repair-disabled", "target": undefined}; 352 376 break; 377 case ACTION_HEAL: 378 if (getActionInfo("heal", target).possible) 379 return {"type": "heal", "cursor": "action-heal", "target": target}; 380 else 381 return {"type": "none", "cursor": "action-heal-disabled", "target": undefined}; 382 break; 353 383 } 354 384 } 355 385 else if (Engine.HotkeyIsPressed("session.garrison")) … … 993 1023 Engine.GuiInterfaceCall("PlaySound", { "name": "order_attack", "entity": selection[0] }); 994 1024 return true; 995 1025 1026 case "promote": 1027 Engine.PostNetworkCommand({"type": "promote", "entities": selection, "target": action.target, "queued": queued}); 1028 //TODO play sound??? 1029 return true; 1030 1031 case "heal": 1032 Engine.PostNetworkCommand({"type": "heal", "entities": selection, "target": action.target, "queued": queued}); 1033 //TODO play sound 1034 // Engine.GuiInterfaceCall("PlaySound", { "name": "order_heal", "entity": selection[0] }); 1035 return true; 1036 996 1037 case "build": // (same command as repair) 997 1038 case "repair": 998 1039 Engine.PostNetworkCommand({"type": "repair", "entities": selection, "target": action.target, "autocontinue": true, "queued": queued}); … … 1205 1246 inputState = INPUT_PRESELECTEDACTION; 1206 1247 preSelectedAction = ACTION_REPAIR; 1207 1248 break; 1249 case "heal": 1250 inputState = INPUT_PRESELECTEDACTION; 1251 preSelectedAction = ACTION_HEAL; 1252 break; 1253 case "promote": 1254 doAction({ "type": "promote"}) 1255 break; 1208 1256 case "unload-all": 1209 1257 unloadAll(entity); 1210 1258 break; -
0ad/binaries/data/mods/public/gui/session/unit_commands.js
5 5 const FORMATION = "Formation"; 6 6 const TRAINING = "Training"; 7 7 const CONSTRUCTION = "Construction"; 8 const ABILITY = "Ability"; 8 9 const COMMAND = "Command"; 9 10 const STANCE = "Stance"; 10 11 … … 20 21 const BARTER_ACTIONS = ["Sell", "Buy"]; 21 22 22 23 // The number of currently visible buttons (used to optimise showing/hiding) 23 var g_unitPanelButtons = {"Selection": 0, "Queue": 0, "Formation": 0, "Garrison": 0, "Barter": 0, "Training": 0, "Construction": 0, " Command": 0, "Stance": 0};24 var g_unitPanelButtons = {"Selection": 0, "Queue": 0, "Formation": 0, "Garrison": 0, "Barter": 0, "Training": 0, "Construction": 0, "Ability": 0, "Command": 0, "Stance": 0}; 24 25 25 26 // Unit panels are panels with row(s) of buttons 26 var g_unitPanels = ["Selection", "Queue", "Formation", "Garrison", "Barter", "Training", "Construction", " Research", "Stance", "Command"];27 var g_unitPanels = ["Selection", "Queue", "Formation", "Garrison", "Barter", "Training", "Construction", "Ability", "Research", "Stance", "Command"]; 27 28 28 29 // Indexes of resources to sell and buy on barter panel 29 30 var g_barterSell = 0; … … 174 175 numberOfItems = 24; 175 176 break; 176 177 178 case ABILITY: 179 if (numberOfItems > 24) 180 numberOfItems = 24; 181 break; 182 177 183 case COMMAND: 178 184 if (numberOfItems > 6) 179 185 numberOfItems = 6; … … 190 196 var item = items[i]; 191 197 var entType = ((guiName == "Queue")? item.template : item); 192 198 var template; 193 if (guiName != "Formation" && guiName != "Command" && guiName != "Stance" )199 if (guiName != "Formation" && guiName != "Command" && guiName != "Stance" && guiName != "Ability") 194 200 { 195 201 template = GetTemplateData(entType); 196 202 if (!template) … … 269 275 270 276 break; 271 277 278 case ABILITY: 279 // TODO read tooltips from some file or template based on 'item' 280 var tooltip; 281 switch(item) 282 { 283 case "heal": 284 tooltip = "Heal units"; 285 break; 286 case "promote": 287 tooltip = "Promote this unit"; 288 break; 289 default: 290 tooltip = "No tooltip defined"; 291 break; 292 } 293 break; 294 272 295 case COMMAND: 273 296 // here, "item" is an object with properties .name (command name), .tooltip and .icon (relative to session/icons/single) 274 297 if (item.name == "unload-all") … … 338 361 icon.sprite = "stretched:session/icons/single/" + item.icon; 339 362 340 363 } 364 else if (guiName == "Ability") 365 { 366 icon.sprite = "stretched:session/icons/single/"+item+".png"; 367 } 341 368 else if (template.icon) 342 369 { 343 370 icon.sprite = "stretched:session/portraits/" + template.icon; … … 511 538 setupUnitBarterPanel(entState); 512 539 } 513 540 541 if (entState.Ability) 542 { 543 var abilities = []; 544 if (entState.Ability.Healer) 545 abilities.push("heal"); 546 if (entState.Ability.Promote) 547 abilities.push("promote"); 548 setupUnitPanel("Ability", usedPanels, entState, abilities, function (item) { performCommand(entState.id, item); }); 549 } 550 514 551 if (entState.buildEntities && entState.buildEntities.length) 515 552 { 516 553 setupUnitPanel("Construction", usedPanels, entState, entState.buildEntities, startBuildingPlacement); -
0ad/binaries/data/mods/public/art/textures/cursors/action-heal-disabled.txt
1 1 1 -
0ad/binaries/data/mods/public/art/textures/cursors/action-heal.txt
1 1 1