forked from 0ad/0ad
historic_bruno
7679d1073b
Fixes behavior when ships are destroyed (garrisoned units at the time will also be killed). Updates ship templates accordingly. This was SVN commit r10020.
603 lines
17 KiB
JavaScript
603 lines
17 KiB
JavaScript
function ProcessCommand(player, cmd)
|
|
{
|
|
// Do some basic checks here that commanding player is valid
|
|
var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
|
|
if (!cmpPlayerMan || player < 0)
|
|
return;
|
|
var playerEnt = cmpPlayerMan.GetPlayerByID(player);
|
|
if (playerEnt == INVALID_ENTITY)
|
|
return;
|
|
var cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player);
|
|
if (!cmpPlayer)
|
|
return;
|
|
var controlAllUnits = cmpPlayer.CanControlAllUnits();
|
|
|
|
// Now handle various commands
|
|
switch (cmd.type)
|
|
{
|
|
case "debug-print":
|
|
print(cmd.message);
|
|
break;
|
|
|
|
case "chat":
|
|
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
|
|
cmpGuiInterface.PushNotification({"type": "chat", "player": player, "message": cmd.message});
|
|
break;
|
|
|
|
case "control-all":
|
|
cmpPlayer.SetControlAllUnits(cmd.flag);
|
|
break;
|
|
|
|
case "reveal-map":
|
|
// Reveal the map for all players, not just the current player,
|
|
// primarily to make it obvious to everyone that the player is cheating
|
|
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
|
|
cmpRangeManager.SetLosRevealAll(-1, cmd.enable);
|
|
break;
|
|
|
|
case "walk":
|
|
var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
|
|
GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) {
|
|
cmpUnitAI.Walk(cmd.x, cmd.z, cmd.queued);
|
|
});
|
|
break;
|
|
|
|
case "attack":
|
|
// Check if target is owned by player's enemy
|
|
if (IsOwnedByEnemyOfPlayer(player, cmd.target))
|
|
{
|
|
var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
|
|
GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) {
|
|
cmpUnitAI.Attack(cmd.target, cmd.queued);
|
|
});
|
|
}
|
|
break;
|
|
|
|
case "repair":
|
|
// This covers both repairing damaged buildings, and constructing unfinished foundations
|
|
// Check if target building is owned by player or an ally
|
|
if (IsOwnedByAllyOfPlayer(player, cmd.target))
|
|
{
|
|
var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
|
|
GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) {
|
|
cmpUnitAI.Repair(cmd.target, cmd.autocontinue, cmd.queued);
|
|
});
|
|
}
|
|
break;
|
|
|
|
case "gather":
|
|
var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
|
|
GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) {
|
|
cmpUnitAI.Gather(cmd.target, cmd.queued);
|
|
});
|
|
break;
|
|
|
|
case "returnresource":
|
|
// Check dropsite is owned by player
|
|
if (IsOwnedByPlayer(player, cmd.target))
|
|
{
|
|
var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
|
|
GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) {
|
|
cmpUnitAI.ReturnResource(cmd.target, cmd.queued);
|
|
});
|
|
}
|
|
break;
|
|
|
|
case "train":
|
|
if (CanControlUnit(cmd.entity, player, controlAllUnits))
|
|
{
|
|
var queue = Engine.QueryInterface(cmd.entity, IID_TrainingQueue);
|
|
if (queue)
|
|
queue.AddBatch(cmd.template, +cmd.count, cmd.metadata);
|
|
}
|
|
break;
|
|
|
|
case "stop-train":
|
|
if (CanControlUnit(cmd.entity, player, controlAllUnits))
|
|
{
|
|
var queue = Engine.QueryInterface(cmd.entity, IID_TrainingQueue);
|
|
if (queue)
|
|
queue.RemoveBatch(cmd.id);
|
|
}
|
|
break;
|
|
|
|
case "construct":
|
|
// Message structure:
|
|
// {
|
|
// "type": "construct",
|
|
// "entities": [...],
|
|
// "template": "...",
|
|
// "x": ...,
|
|
// "z": ...,
|
|
// "angle": ...,
|
|
// "autorepair": true, // whether to automatically start constructing/repairing the new foundation
|
|
// "autocontinue": true, // whether to automatically gather/build/etc after finishing this
|
|
// "queued": true,
|
|
// }
|
|
|
|
/*
|
|
* Construction process:
|
|
* . Take resources away immediately.
|
|
* . Create a foundation entity with 1hp, 0% build progress.
|
|
* . Increase hp and build progress up to 100% when people work on it.
|
|
* . If it's destroyed, an appropriate fraction of the resource cost is refunded.
|
|
* . If it's completed, it gets replaced with the real building.
|
|
*/
|
|
|
|
// Check that we can control these units
|
|
var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
|
|
if (!entities.length)
|
|
break;
|
|
|
|
// Tentatively create the foundation (we might find later that it's a invalid build command)
|
|
var ent = Engine.AddEntity("foundation|" + cmd.template);
|
|
if (ent == INVALID_ENTITY)
|
|
{
|
|
// Error (e.g. invalid template names)
|
|
error("Error creating foundation for '" + cmd.template + "'");
|
|
break;
|
|
}
|
|
|
|
// Move the foundation to the right place
|
|
var cmpPosition = Engine.QueryInterface(ent, IID_Position);
|
|
cmpPosition.JumpTo(cmd.x, cmd.z);
|
|
cmpPosition.SetYRotation(cmd.angle);
|
|
|
|
// TODO: Build restrctions disabled for AI since it lacks a mechanism for checking most of them
|
|
if (!cmpPlayer.IsAI())
|
|
{
|
|
// Check whether it's obstructed by other entities or invalid terrain
|
|
var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
|
|
if (!cmpBuildRestrictions || !cmpBuildRestrictions.CheckPlacement(player))
|
|
{
|
|
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
|
|
cmpGuiInterface.PushNotification({ "player": player, "message": "Building site was obstructed" });
|
|
|
|
// Remove the foundation because the construction was aborted
|
|
Engine.DestroyEntity(ent);
|
|
break;
|
|
}
|
|
|
|
// Check build limits
|
|
var cmpBuildLimits = QueryPlayerIDInterface(player, IID_BuildLimits);
|
|
if (!cmpBuildLimits || !cmpBuildLimits.AllowedToBuild(cmpBuildRestrictions.GetCategory()))
|
|
{
|
|
// TODO: The UI should tell the user they can't build this (but we still need this check)
|
|
|
|
// Remove the foundation because the construction was aborted
|
|
Engine.DestroyEntity(ent);
|
|
break;
|
|
}
|
|
|
|
// Check whether it's in a visible region
|
|
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
|
|
var visible = (cmpRangeManager.GetLosVisibility(ent, player) == "visible");
|
|
if (!visible)
|
|
{
|
|
// TODO: report error to player (the building site was not visible)
|
|
print("Building site was not visible\n");
|
|
|
|
Engine.DestroyEntity(ent);
|
|
break;
|
|
}
|
|
}
|
|
|
|
var cmpCost = Engine.QueryInterface(ent, IID_Cost);
|
|
if (!cmpPlayer.TrySubtractResources(cmpCost.GetResourceCosts()))
|
|
{
|
|
Engine.DestroyEntity(ent);
|
|
break;
|
|
}
|
|
|
|
// Make it owned by the current player
|
|
var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
|
|
cmpOwnership.SetOwner(player);
|
|
|
|
// Initialise the foundation
|
|
var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation);
|
|
cmpFoundation.InitialiseConstruction(player, cmd.template);
|
|
|
|
// Tell the units to start building this new entity
|
|
if (cmd.autorepair)
|
|
{
|
|
ProcessCommand(player, {
|
|
"type": "repair",
|
|
"entities": entities,
|
|
"target": ent,
|
|
"autocontinue": cmd.autocontinue,
|
|
"queued": cmd.queued
|
|
});
|
|
}
|
|
|
|
break;
|
|
|
|
case "delete-entities":
|
|
var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
|
|
for each (var ent in entities)
|
|
{
|
|
var cmpHealth = Engine.QueryInterface(ent, IID_Health);
|
|
if (cmpHealth)
|
|
cmpHealth.Kill();
|
|
else
|
|
Engine.DestroyEntity(ent);
|
|
}
|
|
break;
|
|
|
|
case "set-rallypoint":
|
|
var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
|
|
for each (var ent in entities)
|
|
{
|
|
var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
|
|
if (cmpRallyPoint)
|
|
cmpRallyPoint.SetPosition(cmd.x, cmd.z);
|
|
}
|
|
break;
|
|
|
|
case "unset-rallypoint":
|
|
var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
|
|
for each (var ent in entities)
|
|
{
|
|
var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
|
|
if (cmpRallyPoint)
|
|
cmpRallyPoint.Unset();
|
|
}
|
|
break;
|
|
|
|
case "defeat-player":
|
|
// Send "OnPlayerDefeated" message to player
|
|
Engine.PostMessage(playerEnt, MT_PlayerDefeated, null);
|
|
break;
|
|
|
|
case "garrison":
|
|
if (CanControlUnit(cmd.target, player, controlAllUnits))
|
|
{
|
|
var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
|
|
GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) {
|
|
cmpUnitAI.Garrison(cmd.target);
|
|
});
|
|
}
|
|
break;
|
|
|
|
case "unload":
|
|
if (CanControlUnit(cmd.garrisonHolder, player, controlAllUnits))
|
|
{
|
|
var cmpGarrisonHolder = Engine.QueryInterface(cmd.garrisonHolder, IID_GarrisonHolder);
|
|
if (!cmpGarrisonHolder || !cmpGarrisonHolder.Unload(cmd.entity))
|
|
{
|
|
var cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
|
|
var notification = {"player": cmpPlayer.GetPlayerID(), "message": "Unable to ungarrison unit"};
|
|
var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
|
|
cmpGUIInterface.PushNotification(notification);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "unload-all":
|
|
if (CanControlUnit(cmd.garrisonHolder, player, controlAllUnits))
|
|
{
|
|
var cmpGarrisonHolder = Engine.QueryInterface(cmd.garrisonHolder, IID_GarrisonHolder);
|
|
if (!cmpGarrisonHolder || !cmpGarrisonHolder.UnloadAll())
|
|
{
|
|
var cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
|
|
var notification = {"player": cmpPlayer.GetPlayerID(), "message": "Unable to ungarrison all units"};
|
|
var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
|
|
cmpGUIInterface.PushNotification(notification);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "formation":
|
|
var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
|
|
GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) {
|
|
var cmpFormation = Engine.QueryInterface(cmpUnitAI.entity, IID_Formation);
|
|
if (!cmpFormation)
|
|
return;
|
|
cmpFormation.LoadFormation(cmd.name);
|
|
cmpFormation.MoveMembersIntoFormation(true);
|
|
});
|
|
break;
|
|
|
|
case "promote":
|
|
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
|
|
cmpGuiInterface.PushNotification({"type": "chat", "player": player, "message": "(Cheat - promoted units)"});
|
|
|
|
for each (var ent in cmd.entities)
|
|
{
|
|
var cmpPromotion = Engine.QueryInterface(ent, IID_Promotion);
|
|
if (cmpPromotion)
|
|
cmpPromotion.IncreaseXp(cmpPromotion.GetRequiredXp() - cmpPromotion.GetCurrentXp());
|
|
}
|
|
break;
|
|
|
|
case "stance":
|
|
var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
|
|
for each (var ent in entities)
|
|
{
|
|
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
|
|
if (cmpUnitAI)
|
|
cmpUnitAI.SwitchToStance(cmd.name);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
error("Ignoring unrecognised command type '" + cmd.type + "'");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get some information about the formations used by entities.
|
|
* The entities must have a UnitAI component.
|
|
*/
|
|
function ExtractFormations(ents)
|
|
{
|
|
var entities = []; // subset of ents that have UnitAI
|
|
var members = {}; // { formationentity: [ent, ent, ...], ... }
|
|
for each (var ent in ents)
|
|
{
|
|
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
|
|
var fid = cmpUnitAI.GetFormationController();
|
|
if (fid != INVALID_ENTITY)
|
|
{
|
|
if (!members[fid])
|
|
members[fid] = [];
|
|
members[fid].push(ent);
|
|
}
|
|
entities.push(ent);
|
|
}
|
|
|
|
var ids = [ id for (id in members) ];
|
|
|
|
return { "entities": entities, "members": members, "ids": ids };
|
|
}
|
|
|
|
/**
|
|
* Remove the given list of entities from their current formations.
|
|
*/
|
|
function RemoveFromFormation(ents)
|
|
{
|
|
var formation = ExtractFormations(ents);
|
|
for (var fid in formation.members)
|
|
{
|
|
var cmpFormation = Engine.QueryInterface(+fid, IID_Formation);
|
|
if (cmpFormation)
|
|
cmpFormation.RemoveMembers(formation.members[fid]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a list of UnitAI components, each belonging either to a
|
|
* selected unit or to a formation entity for groups of the selected units.
|
|
*/
|
|
function GetFormationUnitAIs(ents)
|
|
{
|
|
// If an individual was selected, remove it from any formation
|
|
// and command it individually
|
|
if (ents.length == 1)
|
|
{
|
|
// Skip unit if it has no UnitAI
|
|
var cmpUnitAI = Engine.QueryInterface(ents[0], IID_UnitAI);
|
|
if (!cmpUnitAI)
|
|
return [];
|
|
|
|
RemoveFromFormation(ents);
|
|
|
|
return [ cmpUnitAI ];
|
|
}
|
|
|
|
// Separate out the units that don't support the chosen formation
|
|
var formedEnts = [];
|
|
var nonformedUnitAIs = [];
|
|
for each (var ent in ents)
|
|
{
|
|
// Skip units with no UnitAI
|
|
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
|
|
if (!cmpUnitAI)
|
|
continue;
|
|
|
|
var cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
|
|
// TODO: Currently we use LineClosed as effectively a boolean flag
|
|
// to determine whether formations are allowed at all. Instead we
|
|
// should check specific formation names and do something sensible
|
|
// (like what?) when some units don't support them.
|
|
// TODO: We'll also need to fix other formation code to use
|
|
// "LineClosed" instead of "Line Closed" etc consistently.
|
|
if (cmpIdentity && cmpIdentity.CanUseFormation("LineClosed"))
|
|
formedEnts.push(ent);
|
|
else
|
|
nonformedUnitAIs.push(cmpUnitAI);
|
|
}
|
|
|
|
if (formedEnts.length == 0)
|
|
{
|
|
// No units support the foundation - return all the others
|
|
return nonformedUnitAIs;
|
|
}
|
|
|
|
// Find what formations the formationable selected entities are currently in
|
|
var formation = ExtractFormations(formedEnts);
|
|
|
|
var formationEnt = undefined;
|
|
if (formation.ids.length == 1)
|
|
{
|
|
// Selected units all belong to the same formation.
|
|
// Check that it doesn't have any other members
|
|
var fid = formation.ids[0];
|
|
var cmpFormation = Engine.QueryInterface(+fid, IID_Formation);
|
|
if (cmpFormation && cmpFormation.GetMemberCount() == formation.entities.length)
|
|
{
|
|
// The whole formation was selected, so reuse its controller for this command
|
|
formationEnt = +fid;
|
|
}
|
|
}
|
|
|
|
if (!formationEnt)
|
|
{
|
|
// We need to give the selected units a new formation controller
|
|
|
|
// Remove selected units from their current formation
|
|
for (var fid in formation.members)
|
|
{
|
|
var cmpFormation = Engine.QueryInterface(+fid, IID_Formation);
|
|
if (cmpFormation)
|
|
cmpFormation.RemoveMembers(formation.members[fid]);
|
|
}
|
|
|
|
// Create the new controller
|
|
formationEnt = Engine.AddEntity("special/formation");
|
|
var cmpFormation = Engine.QueryInterface(formationEnt, IID_Formation);
|
|
cmpFormation.SetMembers(formation.entities);
|
|
|
|
// If all the selected units were previously in formations of the same shape,
|
|
// then set this new formation to that shape too; otherwise use the default shape
|
|
var lastFormationName = undefined;
|
|
for each (var ent in formation.entities)
|
|
{
|
|
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
|
|
if (cmpUnitAI)
|
|
{
|
|
var name = cmpUnitAI.GetLastFormationName();
|
|
if (lastFormationName === undefined)
|
|
{
|
|
lastFormationName = name;
|
|
}
|
|
else if (lastFormationName != name)
|
|
{
|
|
lastFormationName = undefined;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
var formationName;
|
|
if (lastFormationName)
|
|
formationName = lastFormationName;
|
|
else
|
|
formationName = "Line Closed";
|
|
|
|
if (CanMoveEntsIntoFormation(formation.entities, formationName))
|
|
{
|
|
cmpFormation.LoadFormation(formationName);
|
|
}
|
|
else
|
|
{
|
|
cmpFormation.LoadFormation("Loose");
|
|
}
|
|
}
|
|
|
|
return nonformedUnitAIs.concat(Engine.QueryInterface(formationEnt, IID_UnitAI));
|
|
}
|
|
|
|
function CanMoveEntsIntoFormation(ents, formationName)
|
|
{
|
|
var count = ents.length;
|
|
var classesRequired;
|
|
|
|
// TODO: should check the player's civ is allowed to use this formation
|
|
|
|
if (formationName == "Loose")
|
|
{
|
|
return true;
|
|
}
|
|
else if (formationName == "Box")
|
|
{
|
|
if (count < 4)
|
|
return false;
|
|
}
|
|
else if (formationName == "Column Closed")
|
|
{
|
|
}
|
|
else if (formationName == "Line Closed")
|
|
{
|
|
}
|
|
else if (formationName == "Column Open")
|
|
{
|
|
}
|
|
else if (formationName == "Line Open")
|
|
{
|
|
}
|
|
else if (formationName == "Flank")
|
|
{
|
|
if (count < 8)
|
|
return false;
|
|
}
|
|
else if (formationName == "Skirmish")
|
|
{
|
|
classesRequired = ["Ranged"];
|
|
}
|
|
else if (formationName == "Wedge")
|
|
{
|
|
if (count < 3)
|
|
return false;
|
|
classesRequired = ["Cavalry"];
|
|
}
|
|
else if (formationName == "Formation12")
|
|
{
|
|
}
|
|
else if (formationName == "Phalanx")
|
|
{
|
|
if (count < 10)
|
|
return false;
|
|
classesRequired = ["Melee", "Infantry"];
|
|
}
|
|
else if (formationName == "Syntagma")
|
|
{
|
|
if (count < 9)
|
|
return false;
|
|
classesRequired = ["Melee", "Infantry"]; // TODO: pike only
|
|
}
|
|
else if (formationName == "Testudo")
|
|
{
|
|
if (count < 9)
|
|
return false;
|
|
classesRequired = ["Melee", "Infantry"];
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var looseOnlyUnits = true;
|
|
for each (var ent in ents)
|
|
{
|
|
var cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
|
|
if (cmpIdentity)
|
|
{
|
|
var classes = cmpIdentity.GetClassesList();
|
|
if (looseOnlyUnits && (classes.indexOf("Worker") == -1 || classes.indexOf("Support") == -1))
|
|
looseOnlyUnits = false;
|
|
for each (var classRequired in classesRequired)
|
|
{
|
|
if (classes.indexOf(classRequired) == -1)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (looseOnlyUnits)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Check if player can control this entity
|
|
* returns: true if the entity is valid and owned by the player if
|
|
* or control all units is activated for the player, else false
|
|
*/
|
|
function CanControlUnit(entity, player, controlAll)
|
|
{
|
|
return (IsOwnedByPlayer(player, entity) || controlAll);
|
|
}
|
|
|
|
/**
|
|
* Filter entities which the player can control
|
|
*/
|
|
function FilterEntityList(entities, player, controlAll)
|
|
{
|
|
return entities.filter(function(ent) { return CanControlUnit(ent, player, controlAll);} );
|
|
}
|
|
|
|
Engine.RegisterGlobal("CanMoveEntsIntoFormation", CanMoveEntsIntoFormation);
|
|
Engine.RegisterGlobal("ProcessCommand", ProcessCommand);
|