Training limits. Limit heroes to one living per player. Allow heroes to be trained again. Closes #1432

This was SVN commit r12832.
This commit is contained in:
fcxSanya 2012-11-07 17:56:14 +00:00
parent 669b7e6e2c
commit 7e21db08d5
29 changed files with 474 additions and 154 deletions

View file

@ -1467,17 +1467,76 @@ function exchangeResources(command)
var batchTrainingEntities;
var batchTrainingType;
var batchTrainingCount;
var batchTrainingEntityAllowedCount;
const batchIncrementSize = 5;
function flushTrainingBatch()
{
Engine.PostNetworkCommand({"type": "train", "entities": batchTrainingEntities, "template": batchTrainingType, "count": batchTrainingCount});
var appropriateBuildings = getBuidlingsWhichCanTrainEntity(batchTrainingEntities, batchTrainingType);
// If training limits don't allow us to train batchTrainingCount in each appropriate building
if (batchTrainingEntityAllowedCount !== undefined &&
batchTrainingEntityAllowedCount < batchTrainingCount * appropriateBuildings.length)
{
// Train as many full batches as we can
var buildingsCountToTrainFullBatch = Math.floor(batchTrainingEntityAllowedCount / batchTrainingCount);
var buildingsToTrainFullBatch = appropriateBuildings.slice(0, buildingsCountToTrainFullBatch);
Engine.PostNetworkCommand({"type": "train", "entities": buildingsToTrainFullBatch,
"template": batchTrainingType, "count": batchTrainingCount});
// Train remainer in one more building
var remainderToTrain = batchTrainingEntityAllowedCount % batchTrainingCount;
Engine.PostNetworkCommand({"type": "train",
"entities": [ appropriateBuildings[buildingsCountToTrainFullBatch] ],
"template": batchTrainingType, "count": remainderToTrain});
}
else
{
Engine.PostNetworkCommand({"type": "train", "entities": appropriateBuildings,
"template": batchTrainingType, "count": batchTrainingCount});
}
}
function getBuidlingsWhichCanTrainEntity(entitiesToCheck, trainEntType)
{
return entitiesToCheck.filter(function(entity) {
var state = GetEntityState(entity);
var canTrain = state && state.production && state.production.entities.length &&
state.production.entities.indexOf(trainEntType) != -1;
return canTrain;
});
}
function getEntityLimitAndCount(playerState, entType)
{
var template = GetTemplateData(entType);
var trainingCategory = null;
if (template.trainingRestrictions)
trainingCategory = template.trainingRestrictions.category;
var trainEntLimit = undefined;
var trainEntCount = undefined;
var canBeTrainedCount = undefined;
if (trainingCategory && playerState.entityLimits[trainingCategory])
{
trainEntLimit = playerState.entityLimits[trainingCategory];
trainEntCount = playerState.entityCounts[trainingCategory];
canBeTrainedCount = trainEntLimit - trainEntCount;
}
return [trainEntLimit, trainEntCount, canBeTrainedCount];
}
// Called by GUI when user clicks training button
function addTrainingToQueue(selection, trainEntType)
function addTrainingToQueue(selection, trainEntType, playerState)
{
if (Engine.HotkeyIsPressed("session.batchtrain"))
// Create list of buildings which can train trainEntType
var appropriateBuildings = getBuidlingsWhichCanTrainEntity(selection, trainEntType);
// Check trainEntType entity limit and count
var [trainEntLimit, trainEntCount, canBeTrainedCount] = getEntityLimitAndCount(playerState, trainEntType)
// Batch training possible if we can train at least 2 units
var batchTrainingPossible = canBeTrainedCount == undefined || canBeTrainedCount > 1;
if (Engine.HotkeyIsPressed("session.batchtrain") && batchTrainingPossible)
{
if (inputState == INPUT_BATCHTRAINING)
{
@ -1494,9 +1553,13 @@ function addTrainingToQueue(selection, trainEntType)
}
}
// If we're already creating a batch of this unit (in the same building(s)), then just extend it
// (if training limits allow)
if (sameEnts && batchTrainingType == trainEntType)
{
batchTrainingCount += batchIncrementSize;
if (canBeTrainedCount == undefined ||
canBeTrainedCount > batchTrainingCount * appropriateBuildings.length)
batchTrainingCount += batchIncrementSize;
batchTrainingEntityAllowedCount = canBeTrainedCount;
return;
}
// Otherwise start a new one
@ -1509,12 +1572,18 @@ function addTrainingToQueue(selection, trainEntType)
inputState = INPUT_BATCHTRAINING;
batchTrainingEntities = selection;
batchTrainingType = trainEntType;
batchTrainingEntityAllowedCount = canBeTrainedCount;
batchTrainingCount = batchIncrementSize;
}
else
{
// Non-batched - just create a single entity
Engine.PostNetworkCommand({"type": "train", "template": trainEntType, "count": 1, "entities": selection});
// Non-batched - just create a single entity in each building
// (but no more than entity limit allows)
var buildingsForTraining = appropriateBuildings;
if (trainEntLimit)
buildingsForTraining = buildingsForTraining.slice(0, canBeTrainedCount);
Engine.PostNetworkCommand({"type": "train", "template": trainEntType,
"count": 1, "entities": buildingsForTraining});
}
}
@ -1526,12 +1595,39 @@ function addResearchToQueue(entity, researchType)
// Returns the number of units that will be present in a batch if the user clicks
// the training button with shift down
function getTrainingBatchStatus(entity, trainEntType)
function getTrainingBatchStatus(playerState, entity, trainEntType, selection)
{
if (inputState == INPUT_BATCHTRAINING && batchTrainingEntities.indexOf(entity) != -1 && batchTrainingType == trainEntType)
return [batchTrainingCount, batchIncrementSize];
var apporpriateBuildings = [entity];
if (selection && selection.indexOf(entity) != -1)
appropriateBuildings = getBuidlingsWhichCanTrainEntity(selection, trainEntType);
var nextBatchTrainingCount = 0;
if (inputState == INPUT_BATCHTRAINING && batchTrainingEntities.indexOf(entity) != -1 &&
batchTrainingType == trainEntType)
{
nextBatchTrainingCount = batchTrainingCount;
var canBeTrainedCount = batchTrainingEntityAllowedCount;
}
else
return [0, batchIncrementSize];
{
var [trainEntLimit, trainEntCount, canBeTrainedCount] =
getEntityLimitAndCount(playerState, trainEntType);
var batchSize = Math.min(canBeTrainedCount, batchIncrementSize);
}
// We need to calculate count after the next increment if it's possible
if (canBeTrainedCount == undefined ||
canBeTrainedCount > nextBatchTrainingCount * appropriateBuildings.length)
nextBatchTrainingCount += batchIncrementSize;
// If training limits don't allow us to train batchTrainingCount in each appropriate building
// train as many full batches as we can and remainer in one more building.
var buildingsCountToTrainFullBatch = appropriateBuildings.length;
var remainderToTrain = 0;
if (canBeTrainedCount !== undefined &&
canBeTrainedCount < nextBatchTrainingCount * appropriateBuildings.length)
{
buildingsCountToTrainFullBatch = Math.floor(canBeTrainedCount / nextBatchTrainingCount);
remainderToTrain = canBeTrainedCount % nextBatchTrainingCount;
}
return [buildingsCountToTrainFullBatch, nextBatchTrainingCount, remainderToTrain];
}
// Called by GUI when user clicks production queue item

