Add Preferred and RestrictedClasses, based on patch by Zsol. Fixes #1144.
This was SVN commit r11710.
This commit is contained in:
parent
0990f7f395
commit
d4347a8466
@ -344,7 +344,7 @@ function getActionInfo(action, target)
|
||||
break;
|
||||
case "attack":
|
||||
if (entState.attack && targetState.hitpoints && enemyOwned)
|
||||
return {"possible": true};
|
||||
return {"possible": Engine.GuiInterfaceCall("CanAttack", {"entity": entState.id, "target": target})};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,26 @@ var bonusesSchema =
|
||||
"</element>" +
|
||||
"</optional>";
|
||||
|
||||
var preferredClassesSchema =
|
||||
"<optional>" +
|
||||
"<element name='PreferredClasses' a:help='Space delimited list of classes preferred for attacking. If an entity has any of theses classes, it is preferred. The classes are in decending order of preference.'>" +
|
||||
"<attribute name='datatype'>" +
|
||||
"<value>tokens</value>" +
|
||||
"</attribute>" +
|
||||
"<text/>" +
|
||||
"</element>" +
|
||||
"</optional>";
|
||||
|
||||
var restrictedClassesSchema =
|
||||
"<optional>" +
|
||||
"<element name='RestrictedClasses' a:help='Space delimited list of classes that cannot be attacked by this entity. If target entity has any of these classes, it cannot be attacked'>" +
|
||||
"<attribute name='datatype'>" +
|
||||
"<value>tokens</value>" +
|
||||
"</attribute>" +
|
||||
"<text/>" +
|
||||
"</element>" +
|
||||
"</optional>";
|
||||
|
||||
Attack.prototype.Schema =
|
||||
"<a:help>Controls the attack abilities and strengths of the unit.</a:help>" +
|
||||
"<a:example>" +
|
||||
@ -38,6 +58,8 @@ Attack.prototype.Schema =
|
||||
"<Multiplier>1.5</Multiplier>" +
|
||||
"</BonusCavMelee>" +
|
||||
"</Bonuses>" +
|
||||
"<RestrictedClasses datatype=\"tokens\">Champion</RestrictedClasses>" +
|
||||
"<PreferredClasses datatype=\"tokens\">Cavalry Infantry</PreferredClasses>" +
|
||||
"</Melee>" +
|
||||
"<Ranged>" +
|
||||
"<Hack>0.0</Hack>" +
|
||||
@ -54,6 +76,7 @@ Attack.prototype.Schema =
|
||||
"<Multiplier>2</Multiplier>" +
|
||||
"</Bonus1>" +
|
||||
"</Bonuses>" +
|
||||
"<RestrictedClasses datatype=\"tokens\">Champion</RestrictedClasses>" +
|
||||
"</Ranged>" +
|
||||
"<Charge>" +
|
||||
"<Hack>10.0</Hack>" +
|
||||
@ -74,6 +97,8 @@ Attack.prototype.Schema =
|
||||
"<data type='positiveInteger'/>" +
|
||||
"</element>" +
|
||||
bonusesSchema +
|
||||
preferredClassesSchema +
|
||||
restrictedClassesSchema +
|
||||
"</interleave>" +
|
||||
"</element>" +
|
||||
"</optional>" +
|
||||
@ -95,6 +120,8 @@ Attack.prototype.Schema =
|
||||
"<ref name='nonNegativeDecimal'/>" +
|
||||
"</element>" +
|
||||
bonusesSchema +
|
||||
preferredClassesSchema +
|
||||
restrictedClassesSchema +
|
||||
"</interleave>" +
|
||||
"</element>" +
|
||||
"</optional>" +
|
||||
@ -107,6 +134,8 @@ Attack.prototype.Schema =
|
||||
"<element name='MaxRange'><ref name='nonNegativeDecimal'/></element>" + // TODO: how do these work?
|
||||
"<element name='MinRange'><ref name='nonNegativeDecimal'/></element>" +
|
||||
bonusesSchema +
|
||||
preferredClassesSchema +
|
||||
restrictedClassesSchema +
|
||||
"</interleave>" +
|
||||
"</element>" +
|
||||
"</optional>";
|
||||
@ -117,6 +146,89 @@ Attack.prototype.Init = function()
|
||||
|
||||
Attack.prototype.Serialize = null; // we have no dynamic state to save
|
||||
|
||||
Attack.prototype.GetAttackTypes = function()
|
||||
{
|
||||
var ret = [];
|
||||
if (this.template.Charge) ret.push("Charge");
|
||||
if (this.template.Melee) ret.push("Melee");
|
||||
if (this.template.Ranged) ret.push("Ranged");
|
||||
return ret;
|
||||
};
|
||||
|
||||
Attack.prototype.GetPreferredClasses = function(type)
|
||||
{
|
||||
if (this.template[type] && this.template[type].PreferredClasses)
|
||||
{
|
||||
return this.template[type].PreferredClasses._string.split(/\s+/);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
Attack.prototype.GetRestrictedClasses = function(type)
|
||||
{
|
||||
if (this.template[type] && this.template[type].RestrictedClasses)
|
||||
{
|
||||
return this.template[type].RestrictedClasses._string.split(/\s+/);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
Attack.prototype.CanAttack = function(target)
|
||||
{
|
||||
const cmpIdentity = Engine.QueryInterface(target, IID_Identity);
|
||||
if (!cmpIdentity)
|
||||
return undefined;
|
||||
|
||||
const targetClasses = cmpIdentity.GetClassesList();
|
||||
|
||||
for each (var type in this.GetAttackTypes())
|
||||
{
|
||||
var canAttack = true;
|
||||
var restrictedClasses = this.GetRestrictedClasses(type);
|
||||
|
||||
for each (var targetClass in targetClasses)
|
||||
{
|
||||
if (restrictedClasses.indexOf(targetClass) != -1)
|
||||
{
|
||||
canAttack = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (canAttack)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns null if we have no preference or the lowest index of a preferred class.
|
||||
*/
|
||||
Attack.prototype.GetPreference = function(target)
|
||||
{
|
||||
const cmpIdentity = Engine.QueryInterface(target, IID_Identity);
|
||||
if (!cmpIdentity)
|
||||
return undefined;
|
||||
|
||||
const targetClasses = cmpIdentity.GetClassesList();
|
||||
|
||||
var minPref = null;
|
||||
for each (var type in this.GetAttackTypes())
|
||||
{
|
||||
for each (var targetClass in targetClasses)
|
||||
{
|
||||
var pref = this.GetPreferredClasses(type).indexOf(targetClass);
|
||||
if (pref != -1 && (minPref === null || minPref > pref))
|
||||
{
|
||||
minPref = pref;
|
||||
}
|
||||
}
|
||||
}
|
||||
return minPref;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the type of the best attack.
|
||||
* TODO: this should probably depend on range, target, etc,
|
||||
@ -124,14 +236,35 @@ Attack.prototype.Serialize = null; // we have no dynamic state to save
|
||||
*/
|
||||
Attack.prototype.GetBestAttack = function()
|
||||
{
|
||||
if (this.template.Ranged)
|
||||
return "Ranged";
|
||||
else if (this.template.Melee)
|
||||
return "Melee";
|
||||
else if (this.template.Charge)
|
||||
return "Charge";
|
||||
else
|
||||
return this.GetAttackTypes().pop();
|
||||
};
|
||||
|
||||
Attack.prototype.GetBestAttackAgainst = function(target)
|
||||
{
|
||||
const cmpIdentity = Engine.QueryInterface(target, IID_Identity);
|
||||
if (!cmpIdentity)
|
||||
return undefined;
|
||||
|
||||
const targetClasses = cmpIdentity.GetClassesList();
|
||||
const isTargetClass = function (value, i, a) { return targetClasses.indexOf(value) != -1; };
|
||||
const types = this.GetAttackTypes();
|
||||
const attack = this;
|
||||
const isAllowed = function (value, i, a) { return !attack.GetRestrictedClasses(value).some(isTargetClass); }
|
||||
const isPreferred = function (value, i, a) { return attack.GetPreferredClasses(value).some(isTargetClass); }
|
||||
const byPreference = function (a, b) { return (types.indexOf(a) + (isPreferred(a) ? types.length : 0) ) - (types.indexOf(b) + (isPreferred(b) ? types.length : 0) ); }
|
||||
|
||||
return types.filter(isAllowed).sort(byPreference).pop();
|
||||
};
|
||||
|
||||
Attack.prototype.CompareEntitiesByPreference = function(a, b)
|
||||
{
|
||||
var aPreference = this.GetPreference(a);
|
||||
var bPreference = this.GetPreference(b);
|
||||
|
||||
if (aPreference === null && bPreference === null) return 0;
|
||||
if (aPreference === null) return 1;
|
||||
if (bPreference === null) return -1;
|
||||
return aPreference - bPreference;
|
||||
};
|
||||
|
||||
Attack.prototype.GetTimers = function(type)
|
||||
|
@ -171,7 +171,7 @@ GuiInterface.prototype.GetEntityState = function(player, ent)
|
||||
var cmpAttack = Engine.QueryInterface(ent, IID_Attack);
|
||||
if (cmpAttack)
|
||||
{
|
||||
var type = cmpAttack.GetBestAttack(); // TODO: how should we decide which attack to show?
|
||||
var type = cmpAttack.GetBestAttack(); // TODO: how should we decide which attack to show? show all?
|
||||
ret.attack = cmpAttack.GetAttackStrengths(type);
|
||||
}
|
||||
|
||||
@ -273,9 +273,8 @@ GuiInterface.prototype.GetEntityState = function(player, ent)
|
||||
if (cmpUnitAI)
|
||||
{
|
||||
ret.unitAI = {
|
||||
// TODO: reading properties directly is kind of violating abstraction
|
||||
"state": cmpUnitAI.fsmStateName,
|
||||
"orders": cmpUnitAI.orderQueue,
|
||||
"state": cmpUnitAI.GetCurrentState(),
|
||||
"orders": cmpUnitAI.GetOrders(),
|
||||
};
|
||||
}
|
||||
|
||||
@ -838,6 +837,15 @@ GuiInterface.prototype.GetTradingDetails = function(player, data)
|
||||
return result;
|
||||
};
|
||||
|
||||
GuiInterface.prototype.CanAttack = function(player, data)
|
||||
{
|
||||
var cmpAttack = Engine.QueryInterface(data.entity, IID_Attack);
|
||||
if (!cmpAttack)
|
||||
return false;
|
||||
|
||||
return cmpAttack.CanAttack(data.target);
|
||||
};
|
||||
|
||||
GuiInterface.prototype.SetPathfinderDebugOverlay = function(player, enabled)
|
||||
{
|
||||
var cmpPathfinder = Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder);
|
||||
@ -902,6 +910,7 @@ var exposedFunctions = {
|
||||
"PlaySound": 1,
|
||||
"FindIdleUnit": 1,
|
||||
"GetTradingDetails": 1,
|
||||
"CanAttack": 1,
|
||||
|
||||
"SetPathfinderDebugOverlay": 1,
|
||||
"SetObstructionDebugOverlay": 1,
|
||||
|
@ -249,7 +249,7 @@ var UnitFsmSpec = {
|
||||
}
|
||||
|
||||
// Work out how to attack the given target
|
||||
var type = this.GetBestAttack();
|
||||
var type = this.GetBestAttackAgainst(this.order.data.target);
|
||||
if (!type)
|
||||
{
|
||||
// Oops, we can't attack at all
|
||||
@ -336,7 +336,7 @@ var UnitFsmSpec = {
|
||||
if (this.MustKillGatherTarget(this.order.data.target) && this.CheckTargetVisible(this.order.data.target))
|
||||
{
|
||||
// Make sure we can attack the target, else we'll get very stuck
|
||||
if (!this.GetBestAttack())
|
||||
if (!this.GetBestAttackAgainst(this.order.data.target))
|
||||
{
|
||||
// Oops, we can't attack at all - give up
|
||||
// TODO: should do something so the player knows why this failed
|
||||
@ -644,10 +644,10 @@ var UnitFsmSpec = {
|
||||
if (this.GetStance().targetVisibleEnemies)
|
||||
{
|
||||
// Start attacking one of the newly-seen enemy (if any)
|
||||
this.RespondToTargetedEntities(msg.data.added);
|
||||
this.RespondToTargetedEntities(this.GetAttackableEntitiesByPreference(msg.data.added));
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
"LosHealRangeUpdate": function(msg) {
|
||||
this.RespondToHealableEntities(msg.data.added);
|
||||
},
|
||||
@ -2437,6 +2437,14 @@ UnitAI.prototype.GetBestAttack = function()
|
||||
return cmpAttack.GetBestAttack();
|
||||
};
|
||||
|
||||
UnitAI.prototype.GetBestAttackAgainst = function(target)
|
||||
{
|
||||
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
|
||||
if (!cmpAttack)
|
||||
return undefined;
|
||||
return cmpAttack.GetBestAttackAgainst(target);
|
||||
};
|
||||
|
||||
/**
|
||||
* Try to find one of the given entities which can be attacked,
|
||||
* and start attacking it.
|
||||
@ -2462,9 +2470,9 @@ UnitAI.prototype.AttackVisibleEntity = function(ents)
|
||||
*/
|
||||
UnitAI.prototype.AttackEntityInZone = function(ents)
|
||||
{
|
||||
var type = this.GetBestAttack();
|
||||
for each (var target in ents)
|
||||
{
|
||||
var type = this.GetBestAttackAgainst(target);
|
||||
if (this.CanAttack(target) && this.CheckTargetDistanceFromHeldPosition(target, IID_Attack, type))
|
||||
{
|
||||
this.PushOrderFront("Attack", { "target": target, "force": false });
|
||||
@ -2928,8 +2936,7 @@ UnitAI.prototype.FindNewTargets = function()
|
||||
if (!this.GetStance().targetVisibleEnemies)
|
||||
return false;
|
||||
|
||||
SortEntitiesByPriority(ents);
|
||||
return this.RespondToTargetedEntities(ents);
|
||||
return this.RespondToTargetedEntities(this.GetAttackableEntitiesByPreference(ents));
|
||||
};
|
||||
|
||||
/**
|
||||
@ -3023,6 +3030,9 @@ UnitAI.prototype.CanAttack = function(target)
|
||||
// Verify that we're able to respond to Attack commands
|
||||
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
|
||||
if (!cmpAttack)
|
||||
return false;
|
||||
|
||||
if (!cmpAttack.CanAttack(target))
|
||||
return false;
|
||||
|
||||
// Verify that the target is alive
|
||||
@ -3266,4 +3276,15 @@ UnitAI.prototype.WalkToHeldPosition = function()
|
||||
return false;
|
||||
};
|
||||
|
||||
UnitAI.prototype.GetAttackableEntitiesByPreference = function(ents)
|
||||
{
|
||||
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
|
||||
if (!cmpAttack)
|
||||
return [];
|
||||
|
||||
return ents
|
||||
.filter(function (v, i, a) { return cmpAttack.CanAttack(v); })
|
||||
.sort(function (a, b) { return cmpAttack.CompareEntitiesByPreference(a, b); });
|
||||
};
|
||||
|
||||
Engine.RegisterComponentType(IID_UnitAI, "UnitAI", UnitAI);
|
||||
|
@ -85,7 +85,10 @@ function TestFormationExiting(mode)
|
||||
AddMock(unit, IID_Attack, {
|
||||
GetRange: function() { return 10; },
|
||||
GetBestAttack: function() { return "melee"; },
|
||||
GetBestAttackAgainst: function(t) { return "melee"; },
|
||||
GetTimers: function() { return { "prepare": 500, "repeat": 1000 }; },
|
||||
CanAttack: function(v) { return true; },
|
||||
CompareEntitiesByPreference: function(a, b) { return 0; },
|
||||
});
|
||||
|
||||
unitAI.OnCreate();
|
||||
|
@ -7,6 +7,7 @@
|
||||
<Crush>0.0</Crush>
|
||||
<MaxRange>4.0</MaxRange>
|
||||
<RepeatTime>1000</RepeatTime>
|
||||
<RestrictedClasses datatype="tokens">Structure</RestrictedClasses>
|
||||
</Melee>
|
||||
</Attack>
|
||||
<Identity>
|
||||
|
@ -7,6 +7,7 @@
|
||||
<Crush>0.0</Crush>
|
||||
<MaxRange>4.0</MaxRange>
|
||||
<RepeatTime>1000</RepeatTime>
|
||||
<RestrictedClasses datatype="tokens">Structure</RestrictedClasses>
|
||||
</Melee>
|
||||
</Attack>
|
||||
<Identity>
|
||||
|
@ -7,6 +7,7 @@
|
||||
<Crush>0.0</Crush>
|
||||
<MaxRange>4.0</MaxRange>
|
||||
<RepeatTime>1000</RepeatTime>
|
||||
<RestrictedClasses datatype="tokens">Structure</RestrictedClasses>
|
||||
</Melee>
|
||||
</Attack>
|
||||
<Identity>
|
||||
|
@ -7,6 +7,7 @@
|
||||
<Crush>0.0</Crush>
|
||||
<MaxRange>4.0</MaxRange>
|
||||
<RepeatTime>1000</RepeatTime>
|
||||
<RestrictedClasses datatype="tokens">Structure</RestrictedClasses>
|
||||
</Melee>
|
||||
</Attack>
|
||||
<Identity>
|
||||
|
@ -12,6 +12,7 @@
|
||||
<Crush>0.0</Crush>
|
||||
<MaxRange>5.0</MaxRange>
|
||||
<RepeatTime>1000</RepeatTime>
|
||||
<RestrictedClasses datatype="tokens">Ship</RestrictedClasses>
|
||||
</Melee>
|
||||
</Attack>
|
||||
<Footprint>
|
||||
|
@ -21,6 +21,8 @@
|
||||
<Multiplier>3.0</Multiplier>
|
||||
</BonusGates>
|
||||
</Bonuses>
|
||||
<RestrictedClasses datatype="tokens">Organic</RestrictedClasses>
|
||||
<PreferredClasses datatype="tokens">Gates Structure</PreferredClasses>
|
||||
</Melee>
|
||||
<Charge>
|
||||
<Hack>0.0</Hack>
|
||||
@ -38,6 +40,8 @@
|
||||
<Multiplier>3.0</Multiplier>
|
||||
</BonusGates>
|
||||
</Bonuses>
|
||||
<RestrictedClasses datatype="tokens">Organic</RestrictedClasses>
|
||||
<PreferredClasses datatype="tokens">Gates Structure</PreferredClasses>
|
||||
</Charge>
|
||||
</Attack>
|
||||
<Cost>
|
||||
|
@ -12,6 +12,7 @@
|
||||
<Crush>0.0</Crush>
|
||||
<MaxRange>4.0</MaxRange>
|
||||
<RepeatTime>1000</RepeatTime>
|
||||
<RestrictedClasses datatype="tokens">Infantry Cavalry Champion CitizenSoldier</RestrictedClasses>
|
||||
</Melee>
|
||||
</Attack>
|
||||
<Auras>
|
||||
|
Loading…
Reference in New Issue
Block a user