1
0
forked from 0ad/0ad
0ad/binaries/data/mods/public/simulation/helpers/Commands.js
fcxSanya 894dc30c69 Trade. Closes #30.
This was SVN commit r11279.
2012-03-08 20:42:28 +00:00

708 lines
21 KiB
JavaScript

// Setting this to true will display some warnings when commands
// are likely to fail, which may be useful for debugging AIs
var g_DebugCommands = false;
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();
// Note: checks of UnitAI targets are not robust enough here, as ownership
// can change after the order is issued, they should be checked by UnitAI
// when the specific behavior (e.g. attack, garrison) is performed.
// (Also it's not ideal if a command silently fails, it's nicer if UnitAI
// moves the entities closer to the target before giving up.)
// 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":
if (g_DebugCommands && !IsOwnedByEnemyOfPlayer(player, cmd.target))
{
// This check is for debugging only!
warn("Invalid command: attack target is not owned by enemy of player "+player+": "+uneval(cmd));
}
// See UnitAI.CanAttack for target checks
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
if (g_DebugCommands && !IsOwnedByAllyOfPlayer(player, cmd.target))
{
// This check is for debugging only!
warn("Invalid command: repair target is not owned by ally of player "+player+": "+uneval(cmd));
}
// See UnitAI.CanRepair for target checks
var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) {
cmpUnitAI.Repair(cmd.target, cmd.autocontinue, cmd.queued);
});
break;
case "gather":
if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByGaia(cmd.target)))
{
// This check is for debugging only!
warn("Invalid command: resource is not owned by gaia or player "+player+": "+uneval(cmd));
}
// See UnitAI.CanGather for target checks
var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) {
cmpUnitAI.Gather(cmd.target, cmd.queued);
});
break;
case "gather-near-position":
var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) {
cmpUnitAI.GatherNearPosition(cmd.x, cmd.z, cmd.resourceType, cmd.queued);
});
break;
case "returnresource":
// Check dropsite is owned by player
if (g_DebugCommands && IsOwnedByPlayer(player, cmd.target))
{
// This check is for debugging only!
warn("Invalid command: dropsite is not owned by player "+player+": "+uneval(cmd));
}
// See UnitAI.CanReturnResource for target checks
var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) {
cmpUnitAI.ReturnResource(cmd.target, cmd.queued);
});
break;
case "train":
// Verify that the building can be controlled by the player
if (CanControlUnit(cmd.entity, player, controlAllUnits))
{
var queue = Engine.QueryInterface(cmd.entity, IID_TrainingQueue);
if (queue)
queue.AddBatch(cmd.template, +cmd.count, cmd.metadata);
}
else if (g_DebugCommands)
{
warn("Invalid command: training building cannot be controlled by player "+player+": "+uneval(cmd));
}
break;
case "stop-train":
// Verify that the building can be controlled by the player
if (CanControlUnit(cmd.entity, player, controlAllUnits))
{
var queue = Engine.QueryInterface(cmd.entity, IID_TrainingQueue);
if (queue)
queue.RemoveBatch(cmd.id);
}
else if (g_DebugCommands)
{
warn("Invalid command: training building cannot be controlled by player "+player+": "+uneval(cmd));
}
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 entity 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);
// Check whether it's obstructed by other entities or invalid terrain
var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
if (!cmpBuildRestrictions || !cmpBuildRestrictions.CheckPlacement(player))
{
if (g_DebugCommands)
{
warn("Invalid command: build restrictions check failed for player "+player+": "+uneval(cmd));
}
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()))
{
if (g_DebugCommands)
{
warn("Invalid command: build limits check failed for player "+player+": "+uneval(cmd));
}
// 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;
}
// TODO: AI has no visibility info
if (!cmpPlayer.IsAI())
{
// Check whether it's in a visible or fogged region
// tell GetLosVisibility to force RetainInFog because preview entities set this to false,
// which would show them as hidden instead of fogged
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var visible = (cmpRangeManager && cmpRangeManager.GetLosVisibility(ent, player, true) != "hidden");
if (!visible)
{
if (g_DebugCommands)
{
warn("Invalid command: foundation visibility check failed for player "+player+": "+uneval(cmd));
}
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({ "player": player, "message": "Building site was not visible" });
Engine.DestroyEntity(ent);
break;
}
}
var cmpCost = Engine.QueryInterface(ent, IID_Cost);
if (!cmpPlayer.TrySubtractResources(cmpCost.GetResourceCosts()))
{
if (g_DebugCommands)
{
warn("Invalid command: building cost check failed for player "+player+": "+uneval(cmd));
}
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);
cmpRallyPoint.SetData(cmd.data);
}
}
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, { "playerId": player } );
break;
case "garrison":
// Verify that the building can be controlled by the player
if (CanControlUnit(cmd.target, player, controlAllUnits))
{
var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) {
cmpUnitAI.Garrison(cmd.target);
});
}
else if (g_DebugCommands)
{
warn("Invalid command: garrison target cannot be controlled by player "+player+": "+uneval(cmd));
}
break;
case "unload":
// Verify that the building can be controlled by the player
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);
}
}
else if (g_DebugCommands)
{
warn("Invalid command: unload target cannot be controlled by player "+player+": "+uneval(cmd));
}
break;
case "unload-all":
// Verify that the building can be controlled by the player
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);
}
}
else if (g_DebugCommands)
{
warn("Invalid command: unload-all target cannot be controlled by player "+player+": "+uneval(cmd));
}
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":
// No need to do checks here since this is a cheat anyway
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;
case "setup-trade-route":
for each (var ent in cmd.entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
cmpUnitAI.SetupTradeRoute(cmd.target);
}
break;
case "select-trading-goods":
var cmpTrader = Engine.QueryInterface(cmd.trader, IID_Trader);
if (cmpTrader)
cmpTrader.SetPreferredGoods(cmd.preferredGoods);
break;
case "barter":
var cmpBarter = Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter);
cmpBarter.ExchangeResources(playerEnt, cmd.sell, cmd.buy, cmd.amount);
break;
default:
error("Invalid command: unknown command type: "+uneval(cmd));
}
}
/**
* 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);