1
0
forked from 0ad/0ad

Split treasures from ResourceSupply.

Removes some hardcoding and allows for easier modding of treasures.

Refs.: #5888
Differential revision: D3303
Comments by: @Imarok, @Nescio, @Stan, @wraitii
This was SVN commit r24989.
This commit is contained in:
Freagarach 2021-03-03 07:47:38 +00:00
parent 0de47dd1ec
commit ea96e81098
69 changed files with 837 additions and 263 deletions

View File

@ -18,13 +18,6 @@ function Resources()
if (data.code != data.code.toLowerCase())
warn("Resource codes should use lower case: " + data.code);
// Treasures are supported for every specified resource
if (data.code == "treasure")
{
error("Encountered resource with reserved keyword: " + data.code);
continue;
}
this.resourceData.push(data);
this.resourceDataObj[data.code] = data;
this.resourceCodes.push(data.code);

View File

@ -481,6 +481,16 @@ function GetTemplateDataHelper(template, player, auraTemplates, modifiers = {})
"GainMultiplier": getEntityValue("Trader/GainMultiplier")
};
if (template.Treasure)
{
ret.treasure = {
"collectTime": getEntityValue("Treasure/CollectTime"),
"resources": {}
};
for (let resource in template.Treasure.Resources)
ret.treasure.resources[resource] = getEntityValue("Treasure/Resources/" + resource);
}
if (template.WallSet)
{
ret.wallSet = {

View File

@ -761,17 +761,48 @@ function getResourceSupplyTooltip(template)
return "";
let supply = template.supply;
let type = supply.type[0] == "treasure" ? supply.type[1] : supply.type[0];
// Translation: Label in tooltip showing the resource type and quantity of a given resource supply.
return sprintf(translate("%(label)s %(component)s %(amount)s"), {
"label": headerFont(translate("Resource Supply:")),
"component": resourceIcon(type),
"component": resourceIcon(supply.type[0]),
// Translation: Marks that a resource supply entity has an unending, infinite, supply of its resource.
"amount": Number.isFinite(+supply.amount) ? supply.amount : translate("∞")
});
}
/**
* @param {Object} template - The entity's template.
* @return {string} - The resources this entity rewards to a collecter.
*/
function getTreasureTooltip(template)
{
if (!template.treasure)
return "";
let resources = {};
for (let resource of g_ResourceData.GetResources())
{
let type = resource.code;
if (template.treasure.resources[type])
resources[type] = template.treasure.resources[type];
}
let resourceNames = Object.keys(resources);
if (!resourceNames.length)
return "";
return sprintf(translate("%(label)s %(details)s"), {
"label": headerFont(translate("Reward:")),
"details":
resourceNames.map(
type => sprintf(translate("%(resourceIcon)s %(reward)s"), {
"resourceIcon": resourceIcon(type),
"reward": resources[type]
})
).join(" ")
});
}
function getResourceTrickleTooltip(template)
{
if (!template.resourceTrickle)

View File

@ -10,14 +10,6 @@ function layoutSelectionMultiple()
Engine.GetGUIObjectByName("detailsAreaSingle").hidden = true;
}
function getResourceTypeDisplayName(resourceType)
{
return resourceNameFirstWord(
resourceType.generic == "treasure" ?
resourceType.specific :
resourceType.generic);
}
// Updates the health bar of garrisoned units
function updateGarrisonHealthBar(entState, selection)
{
@ -228,7 +220,7 @@ function displaySingle(entState)
unitResourceBar.size = resourceSize;
Engine.GetGUIObjectByName("resourceLabel").caption = sprintf(translate("%(resource)s:"), {
"resource": getResourceTypeDisplayName(entState.resourceSupply.type)
"resource": resourceNameFirstWord(entState.resourceSupply.type.generic)
});
Engine.GetGUIObjectByName("resourceStats").caption = resources;
@ -347,6 +339,7 @@ function displaySingle(entState)
getVisibleEntityClassesFormatted,
getAurasTooltip,
getEntityTooltip,
getTreasureTooltip,
showTemplateViewerOnRightClickTooltip
].map(func => func(template)));

View File

@ -803,6 +803,49 @@ var g_UnitActions =
"specificness": 40,
},
"collect-treasure":
{
"execute": function(target, action, selection, queued)
{
Engine.PostNetworkCommand({
"type": "collect-treasure",
"entities": selection,
"target": action.target,
"queued": queued,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_collect_treasure",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.treasureCollecter ||
!targetState || !targetState.treasure)
return false;
return {
"possible": true,
"cursor": "action-collect-treasure"
};
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("collect-treasure", target, selection);
return actionInfo.possible && {
"type": "collect-treasure",
"cursor": actionInfo.cursor,
"target": target,
"firstAbleEntity": actionInfo.entity
};
},
"specificness": 1,
},
"remove-guard":
{
"execute": function(target, action, selection, queued)
@ -932,10 +975,7 @@ var g_UnitActions =
else if (targetState && targetState.resourceSupply)
{
let resourceType = targetState.resourceSupply.type;
if (resourceType.generic == "treasure")
cursor = "action-gather-" + resourceType.generic;
else
cursor = "action-gather-" + resourceType.specific;
cursor = "action-gather-" + resourceType.specific;
data.command = "gather-near-position";
data.resourceType = resourceType;
@ -946,6 +986,12 @@ var g_UnitActions =
data.target = targetState.id;
}
}
else if (targetState && targetState.treasure)
{
cursor = "action-collect-treasure";
data.command = "collect-treasure";
data.target = targetState.id;
}
else if (entState.market && targetState && targetState.market &&
entState.id != targetState.id &&
(!entState.market.naval || targetState.market.naval) &&

View File

@ -468,7 +468,7 @@ function placePlayersNomad(playerClass, constraints)
let count = Math.max(0, Math.ceil(
(ccCost[resourceType] - (g_MapSettings.StartingResources || 0)) /
Engine.GetTemplate(treasureTemplate).ResourceSupply.Amount));
Engine.GetTemplate(treasureTemplate).Treasure.Resources[resourceType]));
objects.push(new SimpleObject(treasureTemplate, count, count, 3, 5));
}

View File

