1
0
forked from 0ad/0ad

Technically seperate Turrets from GarrisonHolder.

While they often look alike, their behaviour is totally different.
This split has some implications:
- There are now separate auras for garrisoning and turrets.
- Entities can now have both turret points and garrison slots,
independent of eachother.

In general previous behaviour is maintained as much as possible.

Differential revision: D3150
Comments by: @Nescio, @wraitii
Tested by: @v32itas
This was SVN commit r25123.
This commit is contained in:
Freagarach 2021-03-26 10:18:30 +00:00
parent 7bf2f9ed74
commit 21e866fcf0
60 changed files with 906 additions and 397 deletions

View File

@ -309,11 +309,13 @@ kill = Delete, Backspace ; Destroy selected units
stop = "H" ; Stop the current action
backtowork = "Y" ; The unit will go back to work
unload = "U" ; Unload garrisoned units when a building/mechanical unit is selected
unloadturrets = "U" ; Unload turreted units.
move = "" ; Modifier to move to a point instead of another action (e.g. gather)
attack = Ctrl ; Modifier to attack instead of another action (e.g. capture)
attackmove = Ctrl ; Modifier to attackmove when clicking on a point
attackmoveUnit = "Ctrl+Q" ; Modifier to attackmove targeting only units when clicking on a point
garrison = Ctrl ; Modifier to garrison when clicking on building
occupyturret = Ctrl ; Modifier to occupy a turret when clicking on a turret holder.
autorallypoint = Ctrl ; Modifier to set the rally point on the building itself
guard = "G" ; Modifier to escort/guard when clicking on unit/building
patrol = "P" ; Modifier to patrol a unit

View File