View file

@ -138,6 +138,65 @@ function setOverlay(object, value)
object.hidden = !value;
}
/**
* Format entity count/limit message for the tooltip
*/
function formatLimitString(trainEntLimit, trainEntCount)
{
if (trainEntLimit == undefined)
return "";
var text = "\n\nCurrent count: " + trainEntCount + ", limit: " + trainEntLimit + ".";
if (trainEntCount >= trainEntLimit)
text = "[color=\"red\"]" + text + "[/color]";
return text;
}
/**
* Format batch training string for the tooltip
* Examples:
* buildingsCountToTrainFullBatch = 1, fullBatchSize = 5, remainderBatch = 0:
* "Shift-click to train 5"
* buildingsCountToTrainFullBatch = 2, fullBatchSize = 5, remainderBatch = 0:
* "Shift-click to train 10 (2*5)"
* buildingsCountToTrainFullBatch = 1, fullBatchSize = 15, remainderBatch = 12:
* "Shift-click to train 27 (15 + 12)"
*/
function formatBatchTrainingString(buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch)
{
var totalBatchTrainingCount = buildingsCountToTrainFullBatch * fullBatchSize + remainderBatch;
// Don't show the batch training tooltip if either units of this type can't be trained at all
// or only one unit can be trained
if (totalBatchTrainingCount < 2)
return "";
var batchTrainingString = "";
var fullBatchesString = "";
if (buildingsCountToTrainFullBatch > 0)
{
if (buildingsCountToTrainFullBatch > 1)
fullBatchesString += buildingsCountToTrainFullBatch + "*";
fullBatchesString += fullBatchSize;
}
var remainderBatchString = remainderBatch > 0 ? remainderBatch : "";
var batchDetailsString = "";
// We need to display the batch details part if there is either more than
// one building with full batch or one building with the full batch and
// another with a partial batch
if (buildingsCountToTrainFullBatch > 1 ||
(buildingsCountToTrainFullBatch == 1 && remainderBatch > 0))
{
batchDetailsString += " (";
if (fullBatchesString != "" && remainderBatchString != "")
batchDetailsString += fullBatchesString + " + " + remainderBatchString;
else if (fullBatchesString != "")
batchDetailsString += fullBatchesString;
else
batchDetailsString += remainderBatchString;
batchDetailsString += ")";
}
return "\n\n[font=\"serif-bold-13\"]Shift-click[/font][font=\"serif-13\"] to train "
+ totalBatchTrainingCount + batchDetailsString + ".[/font]";
}
/**
* Helper function for updateUnitCommands; sets up "unit panels" (i.e. panels with rows of icons) for the currently selected
* unit.
@ -150,7 +209,7 @@ function setOverlay(object, value)
* @param items Panel-specific data to construct the icons with.
* @param callback Callback function to argument to execute when an item's icon gets clicked. Takes a single 'item' argument.
*/
function setupUnitPanel(guiName, usedPanels, unitEntState, items, callback)
function setupUnitPanel(guiName, usedPanels, unitEntState, playerState, items, callback)
{
usedPanels[guiName] = 1;
@ -374,9 +433,6 @@ function setupUnitPanel(guiName, usedPanels, unitEntState, items, callback)
if (template.tooltip)
tooltip += "\n[font=\"serif-13\"]" + template.tooltip + "[/font]";
var [batchSize, batchIncrement] = getTrainingBatchStatus(unitEntState.id, entType);
var trainNum = batchSize ? batchSize+batchIncrement : batchIncrement;
tooltip += "\n" + getEntityCostTooltip(template);
if (template.health)
@ -388,8 +444,13 @@ function setupUnitPanel(guiName, usedPanels, unitEntState, items, callback)
if (template.speed)
tooltip += "\n" + getEntitySpeed(template);
tooltip += "\n\n[font=\"serif-bold-13\"]Shift-click[/font][font=\"serif-13\"] to train " + trainNum + ".[/font]";
var [trainEntLimit, trainEntCount, canBeTrainedCount] =
getEntityLimitAndCount(playerState, entType)
tooltip += formatLimitString(trainEntLimit, trainEntCount);
var [buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch] =
getTrainingBatchStatus(playerState, unitEntState.id, entType, selection);
tooltip += formatBatchTrainingString(buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch);
break;
case RESEARCH:
@ -627,17 +688,30 @@ function setupUnitPanel(guiName, usedPanels, unitEntState, items, callback)
pair.hidden = true;
button1.hidden = true;
affordableMask1.hidden = true;
}
}
}
else if (guiName == CONSTRUCTION || guiName == TRAINING)
{
if (guiName == TRAINING)
{
var trainingCategory = null;
if (template.trainingRestrictions)
trainingCategory = template.trainingRestrictions.category;
var grayscale = "";
if (trainingCategory && playerState.entityLimits[trainingCategory] &&
playerState.entityCounts[trainingCategory] >= playerState.entityLimits[trainingCategory])
grayscale = "grayscale:";
icon.sprite = "stretched:" + grayscale + "session/portraits/" + template.icon;
}
affordableMask.hidden = true;
var totalCosts = {};
var trainNum = 1;
if (Engine.HotkeyIsPressed("session.batchtrain") && guiName == TRAINING)
{
var [batchSize, batchIncrement] = getTrainingBatchStatus(unitEntState.id, entType);
trainNum = batchSize + batchIncrement;
var [buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch] =
getTrainingBatchStatus(playerState, unitEntState.id, entType, selection);
trainNum = buildingsCountToTrainFullBatch * fullBatchSize + remainderBatch;
}
// Walls have no cost defined.
@ -767,7 +841,7 @@ function setupUnitTradingPanel(usedPanels, unitEntState, selection)
}
// Sets up "unit barter panel" - special case for setupUnitPanel
function setupUnitBarterPanel(unitEntState)
function setupUnitBarterPanel(unitEntState, playerState)
{
// Amount of player's resource to exchange
var amountToSell = BARTER_RESOURCE_AMOUNT_TO_SELL;
@ -862,13 +936,18 @@ function updateUnitCommands(entState, supplementalDetailsPanel, commandsPanel, s
var player = Engine.GetPlayerID();
if (entState.player == player || g_DevSettings.controlAll)
{
// Get player state to check some constraints
// e.g. presence of a hero or build limits
var simState = Engine.GuiInterfaceCall("GetSimulationState");
var playerState = simState.players[player];
if (selection.length > 1)
setupUnitPanel(SELECTION, usedPanels, entState, g_Selection.groups.getTemplateNames(),
setupUnitPanel(SELECTION, usedPanels, entState, playerState, g_Selection.groups.getTemplateNames(),
function (entType) { changePrimarySelectionGroup(entType); } );
var commands = getEntityCommandsList(entState);
if (commands.length)
setupUnitPanel(COMMAND, usedPanels, entState, commands,
setupUnitPanel(COMMAND, usedPanels, entState, playerState, commands,
function (item) { performCommand(entState.id, item.name); } );
if (entState.garrisonHolder)
@ -881,14 +960,14 @@ function updateUnitCommands(entState, supplementalDetailsPanel, commandsPanel, s
groups.add(state.garrisonHolder.entities)
}
setupUnitPanel(GARRISON, usedPanels, entState, groups.getTemplateNames(),
setupUnitPanel(GARRISON, usedPanels, entState, playerState, groups.getTemplateNames(),
function (item) { unloadTemplate(item); } );
}
var formations = Engine.GuiInterfaceCall("GetAvailableFormations");
if (hasClass(entState, "Unit") && !hasClass(entState, "Animal") && !entState.garrisonHolder && formations.length)
{
setupUnitPanel(FORMATION, usedPanels, entState, formations,
setupUnitPanel(FORMATION, usedPanels, entState, playerState, formations,
function (item) { performFormation(entState.id, item); } );
}
@ -897,7 +976,7 @@ function updateUnitCommands(entState, supplementalDetailsPanel, commandsPanel, s
var stances = ["violent", "aggressive", "passive", "defensive", "standground"];
if (hasClass(entState, "Unit") && !hasClass(entState, "Animal") && stances.length)
{
setupUnitPanel(STANCE, usedPanels, entState, stances,
setupUnitPanel(STANCE, usedPanels, entState, playerState, stances,
function (item) { performStance(entState.id, item); } );
}
@ -905,7 +984,7 @@ function updateUnitCommands(entState, supplementalDetailsPanel, commandsPanel, s
if (entState.barterMarket)
{
usedPanels["Barter"] = 1;
setupUnitBarterPanel(entState);
setupUnitBarterPanel(entState, playerState);
}
var buildableEnts = [];
@ -929,10 +1008,10 @@ function updateUnitCommands(entState, supplementalDetailsPanel, commandsPanel, s
// The first selected entity's type has priority.
if (entState.buildEntities)
setupUnitPanel(CONSTRUCTION, usedPanels, entState, buildableEnts, startBuildingPlacement);
setupUnitPanel(CONSTRUCTION, usedPanels, entState, playerState, buildableEnts, startBuildingPlacement);
else if (entState.production && entState.production.entities)
setupUnitPanel(TRAINING, usedPanels, entState, trainableEnts,
function (trainEntType) { addTrainingToQueue(selection, trainEntType); } );
setupUnitPanel(TRAINING, usedPanels, entState, playerState, trainableEnts,
function (trainEntType) { addTrainingToQueue(selection, trainEntType, playerState); } );
else if (entState.trader)
setupUnitTradingPanel(usedPanels, entState, selection);
else if (!entState.foundation && entState.gate || hasClass(entState, "LongWall"))
@ -994,7 +1073,7 @@ function updateUnitCommands(entState, supplementalDetailsPanel, commandsPanel, s
setupUnitPanel(CONSTRUCTION, usedPanels, entState, buildableEnts, startBuildingPlacement);
else if (trainableEnts.length)
setupUnitPanel(TRAINING, usedPanels, entState, trainableEnts,
function (trainEntType) { addTrainingToQueue(selection, trainEntType); } );
function (trainEntType) { addTrainingToQueue(selection, trainEntType, playerState); } );
}
// Show technologies if the active panel has at most one row of icons.
@ -1007,7 +1086,7 @@ function updateUnitCommands(entState, supplementalDetailsPanel, commandsPanel, s
}
if (entState.production && entState.production.queue.length)
setupUnitPanel(QUEUE, usedPanels, entState, entState.production.queue,
setupUnitPanel(QUEUE, usedPanels, entState, playerState, entState.production.queue,
function (item) { removeFromProductionQueue(entState.id, item.id); } );
supplementalDetailsPanel.hidden = false;

View file

@ -416,33 +416,35 @@ var GameState = Class({
* Returns player build limits
* an object where each key is a category corresponding to a build limit for the player.
*/
getBuildLimits: function()
getEntityLimits: function()
{
return this.playerData.buildLimits;
return this.playerData.entityLimits;
},
/**
* Returns player build counts
* an object where each key is a category corresponding to the current building count for the player.
*/
getBuildCounts: function()
getEntityCounts: function()
{
return this.playerData.buildCounts;
return this.playerData.entityCounts;
},
/**
* Checks if the player's build limit has been reached for the given category.
* The category comes from the entity tenplate, specifically the BuildRestrictions component.
* The category comes from the entity template, specifically the
* BuildRestrictions/TrainingRestrictions components.
*/
isBuildLimitReached: function(category)
isEntityLimitReached: function(category)
{
if (this.playerData.buildLimits[category] === undefined || this.playerData.buildCounts[category] === undefined)
if (this.playerData.entityLimits[category] === undefined || this.playerData.entityCounts[category] === undefined)
return false;
// There's a special case of build limits per civ centre, so check that first
if (this.playerData.buildLimits[category].LimitPerCivCentre !== undefined)
return (this.playerData.buildCounts[category] >= this.playerData.buildCounts["CivilCentre"]*this.playerData.buildLimits[category].LimitPerCivCentre);
if (this.playerData.entityLimits[category].LimitPerCivCentre !== undefined)
return (this.playerData.entityCounts[category] >=
this.playerData.entityCounts["CivilCentre"] * this.playerData.entityLimits[category].LimitPerCivCentre);
else
return (this.playerData.buildCounts[category] >= this.playerData.buildLimits[category]);
return (this.playerData.entityCounts[category] >= this.playerData.entityLimits[category]);
},
});

View file

@ -315,22 +315,23 @@ GameState.prototype.getResourceSupplies = function(resource){
return this.updatingCollection("resource-" + resource, Filters.byResource(resource), this.getEntities());
};
GameState.prototype.getBuildLimits = function() {
return this.playerData.buildLimits;
GameState.prototype.getEntityLimits = function() {
return this.playerData.entityLimits;
};
GameState.prototype.getBuildCounts = function() {
return this.playerData.buildCounts;
GameState.prototype.getEntityCounts = function() {
return this.playerData.entityCounts;
};
// Checks whether the maximum number of buildings have been cnstructed for a certain catergory
GameState.prototype.isBuildLimitReached = function(category) {
if(this.playerData.buildLimits[category] === undefined || this.playerData.buildCounts[category] === undefined)
// Checks whether the maximum number of buildings have been constructed for a certain catergory
GameState.prototype.isEntityLimitReached = function(category) {
if(this.playerData.entityLimits[category] === undefined || this.playerData.entityCounts[category] === undefined)
return false;
if(this.playerData.buildLimits[category].LimitsPerCivCentre != undefined)
return (this.playerData.buildCounts[category] >= this.playerData.buildCounts["CivilCentre"]*this.playerData.buildLimits[category].LimitPerCivCentre);
if(this.playerData.entityLimits[category].LimitsPerCivCentre != undefined)
return (this.playerData.entityCounts[category] >=
this.playerData.entityCounts["CivilCentre"] * this.playerData.entityLimits[category].LimitPerCivCentre);
else
return (this.playerData.buildCounts[category] >= this.playerData.buildLimits[category]);
return (this.playerData.entityCounts[category] >= this.playerData.entityLimits[category]);
};
GameState.prototype.findTrainableUnits = function(classes){

View file

@ -368,7 +368,7 @@ MilitaryAttackManager.prototype.measureEnemyStrength = function(gameState){
// Adds towers to the defenceBuilding queue
MilitaryAttackManager.prototype.buildDefences = function(gameState, queues){
if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv('structures/{civ}_defense_tower'))
+ queues.defenceBuilding.totalLength() < gameState.getBuildLimits()["DefenseTower"]) {
+ queues.defenceBuilding.totalLength() < gameState.getEntityLimits()["DefenseTower"]) {
gameState.getOwnEntities().forEach(function(dropsiteEnt) {
if (dropsiteEnt.resourceDropsiteTypes() && dropsiteEnt.getMetadata("defenseTower") !== true){
@ -386,7 +386,7 @@ MilitaryAttackManager.prototype.buildDefences = function(gameState, queues){
numFortresses += gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bFort[i]));
}
if (numFortresses + queues.defenceBuilding.totalLength() < 1){ //gameState.getBuildLimits()["Fortress"]) {
if (numFortresses + queues.defenceBuilding.totalLength() < 1){ //gameState.getEntityLimits()["Fortress"]) {
if (gameState.getTimeElapsed() > 840 * 1000 + numFortresses * 300 * 1000){
if (gameState.ai.pathsToMe && gameState.ai.pathsToMe.length > 0){
var position = gameState.ai.pathsToMe.shift();

View file

@ -303,20 +303,21 @@ GameState.prototype.getResourceSupplies = function(resource){
return this.updatingCollection("resource-" + resource, Filters.byResource(resource), this.getEntities());
};
GameState.prototype.getBuildLimits = function() {
return this.playerData.buildLimits;
GameState.prototype.getEntityLimits = function() {
return this.playerData.entityLimits;
};
GameState.prototype.getBuildCounts = function() {
return this.playerData.buildCounts;
GameState.prototype.getEntityCounts = function() {
return this.playerData.entityCounts;
};
// Checks whether the maximum number of buildings have been cnstructed for a certain catergory
GameState.prototype.isBuildLimitReached = function(category) {
if(this.playerData.buildLimits[category] === undefined || this.playerData.buildCounts[category] === undefined)
// Checks whether the maximum number of buildings have been constructed for a certain catergory
GameState.prototype.isEntityLimitReached = function(category) {
if(this.playerData.entityLimits[category] === undefined || this.playerData.entityCounts[category] === undefined)
return false;
if(this.playerData.buildLimits[category].LimitsPerCivCentre != undefined)
return (this.playerData.buildCounts[category] >= this.playerData.buildCounts["CivilCentre"]*this.playerData.buildLimits[category].LimitPerCivCentre);
if(this.playerData.entityLimits[category].LimitsPerCivCentre != undefined)
return (this.playerData.entityCounts[category] >=
this.playerData.entityCounts["CivilCentre"] * this.playerData.entityLimits[category].LimitPerCivCentre);
else
return (this.playerData.buildCounts[category] >= this.playerData.buildLimits[category]);
return (this.playerData.entityCounts[category] >= this.playerData.entityLimits[category]);
};

View file

@ -324,7 +324,7 @@ MilitaryAttackManager.prototype.measureEnemyStrength = function(gameState){
// Adds towers to the defenceBuilding queue
MilitaryAttackManager.prototype.buildDefences = function(gameState, queues){
if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv('structures/{civ}_defense_tower'))
+ queues.defenceBuilding.totalLength() < gameState.getBuildLimits()["DefenseTower"]) {
+ queues.defenceBuilding.totalLength() < gameState.getEntityLimits()["DefenseTower"]) {
gameState.getOwnEntities().forEach(function(dropsiteEnt) {
@ -343,7 +343,7 @@ MilitaryAttackManager.prototype.buildDefences = function(gameState, queues){
numFortresses += gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bFort[i]));
}
if (numFortresses + queues.defenceBuilding.totalLength() < gameState.getBuildLimits()["Fortress"]) {
if (numFortresses + queues.defenceBuilding.totalLength() < gameState.getEntityLimits()["Fortress"]) {
if (gameState.countEntitiesByType(gameState.applyCiv("units/{civ}_support_female_citizen")) > gameState.ai.modules["economy"].targetNumWorkers * 0.5){
if (gameState.getTimeElapsed() > 350 * 1000 * numFortresses){
if (gameState.ai.pathsToMe && gameState.ai.pathsToMe.length > 0){

View file

@ -269,33 +269,35 @@ var GameState = Class({
* Returns player build limits
* an object where each key is a category corresponding to a build limit for the player.
*/
getBuildLimits: function()
getEntityLimits: function()
{
return this.playerData.buildLimits;
return this.playerData.entityLimits;
},
/**
* Returns player build counts
* an object where each key is a category corresponding to the current building count for the player.
* Returns player entity counts
* an object where each key is a category corresponding to the current entity count for the player.
*/
getBuildCounts: function()
getEntityCounts: function()
{
return this.playerData.buildCounts;
return this.playerData.entityCounts;
},
/**
* Checks if the player's build limit has been reached for the given category.
* The category comes from the entity tenplate, specifically the BuildRestrictions component.
* Checks if the player's entity limit has been reached for the given category.
* The category comes from the entity template, specifically the
* BuildRestrictions/TrainingRestrictions components.
*/
isBuildLimitReached: function(category)
isEntityLimitReached: function(category)
{
if (this.playerData.buildLimits[category] === undefined || this.playerData.buildCounts[category] === undefined)
if (this.playerData.entityLimits[category] === undefined || this.playerData.entityCounts[category] === undefined)
return false;
// There's a special case of build limits per civ centre, so check that first
if (this.playerData.buildLimits[category].LimitPerCivCentre !== undefined)
return (this.playerData.buildCounts[category] >= this.playerData.buildCounts["CivilCentre"]*this.playerData.buildLimits[category].LimitPerCivCentre);
if (this.playerData.entityLimits[category].LimitPerCivCentre !== undefined)
return (this.playerData.entityCounts[category] >=
this.playerData.entityCounts["CivilCentre"] * this.playerData.entityLimits[category].LimitPerCivCentre);
else
return (this.playerData.buildCounts[category] >= this.playerData.buildLimits[category]);
return (this.playerData.entityCounts[category] >= this.playerData.entityLimits[category]);
},
});

View file

@ -1,12 +1,13 @@
function BuildLimits() {}
function EntityLimits() {}
BuildLimits.prototype.Schema =
"<a:help>Specifies per category limits on number of buildings that can be constructed for each player.</a:help>" +
EntityLimits.prototype.Schema =
"<a:help>Specifies per category limits on number of entities (buildings or units) that can be created for each player.</a:help>" +
"<a:example>" +
"<Limits>" +
"<CivilCentre/>" +
"<DefenseTower>25</DefenseTower>" +
"<Fortress>10</Fortress>" +
"<Hero>1</Hero>" +
"<Special>" +
"<LimitPerCivCentre>1</LimitPerCivCentre>" +
"</Special>" +
@ -17,7 +18,7 @@ BuildLimits.prototype.Schema =
"</element>" +
"<element name='Limits'>" +
"<zeroOrMore>" +
"<element a:help='Specifies a category of building on which to apply this limit. See BuildRestrictions for list of categories.'>" +
"<element a:help='Specifies a category of building/unit on which to apply this limit. See BuildRestrictions/TrainingRestrictions for list of categories.'>" +
"<anyName />" +
"<choice>" +
"<text />" +
@ -33,7 +34,10 @@ BuildLimits.prototype.Schema =
* TODO: Use an inheriting player_{civ}.xml template for civ-specific limits
*/
BuildLimits.prototype.Init = function()
const TRAINING = "training";
const BUILD = "build";
EntityLimits.prototype.Init = function()
{
this.limit = {};
this.count = {};
@ -44,60 +48,70 @@ BuildLimits.prototype.Init = function()
}
};
BuildLimits.prototype.IncrementCount = function(category)
EntityLimits.prototype.IncreaseCount = function(category, value)
{
if (this.count[category] !== undefined)
{
this.count[category]++;
}
this.count[category] += value;
};
BuildLimits.prototype.DecrementCount = function(category)
EntityLimits.prototype.DecreaseCount = function(category, value)
{
if (this.count[category] !== undefined)
{
this.count[category]--;
}
this.count[category] -= value;
};
BuildLimits.prototype.GetLimits = function()
EntityLimits.prototype.IncrementCount = function(category)
{
this.IncreaseCount(category, 1);
};
EntityLimits.prototype.DecrementCount = function(category)
{
this.DecreaseCount(category, 1);
};
EntityLimits.prototype.GetLimits = function()
{
return this.limit;
};
BuildLimits.prototype.GetCounts = function()
EntityLimits.prototype.GetCounts = function()
{
return this.count;
};
BuildLimits.prototype.AllowedToBuild = function(category)
EntityLimits.prototype.AllowedToCreate = function(limitType, category, count)
{
// TODO: The UI should reflect this before the user tries to place the building,
// since the limits are independent of placement location
// Allow unspecified categories and those with no limit
if (this.count[category] === undefined || this.limit[category] === undefined)
{
return true;
}
// Rather than complicating the schema unecessarily, just handle special cases here
if (this.limit[category].LimitPerCivCentre !== undefined)
{
if (this.count[category] >= this.count["CivilCentre"] * this.limit[category].LimitPerCivCentre)
{
var cmpPlayer = Engine.QueryInterface(this.entity, IID_Player);
var notification = {"player": cmpPlayer.GetPlayerID(), "message": category+" build limit of "+this.limit[category].LimitPerCivCentre+" per civil centre reached"};
var cmpPlayer = Engine.QueryInterface(this.entity, IID_Player);
var notification = {
"player": cmpPlayer.GetPlayerID(),
"message": category + " " + limitType + " limit of " +
this.limit[category].LimitPerCivCentre + " per civil centre reached"
};
var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification(notification);
return false;
}
}
else if (this.count[category] >= this.limit[category])
else if (this.count[category] + count > this.limit[category])
{
var cmpPlayer = Engine.QueryInterface(this.entity, IID_Player);
var notification = {"player": cmpPlayer.GetPlayerID(), "message": category+" build limit of "+this.limit[category]+ " reached"};
var notification = {
"player": cmpPlayer.GetPlayerID(),
"message": category + " " + limitType + " limit of " + this.limit[category] + " reached"};
var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification(notification);
@ -105,24 +119,39 @@ BuildLimits.prototype.AllowedToBuild = function(category)
}
return true;
}
EntityLimits.prototype.AllowedToBuild = function(category)
{
// TODO: The UI should reflect this before the user tries to place the building,
// since the limits are independent of placement location
return this.AllowedToCreate(BUILD, category, 1);
};
BuildLimits.prototype.OnGlobalOwnershipChanged = function(msg)
EntityLimits.prototype.AllowedToTrain = function(category, count)
{
// This automatically updates build counts
return this.AllowedToCreate(TRAINING, category, count);
};
EntityLimits.prototype.OnGlobalOwnershipChanged = function(msg)
{
// This automatically updates entity counts
var category = null;
var cmpBuildRestrictions = Engine.QueryInterface(msg.entity, IID_BuildRestrictions);
if (cmpBuildRestrictions)
category = cmpBuildRestrictions.GetCategory();
var cmpTrainingRestrictions = Engine.QueryInterface(msg.entity, IID_TrainingRestrictions);
if (cmpTrainingRestrictions)
category = cmpTrainingRestrictions.GetCategory();
if (category)
{
var playerID = (Engine.QueryInterface(this.entity, IID_Player)).GetPlayerID();
if (msg.from == playerID)
{
this.DecrementCount(cmpBuildRestrictions.GetCategory());
}
this.DecrementCount(category);
if (msg.to == playerID)
{
this.IncrementCount(cmpBuildRestrictions.GetCategory());
}
this.IncrementCount(category);
}
};
Engine.RegisterComponentType(IID_BuildLimits, "BuildLimits", BuildLimits);
Engine.RegisterComponentType(IID_EntityLimits, "EntityLimits", EntityLimits);

View file

@ -49,7 +49,7 @@ GuiInterface.prototype.GetSimulationState = function(player)
for (var i = 0; i < n; ++i)
{
var playerEnt = cmpPlayerMan.GetPlayerByID(i);
var cmpPlayerBuildLimits = Engine.QueryInterface(playerEnt, IID_BuildLimits);
var cmpPlayerEntityLimits = Engine.QueryInterface(playerEnt, IID_EntityLimits);
var cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player);
// Work out what phase we are in
@ -88,8 +88,8 @@ GuiInterface.prototype.GetSimulationState = function(player)
"isAlly": allies,
"isNeutral": neutrals,
"isEnemy": enemies,
"buildLimits": cmpPlayerBuildLimits.GetLimits(),
"buildCounts": cmpPlayerBuildLimits.GetCounts(),
"entityLimits": cmpPlayerEntityLimits.GetLimits(),
"entityCounts": cmpPlayerEntityLimits.GetCounts(),
"techModifications": cmpTechnologyManager.GetTechModifications()
};
ret.players.push(playerData);
@ -384,7 +384,14 @@ GuiInterface.prototype.GetTemplateData = function(player, name)
if (template.BuildRestrictions.Distance.MaxDistance) ret.buildRestrictions.distance.max = +template.BuildRestrictions.Distance.MaxDistance;
}
}
if (template.TrainingRestrictions)
{
ret.trainingRestrictions = {
"category": template.TrainingRestrictions.Category,
};
}
if (template.Cost)
{
ret.cost = {};
@ -453,6 +460,7 @@ GuiInterface.prototype.GetTemplateData = function(player, name)
ret.icon = template.Identity.Icon;
ret.tooltip = template.Identity.Tooltip;
ret.requiredTechnology = template.Identity.RequiredTechnology;
ret.identityClassesString = GetTemplateIdentityClassesString(template);
}
if (template.UnitMotion)

View file

@ -440,23 +440,19 @@ Player.prototype.IsNeutral = function(id)
Player.prototype.OnGlobalOwnershipChanged = function(msg)
{
var isConquestCritical = false;
// Load class list only if we're going to need it
if (msg.from == this.playerID || msg.to == this.playerID)
{
var cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity);
if (cmpIdentity)
{
var classes = cmpIdentity.GetClassesList();
isConquestCritical = classes.indexOf("ConquestCritical") != -1;
isConquestCritical = cmpIdentity.HasClass("ConquestCritical");
}
}
if (msg.from == this.playerID)
{
if (isConquestCritical)
this.conquestCriticalEntitiesCount--;
var cost = Engine.QueryInterface(msg.entity, IID_Cost);
if (cost)
{
@ -464,12 +460,10 @@ Player.prototype.OnGlobalOwnershipChanged = function(msg)
this.popBonuses -= cost.GetPopBonus();
}
}
if (msg.to == this.playerID)
{
if (isConquestCritical)
this.conquestCriticalEntitiesCount++;
var cost = Engine.QueryInterface(msg.entity, IID_Cost);
if (cost)
{

View file

@ -212,6 +212,14 @@ ProductionQueue.prototype.AddBatch = function(templateName, type, count, metadat
if (!cmpPlayer.TrySubtractResources(totalCosts))
return;
// Update entity count in the EntityLimits component
if (template.TrainingRestrictions)
{
var unitCategory = template.TrainingRestrictions.Category;
var cmpPlayerEntityLimits = QueryOwnerInterface(this.entity, IID_EntityLimits);
cmpPlayerEntityLimits.IncreaseCount(unitCategory, count);
}
this.queue.push({
"id": this.nextID++,
"player": cmpPlayer.GetPlayerID(),
@ -307,6 +315,19 @@ ProductionQueue.prototype.RemoveBatch = function(id)
var cmpPlayer = QueryPlayerIDInterface(item.player, IID_Player);
// Update entity count in the EntityLimits component
if (item.unitTemplate)
{
var cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTempMan.GetTemplate(item.unitTemplate);
if (template.TrainingRestrictions)
{
var unitCategory = template.TrainingRestrictions.Category;
var cmpPlayerEntityLimits = QueryOwnerInterface(this.entity, IID_EntityLimits);
cmpPlayerEntityLimits.DecreaseCount(unitCategory, item.count);
}
}
// Refund the resource cost for this batch
var totalCosts = {};
var cmpStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker);
@ -422,7 +443,19 @@ ProductionQueue.prototype.SpawnUnits = function(templateName, count, metadata)
// so only create them once and use as needed
for (var i = 0; i < count; ++i)
{
this.entityCache.push(Engine.AddEntity(templateName));
var ent = Engine.AddEntity(templateName);
this.entityCache.push(ent);
// Decrement entity count in the EntityLimits component
// since it will be increased by EntityLimits.OnGlobalOwnershipChanged function,
// i.e. we replace a 'trained' entity to an 'alive' one
var cmpTrainingRestrictions = Engine.QueryInterface(ent, IID_TrainingRestrictions);
if (cmpTrainingRestrictions)
{
var unitCategory = cmpTrainingRestrictions.GetCategory();
var cmpPlayerEntityLimits = QueryOwnerInterface(this.entity, IID_EntityLimits);
cmpPlayerEntityLimits.DecrementCount(unitCategory);
}
}
}

View file

@ -0,0 +1,22 @@
function TrainingRestrictions() {}
TrainingRestrictions.prototype.Schema =
"<a:help>Specifies unit training restrictions, currently only unit category.</a:help>" +
"<a:example>" +
"<TrainingRestrictions>" +
"<Category>Hero</Category>" +
"</TrainingRestrictions>" +
"</a:example>" +
"<element name='Category' a:help='Specifies the category of this unit, for satisfying special constraints.'>" +
"<choice>" +
"<value>Hero</value>" +
"<value>FemaleCitizen</value>" +
"</choice>" +
"</element>";
TrainingRestrictions.prototype.GetCategory = function()
{
return this.template.Category;
};
Engine.RegisterComponentType(IID_TrainingRestrictions, "TrainingRestrictions", TrainingRestrictions);

View file

@ -1 +0,0 @@
Engine.RegisterInterface("BuildLimits");

View file

@ -0,0 +1 @@
Engine.RegisterInterface("EntityLimits");

View file

@ -0,0 +1,2 @@
Engine.RegisterInterface("TrainingRestrictions");

View file

@ -1,8 +1,8 @@
Engine.LoadComponentScript("interfaces/Attack.js");
Engine.LoadComponentScript("interfaces/Barter.js");
Engine.LoadComponentScript("interfaces/Builder.js");
Engine.LoadComponentScript("interfaces/BuildLimits.js");
Engine.LoadComponentScript("interfaces/DamageReceiver.js");
Engine.LoadComponentScript("interfaces/EntityLimits.js");
Engine.LoadComponentScript("interfaces/Foundation.js");
Engine.LoadComponentScript("interfaces/GarrisonHolder.js");
Engine.LoadComponentScript("interfaces/Gate.js");
@ -69,7 +69,7 @@ AddMock(100, IID_Player, {
IsEnemy: function() { return true; },
});
AddMock(100, IID_BuildLimits, {
AddMock(100, IID_EntityLimits, {
GetLimits: function() { return {"Foo": 10}; },
GetCounts: function() { return {"Foo": 5}; },
});
@ -122,7 +122,7 @@ AddMock(101, IID_Player, {
IsEnemy: function() { return false; },
});
AddMock(101, IID_BuildLimits, {
AddMock(101, IID_EntityLimits, {
GetLimits: function() { return {"Bar": 20}; },
GetCounts: function() { return {"Bar": 0}; },
});
@ -177,8 +177,8 @@ TS_ASSERT_UNEVAL_EQUALS(cmp.GetSimulationState(), {
isAlly: [false, false],
isNeutral: [false, false],
isEnemy: [true, true],
buildLimits: {"Foo": 10},
buildCounts: {"Foo": 5},
entityLimits: {"Foo": 10},
entityCounts: {"Foo": 5},
techModifications: {},
},
{
@ -197,8 +197,8 @@ TS_ASSERT_UNEVAL_EQUALS(cmp.GetSimulationState(), {
isAlly: [true, true],
isNeutral: [false, false],
isEnemy: [false, false],
buildLimits: {"Bar": 20},
buildCounts: {"Bar": 0},
entityLimits: {"Bar": 20},
entityCounts: {"Bar": 0},
techModifications: {},
}
],
@ -224,8 +224,8 @@ TS_ASSERT_UNEVAL_EQUALS(cmp.GetExtendedSimulationState(), {
isAlly: [false, false],
isNeutral: [false, false],
isEnemy: [true, true],
buildLimits: {"Foo": 10},
buildCounts: {"Foo": 5},
entityLimits: {"Foo": 10},
entityCounts: {"Foo": 5},
techModifications: {},
statistics: {
unitsTrained: 10,
@ -260,8 +260,8 @@ TS_ASSERT_UNEVAL_EQUALS(cmp.GetExtendedSimulationState(), {
isAlly: [true, true],
isNeutral: [false, false],
isEnemy: [false, false],
buildLimits: {"Bar": 20},
buildCounts: {"Bar": 0},
entityLimits: {"Bar": 20},
entityCounts: {"Bar": 0},
techModifications: {},
statistics: {
unitsTrained: 10,

View file

@ -1,10 +0,0 @@
{
"genericName": "Future Alpha",
"description": "Temporarily Disable Heroes",
"cost": {"food": 0, "wood": 100, "stone": 0, "metal": 0},
"requirements": {"tech": "phase_city"},
"requirementsTooltip": "Unlocked in City Phase.",
"icon": "arrow.png",
"researchTime": 20,
"tooltip": "Need build limits before renabling this."
}

View file

@ -169,11 +169,30 @@ function ProcessCommand(player, cmd)
case "train":
var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
// Check entity limits
var cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTempMan.GetTemplate(cmd.template);
var unitCategory = null;
if (template.TrainingRestrictions)
unitCategory = template.TrainingRestrictions.Category;
// Verify that the building(s) can be controlled by the player
if (entities.length > 0)
{
for each (var ent in entities)
{
if (unitCategory)
{
var cmpPlayerEntityLimits = QueryOwnerInterface(ent, IID_EntityLimits);
if (!cmpPlayerEntityLimits.AllowedToTrain(unitCategory, cmd.count))
{
if (g_DebugCommands)
warn(unitCategory + " train limit is reached: " + uneval(cmd));
continue;
}
}
var cmpTechnologyManager = QueryOwnerInterface(ent, IID_TechnologyManager);
// TODO: Enable this check once the AI gets technology support
if (cmpTechnologyManager.CanProduce(cmd.template) || true)
@ -568,9 +587,9 @@ function TryConstructBuilding(player, cmpPlayer, controlAllUnits, cmd)
return false;
}
// Check build limits
var cmpBuildLimits = QueryPlayerIDInterface(player, IID_BuildLimits);
if (!cmpBuildLimits || !cmpBuildLimits.AllowedToBuild(cmpBuildRestrictions.GetCategory()))
// Check entity limits
var cmpEntityLimits = QueryPlayerIDInterface(player, IID_EntityLimits);
if (!cmpEntityLimits || !cmpEntityLimits.AllowedToBuild(cmpBuildRestrictions.GetCategory()))
{
if (g_DebugCommands)
{

View file

@ -0,0 +1,24 @@
/**
* Return template.Identity.Classes._string if exists
*/
function GetTemplateIdentityClassesString(template)
{
var identityClassesString = undefined;
if (template.Identity && template.Identity.Classes && "_string" in template.Identity.Classes)
identityClassesString = template.Identity.Classes._string;
return identityClassesString;
}
/**
* Check whether template.Identity.Classes contains specified class
*/
function TemplateHasIdentityClass(template, className)
{
var identityClassesString = GetTemplateIdentityClassesString(template);
var hasClass = identityClassesString && identityClassesString.indexOf(className) != -1;
return hasClass;
}
Engine.RegisterGlobal("GetTemplateIdentityClassesString", GetTemplateIdentityClassesString);
Engine.RegisterGlobal("TemplateHasIdentityClass", TemplateHasIdentityClass);

View file

@ -1,13 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity>
<BuildLimits>
<EntityLimits>
<LimitMultiplier>1.0</LimitMultiplier>
<Limits>
<CivilCentre/>
<DefenseTower>25</DefenseTower>
<Fortress>10</Fortress>
<Hero>1</Hero>
</Limits>
</BuildLimits>
</EntityLimits>
<Player/>
<StatisticsTracker/>
<TechnologyManager/>

View file

@ -36,7 +36,6 @@
<Identity>
<GenericName>Hero</GenericName>
<Classes datatype="tokens">Hero Organic</Classes>
<RequiredTechnology>no_heroes</RequiredTechnology>
</Identity>
<Loot>
<xp>400</xp>
@ -63,6 +62,9 @@
<death>actor/human/death/death.xml</death>
</SoundGroups>
</Sound>
<TrainingRestrictions>
<Category>Hero</Category>
</TrainingRestrictions>
<UnitMotion>
<WalkSpeed>9.0</WalkSpeed>
<Run>

View file

@ -40,7 +40,6 @@
<Identity>
<GenericName>Hero Cavalry</GenericName>
<Classes datatype="tokens">Hero Cavalry</Classes>
<RequiredTechnology>no_heroes</RequiredTechnology>
</Identity>
<Loot>
<xp>500</xp>
@ -70,6 +69,9 @@
<Stamina>
<Max>2500</Max>
</Stamina>
<TrainingRestrictions>
<Category>Hero</Category>
</TrainingRestrictions>
<UnitMotion>
<WalkSpeed>11.0</WalkSpeed>
<Run>

View file

@ -49,7 +49,6 @@
<GenericName>Hero Cavalry Archer</GenericName>
<Tooltip>Hero Aura: n/a.
Ranged attack 2x vs. spearmen. Ranged attack 1.5x vs. Swordsmen.</Tooltip>
<RequiredTechnology>no_heroes</RequiredTechnology>
</Identity>
<Loot>
<xp>450</xp>
@ -70,6 +69,9 @@ Ranged attack 2x vs. spearmen. Ranged attack 1.5x vs. Swordsmen.</Tooltip>
</Texture>
</Overlay>
</Selectable>
<TrainingRestrictions>
<Category>Hero</Category>
</TrainingRestrictions>
<UnitMotion>
<WalkSpeed>11.0</WalkSpeed>
<Run>

View file

@ -39,7 +39,6 @@
<Identity>
<Classes datatype="tokens">Hero</Classes>
<GenericName>Hero Cavalry Skirmisher</GenericName>
<RequiredTechnology>no_heroes</RequiredTechnology>
</Identity>
<Loot>
<xp>450</xp>
@ -60,6 +59,9 @@
</Texture>
</Overlay>
</Selectable>
<TrainingRestrictions>
<Category>Hero</Category>
</TrainingRestrictions>
<UnitMotion>
<WalkSpeed>11.5</WalkSpeed>
<Run>

View file

@ -38,7 +38,6 @@
<Identity>
<Classes datatype="tokens">Hero Infantry</Classes>
<GenericName>Hero</GenericName>
<RequiredTechnology>no_heroes</RequiredTechnology>
</Identity>
<Loot>
<xp>400</xp>
@ -68,6 +67,9 @@
<Stamina>
<Max>1500</Max>
</Stamina>
<TrainingRestrictions>
<Category>Hero</Category>
</TrainingRestrictions>
<UnitMotion>
<WalkSpeed>8.5</WalkSpeed>
<Run>

View file

@ -41,7 +41,6 @@
<Classes datatype="tokens">Hero</Classes>
<Tooltip>Hero Archer.
Counters Swordsmen and Cavalry Spearmen. Countered by Skirmishers and other Cavalry types.</Tooltip>
<RequiredTechnology>no_heroes</RequiredTechnology>
</Identity>
<Loot>
<xp>350</xp>
@ -62,4 +61,7 @@ Counters Swordsmen and Cavalry Spearmen. Countered by Skirmishers and other Cava
</Texture>
</Overlay>
</Selectable>
<TrainingRestrictions>
<Category>Hero</Category>
</TrainingRestrictions>
</Entity>

View file

@ -45,7 +45,6 @@
<Identity>
<Classes datatype="tokens">Hero</Classes>
<GenericName>Hero Skirmisher</GenericName>
<RequiredTechnology>no_heroes</RequiredTechnology>
</Identity>
<Loot>
<xp>350</xp>
@ -66,4 +65,7 @@
</Texture>
</Overlay>
</Selectable>
<TrainingRestrictions>
<Category>Hero</Category>
</TrainingRestrictions>
</Entity>

View file

@ -51,6 +51,9 @@
</Texture>
</Overlay>
</Selectable>
<TrainingRestrictions>
<Category>Hero</Category>
</TrainingRestrictions>
<UnitMotion>
<WalkSpeed>8.5</WalkSpeed>
<Run>