@ -380,14 +380,11 @@ m.Template = m.Class({
let [type, subtype] = this.get("ResourceSupply/Type").split('.');
return { "generic": type, "specific": subtype };
},
// will return either "food", "wood", "stone", "metal" and not treasure.
"getResourceType": function() {
if (!this.get("ResourceSupply"))
return undefined;
let [type, subtype] = this.get("ResourceSupply/Type").split('.');
if (type == "treasure")
return subtype;
return type;
return this.get("ResourceSupply/Type").split('.')[0];
},
"getDiminishingReturns": function() { return +(this.get("ResourceSupply/DiminishingReturns") || 1); },
@ -414,6 +411,17 @@ m.Template = m.Class({
return types ? types.split(/\s+/) : [];
},
"isTreasure": function() { return this.get("Treasure") !== undefined; },
"treasureResources": function() {
if (!this.get("Treasure"))
return undefined;
let ret = {};
for (let r in this.get("Treasure/Resources"))
ret[r] = +this.get("Treasure/Resources/" + r);
return ret;
},
"garrisonableClasses": function() { return this.get("GarrisonHolder/List/_string"); },
@ -557,6 +565,8 @@ m.Template = m.Class({
"canGuard": function() { return this.get("UnitAI/CanGuard") === "true"; },
"canGarrison": function() { return "Garrisonable" in this._template; },
"isTreasureCollecter": function() { return this.get("TreasureCollecter") !== undefined; },
});
@ -723,9 +733,6 @@ m.Entity = m.Class({
if (!type)
return 0;
if (type.generic == "treasure")
return 1000;
let tstring = type.generic + "." + type.specific;
let rate = +this.get("ResourceGatherer/BaseSpeed");
rate *= +this.get("ResourceGatherer/Rates/" +tstring);
@ -795,7 +802,7 @@ m.Entity = m.Class({
let restrictedClasses = this.get("Attack/" + type + "/RestrictedClasses/_string");
if (!restrictedClasses || !MatchesClassList(target.classes(), restrictedClasses))
return true;
};
}
return false;
},
@ -852,6 +859,16 @@ m.Entity = m.Class({
return this;
},
"collectTreasure": function(target, queued = false) {
Engine.PostCommand(PlayerID, {
"type": "collect-treasure",
"entities": [this.id()],
"target": target.id(),
"queued": queued
});
return this;
},
// moveApart from a point in the opposite direction with a distance dist
"moveApart": function(point, dist) {
if (this.position() !== undefined) {

View File

@ -117,6 +117,20 @@ m.Filters = {
"dynamicProperties": []
}),
"isTreasure": () => ({
"func": ent => {
if (!ent.isTreasure())
return false;
// Don't go for floating treasures since we might not be able
// to reach them and that kills the pathfinder.
let template = ent.templateName();
return template != "gaia/treasure/shipwreck_debris" &&
template != "gaia/treasure/shipwreck";
},
"dynamicProperties": []
}),
"byResource": resourceType => ({
"func": ent => {
if (!ent.resourceSupplyMax())
@ -130,14 +144,6 @@ m.Filters = {
if (!ent.isHuntable() || ent.hasClass("SeaCreature"))
return false;
// Don't go for floating treasures since we won't be able to reach them and it kills the pathfinder.
if (ent.templateName() == "gaia/treasure/shipwreck_debris" ||
ent.templateName() == "gaia/treasure/shipwreck")
return false;
if (type.generic == "treasure")
return resourceType == type.specific;
return resourceType == type.generic;
},
"dynamicProperties": []

View File

@ -397,7 +397,7 @@ m.SharedScript.prototype.createResourceMaps = function()
}
for (let ent of this._entities.values())
{
if (!ent || !ent.position() || !ent.resourceSupplyType() || ent.resourceSupplyType().generic === "treasure")
if (!ent || !ent.position() || !ent.resourceSupplyType())
continue;
let resource = ent.resourceSupplyType().generic;
if (!this.resourceMaps[resource])
@ -439,7 +439,7 @@ m.SharedScript.prototype.updateResourceMaps = function(events)
if (!e.entityObj)
continue;
let ent = e.entityObj;
if (!ent || !ent.position() || !ent.resourceSupplyType() || ent.resourceSupplyType().generic === "treasure")
if (!ent || !ent.position() || !ent.resourceSupplyType())
continue;
let resource = ent.resourceSupplyType().generic;
if (!this.resourceMaps[resource])
@ -458,7 +458,7 @@ m.SharedScript.prototype.updateResourceMaps = function(events)
if (!e.entity || !this._entities.has(e.entity))
continue;
let ent = this._entities.get(e.entity);
if (!ent || !ent.position() || !ent.resourceSupplyType() || ent.resourceSupplyType().generic === "treasure")
if (!ent || !ent.position() || !ent.resourceSupplyType())
continue;
let resource = ent.resourceSupplyType().generic;
if (!this.resourceMaps[resource])

View File

@ -170,8 +170,6 @@ PETRA.BaseManager.prototype.assignResourceToDropsite = function(gameState, drops
return;
if (supply.hasClass("Field")) // fields are treated separately
return;
if (supply.resourceSupplyType().generic == "treasure") // treasures are treated separately
return;
// quick accessibility check
if (PETRA.getLandAccess(gameState, supply) != accessIndex)
return;

View File

@ -382,16 +382,13 @@ PETRA.gatherTreasure = function(gameState, ent, water = false)
return false;
if (!ent || !ent.position())
return false;
let rates = ent.resourceGatherRates();
if (!rates || !rates.treasure || rates.treasure <= 0)
if (!ent.isTreasureCollecter)
return false;
let treasureFound;
let distmin = Math.min();
let access = water ? PETRA.getSeaAccess(gameState, ent) : PETRA.getLandAccess(gameState, ent);
for (let treasure of gameState.ai.HQ.treasures.values())
{
if (PETRA.IsSupplyFull(gameState, treasure))
continue;
// let some time for the previous gatherer to reach the treasure before trying again
let lastGathered = treasure.getMetadata(PlayerID, "lastGathered");
if (lastGathered && gameState.ai.elapsedTime - lastGathered < 20)
@ -414,9 +411,8 @@ PETRA.gatherTreasure = function(gameState, ent, water = false)
if (!treasureFound)
return false;
treasureFound.setMetadata(PlayerID, "lastGathered", gameState.ai.elapsedTime);
ent.gather(treasureFound);
gameState.ai.HQ.AddTCGatherer(treasureFound.id());
ent.setMetadata(PlayerID, "supply", treasureFound.id());
ent.collectTreasure(treasureFound);
ent.setMetadata(PlayerID, "treasure", treasureFound.id());
return true;
};

View File

@ -65,10 +65,7 @@ PETRA.HQ.prototype.init = function(gameState, queues)
this.navalMap = false;
this.navalRegions = {};
this.treasures = gameState.getEntities().filter(ent => {
let type = ent.resourceSupplyType();
return type && type.generic == "treasure";
});
this.treasures = gameState.getEntities().filter(ent => ent.isTreasure());
this.treasures.registerUpdates();
this.currentPhase = gameState.currentPhase();
this.decayingStructures = new Set();

View File

@ -268,17 +268,17 @@ PETRA.HQ.prototype.buildFirstBase = function(gameState)
if (ent.isIdle())
PETRA.gatherTreasure(gameState, ent);
// Then count the resources from the treasures being collected
let supplyId = ent.getMetadata(PlayerID, "supply");
if (!supplyId)
let treasureId = ent.getMetadata(PlayerID, "treasure");
if (!treasureId)
continue;
let supply = gameState.getEntityById(supplyId);
if (!supply || supply.resourceSupplyType().generic != "treasure")
let treasure = gameState.getEntityById(treasureId);
if (!treasure)
continue;
let type = supply.resourceSupplyType().specific;
if (!(type in totalExpected))
continue;
totalExpected[type] += supply.resourceSupplyMax();
// If we can collect enough resources from these treasures, wait for them
let types = treasure.treasureResources();
for (let type in types)
if (type in totalExpected)
totalExpected[type] += types[type];
// If we can collect enough resources from these treasures, wait for them.
if (totalExpected.canAfford(new API3.Resources(template.cost())))
return;
}

View File

@ -220,7 +220,6 @@ PETRA.Worker.prototype.update = function(gameState, ent)
let supplyId = ent.unitAIOrderData()[0].target;
let supply = gameState.getEntityById(supplyId);
if (supply && !supply.hasClass("Field") && !supply.hasClass("Animal") &&
supply.resourceSupplyType().generic != "treasure" &&
supplyId != ent.getMetadata(PlayerID, "supply"))
{
let nbGatherers = supply.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(supplyId);

View File

@ -552,6 +552,17 @@ GuiInterface.prototype.GetEntityState = function(player, ent)
"rates": cmpResourceTrickle.GetRates()
};
let cmpTreasure = Engine.QueryInterface(ent, IID_Treasure);
if (cmpTreasure)
ret.treasure = {
"collectTime": cmpTreasure.CollectionTime(),
"resources": cmpTreasure.Resources()
};
let cmpTreasureCollecter = Engine.QueryInterface(ent, IID_TreasureCollecter);
if (cmpTreasureCollecter)
ret.treasureCollecter = true;
let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
if (cmpUnitMotion)
ret.speed = {

View File

@ -25,7 +25,7 @@ ResourceGatherer.prototype.Schema =
"<ref name='positiveDecimal'/>" +
"</element>" +
"<element name='Rates' a:help='Per-resource-type gather rate multipliers. If a resource type is not specified then it cannot be gathered by this unit'>" +
Resources.BuildSchema("positiveDecimal", ["treasure"], true) +
Resources.BuildSchema("positiveDecimal", [], true) +
"</element>" +
"<element name='Capacities' a:help='Per-resource-type maximum carrying capacity'>" +
Resources.BuildSchema("positiveDecimal") +
@ -114,7 +114,7 @@ ResourceGatherer.prototype.RecalculateGatherRates = function()
{
let type = r.split(".");
if (type[0] != "treasure" && type.length > 1 && !Resources.GetResource(type[0]).subtypes[type[1]])
if (!Resources.GetResource(type[0]).subtypes[type[1]])
{
error("Resource subtype not found: " + type[0] + "." + type[1]);
continue;
@ -164,35 +164,6 @@ ResourceGatherer.prototype.GetRange = function()
// maybe this should depend on the unit or target or something?
};
/**
* Try to gather treasure
* @return 'true' if treasure is successfully gathered, otherwise 'false'
*/
ResourceGatherer.prototype.TryInstantGather = function(target)
{
let cmpResourceSupply = Engine.QueryInterface(target, IID_ResourceSupply);
let type = cmpResourceSupply.GetType();
if (type.generic != "treasure")
return false;
let status = cmpResourceSupply.TakeResources(cmpResourceSupply.GetCurrentAmount());
let cmpPlayer = QueryOwnerInterface(this.entity, IID_Player);
if (cmpPlayer)
cmpPlayer.AddResource(type.specific, status.amount);
let cmpStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker);
if (cmpStatisticsTracker)
cmpStatisticsTracker.IncreaseTreasuresCollectedCounter();
let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
if (cmpTrigger && cmpPlayer)
cmpTrigger.CallEvent("TreasureCollected", { "player": cmpPlayer.GetPlayerID(), "type": type.specific, "amount": status.amount });
return true;
};
/**
* Gather from the target entity. This should only be called after a successful range check,
* and if the target has a compatible ResourceSupply.

View File

@ -45,7 +45,7 @@ ResourceSupply.prototype.Schema =
"</element>" +
"</optional>" +
"<element name='Type' a:help='Type and Subtype of resource available from this entity'>" +
Resources.BuildChoicesSchema(true, true) +
Resources.BuildChoicesSchema(true) +
"</element>" +
"<element name='MaxGatherers' a:help='Amount of gatherers who can gather resources from this entity at the same time'>" +
"<data type='nonNegativeInteger'/>" +

View File

@ -0,0 +1,111 @@
function Treasure() {}
Treasure.prototype.Schema =
"<a:help>Provides a bonus when taken. E.g. a supply of resources.</a:help>" +
"<a:example>" +
"<CollectTime>1000</CollectTime>" +
"<Resources>" +
"<Food>1000</Food>" +
"</Resources>" +
"</a:example>" +
"<element name='CollectTime' a:help='Amount of milliseconds that it takes to collect this treasure.'>" +
"<ref name='nonNegativeDecimal'/>" +
"</element>" +
"<optional>" +
"<element name='Resources' a:help='Amount of resources that are in this.'>" +
Resources.BuildSchema("positiveDecimal") +
"</element>" +
"</optional>";
Treasure.prototype.Init = function()
{
};
Treasure.prototype.ComputeReward = function()
{
for (let resource in this.template.Resources)
{
let amount = ApplyValueModificationsToEntity("Treasure/Resources/" + resource, this.template.Resources[resource], this.entity);
if (!amount)
continue;
if (!this.resources)
this.resources = {};
this.resources[resource] = amount;
}
};
/**
* @return {Object} - The resources given by this treasure.
*/
Treasure.prototype.Resources = function()
{
return this.resources || {};
};
/**
* @return {number} - The time in miliseconds it takes to collect this treasure.
*/
Treasure.prototype.CollectionTime = function()
{
return +this.template.CollectTime;
};
/**
* @param {number} entity - The entity collecting us.
* @return {boolean} - Whether the reward was granted.
*/
Treasure.prototype.Reward = function(entity)
{
if (this.isTaken)
return false;
let cmpPlayer = QueryOwnerInterface(entity);
if (!cmpPlayer)
return false;
let cmpFogging = Engine.QueryInterface(this.entity, IID_Fogging);
if (cmpFogging)
cmpFogging.Activate();
if (this.resources)
cmpPlayer.AddResources(this.resources);
let cmpStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker);
if (cmpStatisticsTracker)
cmpStatisticsTracker.IncreaseTreasuresCollectedCounter();
let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
cmpTrigger.CallEvent("TreasureCollected", {
"player": cmpPlayer.GetPlayerID(),
"treasure": this.entity
});
this.isTaken = true;
Engine.DestroyEntity(this.entity);
return true;
};
/**
* We might live long enough for a collecting entity
* to find us again after taking us.
* @return {boolean} - Whether we are taken already.
*/
Treasure.prototype.IsAvailable = function()
{
return !this.isTaken;
};
Treasure.prototype.OnOwnershipChanged = function(msg)
{
if (msg.to != INVALID_PLAYER)
this.ComputeReward();
};
Treasure.prototype.OnValueModification = function(msg)
{
if (msg.component != "Treasure")
return;
this.ComputeReward();
};
Engine.RegisterComponentType(IID_Treasure, "Treasure", Treasure);

View File

@ -0,0 +1,119 @@
function TreasureCollecter() {}
TreasureCollecter.prototype.Schema =
"<a:help>Defines the treasure collecting abilities.</a:help>" +
"<a:example>" +
"<MaxDistance>2.0</MaxDistance>" +
"</a:example>" +
"<element name='MaxDistance' a:help='The maximum treasure taking distance in m.'>" +
"<ref name='positiveDecimal'/>" +
"</element>";
TreasureCollecter.prototype.Init = function()
{
};
/**
* @return {Object} - Min/Max range at which this entity can claim a treasure.
*/
TreasureCollecter.prototype.GetRange = function()
{
return { "min": 0, "max": +this.template.MaxDistance };
};
/**
* @param {number} target - Entity ID of the target.
* @return {boolean} - Whether we can collect from the target.
*/
TreasureCollecter.prototype.CanCollect = function(target)
{
let cmpTreasure = Engine.QueryInterface(target, IID_Treasure);
return cmpTreasure && cmpTreasure.IsAvailable();
};
/**
* @param {number} target - The target to collect.
* @param {number} callerIID - The IID to notify on specific events.
*
* @return {boolean} - Whether we started collecting.
*/
TreasureCollecter.prototype.StartCollecting = function(target, callerIID)
{
if (this.target)
this.StopCollecting();
let cmpTreasure = Engine.QueryInterface(target, IID_Treasure);
if (!cmpTreasure || !cmpTreasure.IsAvailable())
return false;
this.target = target;
this.callerIID = callerIID;
// ToDo: Implement rate modifiers.
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.timer = cmpTimer.SetTimeout(this.entity, IID_TreasureCollecter, "CollectTreasure", cmpTreasure.CollectionTime(), null);
return true;
};
/**
* @param {string} reason - The reason why we stopped collecting, used to notify the caller.
*/
TreasureCollecter.prototype.StopCollecting = function(reason)
{
if (this.timer)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.timer);
delete this.timer;
}
delete this.target;
// The callerIID component may start gathering again,
// replacing the callerIID, which gets deleted after
// the callerIID has finished. Hence save the data.
let callerIID = this.callerIID;
delete this.callerIID;
if (reason && callerIID)
{
let component = Engine.QueryInterface(this.entity, callerIID);
if (component)
component.ProcessMessage(reason, null);
}
};
/**
* @params - Data and lateness are unused.
*/
TreasureCollecter.prototype.CollectTreasure = function(data, lateness)
{
let cmpTreasure = Engine.QueryInterface(this.target, IID_Treasure);
if (!cmpTreasure || !cmpTreasure.IsAvailable())
{
this.StopCollecting("TargetInvalidated");
return;
}
if (!this.IsTargetInRange(this.target))
{
this.StopCollecting("OutOfRange");
return;
}
cmpTreasure.Reward(this.entity);
this.StopCollecting("TargetInvalidated");
};
/**
* @param {number} - The entity ID of the target to check.
* @return {boolean} - Whether this entity is in range of its target.
*/
TreasureCollecter.prototype.IsTargetInRange = function(target)
{
let range = this.GetRange();
let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, range.max, false);
};
Engine.RegisterComponentType(IID_TreasureCollecter, "TreasureCollecter", TreasureCollecter);

View File

@ -477,9 +477,8 @@ UnitAI.prototype.UnitFsmSpec = {
}
// If the unit is full go to the nearest dropsite instead of trying to gather.
// Unless our target is a treasure which we cannot be full enough with (we can't carry treasures).
let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (msg.data.type.generic !== "treasure" && cmpResourceGatherer && !cmpResourceGatherer.CanCarryMore(msg.data.type.generic))
if (cmpResourceGatherer && !cmpResourceGatherer.CanCarryMore(msg.data.type.generic))
{
let nearestDropsite = this.FindNearestDropsite(msg.data.type.generic);
if (nearestDropsite)
@ -645,6 +644,15 @@ UnitAI.prototype.UnitFsmSpec = {
return this.FinishOrder();
},
"Order.CollectTreasure": function(msg) {
let cmpTreasureCollecter = Engine.QueryInterface(this.entity, IID_TreasureCollecter);
if (!cmpTreasureCollecter || !cmpTreasureCollecter.CanCollect(msg.data.target))
return this.FinishOrder();
this.SetNextState("COLLECTTREASURE");
return ACCEPT_ORDER;
},
// States for the special entity representing a group of units moving in formation:
"FORMATIONCONTROLLER": {
@ -2551,14 +2559,9 @@ UnitAI.prototype.UnitFsmSpec = {
return;
}
// Gather the resources:
let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
// Try to gather treasure
if (cmpResourceGatherer.TryInstantGather(this.gatheringTarget))
return;
// If we've already got some resources but they're the wrong type,
// drop them first to ensure we're only ever carrying one type
if (cmpResourceGatherer.IsCarryingAnythingExcept(resourceType.generic))
@ -2566,11 +2569,8 @@ UnitAI.prototype.UnitFsmSpec = {
this.FaceTowardsTarget(this.order.data.target);
// Collect from the target
let status = cmpResourceGatherer.PerformGather(this.gatheringTarget);
// If we've collected as many resources as possible,
// return to the nearest dropsite
if (status.filled)
{
let nearestDropsite = this.FindNearestDropsite(resourceType.generic);
@ -2642,12 +2642,9 @@ UnitAI.prototype.UnitFsmSpec = {
if (previousTarget == ent)
return false;
if (type.generic == "treasure" && resourceType.generic == "treasure")
return true;
return type.specific == resourceType.specific &&
(type.specific != "meat" || resourceTemplate == template);
});
});
if (nearbyResource)
{
@ -2874,6 +2871,76 @@ UnitAI.prototype.UnitFsmSpec = {
},
},
"COLLECTTREASURE": {
"enter": function() {
let cmpTreasureCollecter = Engine.QueryInterface(this.entity, IID_TreasureCollecter);
if (!cmpTreasureCollecter || !cmpTreasureCollecter.CanCollect(this.order.data.target))
{
this.FinishOrder();
return true;
}
if (this.CheckTargetRange(this.order.data.target, IID_TreasureCollecter))
this.SetNextState("COLLECTING");
else
this.SetNextState("APPROACHING");
return true;
},
"leave": function() {
},
"APPROACHING": {
"enter": function() {
if (!this.MoveToTargetRange(this.order.data.target, IID_TreasureCollecter))
{
this.FinishOrder();
return true;
}
return false;
},
"leave": function() {
this.StopMoving();
},
"MovementUpdate": function(msg) {
if (this.CheckTargetRange(this.order.data.target, IID_TreasureCollecter))
this.SetNextState("COLLECTING");
else if (msg.likelyFailure)
this.FinishOrder();
},
},
"COLLECTING": {
"enter": function() {
let cmpTreasureCollecter = Engine.QueryInterface(this.entity, IID_TreasureCollecter);
if (!cmpTreasureCollecter.StartCollecting(this.order.data.target, IID_UnitAI))
{
this.ProcessMessage("TargetInvalidated");
return true;
}
this.FaceTowardsTarget(this.order.data.target);
this.SelectAnimation("collecting_treasure");
return false;
},
"leave": function() {
let cmpTreasureCollecter = Engine.QueryInterface(this.entity, IID_TreasureCollecter);
if (cmpTreasureCollecter)
cmpTreasureCollecter.StopCollecting();
this.ResetAnimation();
},
"OutOfRange": function(msg) {
this.SetNextState("APPROACHING");
},
"TargetInvalidated": function(msg) {
this.FinishOrder();
},
},
},
"TRADE": {
"Attacked": function(msg) {
// Ignore attack
@ -4240,6 +4307,16 @@ UnitAI.prototype.OnPackFinished = function(msg)
this.UnitFsm.ProcessMessage(this, {"type": "PackFinished", "packed": msg.packed});
};
/**
* A general function to process messages sent from components.
* @param {string} type - The type of message to process.
* @param {Object} msg - Optionally extra data to use.
*/
UnitAI.prototype.ProcessMessage = function(type, msg)
{
this.UnitFsm.ProcessMessage(this, { "type": type, "data": msg });
};
//// Helper functions to be called by the FSM ////
UnitAI.prototype.GetWalkSpeed = function()
@ -5607,6 +5684,14 @@ UnitAI.prototype.ReturnResource = function(target, queued)
this.AddOrder("ReturnResource", { "target": target, "force": true }, queued);
};
/**
* Adds order to collect a treasure to queue, forced by the player.
*/
UnitAI.prototype.CollectTreasure = function(target, queued)
{
this.AddOrder("CollectTreasure", { "target": target, "force": true }, queued);
};
UnitAI.prototype.CancelSetupTradeRoute = function(target)
{
let cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);

View File

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

View File

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

View File

@ -32,6 +32,8 @@ Engine.LoadComponentScript("interfaces/TechnologyManager.js");
Engine.LoadComponentScript("interfaces/Trader.js");
Engine.LoadComponentScript("interfaces/TurretHolder.js");
Engine.LoadComponentScript("interfaces/Timer.js");
Engine.LoadComponentScript("interfaces/Treasure.js");
Engine.LoadComponentScript("interfaces/TreasureCollecter.js");
Engine.LoadComponentScript("interfaces/StatisticsTracker.js");
Engine.LoadComponentScript("interfaces/StatusEffectsReceiver.js");
Engine.LoadComponentScript("interfaces/UnitAI.js");

View File

@ -0,0 +1,56 @@
Resources = {
"BuildSchema": () => {
let schema = "";
for (let res of ["food", "metal"])
{
for (let subtype in ["meat", "grain"])
schema += "<value>" + res + "." + subtype + "</value>";
schema += "<value> treasure." + res + "</value>";
}
return "<choice>" + schema + "</choice>";
}
};
Engine.LoadHelperScript("Player.js");
Engine.LoadComponentScript("interfaces/StatisticsTracker.js");
Engine.LoadComponentScript("interfaces/Treasure.js");
Engine.LoadComponentScript("interfaces/Trigger.js");
Engine.LoadComponentScript("Treasure.js");
Engine.LoadComponentScript("Trigger.js");
Engine.RegisterGlobal("ApplyValueModificationsToEntity", (prop, oVal, ent) => oVal);
ConstructComponent(SYSTEM_ENTITY, "Trigger", {});
const entity = 11;
let treasurer = 12;
let treasurerOwner = 1;
let cmpTreasure = ConstructComponent(entity, "Treasure", {
"CollectTime": "1000",
"Resources": {
"Food": "10"
}
});
cmpTreasure.OnOwnershipChanged({ "to": 0 });
TS_ASSERT(!cmpTreasure.Reward(treasurer));
AddMock(treasurer, IID_Ownership, {
"GetOwner": () => treasurerOwner
});
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
"GetPlayerByID": (id) => treasurerOwner
});
let cmpPlayer = AddMock(treasurerOwner, IID_Player, {
"AddResources": (type, amount) => {},
"GetPlayerID": () => treasurerOwner
});
let spy = new Spy(cmpPlayer, "AddResources");
TS_ASSERT(cmpTreasure.Reward(treasurer));
TS_ASSERT_EQUALS(spy._called, 1);
// Don't allow collecting twice.
TS_ASSERT(!cmpTreasure.Reward(treasurer));
TS_ASSERT_EQUALS(spy._called, 1);

View File

@ -0,0 +1,47 @@
Engine.LoadHelperScript("Player.js");
Engine.LoadComponentScript("interfaces/Timer.js");
Engine.LoadComponentScript("interfaces/Treasure.js");
Engine.LoadComponentScript("interfaces/TreasureCollecter.js");
Engine.LoadComponentScript("interfaces/UnitAI.js");
Engine.LoadComponentScript("Timer.js");
Engine.LoadComponentScript("TreasureCollecter.js");
AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
"IsInTargetRange": () => true
});
const entity = 11;
let treasure = 12;
let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer", {});
let cmpTreasurer = ConstructComponent(entity, "TreasureCollecter", {
"MaxDistance": "2.0"
});
TS_ASSERT(!cmpTreasurer.StartCollecting(treasure));
let cmpTreasure = AddMock(treasure, IID_Treasure, {
"Reward": (ent) => true,
"CollectionTime": () => 1000,
"IsAvailable": () => true
});
let spyTreasure = new Spy(cmpTreasure, "Reward");
TS_ASSERT(cmpTreasurer.StartCollecting(treasure));
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(spyTreasure._called, 1);
// Test that starting to collect twice merely collects once.
spyTreasure._called = 0;
TS_ASSERT(cmpTreasurer.StartCollecting(treasure));
TS_ASSERT(cmpTreasurer.StartCollecting(treasure));
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(spyTreasure._called, 1);
// Test callback is called.
let cmpUnitAI = AddMock(entity, IID_UnitAI, {
"ProcessMessage": (type, data) => TS_ASSERT_EQUALS(type, "TargetInvalidated")
});
let spyUnitAI = new Spy(cmpUnitAI, "ProcessMessage");
TS_ASSERT(cmpTreasurer.StartCollecting(treasure, IID_UnitAI));
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(spyUnitAI._called, 1);

View File

@ -0,0 +1,79 @@
Resources = {
"GetCodes": () => ["food", "metal", "stone", "wood"],
"GetTradableCodes": () => ["food", "metal", "stone", "wood"],
"GetBarterableCodes": () => ["food", "metal", "stone", "wood"],
"BuildSchema": () => {
let schema = "";
for (let res of ["food", "metal"])
{
for (let subtype in ["meat", "grain"])
schema += "<value>" + res + "." + subtype + "</value>";
schema += "<value> treasure." + res + "</value>";
}
return "<choice>" + schema + "</choice>";
},
"GetResource": (type) => {
return {
"subtypes": {
"meat": "meat",
"grain": "grain"
}
};
}
};
Engine.LoadHelperScript("Player.js");
Engine.LoadComponentScript("interfaces/Player.js");
Engine.LoadComponentScript("interfaces/StatisticsTracker.js");
Engine.LoadComponentScript("interfaces/Treasure.js");
Engine.LoadComponentScript("interfaces/TreasureCollecter.js");
Engine.LoadComponentScript("interfaces/Timer.js");
Engine.LoadComponentScript("interfaces/Trigger.js");
Engine.LoadComponentScript("interfaces/UnitAI.js");
Engine.LoadComponentScript("Player.js");
Engine.LoadComponentScript("Timer.js");
Engine.LoadComponentScript("Treasure.js");
Engine.LoadComponentScript("TreasureCollecter.js");
Engine.LoadComponentScript("Trigger.js");
let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer", {});
Engine.RegisterGlobal("ApplyValueModificationsToEntity", (prop, oVal, ent) => oVal);
ConstructComponent(SYSTEM_ENTITY, "Trigger", {});
const treasure = 11;
const treasurer = 12;
const owner = 1;
AddMock(treasurer, IID_Ownership, {
"GetOwner": () => owner
});
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
"GetPlayerByID": (id) => owner
});
AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
"IsInTargetRange": (ent, target, min, max, invert) => true
});
let cmpPlayer = ConstructComponent(owner, "Player", {
"SpyCostMultiplier": 1,
"BarterMultiplier": {}
});
let playerSpy = new Spy(cmpPlayer, "AddResources");
let cmpTreasure = ConstructComponent(treasure, "Treasure", {
"CollectTime": "1000",
"Resources": {
"Food": "10"
}
});
cmpTreasure.OnOwnershipChanged({ "to": 0 });
let cmpTreasurer = ConstructComponent(treasurer, "TreasureCollecter", {
"MaxDistance": "2.0"
});
TS_ASSERT(cmpTreasurer.StartCollecting(treasure));
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(playerSpy._called, 1);