@ -491,6 +491,11 @@ function GetTemplateDataHelper(template, player, auraTemplates, modifiers = {})
ret.treasure.resources[resource] = getEntityValue("Treasure/Resources/" + resource);
}
if (template.TurretHolder)
ret.turretHolder = {
"turretPoints": template.TurretHolder.TurretPoints
};
if (template.WallSet)
{
ret.wallSet = {

View File

@ -588,6 +588,16 @@ function getGarrisonTooltip(template)
return tooltips.join("\n");
}
function getTurretsTooltip(template)
{
if (!template.turretHolder)
return "";
return sprintf(translate("%(label)s: %(turretsLimit)s"), {
"label": headerFont(translate("Turret Positions")),
"turretsLimit": Object.keys(template.turretHolder.turretPoints).length
});
}
function getProjectilesTooltip(template)
{
if (!template.garrisonHolder || !template.buildingAI)

View File

@ -27,6 +27,10 @@
"name": "Unload",
"desc": "Unload garrisoned units when a building/mechanical unit is selected."
},
"session.unloadturrets": {
"name": "Unload Turrets",
"desc": "Unload turreted units."
},
"session.unloadtype": {
"name": "Unload unit type",
"desc": "Modifier to unload all units of type."
@ -51,6 +55,10 @@
"name": "Garrison",
"desc": "Modifier to garrison when clicking on building."
},
"session.occupyturret": {
"name": "Occupy Turret Point",
"desc": "Modifier to occupy a turret when clicking on a turret holder."
},
"session.autorallypoint": {
"name": "Auto-rally point",
"desc": "Modifier to set the rally point on the building itself."

View File

@ -57,6 +57,7 @@ ReferencePage.prototype.StatsFunctions = [
getHealerTooltip,
getResistanceTooltip,
getGarrisonTooltip,
getTurretsTooltip,
getProjectilesTooltip,
getSpeedTooltip,
getGatherTooltip,

View File

@ -32,6 +32,10 @@
<action on="KeyDown">unloadAll();</action>
</object>
<object hotkey="session.unloadturrets">
<action on="KeyDown">unloadAllTurrets();</action>
</object>
<object hotkey="session.stop">
<action on="KeyDown">stopUnits(g_Selection.toList());</action>
</object>

View File

@ -325,6 +325,7 @@ function displaySingle(entState)
getGatherTooltip,
getSpeedTooltip,
getGarrisonTooltip,
getTurretsTooltip,
getPopulationBonusTooltip,
getProjectilesTooltip,
getResourceTrickleTooltip,

View File

@ -193,6 +193,7 @@ g_SelectionPanels.Construction = {
getEntityCostTooltip(template, data.player),
getResourceDropsiteTooltip(template),
getGarrisonTooltip(template),
getTurretsTooltip(template),
getPopulationBonusTooltip(template),
showTemplateViewerOnRightClickTooltip(template)
);
@ -975,6 +976,7 @@ g_SelectionPanels.Training = {
getHealerTooltip,
getResistanceTooltip,
getGarrisonTooltip,
getTurretsTooltip,
getProjectilesTooltip,
getSpeedTooltip,
getResourceDropsiteTooltip

View File

@ -415,31 +415,6 @@ function unloadTemplate(template, owner)
});
}
function unloadSelection()
{
let parent = 0;
let ents = [];
for (let ent in g_Selection.selected)
{
let state = GetEntityState(+ent);
if (!state || !state.turretParent)
continue;
if (!parent)
{
parent = state.turretParent;
ents.push(+ent);
}
else if (state.turretParent == parent)
ents.push(+ent);
}
if (parent)
Engine.PostNetworkCommand({
"type": "unload",
"entities": ents,
"garrisonHolder": parent
});
}
function unloadAll()
{
let garrisonHolders = g_Selection.toList().filter(e => {
@ -474,6 +449,44 @@ function unloadAll()
});
}
function unloadAllTurrets()
{
let turretHolders = g_Selection.toList().filter(e => {
let state = GetEntityState(e);
return state && !!state.turretHolder;
});
if (!turretHolders.length)
return;
let ownedHolders = [];
let ejectables = [];
for (let ent of turretHolders)
{
let turretHolderState = GetEntityState(ent);
if (controlsPlayer(turretHolderState.player))
ownedHolders.push(ent);
else
{
for (let turret of turretHolderState.turretHolder.turretPoints.map(tp => tp.entity))
if (turret && controlsPlayer(GetEntityState(turret).player))
ejectables.push(turret);
}
}
if (ejectables.length)
Engine.PostNetworkCommand({
"type": "leave-turret",
"entities": ejectables
});
if (ownedHolders.length)
Engine.PostNetworkCommand({
"type": "unload-turrets",
"entities": ownedHolders
});
}
function backToWork()
{
Engine.PostNetworkCommand({

View File

@ -684,6 +684,77 @@ var g_UnitActions =
"specificness": 0,
},
"occupy-turret":
{
"execute": function(target, action, selection, queued, pushFront)
{
Engine.PostNetworkCommand({
"type": "occupy-turret",
"entities": selection,
"target": action.target,
"queued": queued,
"pushFront": pushFront,
"formation": g_AutoFormation.getNull()
});
Engine.GuiInterfaceCall("PlaySound", {
"name": "order_garrison",
"entity": action.firstAbleEntity
});
return true;
},
"getActionInfo": function(entState, targetState)
{
if (!entState.turretable || !targetState || !targetState.turretHolder ||
!playerCheck(entState, targetState, ["Player", "MutualAlly"]))
return false;
if (!targetState.turretHolder.turretPoints.find(point =>
!point.allowedClasses || MatchesClassList(entState.identity.classes, point.allowedClasses)))
return false;
let occupiedTurrets = targetState.turretHolder.turretPoints.filter(point => point.entity != null);
let tooltip = sprintf(translate("Current turrets: %(occupied)s/%(capacity)s"), {
"occupied": occupiedTurrets.length,
"capacity": targetState.turretHolder.turretPoints.length
});
if (occupiedTurrets.length == targetState.turretHolder.turretPoints.length)
tooltip = coloredText(tooltip, "orange");
return {
"possible": true,
"tooltip": tooltip
};
},
"preSelectedActionCheck": function(target, selection)
{
return preSelectedAction == ACTION_GARRISON &&
(this.actionCheck(target, selection) || {
"type": "none",
"cursor": "action-garrison-disabled",
"target": null
});
},
"hotkeyActionCheck": function(target, selection)
{
return Engine.HotkeyIsPressed("session.occupyturret") &&
this.actionCheck(target, selection);
},
"actionCheck": function(target, selection)
{
let actionInfo = getActionInfo("occupy-turret", target, selection);
return actionInfo.possible && {
"type": "occupy-turret",
"cursor": "action-garrison",
"tooltip": actionInfo.tooltip,
"target": target
};
},
"specificness": 21,
},
"garrison":
{
"execute": function(target, action, selection, queued, pushFront)
@ -984,6 +1055,22 @@ var g_UnitActions =
targetState.garrisonHolder.capacity)
tooltip = coloredText(tooltip, "orange");
}
else if (targetState && targetState.turretHolder &&
playerCheck(entState, targetState, ["Player", "MutualAlly"]))
{
data.command = "occupy-turret";
data.target = targetState.id;
cursor = "action-garrison";
let occupiedTurrets = targetState.turretHolder.turretPoints.filter(point => point.entity != null);
tooltip = sprintf(translate("Current turrets: %(occupied)s/%(capacity)s"), {
"occupied": occupiedTurrets.length,
"capacity": targetState.turretHolder.turretPoints.length
});
if (occupiedTurrets.length >= targetState.turretHolder.turretPoints.length)
tooltip = coloredText(tooltip, "orange");
}
else if (targetState && targetState.resourceSupply)
{
let resourceType = targetState.resourceSupply.type;
@ -1224,6 +1311,41 @@ var g_EntityCommands =
"allowedPlayers": ["Player", "Ally"]
},
"unload-all-turrets": {
"getInfo": function(entStates)
{
let count = 0;
for (let entState of entStates)
{
if (!entState.turretHolder)
continue;
if (allowedPlayersCheck([entState], ["Player"]))
count += entState.turretHolder.turretPoints.filter(turretPoint => turretPoint.entity).length;
else
for (let turretPoint of entState.turretHolder.turretPoints)
if (turretPoint.entity && allowedPlayersCheck([GetEntityState(turretPoint.entity)], ["Player"]))
++count;
}
if (!count)
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.unloadturrets") +
translate("Unload Turrets."),
"icon": "garrison-out.png",
"count": count,
"enabled": true
};
},
"execute": function()
{
unloadAllTurrets();
},
"allowedPlayers": ["Player", "Ally"]
},
"delete": {
"getInfo": function(entStates)
{
@ -1317,15 +1439,11 @@ var g_EntityCommands =
"allowedPlayers": ["Player"]
},
"unload": {
"leave-turret": {
"getInfo": function(entStates)
{
if (entStates.every(entState => {
if (!entState.unitAI || !entState.turretParent)
return true;
let parent = GetEntityState(entState.turretParent);
return !parent || !parent.garrisonHolder || parent.garrisonHolder.entities.indexOf(entState.id) == -1;
}))
if (entStates.every(entState => !entState.turretable ||
entState.turretable.holder == INVALID_ENTITY))
return false;
return {
@ -1334,9 +1452,16 @@ var g_EntityCommands =
"enabled": true
};
},
"execute": function()
"execute": function(entStates)
{
unloadSelection();
if (!entStates.length)
return;
Engine.PostNetworkCommand({
"type": "leave-turret",
"entities": entStates.filter(entState => entState.turretable &&
entState.turretable.holder != INVALID_ENTITY).map(entState => entState.id)
});
},
"allowedPlayers": ["Player"]
},

View File

@ -543,6 +543,8 @@ m.Template = m.Class({
"isGarrisonHolder": function() { return this.get("GarrisonHolder") !== undefined; },
"isTurretHolder": function() { return this.get("TurretHolder") !== undefined; },
/**
* returns true if the tempalte can capture the given target entity
* if no target is given, returns true if the template has the Capture attack
@ -565,6 +567,8 @@ m.Template = m.Class({
"canGarrison": function() { return "Garrisonable" in this._template; },
"canOccupyTurret": function() { return "Turretable" in this._template; },
"isTreasureCollecter": function() { return this.get("TreasureCollecter") !== undefined; },
});
@ -840,23 +844,29 @@ m.Entity = m.Class({
return this;
},
"garrison": function(target, queued = false) {
Engine.PostCommand(PlayerID, { "type": "garrison", "entities": [this.id()], "target": target.id(), "queued": queued });
"garrison": function(target, queued = false, pushFront = false) {
Engine.PostCommand(PlayerID, { "type": "garrison", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront });
return this;
},
"attack": function(unitId, allowCapture = true, queued = false) {
Engine.PostCommand(PlayerID, { "type": "attack", "entities": [this.id()], "target": unitId, "allowCapture": allowCapture, "queued": queued });
"occupy-turret": function(target, queued = false, pushFront = false) {
Engine.PostCommand(PlayerID, { "type": "occupy-turret", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront });
return this;
},
"collectTreasure": function(target, autocontinue = false, queued = false) {
"attack": function(unitId, allowCapture = true, queued = false, pushFront = false) {
Engine.PostCommand(PlayerID, { "type": "attack", "entities": [this.id()], "target": unitId, "allowCapture": allowCapture, "queued": queued, "pushFront": pushFront });
return this;
},
"collectTreasure": function(target, autocontinue = false, queued = false, pushFront = false) {
Engine.PostCommand(PlayerID, {
"type": "collect-treasure",
"entities": [this.id()],
"target": target.id(),
"autocontinue": autocontinue,
"queued": queued
"queued": queued,
"pushFront": pushFront
});
return this;
},

View File

@ -175,6 +175,12 @@ m.EntityCollection.prototype.garrison = function(target, queued = false)
return this;
};
m.EntityCollection.prototype.occupyTurret = function(target, queued = false)
{
Engine.PostCommand(PlayerID, { "type": "occupy-turret", "entities": this.toIdArray(), "target": target.id(), "queued": queued });
return this;
};
m.EntityCollection.prototype.destroy = function()
{
Engine.PostCommand(PlayerID, { "type": "delete-entities", "entities": this.toIdArray() });

View File

@ -180,6 +180,11 @@ Auras.prototype.IsGarrisonedUnitsAura = function(name)
return this.GetType(name) == "garrisonedUnits";
};
Auras.prototype.IsTurretedUnitsAura = function(name)
{
return this.GetType(name) == "turretedUnits";
};
Auras.prototype.IsRangeAura = function(name)
{
return this.GetType(name) == "range";
@ -321,6 +326,15 @@ Auras.prototype.OnGarrisonedUnitsChanged = function(msg)
}
};
Auras.prototype.OnTurretsChanged = function(msg)
{
for (let name of this.GetAuraNames().filter(n => this.IsTurretedUnitsAura(n)))
{
this.ApplyAura(name, msg.added);
this.RemoveAura(name, msg.removed);
}
};
Auras.prototype.ApplyFormationAura = function(memberList)
{
for (let name of this.GetAuraNames().filter(n => this.IsFormationAura(n)))
@ -512,7 +526,7 @@ Auras.prototype.OnGarrisonedStateChanged = function(msg)
if (msg.holderID != INVALID_ENTITY)
this.ApplyGarrisonAura(msg.holderID);
if (msg.olderHolder != INVALID_ENTITY)
if (msg.oldHolder != INVALID_ENTITY)
this.RemoveGarrisonAura(msg.oldHolder);
};

View File

@ -30,36 +30,21 @@ BuildingAI.prototype.Init = function()
this.targetUnits = [];
};
BuildingAI.prototype.OnGarrisonedUnitsChanged = function()
BuildingAI.prototype.OnGarrisonedUnitsChanged = function(msg)
{
this.RecalculateProjectileCount();
};
BuildingAI.prototype.OnTurretsChanged = function()
{
this.RecalculateProjectileCount();
};
BuildingAI.prototype.RecalculateProjectileCount = function()
{
this.archersGarrisoned = 0;
let classes = this.template.GarrisonArrowClasses;
let cmpTurretHolder = Engine.QueryInterface(this.entity, IID_TurretHolder);
let cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
for (let ent of cmpGarrisonHolder.GetEntities())
for (let ent of msg.added)
{
// Only count non-visible garrisoned entities towards extra arrows.
if (cmpTurretHolder && cmpTurretHolder.OccupiesTurret(ent))
continue;
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (!cmpIdentity)
continue;
if (MatchesClassList(cmpIdentity.GetClassesList(), classes))
if (cmpIdentity && MatchesClassList(cmpIdentity.GetClassesList(), classes))
++this.archersGarrisoned;
}
for (let ent of msg.removed)
{
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (cmpIdentity && MatchesClassList(cmpIdentity.GetClassesList(), classes))
--this.archersGarrisoned;
}
};
BuildingAI.prototype.OnOwnershipChanged = function(msg)

View File

@ -199,38 +199,6 @@ GarrisonHolder.prototype.Garrison = function(entity)
return true;
};
/**
* @param {number} entity - EntityID to find the spawn position for.
* @param {boolean} forced - Optionally whether the spawning is forced.
* @return {Vector3D} - An appropriate spawning position.
*/
GarrisonHolder.prototype.GetSpawnPosition = function(entity, forced)
{
let cmpFootprint = Engine.QueryInterface(this.entity, IID_Footprint);
let cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
// If the garrisonHolder is a sinking ship, restrict the location to the intersection of both passabilities
// TODO: should use passability classes to be more generic
let pos;
if ((!cmpHealth || cmpHealth.GetHitpoints() == 0) && cmpIdentity && cmpIdentity.HasClass("Ship"))
pos = cmpFootprint.PickSpawnPointBothPass(entity);
else
pos = cmpFootprint.PickSpawnPoint(entity);
if (pos.y < 0)
{
// Error: couldn't find space satisfying the unit's passability criteria
if (!forced)
return null;
// If ejection is forced, we need to continue, so use center of the building
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
pos = cmpPosition.GetPosition();
}
return pos;
};
/**
* @param {number} entity - The entity ID of the entity to eject.
* @param {boolean} forced - Whether eject is forced (e.g. if building is destroyed).
@ -256,32 +224,6 @@ GarrisonHolder.prototype.Eject = function(entity, forced)
return true;
};
/**
* @param {number} entity - The entity ID of the entity to order to the rally point.
*/
GarrisonHolder.prototype.OrderToRallyPoint = function(entity)
{
let cmpRallyPoint = Engine.QueryInterface(this.entity, IID_RallyPoint);
if (!cmpRallyPoint || !cmpRallyPoint.GetPositions()[0])
return;
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership)
return;
let cmpEntOwnership = Engine.QueryInterface(entity, IID_Ownership);
if (!cmpEntOwnership || cmpOwnership.GetOwner() != cmpEntOwnership.GetOwner())
return;
let commands = GetRallyPointCommands(cmpRallyPoint, [entity]);
// Ignore the rally point if it is autogarrison
if (commands[0].type == "garrison" && commands[0].target == this.entity)
return;
for (let command of commands)
ProcessCommand(cmpOwnership.GetOwner(), command);
};
/**
* Tell unit to unload from this entity.
* @param {number} entity - The entity to unload.

View File

@ -69,7 +69,7 @@ Garrisonable.prototype.CanGarrison = function(target)
* @param {number} target - The entity ID of the entity this entity is being garrisoned in.
* @return {boolean} - Whether garrisoning succeeded.
*/
Garrisonable.prototype.Garrison = function(target, renamed = false)
Garrisonable.prototype.Garrison = function(target)
{
if (!this.CanGarrison(target))
return false;
@ -93,35 +93,24 @@ Garrisonable.prototype.Garrison = function(target, renamed = false)
"holderID": target
});
if (renamed)
return true;
let cmpTurretHolder = Engine.QueryInterface(target, IID_TurretHolder);
if (cmpTurretHolder)
cmpTurretHolder.OccupyTurret(this.entity);
return true;
};
/**
* @param {boolean} forced - Optionally whether the spawning is forced.
* @param {boolean} renamed - Optionally whether the ungarrisoning is due to renaming.
* @return {boolean} - Whether the ungarrisoning succeeded.
*/
Garrisonable.prototype.UnGarrison = function(forced = false, renamed = false)
Garrisonable.prototype.UnGarrison = function(forced = false)
{
if (!this.holder)
return true;
let cmpGarrisonHolder = Engine.QueryInterface(this.holder, IID_GarrisonHolder);
if (!cmpGarrisonHolder)
return false;
let pos = cmpGarrisonHolder.GetSpawnPosition(this.entity, forced);
let pos = PositionHelper.GetSpawnPosition(this.holder, this.entity, forced);
if (!pos)
return false;
if (!cmpGarrisonHolder.Eject(this.entity, forced))
let cmpGarrisonHolder = Engine.QueryInterface(this.holder, IID_GarrisonHolder);
if (!cmpGarrisonHolder || !cmpGarrisonHolder.Eject(this.entity, forced))
return false;
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
@ -147,14 +136,9 @@ Garrisonable.prototype.UnGarrison = function(forced = false, renamed = false)
"holderID": INVALID_ENTITY
});
if (renamed)
return true;
let cmpTurretHolder = Engine.QueryInterface(this.holder, IID_TurretHolder);
if (cmpTurretHolder)
cmpTurretHolder.LeaveTurret(this.entity);
cmpGarrisonHolder.OrderToRallyPoint(this.entity);
let cmpRallyPoint = Engine.QueryInterface(this.holder, IID_RallyPoint);
if (cmpRallyPoint)
cmpRallyPoint.OrderToRallyPoint(this.entity, ["garrison"]);
delete this.holder;
return true;
@ -165,22 +149,11 @@ Garrisonable.prototype.OnEntityRenamed = function(msg)
if (!this.holder)
return;
let cmpGarrisonHolder = Engine.QueryInterface(this.holder, IID_GarrisonHolder);
if (cmpGarrisonHolder)
{
this.UnGarrison(true, true);
let cmpGarrisonable = Engine.QueryInterface(msg.newentity, IID_Garrisonable);
if (cmpGarrisonable)
cmpGarrisonable.Garrison(this.holder, true);
}
// We process EntityRenamed of turrets seperately since we
// want to occupy the same position after being renamed.
let cmpTurretHolder = Engine.QueryInterface(this.holder, IID_TurretHolder);
if (cmpTurretHolder)
cmpTurretHolder.SwapEntities(msg.entity, msg.newentity);
delete this.holder;
let holder = this.holder;
this.UnGarrison(true, true);
let cmpGarrisonable = Engine.QueryInterface(msg.newentity, IID_Garrisonable);
if (cmpGarrisonable)
cmpGarrisonable.Garrison(holder, true);
};
Engine.RegisterComponentType(IID_Garrisonable, "Garrisonable", Garrisonable);

