Add spying to the game

Summary:
With c2d0327af9 we can now add spying into the game: you have first to
research a tech (available in phase 3, espionage), and then you can
bribe a random unit (defined as Bribable in its template) from a chosen
player and share its vision during 15s.
There is also an option in the gamesetup to disable this feature,
analoguous to the treasure disabling option.
In the current version, only traders (land and naval ones) are
bribables.
Reviewed By: Itms, elexis
Differential Revision: https://code.wildfiregames.com/D117
This was SVN commit r19247.
This commit is contained in:
mimo 2017-02-27 18:17:40 +00:00
parent f3e4e619bc
commit d9d1f1bbeb
16 changed files with 258 additions and 27 deletions

View File

@ -538,6 +538,7 @@ function initRadioButtons()
"RevealMap": "revealMap",
"ExploreMap": "exploreMap",
"DisableTreasures": "disableTreasures",
"DisableSpies": "disableSpies",
"LockTeams": "lockTeams",
"LastManStanding" : "lastManStanding",
"CheatsEnabled": "enableCheats"
@ -1442,6 +1443,7 @@ function updateGUIObjects()
setGUIBoolean("enableCheats", "enableCheatsText", !!mapSettings.CheatsEnabled);
setGUIBoolean("disableTreasures", "disableTreasuresText", !!mapSettings.DisableTreasures);
setGUIBoolean("disableSpies", "disableSpiesText", !!mapSettings.DisableSpies);
setGUIBoolean("exploreMap", "exploreMapText", !!mapSettings.ExploreMap);
setGUIBoolean("revealMap", "revealMapText", !!mapSettings.RevealMap);
setGUIBoolean("lockTeams", "lockTeamsText", !!mapSettings.LockTeams);
@ -1469,7 +1471,7 @@ function updateGUIObjects()
for (let ctrl of ["victoryCondition", "wonderDuration", "populationCap",
"startingResources", "ceasefire", "revealMap",
"exploreMap", "disableTreasures", "lockTeams", "lastManStanding"])
"exploreMap", "disableTreasures", "disableSpies", "lockTeams", "lastManStanding"])
hideControl(ctrl, ctrl + "Text", notScenario);
Engine.GetGUIObjectByName("civResetButton").hidden = !notScenario;

View File