View File

@ -16,15 +16,7 @@
{ "value": "Loot/metal", "replace": 6, "affects": "Infantry" },
{ "value": "Loot/metal", "replace": 8, "affects": "Cavalry" },
{ "value": "Loot/metal", "replace": 12, "affects": "Elephant" },
{ "value": "ResourceGatherer/Rates/food.fish", "replace": 0 },
{ "value": "ResourceGatherer/Rates/food.fruit", "replace": 0 },
{ "value": "ResourceGatherer/Rates/food.grain", "replace": 0 },
{ "value": "ResourceGatherer/Rates/food.meat", "replace": 0 },
{ "value": "ResourceGatherer/Rates/wood.tree", "replace": 0 },
{ "value": "ResourceGatherer/Rates/wood.ruins", "replace": 0 },
{ "value": "ResourceGatherer/Rates/stone.rock", "replace": 0 },
{ "value": "ResourceGatherer/Rates/stone.ruins", "replace": 0 },
{ "value": "ResourceGatherer/Rates/metal.ore", "replace": 0 }
{ "value": "ResourceGatherer/BaseSpeed", "replace": 0 }
],
"affects": ["Mercenary !Champion"]
}

View File

@ -72,6 +72,13 @@ var g_Commands = {
Cheat(cmd);
},
"collect-treasure": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
cmpUnitAI.CollectTreasure(cmd.target, cmd.queued);
});
},
"diplomacy": function(player, cmd, data)
{
let cmpCeasefireManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager);