View File

@ -388,6 +388,12 @@ GuiInterface.prototype.GetEntityState = function(player, ent)
"turretPoints": cmpTurretHolder.GetTurretPoints()
};
let cmpTurretable = Engine.QueryInterface(ent, IID_Turretable);
if (cmpTurretable)
ret.turretable = {
"holder": cmpTurretable.HolderID()
};
let cmpGarrisonable = Engine.QueryInterface(ent, IID_Garrisonable);
if (cmpGarrisonable)
ret.garrisonable = {

View File

@ -100,6 +100,31 @@ RallyPoint.prototype.Reset = function()
cmpRallyPointRenderer.Reset();
};
/**
* @param {number} entity - The entity ID of the entity to order to the rally point.
* @param {string[]} ignore - The commands to ignore when performed on this.entity.
* E.g. "garrison" when unloading.
*/
RallyPoint.prototype.OrderToRallyPoint = function(entity, ignore = [])
{
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership)
return;
let owner = cmpOwnership.GetOwner();
let cmpEntOwnership = Engine.QueryInterface(entity, IID_Ownership);
if (!cmpEntOwnership || cmpEntOwnership.GetOwner() != owner)
return;
let commands = GetRallyPointCommands(this, [entity]);
if (!commands.length ||
commands[0].target == this.entity && ignore.includes(commands[0].type))
return;
for (let command of commands)
ProcessCommand(owner, command);
};
RallyPoint.prototype.OnGlobalEntityRenamed = function(msg)
{
for (var data of this.data)

View File

@ -1,8 +1,6 @@
/**
* This class holds the functions regarding entities being visible on
* another entity, but tied to their parents location.
* Currently renaming and changing ownership are still managed by GarrisonHolder.js,
* but in the future these components should be independent.
*/
class TurretHolder
{
@ -54,6 +52,15 @@ class TurretHolder
return cmpIdentity && MatchesClassList(cmpIdentity.GetClassesList(), turretPoint.allowedClasses._string);
}
/**
* @param {number} entity - The entity to check for.
* @return {boolean} - Whether the entity is allowed to occupy any turret point.
*/
CanOccupy(entity)
{
return !!this.turretPoints.find(turretPoint => this.AllowedToOccupyTurret(entity, turretPoint));
}
/**
* Occupy a turret point with the given entity.
* @param {number} entity - The entity to use.
@ -87,6 +94,7 @@ class TurretHolder
return false;
turretPoint.entity = entity;
// Angle of turrets:
// Renamed entities (turretPoint != undefined) should keep their angle.
// Otherwise if an angle is given in the turretPoint, use it.
@ -100,19 +108,6 @@ class TurretHolder
cmpPositionOccupant.SetTurretParent(this.entity, turretPoint.offset);
let cmpUnitMotion = Engine.QueryInterface(entity, IID_UnitMotion);
if (cmpUnitMotion)
cmpUnitMotion.SetFacePointAfterMove(false);
let cmpUnitAI = Engine.QueryInterface(entity, IID_UnitAI);
if (cmpUnitAI)
cmpUnitAI.SetTurretStance();
// Remove the unit's obstruction to avoid interfering with pathing.
let cmpObstruction = Engine.QueryInterface(entity, IID_Obstruction);
if (cmpObstruction)
cmpObstruction.SetActive(false);
Engine.PostMessage(this.entity, MT_TurretsChanged, {
"added": [entity],
"removed": []
@ -152,23 +147,11 @@ class TurretHolder
if (!turretPoint)
return false;
let cmpPositionEntity = Engine.QueryInterface(entity, IID_Position);
cmpPositionEntity.SetTurretParent(INVALID_ENTITY, new Vector3D());
let cmpUnitMotionEntity = Engine.QueryInterface(entity, IID_UnitMotion);
if (cmpUnitMotionEntity)
cmpUnitMotionEntity.SetFacePointAfterMove(true);
let cmpUnitAIEntity = Engine.QueryInterface(entity, IID_UnitAI);
if (cmpUnitAIEntity)
cmpUnitAIEntity.ResetTurretStance();
turretPoint.entity = null;
// Reset the obstruction flags to template defaults.
let cmpObstruction = Engine.QueryInterface(entity, IID_Obstruction);
if (cmpObstruction)
cmpObstruction.SetActive(true);
let cmpPositionEntity = Engine.QueryInterface(entity, IID_Position);
if (cmpPositionEntity)
cmpPositionEntity.SetTurretParent(INVALID_ENTITY, new Vector3D());
Engine.PostMessage(this.entity, MT_TurretsChanged, {
"added": [],
@ -205,7 +188,8 @@ class TurretHolder
*/
GetOccupiedTurretName(entity)
{
return this.GetOccupiedTurret(entity).name || "";
let turret = this.GetOccupiedTurret(entity);
return turret ? turret.name : "";
}
/**
@ -220,6 +204,60 @@ class TurretHolder
return entities;
}
/**
* @return {boolean} - Whether all the turret points are occupied.
*/
IsFull()
{
return !!this.turretPoints.find(turretPoint => turretPoint.entity == null);
}
/**
* @return {Object} - Max and min ranges at which entities can occupy any turret.
*/
GetLoadingRange()
{
return { "min": 0, "max": +(this.template.LoadingRange || 2) };
}
/**
* @param {number} ent - The entity ID of the turret to be potentially picked up.
* @return {boolean} - Whether this entity can pick the specified entity up.
*/
CanPickup(ent)
{
if (!this.template.Pickup || this.IsFull())
return false;
let cmpOwner = Engine.QueryInterface(this.entity, IID_Ownership);
return !!cmpOwner && IsOwnedByPlayer(cmpOwner.GetOwner(), ent);
}
/**
* @param {number[]} entities - The entities to ask to leave or to kill.
*/
EjectOrKill(entities)
{
let removedEntities = [];
for (let entity of entities)
{
let cmpTurretable = Engine.QueryInterface(entity, IID_Turretable);
if (!cmpTurretable || !cmpTurretable.LeaveTurret(true))
{
let cmpHealth = Engine.QueryInterface(entity, IID_Health);
if (cmpHealth)
cmpHealth.Kill();
else
Engine.DestroyEntity(entity);
removedEntities.push(entity);
}
}
if (removedEntities.length)
Engine.PostMessage(this.entity, MT_TurretsChanged, {
"added": [],
"removed": removedEntities
});
}
/**
* Sets an init turret, present from game start. (E.g. set in Atlas.)
* @param {String} turretName - The name of the turret point to be used.
@ -237,20 +275,6 @@ class TurretHolder
this.initTurrets.set(turretName, entity);
}
/**
* @param {number} from - The entity to substitute.
* @param {number} to - The entity to subtitute with.
*/
SwapEntities(from, to)
{
let turretPoint = this.GetOccupiedTurret(from);
if (turretPoint)
{
this.LeaveTurret(from, turretPoint);
this.OccupyTurret(to, turretPoint);
}
}
/**
* Update list of turreted entities when a game inits.
*/
@ -274,10 +298,7 @@ class TurretHolder
}
/**
* Initialise the turreted units.
* Really ugly, but because GarrisonHolder is processed earlier, and also turrets
* entities on init, we can find an entity that already is present.
* In that case we reject and occupy.
* Initialise turreted units.
*/
OnGlobalInitGame(msg)
{
@ -285,16 +306,48 @@ class TurretHolder
return;
for (let [turretPointName, entity] of this.initTurrets)
{
if (this.OccupiesTurret(entity))
this.LeaveTurret(entity);
if (!this.OccupyNamedTurret(entity, turretPointName))
warn("Entity " + entity + " could not occupy the turret point " +
turretPointName + " of turret holder " + this.entity + ".");
}
delete this.initTurrets;
}
/**
* @param {Object} msg - { "entity": number, "newentity": number }.
*/
OnEntityRenamed(msg)
{
for (let entity of this.GetEntities())
{
let cmpTurretable = Engine.QueryInterface(entity, IID_Turretable);
if (!cmpTurretable)
continue;
let currentPoint = this.GetOccupiedTurretName(entity);
cmpTurretable.LeaveTurret(true);
cmpTurretable.OccupyTurret(msg.newentity, currentPoint);
}
}
/**
* @param {Object} msg - { "entity": number, "from": number, "to": number }.
*/
OnOwnershipChanged(msg)
{
let entities = this.GetEntities();
if (!entities.length)
return;
if (msg.to == INVALID_PLAYER)
this.EjectOrKill(entities);
else
for (let entity of entities.filter(entity => !IsOwnedByMutualAllyOfEntity(entity, this.entity)))
{
let cmpTurretable = Engine.QueryInterface(entity, IID_Turretable);
if (cmpTurretable)
cmpTurretable.LeaveTurret();
}
}
}
TurretHolder.prototype.Schema =
@ -328,6 +381,16 @@ TurretHolder.prototype.Schema =
"</interleave>" +
"</element>" +
"</oneOrMore>" +
"</element>";
"</element>" +
"<optional>" +
"<element name='LoadingRange' a:help='The maximum distance from this holder at which entities are allowed to occupy a turret point. Should be about 2.0 for land entities and preferably greater for ships.'>" +
"<ref name='nonNegativeDecimal'/>" +
"</element>" +
"</optional>"
"<optional>" +
"<element name='Pickup' a:help='This entity will try to move to pick up units to be turreted.'>" +
"<data type='boolean'/>" +
"</element>" +
"</optional>";
Engine.RegisterComponentType(IID_TurretHolder, "TurretHolder", TurretHolder);