@ -407,7 +407,17 @@
</object>
</object>
<object name="optionLockTeams" size="14 308 94% 336">
<object name="optionDisableSpies" size="14 308 94% 336">
<object size="0 0 40% 28" type="text" style="ModernRightLabelText">
<translatableAttribute id="caption">Disable Spies:</translatableAttribute>
</object>
<object name="disableSpiesText" size="40% 0 100% 28" type="text" style="ModernLeftLabelText"/>
<object name="disableSpies" size="40%+10 5 40%+30 100%-5" type="checkbox" style="ModernTickBox" hidden="true" tooltip_style="onscreenToolTip">
<translatableAttribute id="tooltip">Disable spies during the game.</translatableAttribute>
</object>
</object>
<object name="optionLockTeams" size="14 338 94% 366">
<object size="0 0 40% 28" type="text" style="ModernRightLabelText">
<translatableAttribute id="caption">Teams Locked:</translatableAttribute>
</object>
@ -417,7 +427,7 @@
</object>
</object>
<object name="optionLastManStanding" size="14 338 94% 366">
<object name="optionLastManStanding" size="14 368 94% 396">
<object size="0 0 40% 28" type="text" style="ModernRightLabelText">
<translatableAttribute id="caption">Last Man Standing:</translatableAttribute>
</object>
@ -427,7 +437,7 @@
</object>
</object>
<object name="optionCheats" size="14 368 94% 396" hidden="true">
<object name="optionCheats" size="14 398 94% 426" hidden="true">
<object size="0 0 40% 28" type="text" style="ModernRightLabelText">
<translatableAttribute id="caption">Cheats:</translatableAttribute>
</object>
@ -437,7 +447,7 @@
</object>
</object>
<object name="optionRating" size="14 398 94% 426" hidden="true">
<object name="optionRating" size="14 428 94% 456" hidden="true">
<object size="0 0 40% 28" hidden="false" type="text" style="ModernRightLabelText">
<translatableAttribute id="caption">Rated Game:</translatableAttribute>
</object>
@ -452,7 +462,7 @@
name="hideMoreOptions"
type="button"
style="StoneButton"
size="50%-70 428 50%+70 456"
size="50%-70 458 50%+70 486"
tooltip_style="onscreenToolTip"
hotkey="cancel"
>

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<object name="diplomacyDialogPanel"
size="50%-260 50%-200 50%+260 50%+150"
size="50%-280 50%-200 50%+280 50%+150"
type="image"
hidden="true"
sprite="ModernDialog"
@ -35,7 +35,7 @@
<translatableAttribute id="caption">E</translatableAttribute>
<translatableAttribute id="tooltip">Enemy</translatableAttribute>
</object>
<object name="diplomacyHeaderTribute" size="430 0 100%-30 100%" type="text" style="DiplomacyText" text_align="center">
<object name="diplomacyHeaderTribute" size="430 0 100%-70 100%" type="text" style="DiplomacyText" text_align="center">
<translatableAttribute id="caption">Tribute</translatableAttribute>
</object>
</object>
@ -55,7 +55,7 @@
<object name="diplomacyPlayerEnemy[n]" size="400 0 420 100%" type="button" style="StoneButton" hidden="true"/>
<!-- Tribute -->
<object size="430 0 100%-40 100%">
<object size="430 0 100%-80 100%">
<repeat count="8" var="r">
<object name="diplomacyPlayer[n]_tribute[r]" size="0 0 20 100%" type="button" style="iconButton" tooltip_style="sessionToolTipBold" hidden="true">
<object name="diplomacyPlayer[n]_tribute[r]_image" type="image" size="0 0 100% 100%" ghost="true"/>
@ -63,9 +63,13 @@
</repeat>
</object>
<object name="diplomacyAttackRequest[n]" size="100%-20 0 100% 100%" type="button" style="iconButton" tooltip_style="sessionToolTipBold" hidden="true">
<object name="diplomacyAttackRequest[n]" size="100%-58 0 100%-38 100%" type="button" style="iconButton" tooltip_style="sessionToolTipBold" hidden="true">
<object name="diplomacyAttackRequestImage[n]" type="image" size="0 0 100% 100%" sprite="stretched:session/icons/attack-request.png" ghost="true"/>
</object>
<object name="diplomacySpyRequest[n]" size="100%-30 0 100%-10 100%" type="button" style="iconButton" tooltip_style="sessionToolTipBold" hidden="true">
<object name="diplomacySpyRequestImage[n]" type="image" size="2 2 100%-2 100%-2" ghost="true"/>
</object>
</object>
</repeat>
</object>

View File

