Implement building capturing. Fixes #996

This was SVN commit r16550.
This commit is contained in:
sanderd17 2015-04-20 07:45:45 +00:00
parent 751c46c60b
commit ad27deeb9d
49 changed files with 607 additions and 109 deletions

View file

@ -90,18 +90,28 @@ function GetTemplateDataHelper(template, player)
if (template.Attack)
{
ret.attack = {};
for (var type in template.Attack)
let getAttackStat = function(type, stat)
{
ret.attack[type] = {
"hack": func("Attack/"+type+"/Hack", +(template.Attack[type].Hack || 0), player, template),
"pierce": func("Attack/"+type+"/Pierce", +(template.Attack[type].Pierce || 0), player, template),
"crush": func("Attack/"+type+"/Crush", +(template.Attack[type].Crush || 0), player, template),
"minRange": func("Attack/"+type+"/MinRange", +(template.Attack[type].MinRange || 0), player, template),
"maxRange": func("Attack/"+type+"/MaxRange", +template.Attack[type].MaxRange, player, template),
"elevationBonus": func("Attack/"+type+"/ElevationBonus", +(template.Attack[type].ElevationBonus || 0), player, template),
"repeatTime": +(template.Attack[type].RepeatTime || 0),
};
return func("Attack/"+type+"/"+stat, +(template.Attack[type][stat] || 0), player, template);
};
ret.attack = {};
for (let type in template.Attack)
{
if (type == "Capture")
ret.attack.Capture = {
"value": getAttackStat(type,"Value"),
};
else
ret.attack[type] = {
"hack": getAttackStat(type, "Hack"),
"pierce": getAttackStat(type, "Pierce"),
"crush": getAttackStat(type, "Crush"),
"minRange": getAttackStat(type, "MinRange"),
"maxRange": getAttackStat(type, "MaxRange"),
"elevationBonus": getAttackStat(type, "ElevationBonus"),
};
ret.attack[type].repeatTime = +(template.Attack[type].RepeatTime || 0);
}
}

View file

@ -166,12 +166,16 @@ function initGameSpeeds()
// ====================================================================
// Convert integer color values to string (for use in GUI objects)
function rgbToGuiColor(color)
function rgbToGuiColor(color, alpha)
{
var ret;
if (color && ("r" in color) && ("g" in color) && ("b" in color))
return color.r + " " + color.g + " " + color.b;
return "0 0 0";
ret = color.r + " " + color.g + " " + color.b;
else
ret = "0 0 0";
if (alpha)
ret += " " + alpha;
return ret;
}
// ====================================================================

View file