View File

@ -97,6 +97,14 @@ function GetRallyPointCommands(cmpRallyPoint, spawnedEnts)
"queued": true
});
break;
case "collect-treasure":
ret.push({
"type": "collect-treasure",
"entities": spawnedEnts,
"target": data[i].target,
"queued": true,
});
break;
default:
ret.push({
"type": "walk",

View File

@ -4,7 +4,7 @@
* To prevent validation errors, disabled resources are included in the schema.
*
* @param datatype - The datatype of the element
* @param additional - Array of additional data elements. Time, xp, treasure, etc.
* @param additional - Array of additional data elements. Time, xp, etc.
* @param subtypes - If true, resource subtypes will be included as well.
* @return RelaxNG schema string
*/
@ -47,15 +47,6 @@ Resources.prototype.BuildSchema = function(datatype, additional = [], subtypes =
"</element>" +
"</optional>";
if (additional.indexOf("treasure") !== -1)
for (let res of resCodes)
schema +=
"<optional>" +
"<element name='" + "treasure." + res + "'>" +
datatype +
"</element>" +
"</optional>";
return "<interleave>" + schema + "</interleave>";
};
@ -63,29 +54,19 @@ Resources.prototype.BuildSchema = function(datatype, additional = [], subtypes =
* Builds the value choices for a RelaxNG `<choice></choice>` object, based on currently valid resources.
*
* @oaram subtypes - If set to true, the choices returned will be resource subtypes, rather than main types
* @param treasure - If set to true, the pseudo resource 'treasure' (or its subtypes) will be included
* @return String of RelaxNG Schema `<choice/>` values.
*/
Resources.prototype.BuildChoicesSchema = function(subtypes = false, treasure = false)
Resources.prototype.BuildChoicesSchema = function(subtypes = false)
{
let schema = "";
if (!subtypes)
{
let resCodes = this.resourceData.map(resource => resource.code);
if (treasure)
resCodes.push("treasure");
for (let res of resCodes)
for (let res of this.resourceData.map(resource => resource.code))
schema += "<value>" + res + "</value>";
}
else
for (let res of this.resourceData)
{
for (let subtype in res.subtypes)
schema += "<value>" + res.code + "." + subtype + "</value>";
if (treasure)
schema += "<value>" + "treasure." + res.code + "</value>";
}
return "<choice>" + schema + "</choice>";
};

View File

@ -23,12 +23,8 @@ function LoadMapSettings(settings)
}
if (settings.DisableTreasures)
for (let ent of Engine.GetEntitiesWithInterface(IID_ResourceSupply))
{
let cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply);
if (cmpResourceSupply.GetType().generic == "treasure")
Engine.DestroyEntity(ent);
}
for (let ent of Engine.GetEntitiesWithInterface(IID_Treasure))
Engine.DestroyEntity(ent);
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (cmpRangeManager)

View File

@ -8,10 +8,6 @@
<SpecificName>Food Treasure</SpecificName>
<Icon>gaia/special_treasure_food.png</Icon>
</Identity>
<ResourceSupply>
<Max>100</Max>
<Type>treasure.food</Type>
</ResourceSupply>
<Selectable replace="">
<Overlay>
<Texture>
@ -20,6 +16,11 @@
</Texture>
</Overlay>
</Selectable>
<Treasure>
<Resources>
<food>100</food>
</Resources>
</Treasure>
<VisualActor>
<Actor>props/special/eyecandy/barrel_a.xml</Actor>
</VisualActor>

View File

@ -15,10 +15,11 @@
<Floating>false</Floating>
<FloatDepth>0.0</FloatDepth>
</Position>
<ResourceSupply>
<Max>200</Max>
<Type>treasure.food</Type>
</ResourceSupply>
<Treasure>
<Resources>
<food>200</food>
</Resources>
</Treasure>
<VisualActor>
<Actor>props/special/eyecandy/barrels_buried.xml</Actor>
</VisualActor>

View File

@ -11,10 +11,11 @@
<Obstruction>
<Static width="2.0" depth="3.0"/>
</Obstruction>
<ResourceSupply>
<Max>300</Max>
<Type>treasure.food</Type>
</ResourceSupply>
<Treasure>
<Resources>
<food>300</food>
</Resources>
</Treasure>
<VisualActor>
<Actor>props/special/eyecandy/produce_bin_a.xml</Actor>
</VisualActor>

View File

@ -11,10 +11,11 @@
<Obstruction>
<Static width="1.75" depth="1.75"/>
</Obstruction>
<ResourceSupply>
<Max>200</Max>
<Type>treasure.food</Type>
</ResourceSupply>
<Treasure>
<Resources>
<food>200</food>
</Resources>
</Treasure>
<VisualActor>
<Actor>props/special/eyecandy/crate_a.xml</Actor>
</VisualActor>

View File

@ -11,10 +11,11 @@
<Obstruction>
<Static width="6.5" depth="6.5"/>
</Obstruction>
<ResourceSupply>
<Max>300</Max>
<Type>treasure.food</Type>
</ResourceSupply>
<Treasure>
<Resources>
<food>300</food>
</Resources>
</Treasure>
<VisualActor>
<Actor>props/special/eyecandy/amphorae.xml</Actor>
</VisualActor>

View File

@ -11,10 +11,11 @@
<Obstruction>
<Static width="8.0" depth="8.0"/>
</Obstruction>
<ResourceSupply>
<Max>600</Max>
<Type>treasure.food</Type>
</ResourceSupply>
<Treasure>
<Resources>
<food>600</food>
</Resources>
</Treasure>
<VisualActor>
<Actor>props/special/eyecandy/treasure_persian_food_big.xml</Actor>
</VisualActor>

View File

@ -11,10 +11,11 @@
<Obstruction>
<Static width="5.0" depth="5.0"/>
</Obstruction>
<ResourceSupply>
<Max>400</Max>
<Type>treasure.food</Type>
</ResourceSupply>
<Treasure>
<Resources>
<food>400</food>
</Resources>
</Treasure>
<VisualActor>
<Actor>props/special/eyecandy/treasure_persian_food_small.xml</Actor>
</VisualActor>

View File

@ -7,10 +7,11 @@
<Identity>
<SpecificName>Golden Fleece</SpecificName>
</Identity>
<ResourceSupply>
<Max>1000</Max>
<Type>treasure.metal</Type>
</ResourceSupply>
<Treasure>
<Resources>
<metal>1000</metal>
</Resources>
</Treasure>
<VisualActor>
<Actor>special/golden_fleece.xml</Actor>
</VisualActor>

View File

@ -7,15 +7,16 @@
<Identity>
<SpecificName>Secret Box</SpecificName>
</Identity>
<ResourceSupply>
<Max>300</Max>
<Type>treasure.metal</Type>
</ResourceSupply>
<Sound>
<SoundGroups>
<select>interface/select/resource/sel_metal.xml</select>
</SoundGroups>
</Sound>
<Treasure>
<Resources>
<metal>300</metal>
</Resources>
</Treasure>
<VisualActor>
<Actor>props/special/eyecandy/barrel_a.xml</Actor>
</VisualActor>

View File

@ -10,15 +10,16 @@
<Obstruction>
<Static width="7.0" depth="7.0"/>
</Obstruction>
<ResourceSupply>
<Max>500</Max>
<Type>treasure.metal</Type>
</ResourceSupply>
<Sound>
<SoundGroups>
<select>interface/select/resource/sel_metal.xml</select>
</SoundGroups>
</Sound>
<Treasure>
<Resources>
<metal>500</metal>
</Resources>
</Treasure>
<VisualActor>
<Actor>props/special/eyecandy/treasure_persian_metal_big.xml</Actor>
</VisualActor>

View File

@ -10,15 +10,16 @@
<Obstruction>
<Static width="5.0" depth="3.0"/>
</Obstruction>
<ResourceSupply>
<Max>300</Max>
<Type>treasure.metal</Type>
</ResourceSupply>
<Sound>
<SoundGroups>
<select>interface/select/resource/sel_metal.xml</select>
</SoundGroups>
</Sound>
<Treasure>
<Resources>
<metal>300</metal>
</Resources>
</Treasure>
<VisualActor>
<Actor>props/special/eyecandy/treasure_persian_metal_small.xml</Actor>
</VisualActor>

View File

@ -7,10 +7,11 @@
<Identity>
<SpecificName>Pegasus</SpecificName>
</Identity>
<ResourceSupply>
<Max>1000</Max>
<Type>treasure.metal</Type>
</ResourceSupply>
<Treasure>
<Resources>
<metal>1000</metal>
</Resources>
</Treasure>
<VisualActor>
<Actor>special/pegasus.xml</Actor>
</VisualActor>

View File

@ -14,10 +14,11 @@
<Floating>true</Floating>
<FloatDepth>0.0</FloatDepth>
</Position>
<ResourceSupply>
<Max>500</Max>
<Type>treasure.wood</Type>
</ResourceSupply>
<Treasure>
<Resources>
<wood>500</wood>
</Resources>
</Treasure>
<VisualActor>
<Actor>props/special/eyecandy/shipwreck_ram_side.xml</Actor>
</VisualActor>

View File

@ -17,10 +17,11 @@
<Floating>true</Floating>
<FloatDepth>0.0</FloatDepth>
</Position>
<ResourceSupply>
<Max>200</Max>
<Type>treasure.food</Type>
</ResourceSupply>
<Treasure>
<Resources>
<food>200</food>
</Resources>
</Treasure>
<VisualActor>
<Actor>props/special/eyecandy/barrels_floating.xml</Actor>
</VisualActor>

View File

@ -14,10 +14,11 @@
<Floating>true</Floating>
<FloatDepth>0.0</FloatDepth>
</Position>
<ResourceSupply>
<Max>550</Max>
<Type>treasure.wood</Type>
</ResourceSupply>
<Treasure>
<Resources>
<wood>550</wood>
</Resources>
</Treasure>
<VisualActor>
<Actor>props/special/eyecandy/shipwreck_ram_bow.xml</Actor>
</VisualActor>

View File

@ -14,10 +14,11 @@
<Floating>true</Floating>
<FloatDepth>0.0</FloatDepth>
</Position>
<ResourceSupply>
<Max>400</Max>
<Type>treasure.wood</Type>
</ResourceSupply>
<Treasure>
<Resources>
<wood>400</wood>
</Resources>
</Treasure>
<VisualActor>
<Actor>props/special/eyecandy/shipwreck_sail_boat.xml</Actor>
</VisualActor>

View File

@ -14,10 +14,11 @@
<Floating>true</Floating>
<FloatDepth>0.0</FloatDepth>
</Position>
<ResourceSupply>
<Max>450</Max>
<Type>treasure.wood</Type>
</ResourceSupply>
<Treasure>
<Resources>
<wood>450</wood>
</Resources>
</Treasure>
<VisualActor>
<Actor>props/special/eyecandy/shipwreck_sail_boat_cut.xml</Actor>
</VisualActor>

View File

@ -10,15 +10,16 @@
<Obstruction>
<Static width="2.0" depth="2.0"/>
</Obstruction>
<ResourceSupply>
<Max>300</Max>
<Type>treasure.stone</Type>
</ResourceSupply>
<Sound>
<SoundGroups>
<select>interface/select/resource/sel_stone.xml</select>
</SoundGroups>
</Sound>
<Treasure>
<Resources>
<stone>300</stone>
</Resources>
</Treasure>
<VisualActor>
<Actor>props/special/eyecandy/standing_stones.xml</Actor>
</VisualActor>

View File

@ -10,15 +10,16 @@
<Obstruction>
<Static width="6.5" depth="6.5"/>
</Obstruction>
<ResourceSupply>
<Max>300</Max>
<Type>treasure.stone</Type>
</ResourceSupply>
<Sound>
<SoundGroups>
<select>interface/select/resource/sel_stone.xml</select>
</SoundGroups>
</Sound>
<Treasure>
<Resources>
<stone>300</stone>
</Resources>
</Treasure>
<VisualActor>
<Actor>props/special/eyecandy/stone_pile.xml</Actor>
</VisualActor>

View File

@ -10,15 +10,16 @@
<Obstruction>
<Static width="3.5" depth="6.5"/>
</Obstruction>
<ResourceSupply>
<Max>300</Max>
<Type>treasure.wood</Type>
</ResourceSupply>
<Sound>
<SoundGroups>
<select>interface/select/resource/sel_tree.xml</select>
</SoundGroups>
</Sound>
<Treasure>
<Resources>
<wood>300</wood>
</Resources>
</Treasure>
<VisualActor>
<Actor>props/special/eyecandy/wood_pile.xml</Actor>
</VisualActor>

View File

@ -16,11 +16,6 @@
<Obstruction>
<Static width="1.5" depth="1.5"/>
</Obstruction>
<ResourceSupply>
<KillBeforeGather>false</KillBeforeGather>
<Max>300</Max>
<MaxGatherers>1</MaxGatherers>
</ResourceSupply>
<Selectable>
<Overlay>
<Outline>
@ -36,4 +31,7 @@
<StatusBars>
<HeightOffset>3.75</HeightOffset>
</StatusBars>
<Treasure>
<CollectTime>1000</CollectTime>
</Treasure>
</Entity>

View File

@ -85,19 +85,6 @@
</Damage>
</Entity>
</Resistance>
<ResourceGatherer>
<MaxDistance>2.0</MaxDistance>
<BaseSpeed>1.0</BaseSpeed>
<Rates>
<treasure>1</treasure>
</Rates>
<Capacities>
<food>10</food>
<wood>10</wood>
<stone>10</stone>
<metal>10</metal>
</Capacities>
</ResourceGatherer>
<Selectable>
<Overlay>
<Texture>
@ -120,6 +107,9 @@
<HeightOffset>5.0</HeightOffset>
</StatusBars>
<StatusEffectsReceiver/>
<TreasureCollecter>
<MaxDistance>2</MaxDistance>
</TreasureCollecter>
<UnitAI>
<DefaultStance>aggressive</DefaultStance>
<FleeDistance>12.0</FleeDistance>

View File

@ -28,7 +28,6 @@
<Anchor>pitch-roll</Anchor>
</Position>
<Resistance replace=""/>
<ResourceGatherer disable=""/>
<Selectable>
<Overlay>
<Texture>
@ -45,6 +44,7 @@
<walk>actor/singlesteps/steps_grass.xml</walk>
</SoundGroups>
</Sound>
<TreasureCollecter disable=""/>
<UnitAI>
<DefaultStance>standground</DefaultStance>
<CanGuard>false</CanGuard>

View File

@ -65,9 +65,6 @@
</Rates>
<Capacities>
<food>20</food>
<wood>20</wood>
<stone>20</stone>
<metal>20</metal>
</Capacities>
</ResourceGatherer>
<Selectable>

View File

@ -38,7 +38,6 @@
<xp>100</xp>
<food>10</food>
</Loot>
<ResourceGatherer disable=""/>
<Selectable>
<Overlay>
<Texture>
@ -65,6 +64,7 @@
<TrainingRestrictions>
<Category>WarDog</Category>
</TrainingRestrictions>
<TreasureCollecter disable=""/>
<UnitMotion>
<WalkSpeed op="mul">1.5</WalkSpeed>
<RunMultiplier>2</RunMultiplier>

View File

@ -21,7 +21,6 @@
<Position>
<TurnRate>4</TurnRate>
</Position>
<ResourceGatherer disable=""/>
<Selectable>
<Overlay>
<Texture>
@ -30,6 +29,7 @@
</Texture>
</Overlay>
</Selectable>
<TreasureCollecter disable=""/>
<UnitAI>
<DefaultStance>passive</DefaultStance>
<CanGuard>false</CanGuard>

View File

@ -100,6 +100,12 @@
<stone.ruins>2</stone.ruins>
<metal.ore>0.5</metal.ore>
</Rates>
<Capacities>
<food>10</food>
<wood>10</wood>
<stone>10</stone>
<metal>10</metal>
</Capacities>
</ResourceGatherer>
<Sound>
<SoundGroups>

View File

@ -70,6 +70,7 @@
<BarHeight>0.5</BarHeight>
<HeightOffset>6.0</HeightOffset>
</StatusBars>
<TreasureCollecter disable=""/>
<UnitMotion>
<PassabilityClass>ship</PassabilityClass>
</UnitMotion>

View File

@ -55,7 +55,6 @@
<wood>25</wood>
<metal>15</metal>
</Loot>
<ResourceGatherer disable=""/>
<Sound>
<SoundGroups>
<attack_impact_ranged>attack/impact/arrow_impact.xml</attack_impact_ranged>

View File

@ -44,7 +44,6 @@
</Identity>
<Loot disable=""/>
<Repairable disable=""/>
<ResourceGatherer disable=""/>
<UnitMotion>
<PassabilityClass>ship-small</PassabilityClass>
<WalkSpeed op="mul">1.6</WalkSpeed>

View File

@ -48,9 +48,9 @@
</Resistance>
<ResourceGatherer>
<MaxDistance>6.0</MaxDistance>
<BaseSpeed>1.0</BaseSpeed>
<Rates>
<food.fish>1.8</food.fish>
<treasure disable=""/>
</Rates>
<Capacities>
<food>40</food>

View File

@ -34,13 +34,13 @@
</Damage>
</Entity>
</Resistance>
<ResourceGatherer>
<MaxDistance>12.0</MaxDistance>
</ResourceGatherer>
<Trader>
<GainMultiplier>0.75</GainMultiplier>
<GarrisonGainMultiplier>0.2</GarrisonGainMultiplier>
</Trader>
<TreasureCollecter>
<MaxDistance>12</MaxDistance>
</TreasureCollecter>
<UnitAI>
<DefaultStance>passive</DefaultStance>
<CanGuard>false</CanGuard>

View File

@ -59,7 +59,6 @@
<Position>
<TurnRate>2</TurnRate>
</Position>
<ResourceGatherer disable=""/>
<Sound>
<SoundGroups>
<attack_ranged>attack/siege/ballist_attack.xml</attack_ranged>

View File

@ -58,7 +58,6 @@
<Position>
<TurnRate>2</TurnRate>
</Position>
<ResourceGatherer disable=""/>
<Sound>
<SoundGroups>
<attack_impact_ranged>attack/impact/arrow_impact.xml</attack_impact_ranged>

View File

@ -85,6 +85,12 @@
<stone.ruins>2</stone.ruins>
<metal.ore>0.35</metal.ore>
</Rates>
<Capacities>
<food>10</food>
<wood>10</wood>
<stone>10</stone>
<metal>10</metal>
</Capacities>
</ResourceGatherer>
<Sound>
<SoundGroups>

View File

@ -53,6 +53,7 @@
<metal>5</metal>
</Loot>
<ResourceGatherer>
<MaxDistance>2.0</MaxDistance>
<BaseSpeed>1.0</BaseSpeed>
<Rates>
<food.fish>0.5</food.fish>
@ -64,9 +65,15 @@
<stone.rock>1.0</stone.rock>
<stone.ruins>5</stone.ruins>
<metal.ore>1.0</metal.ore>
<treasure disable=""/>
</Rates>
<Capacities>
<food>10</food>
<wood>10</wood>
<stone>10</stone>
<metal>10</metal>
</Capacities>
</ResourceGatherer>
<TreasureCollecter disable=""/>
<Sound>
<SoundGroups>
<build>resource/construction/con_wood.xml</build>