1
0
forked from 0ad/0ad

Add Preferred and RestrictedClasses, based on patch by Zsol. Fixes #1144.

This was SVN commit r11710.
This commit is contained in:
leper 2012-05-01 22:20:08 +00:00
parent 0990f7f395
commit d4347a8466
12 changed files with 195 additions and 19 deletions

View File

@ -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;
}
}

View File

@ -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)

View File

@ -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,

View File

@ -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);

View File

@ -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();

View File

@ -7,6 +7,7 @@
<Crush>0.0</Crush>
<MaxRange>4.0</MaxRange>
<RepeatTime>1000</RepeatTime>
<RestrictedClasses datatype="tokens">Structure</RestrictedClasses>
</Melee>
</Attack>
<Identity>

View File

@ -7,6 +7,7 @@
<Crush>0.0</Crush>
<MaxRange>4.0</MaxRange>
<RepeatTime>1000</RepeatTime>
<RestrictedClasses datatype="tokens">Structure</RestrictedClasses>
</Melee>
</Attack>
<Identity>

View File

@ -7,6 +7,7 @@
<Crush>0.0</Crush>
<MaxRange>4.0</MaxRange>
<RepeatTime>1000</RepeatTime>
<RestrictedClasses datatype="tokens">Structure</RestrictedClasses>
</Melee>
</Attack>
<Identity>

View File

@ -7,6 +7,7 @@
<Crush>0.0</Crush>
<MaxRange>4.0</MaxRange>
<RepeatTime>1000</RepeatTime>
<RestrictedClasses datatype="tokens">Structure</RestrictedClasses>
</Melee>
</Attack>
<Identity>

View File

@ -12,6 +12,7 @@
<Crush>0.0</Crush>
<MaxRange>5.0</MaxRange>
<RepeatTime>1000</RepeatTime>
<RestrictedClasses datatype="tokens">Ship</RestrictedClasses>
</Melee>
</Attack>
<Footprint>

View File

@ -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>

View File

@ -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>