@ -140,6 +140,7 @@ function getAttackTypeLabel(type)
if (type === "Charge") return translate("Charge Attack:");
if (type === "Melee") return translate("Melee Attack:");
if (type === "Ranged") return translate("Ranged Attack:");
if (type === "Capture") return translate("Capture Attack:");
warn(sprintf("Internationalization: Unexpected attack type found with code %(attackType)s. This attack type must be internationalized.", { attackType: type }));
return translate("Attack:");
@ -169,6 +170,15 @@ function getAttackTooltip(template)
});
var attackLabel = txtFormats.header[0] + getAttackTypeLabel(type) + txtFormats.header[1];
if (type == "Capture")
{
attacks.push(sprintf(translate("%(attackLabel)s %(details)s, %(rate)s"), {
attackLabel: attackLabel,
details: template.attack[type].value,
rate: rate
}));
continue;
}
if (type != "Ranged")
{
attacks.push(sprintf(translate("%(attackLabel)s %(details)s, %(rate)s"), {
@ -206,7 +216,7 @@ function getAttackTooltip(template)
}));
}
return attacks.join(translate(", "));
return attacks.join("\n");
}
/**

View file

@ -58,6 +58,7 @@ function displaySingle(entState, template)
}
// Hitpoints
Engine.GetGUIObjectByName("healthSection").hidden = !entState.hitpoints;
if (entState.hitpoints)
{
var unitHealthBar = Engine.GetGUIObjectByName("healthBar");
@ -79,21 +80,44 @@ function displaySingle(entState, template)
hitpoints: Math.ceil(entState.hitpoints),
maxHitpoints: entState.maxHitpoints
});
Engine.GetGUIObjectByName("healthSection").hidden = false;
}
else
// CapturePoints
Engine.GetGUIObjectByName("captureSection").hidden = !entState.capturePoints;
if (entState.capturePoints)
{
Engine.GetGUIObjectByName("healthSection").hidden = true;
let setCaptureBarPart = function(playerID, startSize)
{
var unitCaptureBar = Engine.GetGUIObjectByName("captureBar["+playerID+"]");
var sizeObj = unitCaptureBar.size;
sizeObj.rleft = startSize;
var size = 100*Math.max(0, Math.min(1, entState.capturePoints[playerID] / entState.maxCapturePoints));
sizeObj.rright = startSize + size;
unitCaptureBar.size = sizeObj;
unitCaptureBar.sprite = "color: " + rgbToGuiColor(g_Players[playerID].color, 128);
unitCaptureBar.hidden=false;
return startSize + size;
}
// first handle the owner's points, to keep those points on the left for clarity
let size = setCaptureBarPart(entState.player, 0);
for (let i in entState.capturePoints)
if (i != entState.player)
size = setCaptureBarPart(i, size);
Engine.GetGUIObjectByName("captureStats").caption = sprintf(translate("%(capturePoints)s / %(maxCapturePoints)s"), {
capturePoints: Math.ceil(entState.capturePoints[entState.player]),
maxCapturePoints: entState.maxCapturePoints
});
}
// TODO: Stamina
var player = Engine.GetPlayerID();
if (entState.stamina && (entState.player == player || g_DevSettings.controlAll))
Engine.GetGUIObjectByName("staminaSection").hidden = false;
else
Engine.GetGUIObjectByName("staminaSection").hidden = true;
// Experience
Engine.GetGUIObjectByName("experience").hidden = !entState.promotion;
if (entState.promotion)
{
var experienceBar = Engine.GetGUIObjectByName("experienceBar");
@ -112,14 +136,10 @@ function displaySingle(entState, template)
experience: "[font=\"sans-bold-13\"]" + translate("Experience:") + "[/font]",
current: Math.floor(entState.promotion.curr)
});
Engine.GetGUIObjectByName("experience").hidden = false;
}
else
{
Engine.GetGUIObjectByName("experience").hidden = true;
}
// Resource stats
Engine.GetGUIObjectByName("resourceSection").hidden = !entState.resourceSupply;
if (entState.resourceSupply)
{
var resources = entState.resourceSupply.isInfinite ? translate("∞") : // Infinity symbol
@ -136,15 +156,10 @@ function displaySingle(entState, template)
Engine.GetGUIObjectByName("resourceStats").caption = resources;
if (entState.hitpoints)
Engine.GetGUIObjectByName("resourceSection").size = Engine.GetGUIObjectByName("staminaSection").size;
Engine.GetGUIObjectByName("resourceSection").size = Engine.GetGUIObjectByName("captureSection").size;
else
Engine.GetGUIObjectByName("resourceSection").size = Engine.GetGUIObjectByName("healthSection").size;
Engine.GetGUIObjectByName("resourceSection").hidden = false;
}
else
{
Engine.GetGUIObjectByName("resourceSection").hidden = true;
}
// Resource carrying
@ -290,20 +305,29 @@ function displayMultiple(selection, template)
{
var averageHealth = 0;
var maxHealth = 0;
var maxCapturePoints = 0;
var capturePoints = (new Array(9)).fill(0);
var playerID = 0;
for (var i = 0; i < selection.length; i++)
{
var entState = GetEntityState(selection[i])
if (entState)
if (!entState)
continue;
playerID = entState.player; // trust that all selected entities have the same owner
if (entState.hitpoints)
{
if (entState.hitpoints)
{
averageHealth += entState.hitpoints;
maxHealth += entState.maxHitpoints;
}
averageHealth += entState.hitpoints;
maxHealth += entState.maxHitpoints;
}
if (entState.capturePoints)
{
maxCapturePoints += entState.maxCapturePoints;
capturePoints = entState.capturePoints.map(function(v, i) { return v + capturePoints[i]; });
}
}
Engine.GetGUIObjectByName("healthMultiple").hidden = averageHealth <= 0;
if (averageHealth > 0)
{
var unitHealthBar = Engine.GetGUIObjectByName("healthBarMultiple");
@ -313,13 +337,37 @@ function displayMultiple(selection, template)
var hitpointsLabel = "[font=\"sans-bold-13\"]" + translate("Hitpoints:") + "[/font]"
var hitpoints = sprintf(translate("%(label)s %(current)s / %(max)s"), { label: hitpointsLabel, current: averageHealth, max: maxHealth });
var healthMultiple = Engine.GetGUIObjectByName("healthMultiple");
healthMultiple.tooltip = hitpoints;
healthMultiple.hidden = false;
Engine.GetGUIObjectByName("healthMultiple").tooltip = hitpoints;
}
else
Engine.GetGUIObjectByName("captureMultiple").hidden = maxCapturePoints <= 0;
if (maxCapturePoints > 0)
{
Engine.GetGUIObjectByName("healthMultiple").hidden = true;
let setCaptureBarPart = function(playerID, startSize)
{
var unitCaptureBar = Engine.GetGUIObjectByName("captureBarMultiple["+playerID+"]");
var sizeObj = unitCaptureBar.size;
sizeObj.rtop = startSize;
var size = 100*Math.max(0, Math.min(1, capturePoints[playerID] / maxCapturePoints));
sizeObj.rbottom = startSize + size;
unitCaptureBar.size = sizeObj;
unitCaptureBar.sprite = "color: " + rgbToGuiColor(g_Players[playerID].color, 128);
unitCaptureBar.hidden=false;
return startSize + size;
}
let size = 0;
for (let i in entState.capturePoints)
if (i != playerID)
size = setCaptureBarPart(i, size);
// last handle the owner's points, to keep those points on the bottom for clarity
setCaptureBarPart(playerID, size);
var capturePointsLabel = "[font=\"sans-bold-13\"]" + translate("Capture points:") + "[/font]"
var capturePointsTooltip = sprintf(translate("%(label)s %(current)s / %(max)s"), { label: capturePointsLabel, current: Math.ceil(capturePoints[playerID]), max: Math.ceil(maxCapturePoints) });
Engine.GetGUIObjectByName("captureMultiple").tooltip = capturePointsTooltip;
}
// TODO: Stamina

View file

@ -33,12 +33,13 @@
<object type="image" sprite="statsBarShaderVertical" ghost="true"/>
</object>
<!-- Stamina bar -->
<object size="15 0 22 100%" type="image" name="staminaMultiple" tooltip_style="sessionToolTipBold">
<translatableAttribute id="tooltip">Stamina</translatableAttribute>
<!-- Capture bar -->
<object size="15 0 22 100%" type="image" name="captureMultiple" tooltip_style="sessionToolTipBold">
<translatableAttribute id="tooltip">Capture points</translatableAttribute>
<object type="image" sprite="barBorder" ghost="true" size="-1 -1 100%+1 100%+1"/>
<object type="image" sprite="staminaBackground" ghost="true"/>
<object type="image" sprite="staminaForeground" ghost="true" name="staminaBarMultiple"/>
<repeat count="9">
<object type="image" sprite="playerColorBackground" ghost="true" name="captureBarMultiple[n]" hidden="true"/>
</repeat>
<object type="image" sprite="statsBarShaderVertical" ghost="true"/>
</object>
</object>

View file

@ -20,16 +20,17 @@
</object>
</object>
<!-- Stamina bar -->
<object size="88 28 100% 52" name="staminaSection">
<object size="0 0 100% 16" name="staminaLabel" type="text" style="StatsTextLeft" ghost="true">
<translatableAttribute id="tooltip">Stamina:</translatableAttribute>
<!-- Capture bar -->
<object size="88 28 100% 52" name="captureSection">
<object size="0 0 100% 16" name="captureLabel" type="text" style="StatsTextLeft" ghost="true">
<translatableAttribute id="tooltip">Capture points:</translatableAttribute>
</object>
<object size="0 0 100% 16" name="staminaStats" type="text" style="StatsTextRight" ghost="true"/>
<object size="1 16 100% 23" name="stamina" type="image">
<object size="0 0 100% 16" name="captureStats" type="text" style="StatsTextRight" ghost="true"/>
<object size="1 16 100% 23" name="capture" type="image">
<object type="image" sprite="barBorder" ghost="true" size="-1 -1 100%+1 100%+1"/>
<object type="image" sprite="staminaBackground" ghost="true"/>
<object type="image" sprite="staminaForeground" ghost="true" name="staminaBar"/>
<repeat count="9">
<object type="image" sprite="playerColorBackground" ghost="true" name="captureBar[n]" hidden="true"/>
</repeat>
<object type="image" sprite="statsBarShaderHorizontal" ghost="true"/>
</object>
</object>

View file

@ -99,9 +99,7 @@ var unitActions =
{
if (!entState.attack || !targetState.hitpoints)
return false;
if (playerCheck(entState, targetState, ["Neutral", "Enemy"]))
return {"possible": Engine.GuiInterfaceCall("CanAttack", {"entity": entState.id, "target": targetState.id})};
return false;
return {"possible": Engine.GuiInterfaceCall("CanAttack", {"entity": entState.id, "target": targetState.id})};
},
"hotkeyActionCheck": function(target)
{
@ -672,6 +670,13 @@ var g_EntityCommands =
"icon": "kill_small.png"
};
if (entState.capturePoints && entState.capturePoints[entState.player] < entState.maxCapturePoints / 2)
return {
"tooltip": translate("You cannot destroy this entity as you own less than half the capture points"),
"icon": "kill_small.png"
};
return {
"tooltip": translate("Delete"),
"icon": "kill_small.png"

View file

@ -111,6 +111,13 @@ AIProxy.prototype.OnHealthChanged = function(msg)
this.changes.hitpoints = msg.to;
};
AIProxy.prototype.OnCapturePointsChanged = function(msg)
{
if (!this.NotifyChange())
return;
this.changes.capturePoints = msg.capturePoints;
};
AIProxy.prototype.OnUnitIdleChanged = function(msg)
{
if (!this.NotifyChange())

View file

@ -158,6 +158,20 @@ Attack.prototype.Schema =
"</interleave>" +
"</element>" +
"</optional>" +
"<optional>" +
"<element name='Capture'>" +
"<interleave>" +
"<element name='Value' a:help='Capture points value'><ref name='nonNegativeDecimal'/></element>" +
"<element name='MaxRange' a:help='Maximum attack range (in meters)'><ref name='nonNegativeDecimal'/></element>" +
"<element name='RepeatTime' a:help='Time between attacks (in milliseconds). The attack animation will be stretched to match this time'>" + // TODO: it shouldn't be stretched
"<data type='positiveInteger'/>" +
"</element>" +
Attack.prototype.bonusesSchema +
Attack.prototype.preferredClassesSchema +
Attack.prototype.restrictedClassesSchema +
"</interleave>" +
"</element>" +
"</optional>" +
"<optional>" +
"<element name='Charge'>" +
"<interleave>" +
@ -198,6 +212,7 @@ Attack.prototype.GetAttackTypes = function()
if (this.template.Charge) ret.push("Charge");
if (this.template.Melee) ret.push("Melee");
if (this.template.Ranged) ret.push("Ranged");
if (this.template.Capture) ret.push("Capture");
return ret;
};
@ -223,6 +238,10 @@ Attack.prototype.GetRestrictedClasses = function(type)
Attack.prototype.CanAttack = function(target)
{
var cmpArmour = Engine.QueryInterface(target, IID_DamageReceiver);
if (!cmpArmour)
return false;
var cmpFormation = Engine.QueryInterface(target, IID_Formation);
if (cmpFormation)
return true;
@ -309,23 +328,40 @@ Attack.prototype.GetBestAttackAgainst = function(target)
if (cmpFormation)
return this.GetBestAttack();
const cmpIdentity = Engine.QueryInterface(target, IID_Identity);
var cmpIdentity = Engine.QueryInterface(target, IID_Identity);
if (!cmpIdentity)
return undefined;
const targetClasses = cmpIdentity.GetClassesList();
const isTargetClass = function (value, i, a) { return targetClasses.indexOf(value) != -1; };
const types = this.GetAttackTypes();
const attack = this;
const isAllowed = function (value, i, a) { return !attack.GetRestrictedClasses(value).some(isTargetClass); }
const isPreferred = function (value, i, a) { return attack.GetPreferredClasses(value).some(isTargetClass); }
const byPreference = function (a, b) { return (types.indexOf(a) + (isPreferred(a) ? types.length : 0) ) - (types.indexOf(b) + (isPreferred(b) ? types.length : 0) ); }
var targetClasses = cmpIdentity.GetClassesList();
var isTargetClass = function (className) { return targetClasses.indexOf(className) != -1; };
// Always slaughter domestic animals instead of using a normal attack
if (isTargetClass("Domestic") && this.template.Slaughter)
return "Slaughter";
return types.filter(isAllowed).sort(byPreference).pop();
var attack = this;
var isAllowed = function (type) { return !attack.GetRestrictedClasses(type).some(isTargetClass); }
var types = this.GetAttackTypes().filter(isAllowed);
// check if the target is capturable
var captureIndex = types.indexOf("Capture")
if (captureIndex != -1)
{
var cmpCapturable = Engine.QueryInterface(target, IID_Capturable);
var cmpPlayer = QueryOwnerInterface(this.entity);
if (cmpPlayer && cmpCapturable && cmpCapturable.CanCapture(cmpPlayer.GetPlayerID()))
return "Capture";
// not captureable, so remove this attack
types.splice(captureIndex, 1);
}
var isPreferred = function (className) { return attack.GetPreferredClasses(className).some(isTargetClass); }
var byPreference = function (a, b) { return (types.indexOf(a) + (isPreferred(a) ? types.length : 0) ) - (types.indexOf(b) + (isPreferred(b) ? types.length : 0) ); }
return types.sort(byPreference).pop();
};
Attack.prototype.CompareEntitiesByPreference = function(a, b)
@ -367,7 +403,10 @@ Attack.prototype.GetAttackStrengths = function(type)
{
return ApplyValueModificationsToEntity("Attack/" + type + splash + "/" + damageType, +(template[damageType] || 0), self.entity);
};
if (type == "Capture")
return {value: applyMods("Value")};
return {
hack: applyMods("Hack"),
pierce: applyMods("Pierce"),
@ -517,6 +556,25 @@ Attack.prototype.PerformAttack = function(type, target)
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.SetTimeout(this.entity, IID_Attack, "MissileHit", timeToTarget*1000, {"type": type, "target": target, "position": realTargetPosition, "direction": missileDirection, "projectileId": id, "playerId":playerId});
}
else if (type == "Capture")
{
var multiplier = this.GetAttackBonus(type, target);
var cmpHealth = Engine.QueryInterface(target, IID_Health);
if (!cmpHealth || cmpHealth.GetHitpoints() == 0)
return;
multiplier *= cmpHealth.GetMaxHitpoints() / cmpHealth.GetHitpoints();
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership || cmpOwnership.GetOwner() == -1)
return;
var owner = cmpOwnership.GetOwner();
var cmpCapturable = Engine.QueryInterface(target, IID_Capturable);
if (!cmpCapturable || !cmpCapturable.CanCapture(owner))
return;
var strength = this.GetAttackStrengths("Capture").value;
cmpCapturable.Reduce(strength * multiplier, owner);
}
else
{
// Melee attack - hurt the target immediately
@ -585,7 +643,7 @@ Attack.prototype.MissileHit = function(data, lateness)
// If friendlyFire isn't enabled, get all player enemies to pass to "Damage.CauseSplashDamage".
if (friendlyFire == "false")
{
var cmpPlayer = Engine.QueryInterface(Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetPlayerByID(data.playerId), IID_Player)
var cmpPlayer = QueryPlayerIDInterface(data.playerId);
playersToDamage = cmpPlayer.GetEnemies();
}
// Damage the units.
@ -607,7 +665,7 @@ Attack.prototype.MissileHit = function(data, lateness)
else
{
// If we didn't hit the main target look for nearby units
var cmpPlayer = Engine.QueryInterface(Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetPlayerByID(data.playerId), IID_Player)
var cmpPlayer = QueryPlayerIDInterface(data.playerId);
var ents = Damage.EntitiesNearPoint(Vector2D.from3D(data.position), targetPosition.horizDistanceTo(data.position) * 2, cmpPlayer.GetEnemies());
for (var i = 0; i < ents.length; i++)

View file

@ -0,0 +1,212 @@
function Capturable() {}
Capturable.prototype.Schema =
"<element name='CapturePoints' a:help='Maximum capture points'>" +
"<ref name='positiveDecimal'/>" +
"</element>" +
"<element name='RegenRate' a:help='Number of capture are regenerated per second in favour of the owner'>" +
"<ref name='nonNegativeDecimal'/>" +
"</element>" +
"<element name='GarrisonRegenRate' a:help='Number of capture are regenerated per second and per garrisoned unit in favour of the owner'>" +
"<ref name='nonNegativeDecimal'/>" +
"</element>";
Capturable.prototype.Init = function()
{
// Cache this value
this.maxCp = +this.template.CapturePoints;
this.cp = [];
this.startRegenTimer();
};
//// Interface functions ////
/**
* Returns the current capture points array
*/
Capturable.prototype.GetCapturePoints = function()
{
return this.cp;
};
Capturable.prototype.GetMaxCapturePoints = function()
{
return this.maxCp;
};
/**
* Set the new capture points, used for cloning entities
* The caller should assure that the sum of capture points
* matches the max.
*/
Capturable.prototype.SetCapturePoints = function(capturePointsArray)
{
this.cp = capturePointsArray;
};
/**
* Reduces the amount of capture points of an entity,
* in favour of the player of the source
* Returns the number of capture points actually taken
*/
Capturable.prototype.Reduce = function(amount, playerID)
{
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership || cmpOwnership.GetOwner() == -1)
return 0;
var cmpPlayerSource = QueryPlayerIDInterface(playerID);
if (!cmpPlayerSource)
return 0;
// Before changing the value, activate Fogging if necessary to hide changes
var cmpFogging = Engine.QueryInterface(this.entity, IID_Fogging);
if (cmpFogging)
cmpFogging.Activate();
var enemiesFilter = function(v, i) { return v > 0 && !cmpPlayerSource.IsAlly(i); };
var numberOfEnemies = this.cp.filter(enemiesFilter).length;
if (numberOfEnemies == 0)
return 0;
// distribute the capture points over all enemies
var distributedAmount = amount / numberOfEnemies;
for (let i in this.cp)
{
if (cmpPlayerSource.IsAlly(i))
continue;
if (this.cp[i] > distributedAmount)
this.cp[i] -= distributedAmount;
else
this.cp[i] = 0;
}
// give all cp taken to the player
var takenCp = this.maxCp - this.cp.reduce(function(a, b) { return a + b; });
this.cp[playerID] += takenCp;
this.startRegenTimer();
Engine.PostMessage(this.entity, MT_CapturePointsChanged, { "capturePoints": this.cp })
if (this.cp[cmpOwnership.GetOwner()] > 0)
return takenCp;
// if all cp has been taken from the owner, convert it to the best player
var bestPlayer = 0;
for (let i in this.cp)
if (this.cp[i] >= this.cp[bestPlayer])
bestPlayer = +i;
cmpOwnership.SetOwner(bestPlayer);
return takenCp;
};
/**
* Check if the source can (re)capture points from this building
*/
Capturable.prototype.CanCapture = function(playerID)
{
var cmpPlayerSource = QueryPlayerIDInterface(playerID);
if (!cmpPlayerSource)
warn(source + " has no player component defined on its owner ");
var cp = this.GetCapturePoints()
var sourceEnemyCp = 0;
for (let i in this.GetCapturePoints())
if (!cmpPlayerSource.IsAlly(i))
sourceEnemyCp += cp[i];
return sourceEnemyCp > 0;
};
//// Private functions ////
Capturable.prototype.GetRegenRate = function()
{
var regenRate = +this.template.RegenRate;
regenRate = ApplyValueModificationsToEntity("Capturable/RegenRate", regenRate, this.entity);
var cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
if (!cmpGarrisonHolder)
return regenRate;
var garrisonRegenRate = +this.template.GarrisonRegenRate;
garrisonRegenRate = ApplyValueModificationsToEntity("Capturable/GarrisonRegenRate", garrisonRegenRate, this.entity);
var garrisonedUnits = cmpGarrisonHolder.GetEntities().length;
return regenRate + garrisonedUnits * garrisonRegenRate;
};
Capturable.prototype.RegenCapturePoints = function()
{
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership || cmpOwnership.GetOwner() == -1)
return;
var takenCp = this.Reduce(this.GetRegenRate(), cmpOwnership.GetOwner())
if (takenCp > 0)
return;
// no capture points taken, stop the timer
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.regenTimer);
this.regenTimer = 0;
};
/**
* Start the regeneration timer when no timer exists
* When nothing can be regenerated (f.e. because the
* rate is 0, or because it is fully regenerated),
* the timer stops automatically after one execution.
*/
Capturable.prototype.startRegenTimer = function()
{
if (this.regenTimer)
return;
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.regenTimer = cmpTimer.SetInterval(this.entity, IID_Capturable, "RegenCapturePoints", 1000, 1000, null);
};
//// Message Listeners ////
Capturable.prototype.OnValueModification = function(msg)
{
if (msg.component != "Capturable")
return;
var oldMaxCp = this.GetMaxCapturePoints();
this.maxCp = ApplyValueModificationsToEntity("Capturable/Max", +this.template.Max, this.entity);
if (oldMaxCp == this.maxCp)
return;
var scale = this.maxCp / oldMaxCp;
for (let i in this.cp)
this.cp[i] *= scale;
Engine.PostMessage(this.entity, MT_CapturePointsChanged, { "capturePoints": this.cp });
this.startRegenTimer();
};
Capturable.prototype.OnGarrisonedUnitsChanged = function(msg)
{
this.startRegenTimer();
};
Capturable.prototype.OnOwnershipChanged = function(msg)
{
this.startRegenTimer();
if (msg.from != -1)
return;
// initialise the capture points when created
this.cp = [];
var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
for (let i = 0; i < cmpPlayerManager.GetNumPlayers(); ++i)
if (i == msg.to)
this.cp[i] = this.maxCp;
else
this.cp[i] = 0;
};
Engine.RegisterComponentType(IID_Capturable, "Capturable", Capturable);