View File

@ -0,0 +1,165 @@
function Turretable() {}
Turretable.prototype.Schema =
"<empty/>";
Turretable.prototype.Init = function()
{
};
/**
* @return {number} - The entity ID of the entity this entity is turreted on.
*/
Turretable.prototype.HolderID = function()
{
return this.holder || INVALID_ENTITY;
};
/**
* @return {boolean} - Whether we're turreted.
*/
Turretable.prototype.IsTurreted = function()
{
return !!this.holder;
};
/**
* @param {number} target - The entity ID to check.
* @return {boolean} - Whether we can occupy the turret.
*/
Turretable.prototype.CanOccupy = function(target)
{
if (this.holder)
return false;
let cmpTurretHolder = Engine.QueryInterface(target, IID_TurretHolder);
return cmpTurretHolder && cmpTurretHolder.CanOccupy(this.entity);
};
/**
* @param {number} target - The entity ID of the entity this entity is being turreted on.
* @param {string} turretPointName - Optionally the turret point name to occupy.
* @return {boolean} - Whether occupying succeeded.
*/
Turretable.prototype.OccupyTurret = function(target, turretPointName = "")
{
if (!this.CanOccupy(target))
return false;
let cmpTurretHolder = Engine.QueryInterface(target, IID_TurretHolder);
if (!cmpTurretHolder || !cmpTurretHolder.OccupyNamedTurret(this.entity, turretPointName))
return false;
this.holder = target;
let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
if (cmpUnitAI)
{
cmpUnitAI.SetGarrisoned();
cmpUnitAI.SetTurretStance();
}
let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (cmpUnitMotion)
cmpUnitMotion.SetFacePointAfterMove(false);
// Remove the unit's obstruction to avoid interfering with pathing.
let cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
if (cmpObstruction)
cmpObstruction.SetActive(false);
Engine.PostMessage(this.entity, MT_TurretedStateChanged, {
"oldHolder": INVALID_ENTITY,
"holderID": target
});
return true;
};
/**
* @param {boolean} forced - Optionally whether the leaving the turret is forced.
* @return {boolean} - Whether leaving the turret succeeded.
*/
Turretable.prototype.LeaveTurret = function(forced = false)
{
if (!this.holder)
return true;
let pos = PositionHelper.GetSpawnPosition(this.holder, this.entity, forced);
if (!pos)
return false;
let cmpTurretHolder = Engine.QueryInterface(this.holder, IID_TurretHolder);
if (!cmpTurretHolder || !cmpTurretHolder.LeaveTurret(this.entity))
return false;
let cmpUnitMotionEntity = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (cmpUnitMotionEntity)
cmpUnitMotionEntity.SetFacePointAfterMove(true);
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (cmpPosition)
{
cmpPosition.JumpTo(pos.x, pos.z);
cmpPosition.SetHeightOffset(0);
}
let cmpHolderPosition = Engine.QueryInterface(this.holder, IID_Position);
if (cmpHolderPosition)
cmpPosition.SetYRotation(cmpHolderPosition.GetPosition().horizAngleTo(pos));
let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
if (cmpUnitAI)
{
cmpUnitAI.Ungarrison();
cmpUnitAI.UnsetGarrisoned();
cmpUnitAI.ResetTurretStance();
}
// Reset the obstruction flags to template defaults.
let cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
if (cmpObstruction)
cmpObstruction.SetActive(true);
Engine.PostMessage(this.entity, MT_TurretedStateChanged, {
"oldHolder": this.holder,
"holderID": INVALID_ENTITY
});
let cmpRallyPoint = Engine.QueryInterface(this.holder, IID_RallyPoint);
if (cmpRallyPoint)
cmpRallyPoint.OrderToRallyPoint(this.entity, ["occupy-turret"]);
delete this.holder;
return true;
};
Turretable.prototype.OnEntityRenamed = function(msg)
{
if (!this.holder)
return;
let cmpTurretHolder = Engine.QueryInterface(this.holder, IID_TurretHolder);
if (!cmpTurretHolder)
return;
let holder = this.holder;
let currentPoint = cmpTurretHolder.GetOccupiedTurretName(this.entity);
this.LeaveTurret(true);
let cmpTurretableNew = Engine.QueryInterface(msg.newentity, IID_Turretable);
if (cmpTurretableNew)
cmpTurretableNew.OccupyTurret(holder, currentPoint);
};
Turretable.prototype.OnOwnershipChanged = function(msg)
{
if (!this.holder)
return;
if (msg.to == INVALID_PLAYER)
this.LeaveTurret(true);
else if (!IsOwnedByMutualAllyOfEntity(this.entity, this.holder))
this.LeaveTurret();
};
Engine.RegisterComponentType(IID_Turretable, "Turretable", Turretable);