@ -314,7 +314,16 @@ function openDiplomacy()
g_IsDiplomacyOpen = true;
updateDiplomacyPanel(true);
}
function updateDiplomacyPanel(opening = false)
{
if (g_ViewedPlayer < 1 || !g_IsDiplomacyOpen)
return;
let isCeasefireActive = GetSimState().ceasefireActive;
let hasSharedLos = GetSimState().players[g_ViewedPlayer].hasSharedLos;
// Get offset for one line
let onesize = Engine.GetGUIObjectByName("diplomacyPlayer[0]").size;
@ -329,8 +338,11 @@ function openDiplomacy()
diplomacySetupTexts(i, rowsize);
diplomacyFormatStanceButtons(i, myself || playerInactive || isCeasefireActive || g_Players[g_ViewedPlayer].teamsLocked);
diplomacyFormatTributeButtons(i, myself || playerInactive);
// Tribute buttons do not need to be updated onTick, and should not because of massTributing
if (opening)
diplomacyFormatTributeButtons(i, myself || playerInactive);
diplomacyFormatAttackRequestButton(i, myself || playerInactive || isCeasefireActive || !hasAllies || !g_Players[i].isEnemy[g_ViewedPlayer]);
diplomacyFormatSpyRequestButton(i, myself || playerInactive || g_Players[i].isMutualAlly[g_ViewedPlayer] && hasSharedLos);
}
Engine.GetGUIObjectByName("diplomacyDialogPanel").hidden = false;
}
@ -449,8 +461,61 @@ function diplomacyFormatAttackRequestButton(i, hidden)
button.enabled = controlsPlayer(g_ViewedPlayer);
button.tooltip = translate("Request your allies to attack this enemy");
button.onpress = (function(i) { return function() {
Engine.PostNetworkCommand({ "type": "attack-request", "source": g_ViewedPlayer, "target": i });
button.onPress = (function(i) { return function() {
Engine.PostNetworkCommand({ "type": "attack-request", "source": g_ViewedPlayer, "player": i });
}; })(i);
}
function diplomacyFormatSpyRequestButton(i, hidden)
{
let button = Engine.GetGUIObjectByName("diplomacySpyRequest["+(i-1)+"]");
let template = GetTemplateData("special/spy");
button.hidden = hidden || !template || GetSimState().players[g_ViewedPlayer].disabledTemplates["special/spy"];
if (button.hidden)
return;
button.enabled = controlsPlayer(g_ViewedPlayer);
let modifier = "";
let tooltips = [translate("Bribe a random unit from this player and share its vision during a limited period.")];
if (!button.enabled)
modifier = "color:0 0 0 127:grayscale:";
else
{
if (template.requiredTechnology)
{
let technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", {
"tech": template.requiredTechnology,
"player": g_ViewedPlayer
});
if (!technologyEnabled)
{
modifier = "color:0 0 0 127:grayscale:"
button.enabled = false;
tooltips.push(getRequiredTechnologyTooltip(technologyEnabled, template.requiredTechnology, GetSimState().players[g_ViewedPlayer].civ));
}
}
if (template.cost)
{
let neededResources = Engine.GuiInterfaceCall("GetNeededResources", {
"cost": template.cost,
"player": g_ViewedPlayer
});
if (neededResources)
{
if (button.enabled)
modifier = resourcesToAlphaMask(neededResources) +":";
button.enabled = false;
tooltips.push(getNeededResourcesTooltip(neededResources));
}
}
}
let icon = Engine.GetGUIObjectByName("diplomacySpyRequestImage["+(i-1)+"]");
icon.sprite = modifier + "stretched:session/icons/economics.png";
button.tooltip = tooltips.filter(tip => tip).join("\n");
button.onPress = (function(i) { return function() {
Engine.PostNetworkCommand({ "type": "spy-request", "source": g_ViewedPlayer, "player": i });
closeDiplomacy();
}; })(i);
}
@ -540,7 +605,7 @@ function openTrade()
let buttonResource = Engine.GetGUIObjectByName("tradeResourceButton["+i+"]");
buttonResource.enabled = controlsPlayer(g_ViewedPlayer);
buttonResource.onpress = (function(resource){
buttonResource.onPress = (function(resource){
return function() {
if (Engine.HotkeyIsPressed("session.fulltradeswap"))
{
@ -555,7 +620,7 @@ function openTrade()
})(resCode);
buttonUp.enabled = controlsPlayer(g_ViewedPlayer);
buttonUp.onpress = (function(resource){
buttonUp.onPress = (function(resource){
return function() {
proba[resource] += Math.min(STEP, proba[selec]);
proba[selec] -= Math.min(STEP, proba[selec]);
@ -565,7 +630,7 @@ function openTrade()
})(resCode);
buttonDn.enabled = controlsPlayer(g_ViewedPlayer);
buttonDn.onpress = (function(resource){
buttonDn.onPress = (function(resource){
return function() {
proba[selec] += Math.min(STEP, proba[resource]);
proba[resource] -= Math.min(STEP, proba[resource]);

View File

@ -515,9 +515,6 @@ function handleNotifications()
function updateDiplomacy()
{
updatePlayerData();
if (g_IsDiplomacyOpen)
openDiplomacy();
}
/**

View File

@ -847,6 +847,8 @@ function updateGUIObjects()
if (battleState)
global.music.setState(global.music.states[battleState]);
}
updateDiplomacyPanel();
}
function onReplayFinished()

View File

@ -1,12 +1,21 @@
function VisionSharing() {}
VisionSharing.prototype.Schema =
"<empty/>";
"<element name='Bribable'>" +
"<data type='boolean'/>" +
"</element>" +
"<optional>" +
"<element name='Duration' a:help='Duration (in second) of the vision sharing for spies'>" +
"<ref name='positiveDecimal'/>" +
"</element>" +
"</optional>";
VisionSharing.prototype.Init = function()
{
this.activated = false;
this.shared = new Set();
this.shared = undefined;
this.spyId = 0;
this.spies = undefined;
};
/**
@ -20,7 +29,7 @@ VisionSharing.prototype.Activate = function()
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership || cmpOwnership.GetOwner() <= 0)
return;
this.shared.add(cmpOwnership.GetOwner());
this.shared = new Set([cmpOwnership.GetOwner()]);
Engine.PostMessage(this.entity, MT_VisionSharingChanged,
{ "entity": this.entity, "player": cmpOwnership.GetOwner(), "add": true });
this.activated = true;
@ -56,6 +65,12 @@ VisionSharing.prototype.CheckVisionSharings = function()
}
}
}
// vision sharing due to spies
if (this.spies)
for (let spy of this.spies.values())
if (spy > 0 && spy != owner)
shared.add(spy);
}
if (!this.activated)
@ -73,9 +88,9 @@ VisionSharing.prototype.CheckVisionSharings = function()
this.shared = shared;
};
VisionSharing.prototype.OnDiplomacyChanged = function(msg)
VisionSharing.prototype.IsBribable = function()
{
this.CheckVisionSharings();
return this.template.Bribable == "true";
};
VisionSharing.prototype.OnGarrisonedUnitsChanged = function(msg)
@ -89,4 +104,69 @@ VisionSharing.prototype.OnOwnershipChanged = function(msg)
this.CheckVisionSharings();
};
VisionSharing.prototype.AddSpy = function(player, timeLength)
{
if (!this.IsBribable())
return;
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership || cmpOwnership.GetOwner() == player || player <= 0)
return;
let cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager);
if (!cmpTechnologyManager || !cmpTechnologyManager.CanProduce("special/spy"))
return;
let template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate("special/spy");
let costs = {};
for (let res in template.Cost.Resources)
costs[res] = Math.floor(ApplyValueModificationsToTemplate("Cost/Resources/"+res, +template.Cost.Resources[res], player, template));
let cmpPlayer = QueryPlayerIDInterface(player);
if (!cmpPlayer || !cmpPlayer.TrySubtractResources(costs))
return;
// If no duration given, take it from the spy template and scale it with the ent vision
// When no duration argument nor in spy template, it is a permanent spy
let duration = timeLength;
if (!duration && template.VisionSharing && template.VisionSharing.Duration)
{
duration = ApplyValueModificationsToTemplate("VisionSharing/Duration", +template.VisionSharing.Duration, player, template);
let cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
if (cmpVision)
duration *= 60 / Math.max(30, cmpVision.GetRange());
}
if (!this.spies)
this.spies = new Map();
this.spies.set(++this.spyId, player);
if (duration)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.SetTimeout(this.entity, IID_VisionSharing, "RemoveSpy", duration * 1000, { "id": this.spyId });
}
this.Activate();
this.CheckVisionSharings();
return this.spyId;
};
VisionSharing.prototype.RemoveSpy = function(data)
{
this.spies.delete(data.id);
this.CheckVisionSharings();
};
/**
* Returns true if this entity share its vision with player
*/
VisionSharing.prototype.ShareVisionWith = function(player)
{
if (this.activated)
return this.shared.has(player);
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
return cmpOwnership && cmpOwnership.GetOwner() == player;
};
Engine.RegisterComponentType(IID_VisionSharing, "VisionSharing", VisionSharing);

View File

@ -0,0 +1,10 @@
{
"genericName": "Espionage",
"description": "Merchants' first goal was trading, but they also gathered information about the countries they crossed.",
"cost": { "food": 500, "wood": 500, "stone": 300, "metal": 300 },
"requirements": { "tech": "phase_city" },
"icon": "spy_trader.png",
"researchTime": 80,
"tooltip": "Allows to bribe other players' units to share their vision.",
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
}

View File

@ -748,6 +748,25 @@ var g_Commands = {
cmpAIInterface.PushEvent("AttackRequest", cmd);
},
"spy-request": function(player, cmd, data)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let ents = cmpRangeManager.GetEntitiesByPlayer(cmd.player).filter(ent => {
let cmpVisionSharing = Engine.QueryInterface(ent, IID_VisionSharing);
return cmpVisionSharing && cmpVisionSharing.IsBribable() && !cmpVisionSharing.ShareVisionWith(player);
});
let ent = pickRandom(ents);
if (ent)
Engine.QueryInterface(ent, IID_VisionSharing).AddSpy(cmd.source);
else
Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).PushNotification({
"type": "text",
"players": [player],
"message": markForTranslation("There are no bribable units"),
"translateMessage": true
});
},
"dialog-answer": function(player, cmd, data)
{
// Currently nothing. Triggers can read it anyway, and send this

View File

@ -121,6 +121,12 @@ function LoadPlayerSettings(settings, newPlayers)
if (disabledTemplates.length)
cmpPlayer.SetDisabledTemplates(disabledTemplates);
if (settings.DisableSpies)
{
cmpPlayer.AddDisabledTechnology("unlock_spies");
cmpPlayer.AddDisabledTemplate("special/spy");
}
// If diplomacy explicitly defined, use that; otherwise use teams
if (getSetting(playerData, playerDefaults, i, "Diplomacy") !== undefined)
cmpPlayer.SetDiplomacy(getSetting(playerData, playerDefaults, i, "Diplomacy"));

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity>
<Cost>
<Population>0</Population>
<PopulationBonus>0</PopulationBonus>
<BuildTime>0</BuildTime>
<Resources>
<food>0</food>
<wood>0</wood>
<stone>0</stone>
<metal>900</metal>
</Resources>
</Cost>
<Identity>
<Civ>gaia</Civ>
<Classes datatype="tokens">Spy</Classes>
<GenericName>Spy</GenericName>
<RequiredTechnology>unlock_spies</RequiredTechnology>
</Identity>
<VisionSharing>
<Bribable>false</Bribable>
<Duration>15</Duration>
</VisionSharing>
</Entity>

View File

@ -140,7 +140,9 @@
<Vision>
<Range>40</Range>
</Vision>
<VisionSharing/>
<VisionSharing>
<Bribable>false</Bribable>
</VisionSharing>
<VisualActor>
<ConstructionPreview/>
<SilhouetteDisplay>false</SilhouetteDisplay>

View File

@ -48,6 +48,7 @@
trade_gain_01
trade_gain_02
trade_commercial_treaty
unlock_spies
</Technologies>
<Entities datatype="tokens">
units/{civ}_support_trader

View File

@ -124,6 +124,9 @@
<Vision>
<Range>12</Range>
</Vision>
<VisionSharing>
<Bribable>false</Bribable>
</VisionSharing>
<VisualActor>
<SilhouetteDisplay>true</SilhouetteDisplay>
<SilhouetteOccluder>false</SilhouetteOccluder>

View File

@ -26,7 +26,7 @@
<Identity>
<GenericName>Merchantman</GenericName>
<Tooltip>Trade between docks. Garrison a Trader aboard for additional profit (+20% for each garrisoned). Gather profitable aquatic treasures.</Tooltip>
<VisibleClasses datatype="tokens">Trader</VisibleClasses>
<VisibleClasses datatype="tokens">Trader Bribable</VisibleClasses>
<RequiredTechnology>phase_town</RequiredTechnology>
<Formations disable=""/>
</Identity>
@ -63,4 +63,7 @@
<Vision>
<Range>50</Range>
</Vision>
<VisionSharing>
<Bribable>true</Bribable>
</VisionSharing>
</Entity>

View File

@ -12,7 +12,7 @@
</Health>
<Identity>
<Classes datatype="tokens">-ConquestCritical</Classes>
<VisibleClasses datatype="tokens">Trader</VisibleClasses>
<VisibleClasses datatype="tokens">Trader Bribable</VisibleClasses>
<GenericName>Trader</GenericName>
<History>Trade was a very important part of ancient civilization - effective trading and control of trade routes equaled wealth. Trade took place by many forms from foot to caravans to merchant ships. One of the most notorious examples of the power of trade was the Silk Road.</History>
<Tooltip>Trade resources between your own markets and those of your allies.</Tooltip>
@ -50,4 +50,7 @@
<Vision>
<Range>60</Range>
</Vision>
<VisionSharing>
<Bribable>true</Bribable>
</VisionSharing>
</Entity>