View file

@ -125,6 +125,13 @@ Fogging.prototype.LoadMirage = function(player)
cmpHealth.IsRepairable() && (cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints())
);
var cmpCapturable = Engine.QueryInterface(this.entity, IID_Capturable);
if (cmpCapturable)
cmpMirage.CopyCapturable(
cmpCapturable.GetCapturePoints(),
cmpCapturable.GetMaxCapturePoints()
);
var cmpResourceSupply = Engine.QueryInterface(this.entity, IID_ResourceSupply);
if (cmpResourceSupply)
cmpMirage.CopyResourceSupply(

View file

@ -267,6 +267,18 @@ GuiInterface.prototype.GetEntityState = function(player, ent)
ret.needsRepair = cmpMirage.NeedsRepair();
}
var cmpCapturable = Engine.QueryInterface(ent, IID_Capturable);
if (cmpCapturable)
{
ret.capturePoints = cmpCapturable.GetCapturePoints();
ret.maxCapturePoints = cmpCapturable.GetMaxCapturePoints();
}
if (cmpMirage && cmpMirage.Capturable())
{
ret.capturePoints = cmpMirage.GetCapturePoints();
ret.maxCapturePoints = cmpMirage.GetMaxCapturePoints();
}
var cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
if (cmpBuilder)
ret.builder = true;
@ -1701,8 +1713,22 @@ GuiInterface.prototype.CanAttack = function(player, data)
var cmpAttack = Engine.QueryInterface(data.entity, IID_Attack);
if (!cmpAttack)
return false;
var cmpEntityPlayer = QueryOwnerInterface(data.entity, IID_Player);
var cmpTargetPlayer = QueryOwnerInterface(data.target, IID_Player);
if (!cmpEntityPlayer || !cmpTargetPlayer)
return false;
return cmpAttack.CanAttack(data.target);
// if the owner is an enemy, it's up to the attack component to decide
if (!cmpEntityPlayer.IsAlly(cmpTargetPlayer.GetPlayerID()))
return cmpAttack.CanAttack(data.target);
// if the owner is an ally, we could still want to capture some capture points back
var cmpCapturable = Engine.QueryInterface(data.target, IID_Capturable);
if (cmpCapturable && cmpCapturable.CanCapture(cmpEntityPlayer.GetPlayerID()) && cmpAttack.GetAttackTypes().indexOf("Capture") != -1)
return cmpAttack.CanAttack(data.target);
return false;
};
/*

View file

@ -21,6 +21,10 @@ Mirage.prototype.Init = function()
this.hitpoints = null;
this.needsRepair = null;
this.capturable = false;
this.capturePoints = [];
this.maxCapturePoints = 0;
this.resourceSupply = false;
this.maxAmount = null;
this.amount = null;
@ -94,6 +98,30 @@ Mirage.prototype.NeedsRepair = function()
return this.needsRepair;
};
// Capture data
Mirage.prototype.CopyCapturable = function(capturePoints, maxCapturePoints)
{
this.capturable = true;
this.capturePoints = capturePoints;
this.maxCapturePoints = maxCapturePoints;
};
Mirage.prototype.Capturable = function()
{
return this.capturable;
};
Mirage.prototype.GetMaxCapturePoints = function()
{
return this.maxCapturePoints;
};
Mirage.prototype.GetCapturePoints = function()
{
return this.capturePoints;
};
// ResourceSupply data
Mirage.prototype.CopyResourceSupply = function(maxAmount, amount, type, isInfinite)

View file

@ -148,6 +148,16 @@ Pack.prototype.PackProgress = function(data, lateness)
var cmpNewOwnership = Engine.QueryInterface(newEntity, IID_Ownership);
cmpNewOwnership.SetOwner(cmpOwnership.GetOwner());
// rescale capture points
var cmpCapturable = Engine.QueryInterface(this.entity, IID_Capturable);
var cmpNewCapturable = Engine.QueryInterface(newEntity, IID_Capturable);
if (cmpCapturable && cmpNewCapturable)
{
let scale = cmpCapturable.GetMaxCapturePoints() / cmpNewCapturable.GetMaxCapturePoints();
let newCp = cmpCapturable.GetCapturePoints().map(function (v) { return v / scale; });
cmpNewCapturable.SetCapturePoints(newCp);
}
// Maintain current health level
var cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
var cmpNewHealth = Engine.QueryInterface(newEntity, IID_Health);

View file

@ -1,7 +1,7 @@
function TerritoryDecay() {}
TerritoryDecay.prototype.Schema =
"<element name='HealthDecayRate' a:help='Decay rate in hitpoints per second'>" +
"<element name='DecayRate' a:help='Decay rate in hitpoints per second'>" +
"<data type='positiveInteger'/>" +
"</element>";
@ -33,8 +33,6 @@ TerritoryDecay.prototype.IsConnected = function()
var tileOwner = cmpTerritoryManager.GetOwner(pos.x, pos.y);
if (tileOwner != cmpOwnership.GetOwner())
return false;
// TODO: this should probably use the same territory restriction
// logic as BuildRestrictions, to handle allies etc
return cmpTerritoryManager.IsConnected(pos.x, pos.y);
};
@ -64,7 +62,7 @@ TerritoryDecay.prototype.UpdateDecayState = function()
if (connected)
var decaying = false;
else
var decaying = (Math.round(ApplyValueModificationsToEntity("TerritoryDecay/HealthDecayRate", +this.template.HealthDecayRate, this.entity)) > 0);
var decaying = (Math.round(ApplyValueModificationsToEntity("TerritoryDecay/DecayRate", +this.template.DecayRate, this.entity)) > 0);
if (decaying === this.decaying)
return;
this.decaying = decaying;
@ -88,13 +86,17 @@ TerritoryDecay.prototype.OnOwnershipChanged = function(msg)
TerritoryDecay.prototype.Decay = function()
{
var cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
if (!cmpHealth)
var cmpCapturable = Engine.QueryInterface(this.entity, IID_Capturable);
if (!cmpCapturable)
return; // error
var decayRate = ApplyValueModificationsToEntity("TerritoryDecay/HealthDecayRate", +this.template.HealthDecayRate, this.entity);
var decayRate = ApplyValueModificationsToEntity(
"TerritoryDecay/DecayRate",
+this.template.DecayRate,
this.entity);
cmpHealth.Reduce(Math.round(decayRate));
// Reduce capture points in favour of Gaia
cmpCapturable.Reduce(decayRate, 0);
};
Engine.RegisterComponentType(IID_TerritoryDecay, "TerritoryDecay", TerritoryDecay);

View file

@ -5559,20 +5559,25 @@ UnitAI.prototype.CanAttack = function(target, forceResponse)
return false;
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership)
if (!cmpOwnership || cmpOwnership.GetOwner() < 0)
return false;
var owner = cmpOwnership.GetOwner();
// Verify that the target is an attackable resource supply like a domestic animal
// or that it isn't owned by an ally of this entity's player or is responding to
// an attack.
var owner = cmpOwnership.GetOwner();
if (!this.MustKillGatherTarget(target)
&& !(IsOwnedByEnemyOfPlayer(owner, target)
|| IsOwnedByNeutralOfPlayer(owner, target)
|| (forceResponse && !IsOwnedByPlayer(owner, target))))
return false;
if (this.MustKillGatherTarget(target))
return true;
return true;
var cmpCapturable = Engine.QueryInterface(target, IID_Capturable);
if (cmpCapturable && cmpCapturable.CanCapture(owner) && cmpAttack.GetAttackTypes().indexOf("Capture") != -1)
return true;
if (IsOwnedByEnemyOfPlayer(owner, target) || IsOwnedByNeutralOfPlayer(owner, target))
return true;
if (forceResponse && !IsOwnedByPlayer(owner, target))
return true;
return false;
};
UnitAI.prototype.CanGarrison = function(target)

View file

@ -0,0 +1,5 @@
Engine.RegisterInterface("Capturable");
// Message in the form of {"capturePoints": [gaia, p1, p2, ...]}
Engine.RegisterMessageType("CapturePointsChanged");

View file

@ -7,7 +7,7 @@
"icon": "blocks_three.png",
"researchTime": 40,
"tooltip": "Territory decay -50% for Outposts.",
"modifications": [{"value": "TerritoryDecay/HealthDecayRate", "multiply": 0.5}],
"modifications": [{"value": "TerritoryDecay/DecayRate", "multiply": 0.5}],
"affects": ["Outpost"],
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
}

View file

@ -7,7 +7,7 @@
"icon": "handcart_empty.png",
"researchTime": 40,
"tooltip": "Entrenched Camps and Siege Walls decay 50% slower.",
"modifications": [{"value": "TerritoryDecay/HealthDecayRate", "multiply": 0.5}],
"modifications": [{"value": "TerritoryDecay/DecayRate", "multiply": 0.5}],
"affects": ["ArmyCamp", "SiegeWall"],
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
}

View file

@ -349,8 +349,18 @@ var commands = {
"delete-entities": function(player, cmd, data)
{
for each (var ent in data.entities)
for (let ent of data.entities)
{
// don't allow to delete entities who are half-captured
var cmpCapturable = Engine.QueryInterface(ent, IID_Capturable);
if (cmpCapturable)
{
var capturePoints = cmpCapturable.GetCapturePoints();
var maxCapturePoints = cmpCapturable.GetMaxCapturePoints();
if (capturePoints[player] < maxCapturePoints / 2)
return;
}
// either kill or delete the entity
var cmpHealth = Engine.QueryInterface(ent, IID_Health);
if (cmpHealth)
{

View file

@ -60,7 +60,7 @@
</SoundGroups>
</Sound>
<TerritoryDecay>
<HealthDecayRate>1</HealthDecayRate>
<DecayRate>1</DecayRate>
</TerritoryDecay>
<TerritoryInfluence disable=""/>
<VisualActor>

View file

@ -62,7 +62,7 @@
</SoundGroups>
</Sound>
<TerritoryDecay>
<HealthDecayRate>1</HealthDecayRate>
<DecayRate>1</DecayRate>
</TerritoryDecay>
<TerritoryInfluence disable=""/>
<VisualActor>

View file

@ -76,7 +76,7 @@
</SoundGroups>
</Sound>
<TerritoryDecay>
<HealthDecayRate>1</HealthDecayRate>
<DecayRate>1</DecayRate>
</TerritoryDecay>
<TerritoryInfluence>
<Radius>80</Radius>

View file

@ -76,7 +76,7 @@
</SoundGroups>
</Sound>
<TerritoryDecay>
<HealthDecayRate>10</HealthDecayRate>
<DecayRate>10</DecayRate>
</TerritoryDecay>
<TerritoryInfluence disable=""/>
<ProductionQueue>

View file

@ -44,7 +44,7 @@
</Obstructions>
</Obstruction>
<TerritoryDecay>
<HealthDecayRate>1</HealthDecayRate>
<DecayRate>1</DecayRate>
</TerritoryDecay>
<TerritoryInfluence disable=""/>
<VisualActor>

View file

@ -62,7 +62,7 @@
<Static width="37.0" depth="5.0"/>
</Obstruction>
<TerritoryDecay>
<HealthDecayRate>1</HealthDecayRate>
<DecayRate>1</DecayRate>
</TerritoryDecay>
<TerritoryInfluence disable=""/>
<VisualActor>

View file

@ -55,7 +55,7 @@
<Static width="25.0" depth="5.0"/>
</Obstruction>
<TerritoryDecay>
<HealthDecayRate>1</HealthDecayRate>
<DecayRate>1</DecayRate>
</TerritoryDecay>
<TerritoryInfluence disable=""/>
<VisualActor>

View file

@ -42,7 +42,7 @@
<Static width="13.0" depth="5.0"/>
</Obstruction>
<TerritoryDecay>
<HealthDecayRate>1</HealthDecayRate>
<DecayRate>1</DecayRate>
</TerritoryDecay>
<TerritoryInfluence disable=""/>
<VisualActor>

View file

@ -36,7 +36,7 @@
<Static width="7.0" depth="7.0"/>
</Obstruction>
<TerritoryDecay>
<HealthDecayRate>1</HealthDecayRate>
<DecayRate>1</DecayRate>
</TerritoryDecay>
<TerritoryInfluence disable=""/>
<VisualActor>

View file

@ -50,7 +50,7 @@
</SoundGroups>
</Sound>
<TerritoryDecay>
<HealthDecayRate>1</HealthDecayRate>
<DecayRate>1</DecayRate>
</TerritoryDecay>
<TerritoryInfluence disable=""/>
<VisualActor>

View file

@ -75,7 +75,7 @@
</SoundGroups>
</Sound>
<TerritoryDecay>
<HealthDecayRate>1</HealthDecayRate>
<DecayRate>1</DecayRate>
</TerritoryDecay>
<TerritoryInfluence>
<Radius>80</Radius>

View file

@ -20,6 +20,11 @@
<PlacementType>land</PlacementType>
<Territory>own</Territory>
</BuildRestrictions>
<Capturable>
<CapturePoints>1000</CapturePoints>
<RegenRate>0</RegenRate>
<GarrisonRegenRate>3</GarrisonRegenRate>
</Capturable>
<Cost>
<Population>0</Population>
<PopulationBonus>0</PopulationBonus>
@ -100,7 +105,7 @@
<HeightOffset>12.0</HeightOffset>
</StatusBars>
<TerritoryDecay>
<HealthDecayRate>5</HealthDecayRate>
<DecayRate>5</DecayRate>
</TerritoryDecay>
<Visibility>
<RetainInFog>true</RetainInFog>

View file

@ -35,6 +35,9 @@
<MinDistance>200</MinDistance>
</Distance>
</BuildRestrictions>
<Capturable>
<CapturePoints>3000</CapturePoints>
</Capturable>
<Cost>
<PopulationBonus>20</PopulationBonus>
<BuildTime>500</BuildTime>

View file

@ -3,6 +3,9 @@
<BuildRestrictions>
<Category>House</Category>
</BuildRestrictions>
<Capturable>
<CapturePoints>300</CapturePoints>
</Capturable>
<Cost>
<PopulationBonus>5</PopulationBonus>
<BuildTime>30</BuildTime>

View file

@ -90,7 +90,7 @@
<HeightOffset>18.0</HeightOffset>
</StatusBars>
<TerritoryDecay>
<HealthDecayRate>2</HealthDecayRate>
<DecayRate>2</DecayRate>
</TerritoryDecay>
<Vision>
<Range>80</Range>

View file

@ -4,6 +4,7 @@
<PlacementType>land-shore</PlacementType>
<Category>Wall</Category>
</BuildRestrictions>
<Capturable disable=""/>
<Cost>
<BuildTime>10</BuildTime>
<Resources>

View file

@ -6,6 +6,7 @@
<BuildRestrictions>
<Category>Wall</Category>
</BuildRestrictions>
<Capturable disable=""/>
<Cost>
<BuildTime>0</BuildTime>
<Resources>

View file

@ -23,6 +23,7 @@
<PlacementType>land-shore</PlacementType>
<Category>Wall</Category>
</BuildRestrictions>
<Capturable disable=""/>
<Cost>
<BuildTime>120</BuildTime>
<Resources>

View file

@ -3,6 +3,9 @@
<BuildRestrictions>
<Category>Farmstead</Category>
</BuildRestrictions>
<Capturable>
<CapturePoints>300</CapturePoints>
</Capturable>
<Cost>
<BuildTime>45</BuildTime>
<Resources>

View file

@ -3,6 +3,9 @@
<BuildRestrictions>
<Category>Storehouse</Category>
</BuildRestrictions>
<Capturable>
<CapturePoints>300</CapturePoints>
</Capturable>
<Cost>
<BuildTime>40</BuildTime>
<Resources>

View file

@ -26,6 +26,9 @@
<MinDistance>80</MinDistance>
</Distance>
</BuildRestrictions>
<Capturable>
<CapturePoints>4000</CapturePoints>
</Capturable>
<Cost>
<PopulationBonus>10</PopulationBonus>
<BuildTime>300</BuildTime>

View file

@ -8,6 +8,7 @@
<BuildRestrictions>
<Category>Field</Category>
</BuildRestrictions>
<Capturable disable=""/>
<Cost>
<BuildTime>50</BuildTime>
<Resources>

View file

@ -13,6 +13,9 @@
<BuildRestrictions>
<Category>Wonder</Category>
</BuildRestrictions>
<Capturable>
<CapturePoints>5000</CapturePoints>
</Capturable>
<Cost>
<BuildTime>1000</BuildTime>
<Resources>

View file

@ -6,6 +6,11 @@
<Crush>15</Crush>
</Armour>
<Attack>
<Capture>
<Value>3</Value>
<MaxRange>4</MaxRange>
<RepeatTime>1000</RepeatTime>
</Capture>
<Slaughter>
<Hack>100.0</Hack>
<Pierce>0.0</Pierce>

View file

@ -1,5 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity parent="template_unit">
<Attack>
<Capture>
<Value>3</Value>
<MaxRange>4</MaxRange>
<RepeatTime>1000</RepeatTime>
</Capture>
</Attack>
<Identity>
<GenericName>Champion Unit</GenericName>
<Classes datatype="tokens">Organic Human</Classes>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity parent="template_unit_champion_elephant">
<Attack>
<Attack replace="">
<Melee>
<Hack>20</Hack>
<Pierce>0</Pierce>

View file

@ -5,7 +5,7 @@
<Pierce>10</Pierce>
<Crush>12</Crush>
</Armour>
<Attack>
<Attack replace="">
<Melee>
<Hack>17.5</Hack>
<Pierce>0</Pierce>

View file

@ -6,6 +6,11 @@
<Crush>15</Crush>
</Armour>
<Attack>
<Capture>
<Value>3</Value>
<MaxRange>4</MaxRange>
<RepeatTime>1000</RepeatTime>
</Capture>
<Slaughter>
<Hack>50.0</Hack>
<Pierce>0.0</Pierce>

View file

@ -5,6 +5,11 @@
<Pierce>5</Pierce>
<Crush>5</Crush>
</Armour>
<Capturable>
<CapturePoints>100</CapturePoints>
<RegenRate>0</RegenRate>
<GarrisonRegenRate>1.5</GarrisonRegenRate>
</Capturable>
<Cost>
<Population>5</Population>
</Cost>