View File

@ -592,7 +592,8 @@ UnitAI.prototype.UnitFsmSpec = {
return ACCEPT_ORDER;
}
if (this.CheckGarrisonRange(msg.data.target))
if (msg.data.garrison ? this.CheckGarrisonRange(msg.data.target) :
this.CheckOccupyTurretRange(msg.data.target))
this.SetNextState("INDIVIDUAL.GARRISON.GARRISONING");
else
this.SetNextState("INDIVIDUAL.GARRISON.APPROACHING");
@ -744,9 +745,11 @@ UnitAI.prototype.UnitFsmSpec = {
},
"Order.Garrison": function(msg) {
if (!Engine.QueryInterface(msg.data.target, IID_GarrisonHolder))
if (!Engine.QueryInterface(msg.data.target,
msg.data.garrison ? IID_GarrisonHolder : IID_TurretHolder))
return this.FinishOrder();
if (!this.CheckGarrisonRange(msg.data.target))
if (!(msg.data.garrison ? this.CheckGarrisonRange(msg.data.target) :
this.CheckOccupyTurretRange(msg.data.target)))
{
if (!this.CheckTargetVisible(msg.data.target))
return this.FinishOrder();
@ -1100,19 +1103,20 @@ UnitAI.prototype.UnitFsmSpec = {
"GARRISON": {
"APPROACHING": {
"enter": function() {
let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(true);
cmpFormation.MoveMembersIntoFormation(true, true);
if (!this.MoveToGarrisonRange(this.order.data.target))
if (!(this.order.data.garrison ? this.MoveToGarrisonRange(this.order.data.target) :
this.MoveToOccupyTurretRange(this.order.data.target)))
{
this.FinishOrder();
return true;
}
// If the garrisonholder should pickup, warn it so it can take needed action.
let cmpGarrisonHolder = Engine.QueryInterface(this.order.data.target, IID_GarrisonHolder);
if (cmpGarrisonHolder && cmpGarrisonHolder.CanPickup(this.entity))
let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(true);
cmpFormation.MoveMembersIntoFormation(true, true);
// If the holder should pickup, warn it so it can take needed action.
let cmpHolder = Engine.QueryInterface(this.order.data.target, this.order.data.garrison ? IID_GarrisonHolder : IID_TurretHolder);
if (cmpHolder && cmpHolder.CanPickup(this.entity))
{
this.pickup = this.order.data.target; // temporary, deleted in "leave"
Engine.PostMessage(this.pickup, MT_PickupRequested, { "entity": this.entity });
@ -1137,7 +1141,7 @@ UnitAI.prototype.UnitFsmSpec = {
"GARRISONING": {
"enter": function() {
this.CallMemberFunction("Garrison", [this.order.data.target, false]);
this.CallMemberFunction(this.order.data.garrison ? "Garrison" : "OccupyTurret", [this.order.data.target, false]);
// We might have been disbanded due to the lack of members.
if (Engine.QueryInterface(this.entity, IID_Formation).GetMemberCount())
this.SetNextState("MEMBER");
@ -3209,13 +3213,15 @@ UnitAI.prototype.UnitFsmSpec = {
"GARRISON": {
"APPROACHING": {
"enter": function() {
if (!this.CanGarrison(this.order.data.target))
if (this.order.data.garrison ? !this.CanGarrison(this.order.data.target) :
!this.CanOccupyTurret(this.order.data.target))
{
this.FinishOrder();
return true;
}
if (!this.MoveToGarrisonRange(this.order.data.target))
if (this.order.data.garrison ? !this.MoveToGarrisonRange(this.order.data.target) :
!this.MoveToOccupyTurretRange(this.order.data.target))
{
this.FinishOrder();
return true;
@ -3224,8 +3230,8 @@ UnitAI.prototype.UnitFsmSpec = {
if (this.pickup)
Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity });
let cmpGarrisonHolder = Engine.QueryInterface(this.order.data.target, IID_GarrisonHolder);
if (cmpGarrisonHolder && cmpGarrisonHolder.CanPickup(this.entity))
let cmpHolder = Engine.QueryInterface(this.order.data.target, this.order.data.garrison ? IID_GarrisonHolder : IID_TurretHolder);
if (cmpHolder && cmpHolder.CanPickup(this.entity))
{
this.pickup = this.order.data.target;
Engine.PostMessage(this.pickup, MT_PickupRequested, { "entity": this.entity });
@ -3246,7 +3252,8 @@ UnitAI.prototype.UnitFsmSpec = {
if (!msg.likelyFailure && !msg.likelySuccess)
return;
if (this.CheckGarrisonRange(this.order.data.target))
if (this.order.data.garrison ? this.CheckGarrisonRange(this.order.data.target) :
this.CheckOccupyTurretRange(this.order.data.target))
this.SetNextState("GARRISONING");
else
{
@ -3265,11 +3272,23 @@ UnitAI.prototype.UnitFsmSpec = {
"GARRISONING": {
"enter": function() {
let target = this.order.data.target;
let cmpGarrisonable = Engine.QueryInterface(this.entity, IID_Garrisonable);
if (!cmpGarrisonable || !cmpGarrisonable.Garrison(target))
if (this.order.data.garrison)
{
this.FinishOrder();
return true;
let cmpGarrisonable = Engine.QueryInterface(this.entity, IID_Garrisonable);
if (!cmpGarrisonable || !cmpGarrisonable.Garrison(target))
{
this.FinishOrder();
return true;
}
}
else
{
let cmpTurretable = Engine.QueryInterface(this.entity, IID_Turretable);
if (!cmpTurretable || !cmpTurretable.OccupyTurret(target))
{
this.FinishOrder();
return true;
}
}
if (this.formationController)
@ -3459,8 +3478,10 @@ UnitAI.prototype.Init = function()
UnitAI.prototype.IsTurret = function()
{
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
return cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY;
if (!this.isGarrisoned)
return false;
let cmpTurretable = Engine.QueryInterface(this.entity, IID_Turretable);
return cmpTurretable && cmpTurretable.HolderID() != INVALID_ENTITY;
};
UnitAI.prototype.IsFormationController = function()
@ -4170,6 +4191,12 @@ UnitAI.prototype.BackToWork = function()
if (this.isGarrisoned)
{
if (this.IsTurret())
{
let cmpTurretable = Engine.QueryInterface(this.entity, IID_Turretable);
if (!cmpTurretable || !cmpTurretable.LeaveTurret())
return false;
}
let cmpGarrisonable = Engine.QueryInterface(this.entity, IID_Garrisonable);
if (!cmpGarrisonable || !cmpGarrisonable.UnGarrison(false))
return false;
@ -4795,6 +4822,20 @@ UnitAI.prototype.MoveToGarrisonRange = function(target)
return this.AbleToMove(cmpUnitMotion) && cmpUnitMotion.MoveToTargetRange(target, range.min, range.max);
};
UnitAI.prototype.MoveToOccupyTurretRange = function(target)
{
if (!this.CheckTargetVisible(target))
return false;
let cmpTurretHolder = Engine.QueryInterface(target, IID_TurretHolder);
if (!cmpTurretHolder)
return false;
let range = cmpTurretHolder.GetLoadingRange();
let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return this.AbleToMove(cmpUnitMotion) && cmpUnitMotion.MoveToTargetRange(target, range.min, range.max);
};
/**
* Generic dispatcher for other Check...Range functions.
* @param iid - Interface ID (optional) implementing GetRange
@ -4920,6 +4961,16 @@ UnitAI.prototype.CheckGarrisonRange = function(target)
return this.CheckTargetRangeExplicit(target, range.min, range.max);
};
UnitAI.prototype.CheckOccupyTurretRange = function(target)
{
let cmpTurretHolder = Engine.QueryInterface(target, IID_TurretHolder);
if (!cmpTurretHolder)
return false;
let range = cmpTurretHolder.GetLoadingRange();
return this.CheckTargetRangeExplicit(target, range.min, range.max);
};
/**
* Returns true if the target entity is visible through the FoW/SoD.
*/
@ -5353,7 +5404,7 @@ UnitAI.prototype.AddOrder = function(type, data, queued, pushFront)
// May happen if an order arrives on the same turn the unit is garrisoned
// in that case, just forget the order as this will lead to an infinite loop.
// ToDo: Fix that by checking for the ability to move on orders that need that.
if (this.isGarrisoned && !this.IsTurret() && type != "Ungarrison")
if (this.isGarrisoned && type != "Ungarrison")
return;
this.ReplaceOrder(type, data);
}
@ -5591,7 +5642,7 @@ UnitAI.prototype.Garrison = function(target, queued, pushFront)
this.WalkToTarget(target, queued);
return;
}
this.AddOrder("Garrison", { "target": target, "force": true }, queued, pushFront);
this.AddOrder("Garrison", { "target": target, "force": true, "garrison": true }, queued, pushFront);
};
/**
@ -5604,6 +5655,21 @@ UnitAI.prototype.Ungarrison = function()
this.AddOrder("Ungarrison", null, false);
};
/**
* Adds garrison order to the queue, forced by the player.
*/
UnitAI.prototype.OccupyTurret = function(target, queued, pushFront)
{
if (target == this.entity)
return;
if (!this.CanOccupyTurret(target))
{
this.WalkToTarget(target, queued);
return;
}
this.AddOrder("Garrison", { "target": target, "force": true, "garrison": false }, queued, pushFront);
};
/**
* Adds gather order to the queue, forced by the player
* until the target is reached
@ -6443,6 +6509,17 @@ UnitAI.prototype.CanRepair = function(target)
return cmpOwnership && IsOwnedByAllyOfPlayer(cmpOwnership.GetOwner(), target);
};
UnitAI.prototype.CanOccupyTurret = function(target)
{
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds).
if (this.IsFormationController())
return true;
let cmpTurretable = Engine.QueryInterface(this.entity, IID_Turretable);
return cmpTurretable && cmpTurretable.CanOccupy(target);
};
UnitAI.prototype.CanPack = function()
{
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);

View File

@ -0,0 +1,7 @@
Engine.RegisterInterface("Turretable");
/**
* Message of the form { "holderID": number }
* sent from the Turretable component whenever the turreted state changes.
*/
Engine.RegisterMessageType("TurretedStateChanged");

View File

@ -2,7 +2,6 @@ Engine.LoadHelperScript("ValueModification.js");
Engine.LoadHelperScript("Player.js");
Engine.LoadComponentScript("interfaces/Garrisonable.js");
Engine.LoadComponentScript("interfaces/GarrisonHolder.js");
Engine.LoadComponentScript("interfaces/TurretHolder.js");
Engine.LoadComponentScript("interfaces/Health.js");
Engine.LoadComponentScript("interfaces/ModifiersManager.js");
Engine.LoadComponentScript("interfaces/Timer.js");

View File

@ -1,5 +1,7 @@
Engine.LoadHelperScript("Position.js");
Engine.LoadComponentScript("interfaces/Garrisonable.js");
Engine.LoadComponentScript("interfaces/GarrisonHolder.js");
Engine.LoadComponentScript("interfaces/Health.js");
Engine.LoadComponentScript("interfaces/UnitAI.js");
Engine.LoadComponentScript("Garrisonable.js");
@ -9,12 +11,15 @@ const garrisonHolderID = 1;
const garrisonableID = 2;
AddMock(garrisonHolderID, IID_GarrisonHolder, {
"Garrison": () => true,
"GetSpawnPosition": () => new Vector3D(0, 0, 0),
"IsAllowedToGarrison": () => true,
"OrderToRallyPoint": () => {},
"Eject": () => true
});
AddMock(garrisonHolderID, IID_Footprint, {
"PickSpawnPointBothPass": entity => new Vector3D(4, 3, 30),
"PickSpawnPoint": entity => new Vector3D(4, 3, 30)
});
let size = 1;
let cmpGarrisonable = ConstructComponent(garrisonableID, "Garrisonable", {
"Size": size
@ -39,7 +44,7 @@ TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonable.HolderID(), garrisonHolderID);
TS_ASSERT(!cmpGarrisonable.Garrison(garrisonHolderID));
TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonable.HolderID(), garrisonHolderID);
cmpGarrisonable.UnGarrison();
TS_ASSERT(cmpGarrisonable.UnGarrison());
TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonable.HolderID(), INVALID_ENTITY);
// Test renaming.

View File

@ -1,15 +1,14 @@
Engine.LoadHelperScript("ValueModification.js");
Engine.LoadHelperScript("Player.js");
Engine.LoadHelperScript("Position.js");
Engine.LoadComponentScript("interfaces/Garrisonable.js");
Engine.LoadComponentScript("interfaces/GarrisonHolder.js");
Engine.LoadComponentScript("interfaces/Health.js");
Engine.LoadComponentScript("interfaces/ModifiersManager.js");
Engine.LoadComponentScript("interfaces/Timer.js");
Engine.LoadComponentScript("interfaces/TurretHolder.js");
Engine.LoadComponentScript("interfaces/UnitAI.js");
Engine.LoadComponentScript("Garrisonable.js");
Engine.LoadComponentScript("GarrisonHolder.js");
Engine.LoadComponentScript("TurretHolder.js");
const player = 1;
const enemyPlayer = 2;
@ -34,7 +33,6 @@ let createGarrisonCmp = entity => {
"JumpTo": (posX, posZ) => {},
"MoveOutOfWorld": () => {},
"SetHeightOffset": height => {},
"SetTurretParent": ent => {},
"SetYRotation": angle => {}
});
@ -88,7 +86,6 @@ AddMock(garrison, IID_Position, {
"JumpTo": (posX, posZ) => {},
"MoveOutOfWorld": () => {},
"SetHeightOffset": height => {},
"SetTurretParent": entity => {},
"SetYRotation": angle => {}
});
@ -143,52 +140,3 @@ TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetGarrisonedEntitiesCount(), entities
TS_ASSERT(cmpGarrisonHolder.UnloadAll());
TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), []);
// Turrets!
AddMock(holder, IID_Position, {
"GetPosition": () => new Vector3D(4, 3, 25),
"GetRotation": () => new Vector3D(4, 0, 6)
});
let cmpTurretHolder = ConstructComponent(holder, "TurretHolder", {
"TurretPoints": {
"archer1": {
"X": "12.0",
"Y": "5.",
"Z": "6.0"
},
"archer2": {
"X": "15.0",
"Y": "5.0",
"Z": "6.0"
}
}
});
TS_ASSERT(cmpGarrisonable.Garrison(holder));
TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), [garrison]);
TS_ASSERT(cmpTurretHolder.OccupiesTurret(garrison));
TS_ASSERT(cmpGarrisonable.UnGarrison());
TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), []);
TS_ASSERT_UNEVAL_EQUALS(cmpTurretHolder.GetEntities(), []);
// Test renaming on a turret.
// Ensure we test renaming from the second spot, not the first.
const newGarrison = 31;
let cmpGarrisonableNew = createGarrisonCmp(newGarrison);
TS_ASSERT(cmpGarrisonableNew.Garrison(holder));
TS_ASSERT(cmpGarrisonable.Garrison(holder));
TS_ASSERT(cmpGarrisonableNew.UnGarrison());
let previousTurret = cmpTurretHolder.GetOccupiedTurretName(garrison);
cmpGarrisonable.OnEntityRenamed({
"entity": garrison,
"newentity": newGarrison
});
let newTurret = cmpTurretHolder.GetOccupiedTurretName(newGarrison);
TS_ASSERT_UNEVAL_EQUALS(newTurret, previousTurret);
TS_ASSERT(cmpGarrisonableNew.UnGarrison());
// Test initTurrets.
cmpTurretHolder.SetInitEntity("archer1", garrison);
cmpTurretHolder.SetInitEntity("archer2", newGarrison);
cmpTurretHolder.OnGlobalInitGame();
TS_ASSERT_UNEVAL_EQUALS(cmpTurretHolder.GetEntities(), [garrison, newGarrison]);

View File

@ -34,6 +34,7 @@ Engine.LoadComponentScript("interfaces/TurretHolder.js");
Engine.LoadComponentScript("interfaces/Timer.js");
Engine.LoadComponentScript("interfaces/Treasure.js");
Engine.LoadComponentScript("interfaces/TreasureCollecter.js");
Engine.LoadComponentScript("interfaces/Turretable.js");
Engine.LoadComponentScript("interfaces/StatisticsTracker.js");
Engine.LoadComponentScript("interfaces/StatusEffectsReceiver.js");
Engine.LoadComponentScript("interfaces/UnitAI.js");

View File

@ -122,20 +122,3 @@ TS_ASSERT(cmpTurretHolder.OccupyTurret(cavID, cmpTurretHolder.turretPoints[2]));
TS_ASSERT(cmpTurretHolder.LeaveTurret(archerID));
TS_ASSERT(!cmpTurretHolder.LeaveTurret(cavID, cmpTurretHolder.turretPoints[1]));
TS_ASSERT(cmpTurretHolder.LeaveTurret(cavID, cmpTurretHolder.turretPoints[2]));
// Test renaming.
AddMock(turretHolderID, IID_GarrisonHolder, {
"IsGarrisoned": () => true
});
TS_ASSERT(cmpTurretHolder.OccupyTurret(siegeEngineID, cmpTurretHolder.turretPoints[2]));
cmpTurretHolder.SwapEntities(siegeEngineID, archerID);
TS_ASSERT(!cmpTurretHolder.OccupiesTurret(siegeEngineID));
TS_ASSERT(cmpTurretHolder.LeaveTurret(archerID));
// Renaming into an entity not allowed on the same turret point hides us.
TS_ASSERT(cmpTurretHolder.OccupyTurret(siegeEngineID, cmpTurretHolder.turretPoints[1]));
cmpTurretHolder.SwapEntities(siegeEngineID, archerID);
TS_ASSERT(!cmpTurretHolder.OccupiesTurret(siegeEngineID));
TS_ASSERT(!cmpTurretHolder.OccupiesTurret(archerID));
// ToDo: Ownership changes are handled by GarrisonHolder.js.

View File

@ -0,0 +1,114 @@
Engine.LoadHelperScript("ValueModification.js");
Engine.LoadHelperScript("Player.js");
Engine.LoadHelperScript("Position.js");
Engine.LoadComponentScript("interfaces/Health.js");
Engine.LoadComponentScript("interfaces/Turretable.js");
Engine.LoadComponentScript("interfaces/TurretHolder.js");
Engine.LoadComponentScript("interfaces/UnitAI.js");
Engine.LoadComponentScript("Turretable.js");
Engine.LoadComponentScript("TurretHolder.js");
const player = 1;
const enemyPlayer = 2;
const friendlyPlayer = 3;
const turret = 10;
const holder = 11;
let createTurretCmp = entity => {
AddMock(entity, IID_Identity, {
"GetClassesList": () => ["Ranged"],
"GetSelectionGroupName": () => "mace_infantry_archer_a"
});
AddMock(entity, IID_Ownership, {
"GetOwner": () => player
});
AddMock(entity, IID_Position, {
"GetHeightOffset": () => 0,
"GetPosition": () => new Vector3D(4, 3, 25),
"GetRotation": () => new Vector3D(4, 0, 6),
"JumpTo": (posX, posZ) => {},
"MoveOutOfWorld": () => {},
"SetHeightOffset": height => {},
"SetTurretParent": entity => {},
"SetYRotation": angle => {}
});
return ConstructComponent(entity, "Turretable", null);
};
AddMock(holder, IID_Footprint, {
"PickSpawnPointBothPass": entity => new Vector3D(4, 3, 30),
"PickSpawnPoint": entity => new Vector3D(4, 3, 30)
});
AddMock(holder, IID_Ownership, {
"GetOwner": () => player
});
AddMock(player, IID_Player, {
"IsAlly": id => id != enemyPlayer,
"IsMutualAlly": id => id != enemyPlayer,
"GetPlayerID": () => player
});
AddMock(friendlyPlayer, IID_Player, {
"IsAlly": id => true,
"IsMutualAlly": id => true,
"GetPlayerID": () => friendlyPlayer
});
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
"GetPlayerByID": id => id
});
AddMock(holder, IID_Position, {
"GetPosition": () => new Vector3D(4, 3, 25),
"GetRotation": () => new Vector3D(4, 0, 6)
});
let cmpTurretable = createTurretCmp(turret);
let cmpTurretHolder = ConstructComponent(holder, "TurretHolder", {
"TurretPoints": {
"archer1": {
"X": "12.0",
"Y": "5.",
"Z": "6.0"
},
"archer2": {
"X": "15.0",
"Y": "5.0",
"Z": "6.0"
}
}
});
TS_ASSERT(cmpTurretable.OccupyTurret(holder));
TS_ASSERT_UNEVAL_EQUALS(cmpTurretHolder.GetEntities(), [turret]);
TS_ASSERT(cmpTurretHolder.OccupiesTurret(turret));
TS_ASSERT(cmpTurretable.LeaveTurret());
TS_ASSERT_UNEVAL_EQUALS(cmpTurretHolder.GetEntities(), []);
// Test renaming on a turret.
// Ensure we test renaming from the second spot, not the first.
const newTurret = 31;
let cmpTurretableNew = createTurretCmp(newTurret);
TS_ASSERT(cmpTurretableNew.OccupyTurret(holder));
TS_ASSERT(cmpTurretable.OccupyTurret(holder));
TS_ASSERT(cmpTurretableNew.LeaveTurret());
let previousTurret = cmpTurretHolder.GetOccupiedTurretName(turret);
cmpTurretable.OnEntityRenamed({
"entity": turret,
"newentity": newTurret
});
let newTurretPos = cmpTurretHolder.GetOccupiedTurretName(newTurret);
TS_ASSERT_UNEVAL_EQUALS(newTurretPos, previousTurret);
TS_ASSERT(cmpTurretableNew.LeaveTurret());
// Test initTurrets.
cmpTurretHolder.SetInitEntity("archer1", turret);
cmpTurretHolder.SetInitEntity("archer2", newTurret);
cmpTurretHolder.OnGlobalInitGame();
TS_ASSERT_UNEVAL_EQUALS(cmpTurretHolder.GetEntities(), [turret, newTurret]);

View File

@ -1,5 +1,5 @@
{
"type": "garrisonedUnits",
"type": "turretedUnits",
"affects": ["Soldier"],
"modifications": [
{ "value": "Resistance/Entity/Damage/Hack", "add": 3 },
@ -8,5 +8,5 @@
{ "value": "Vision/Range", "add": 20 }
],
"auraName": "Wall Protection",
"auraDescription": "Garrisoned Soldiers +3 crush, hack, pierce resistance and +20 vision range."
"auraDescription": "Turreted Soldiers +3 crush, hack, pierce resistance and +20 vision range."
}

View File

@ -462,6 +462,13 @@ var g_Commands = {
data.cmpPlayer.SetState("defeated", markForTranslation("%(player)s has resigned."));
},
"occupy-turret": function(player, cmd, data)
{
GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
cmpUnitAI.OccupyTurret(cmd.target, cmd.queued);
});
},
"garrison": function(player, cmd, data)
{
if (!CanPlayerOrAllyControlUnit(cmd.target, player, data.controlAllUnits))
@ -497,6 +504,38 @@ var g_Commands = {
});
},
"leave-turret": function(player, cmd, data)
{
let notUnloaded = 0;
for (let ent of data.entities)
{
let cmpTurretable = Engine.QueryInterface(ent, IID_Turretable);
if (!cmpTurretable || !cmpTurretable.LeaveTurret())
++notUnloaded;
}
if (notUnloaded)
notifyUnloadFailure(player);
},
"unload-turrets": function(player, cmd, data)
{
let notUnloaded = 0;
for (let ent of data.entities)
{
let cmpTurretHolder = Engine.QueryInterface(ent, IID_TurretHolder);
for (let turret of cmpTurretHolder.GetEntities())
{
let cmpTurretable = Engine.QueryInterface(turret, IID_Turretable);
if (!cmpTurretable || !cmpTurretable.LeaveTurret())
++notUnloaded;
}
}
if (notUnloaded)
notifyUnloadFailure(player);
},
"unload": function(player, cmd, data)
{
if (!CanPlayerOrAllyControlUnit(cmd.garrisonHolder, player, data.controlAllUnits))
@ -843,13 +882,13 @@ var g_Commands = {
/**
* Sends a GUI notification about unit(s) that failed to ungarrison.
*/
function notifyUnloadFailure(player, garrisonHolder)
function notifyUnloadFailure(player)
{
var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"type": "text",
"players": [player],
"message": markForTranslation("Unable to ungarrison unit(s)"),
"message": markForTranslation("Unable to unload unit(s)."),
"translateMessage": true
});
}

View File

@ -133,4 +133,39 @@ PositionHelper.prototype.PredictTimeToTarget = function(firstPosition, selfSpeed
return false;
};
/**
* @param {number} target - EntityID to find the spawn position for.
* @param {number} entity - EntityID to find the spawn position for.
* @param {boolean} forced - Optionally whether the spawning is forced.
* @return {Vector3D} - An appropriate spawning position.
*/
PositionHelper.prototype.GetSpawnPosition = function(target, entity, forced)
{
let cmpFootprint = Engine.QueryInterface(target, IID_Footprint);
let cmpHealth = Engine.QueryInterface(target, IID_Health);
let cmpIdentity = Engine.QueryInterface(target, IID_Identity);
if (!cmpFootprint)
return null;
// If the spawner is a sinking ship, restrict the location to the intersection of both passabilities.
// TODO: should use passability classes to be more generic.
let pos;
if ((!cmpHealth || cmpHealth.GetHitpoints() == 0) && cmpIdentity && cmpIdentity.HasClass("Ship"))
pos = cmpFootprint.PickSpawnPointBothPass(entity);
else
pos = cmpFootprint.PickSpawnPoint(entity);
if (pos.y < 0)
{
if (!forced)
return null;
// If ejection is forced, we need to continue, so use center of the entity.
let cmpPosition = Engine.QueryInterface(target, IID_Position);
pos = cmpPosition.GetPosition();
}
return pos;
};
Engine.RegisterGlobal("PositionHelper", new PositionHelper());

View File

@ -57,6 +57,14 @@ function GetRallyPointCommands(cmpRallyPoint, spawnedEnts)
"autocontinue": i == rallyPos.length - 1
});
break;
case "occupy-turret":
ret.push({
"type": "occupy-turret",
"entities": spawnedEnts,
"target": data[i].target,
"queued": true
});
break;
case "garrison":
ret.push({
"type": "garrison",

View File

@ -4,9 +4,6 @@
<Square width="37.0" depth="8.0"/>
<Height>15.5</Height>
</Footprint>
<GarrisonHolder>
<Max>8</Max>
</GarrisonHolder>
<Identity>
<Civ>athen</Civ>
<SpecificName>Pylai</SpecificName>

View File

@ -4,9 +4,6 @@
<Square width="37.0" depth="8.0"/>
<Height>18.0</Height>
</Footprint>
<GarrisonHolder>
<Max>4</Max>
</GarrisonHolder>
<Identity>
<Civ>brit</Civ>
<SpecificName>Duoricos</SpecificName>

View File

@ -4,9 +4,6 @@
<Square width="37.0" depth="9.0"/>
<Height>16.5</Height>
</Footprint>
<GarrisonHolder>
<Max>8</Max>
</GarrisonHolder>
<Identity>
<Civ>cart</Civ>
<SpecificName>Mijdil-šaʿar</SpecificName>

View File

@ -4,9 +4,6 @@
<Square width="37.0" depth="8.0"/>
<Height>12.0</Height>
</Footprint>
<GarrisonHolder>
<Max>4</Max>
</GarrisonHolder>
<Identity>
<Civ>gaul</Civ>
<SpecificName>Duoricos</SpecificName>

View File

@ -4,9 +4,6 @@
<Square width="37.0" depth="8.0"/>
<Height>12.7</Height>
</Footprint>
<GarrisonHolder>
<Max>8</Max>
</GarrisonHolder>
<Identity>
<Civ>iber</Civ>
<SpecificName>Biko Sarbide</SpecificName>

View File

@ -4,9 +4,6 @@
<Square width="37.0" depth="9.0"/>
<Height>12.6</Height>
</Footprint>
<GarrisonHolder>
<Max>6</Max>
</GarrisonHolder>
<Identity>
<Civ>kush</Civ>
<SpecificName>ʿryt</SpecificName>

View File

@ -4,9 +4,6 @@
<Square width="37.0" depth="7.5"/>
<Height>15.5</Height>
</Footprint>
<GarrisonHolder>
<Max>8</Max>
</GarrisonHolder>
<Identity>
<Civ>mace</Civ>
<SpecificName>Pylai</SpecificName>

View File

@ -9,10 +9,6 @@
<stone>200</stone>
</Resources>
</Cost>
<GarrisonHolder>
<Max>20</Max>
<List>Infantry+Archer</List>
</GarrisonHolder>
<Health>
<Max>1200</Max>
</Health>

View File

@ -10,9 +10,6 @@
<Square width="37.0" depth="8.0"/>
<Height>22.0</Height>
</Footprint>
<GarrisonHolder>
<Max>4</Max>
</GarrisonHolder>
<Identity>
<Civ>maur</Civ>
<SpecificName>Dwara</SpecificName>

View File

@ -4,9 +4,6 @@
<Square width="37.0" depth="7.0"/>
<Height>13.8</Height>
</Footprint>
<GarrisonHolder>
<Max>6</Max>
</GarrisonHolder>
<Identity>
<Civ>pers</Civ>
<SpecificName>Duvarθiš</SpecificName>

View File

@ -4,9 +4,6 @@
<Square width="40.0" depth="12.0"/>
<Height>17.8</Height>
</Footprint>
<GarrisonHolder>
<Max>10</Max>
</GarrisonHolder>
<Identity>
<Civ>ptol</Civ>
<SpecificName>Pylai</SpecificName>

View File

@ -14,9 +14,6 @@
<Square width="37.0" depth="7.0"/>
<Height>12.5</Height>
</Footprint>
<GarrisonHolder>
<Max>8</Max>
</GarrisonHolder>
<Health>
<Max op="mul">0.75</Max>
</Health>

View File

@ -14,9 +14,6 @@
<Square width="37.0" depth="5.0"/>
<Height>6.7</Height>
</Footprint>
<GarrisonHolder>
<Max>7</Max>
</GarrisonHolder>
<Health>
<Max op="mul">0.75</Max>
</Health>

View File

@ -19,10 +19,6 @@
<Square width="7.0" depth="7.0"/>
<Height>12.5</Height>
</Footprint>
<GarrisonHolder>
<Max>4</Max>
<List datatype="tokens">-Support -Infantry</List>
</GarrisonHolder>
<Health>
<Max op="mul">0.75</Max>
</Health>

View File

@ -4,9 +4,6 @@
<Square width="37.0" depth="7.0"/>
<Height>11.9</Height>
</Footprint>
<GarrisonHolder>
<Max>10</Max>
</GarrisonHolder>
<Identity>
<Civ>rome</Civ>
<SpecificName>Porta</SpecificName>

View File

@ -4,9 +4,6 @@
<Square width="35.0" depth="8.0"/>
<Height>11.6</Height>
</Footprint>
<GarrisonHolder>
<Max>5</Max>
</GarrisonHolder>
<Identity>
<Civ>sele</Civ>
<SpecificName>Pylai</SpecificName>

View File

@ -4,9 +4,6 @@
<Square width="37.0" depth="7.0"/>
<Height>15.5</Height>
</Footprint>
<GarrisonHolder>
<Max>8</Max>
</GarrisonHolder>
<Identity>
<Civ>spart</Civ>
<SpecificName>Pylai</SpecificName>

View File

@ -20,14 +20,6 @@
<Square width="7.0" depth="7.0"/>
<Height>13.0</Height>
</Footprint>
<GarrisonHolder>
<Max>1</Max>
<EjectHealth>0.1</EjectHealth>
<EjectClassesOnDestroy datatype="tokens">Unit</EjectClassesOnDestroy>
<List datatype="tokens">Infantry</List>
<BuffHeal>0</BuffHeal>
<LoadingRange>2</LoadingRange>
</GarrisonHolder>
<Health>
<Max>400</Max>
<SpawnEntityOnDeath>decay|rubble/rubble_stone_2x2</SpawnEntityOnDeath>

View File

@ -11,14 +11,6 @@
<Square width="6.0" depth="6.0"/>
<Height>8.0</Height>
</Footprint>
<GarrisonHolder>
<Max>1</Max>
<List datatype="tokens">Ranged+Infantry</List>
<EjectHealth>0.1</EjectHealth>
<EjectClassesOnDestroy datatype="tokens">Unit</EjectClassesOnDestroy>
<BuffHeal>0</BuffHeal>
<LoadingRange>2</LoadingRange>
</GarrisonHolder>
<Identity>
<GenericName>Wall</GenericName>
<SelectionGroupName>template_structure_defensive_wall</SelectionGroupName>

View File

@ -9,9 +9,6 @@
<stone>36</stone>
</Resources>
</Cost>
<GarrisonHolder>
<Max>8</Max>
</GarrisonHolder>
<Health>
<Max>3000</Max>
<SpawnEntityOnDeath>decay|rubble/rubble_stone_wall_long</SpawnEntityOnDeath>

View File

@ -9,9 +9,6 @@
<stone>24</stone>
</Resources>
</Cost>
<GarrisonHolder>
<Max>4</Max>
</GarrisonHolder>
<Health>
<Max>2000</Max>
<SpawnEntityOnDeath>decay|rubble/rubble_stone_wall_medium</SpawnEntityOnDeath>

View File

@ -6,7 +6,6 @@
<stone>12</stone>
</Resources>
</Cost>
<GarrisonHolder disable=""/>
<Health>
<Max>1000</Max>
<SpawnEntityOnDeath>decay|rubble/rubble_stone_wall_short</SpawnEntityOnDeath>

View File

@ -40,6 +40,10 @@
<GarrisonHolder>
<Max>2</Max>
<List datatype="tokens">Support Infantry</List>
<EjectHealth>0.1</EjectHealth>
<EjectClassesOnDestroy datatype="tokens">Unit</EjectClassesOnDestroy>
<BuffHeal>0</BuffHeal>
<LoadingRange>2</LoadingRange>
</GarrisonHolder>
<Health>
<Max>4000</Max>

View File

@ -45,6 +45,7 @@
<attack_ranged>attack/weapon/bow_attack.xml</attack_ranged>
</SoundGroups>
</Sound>
<Turretable/>
<UnitMotion>
<WalkSpeed op="mul">1.2</WalkSpeed>
</UnitMotion>

View File

@ -45,6 +45,7 @@
<attack_impact_ranged>attack/impact/javelin_impact.xml</attack_impact_ranged>
</SoundGroups>
</Sound>
<Turretable/>
<UnitMotion>
<WalkSpeed op="mul">1.2</WalkSpeed>
</UnitMotion>

View File

@ -42,4 +42,5 @@
<attack_ranged>attack/weapon/bow_attack.xml</attack_ranged>
</SoundGroups>
</Sound>
<Turretable/>
</Entity>

View File

@ -42,4 +42,5 @@
<attack_impact_ranged>attack/impact/javelin_impact.xml</attack_impact_ranged>
</SoundGroups>
</Sound>
<Turretable/>
</Entity>

View File

@ -19,4 +19,5 @@
</Damage>
</Entity>
</Resistance>
<Turretable/>
</Entity>