From 9bd26db3537d134e200c136e1bde07fc56f24df3 Mon Sep 17 00:00:00 2001
From: Marko Lindqvist <cazfi74@gmail.com>
Date: Sat, 14 Sep 2024 18:41:37 +0300
Subject: [PATCH 36/36] Add Stack Bribe action type
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Requested by Zoltán Zarkov

See RM #803

Signed-off-by: Marko Lindqvist <cazfi74@gmail.com>
---
 ai/default/aidiplomat.c              |   2 +-
 ai/default/daicity.c                 |   1 +
 ai/default/daidiplomacy.c            |   1 +
 client/gui-gtk-3.22/action_dialog.c  |  74 ++++++++-
 client/gui-gtk-4.0/action_dialog.c   |  71 ++++++++-
 client/gui-gtk-5.0/action_dialog.c   |  71 ++++++++-
 client/gui-qt/dialogs.cpp            |  77 ++++++++-
 client/gui-qt/menu.cpp               |   2 +-
 client/gui-sdl2/action_dialog.c      | 230 +++++++++++++++++++++++++--
 client/gui-sdl3/action_dialog.c      | 229 ++++++++++++++++++++++++--
 client/gui-stub/dialogs.c            |  14 +-
 client/include/dialogs_g.h           |   4 +-
 client/packhand.c                    |  28 +++-
 common/actions.c                     |  21 +++
 common/actions.h                     |  15 +-
 common/actres.c                      |   8 +
 common/fc_types.h                    |   2 +-
 common/unit.c                        |  20 +++
 common/unit.h                        |   2 +
 doc/README.actions                   |   8 +
 gen_headers/enums/actions_enums.def  |   1 +
 gen_headers/enums/fc_types_enums.def |   1 +
 server/advisors/advdata.c            |   5 +-
 server/diplomats.c                   | 123 +++++++++++++-
 server/diplomats.h                   |  11 +-
 server/ruleset/ruleload.c            |   7 +-
 server/unithand.c                    |  34 +++-
 tools/ruleutil/rulesave.c            |   9 +-
 28 files changed, 984 insertions(+), 87 deletions(-)

diff --git a/ai/default/aidiplomat.c b/ai/default/aidiplomat.c
index 913c74c2a4..dcf1f0fcff 100644
--- a/ai/default/aidiplomat.c
+++ b/ai/default/aidiplomat.c
@@ -679,7 +679,7 @@ static bool dai_diplomat_bribe_nearby(struct ai_type *ait,
       unit_do_action(pplayer, punit->id,
                      pvictim->id, -1, "",
                      ACTION_SPY_BRIBE_UNIT);
-      /* autoattack might kill us as we move in */
+      /* Autoattack might kill us as we move in */
       if (game_unit_by_number(sanity) && punit->moves_left > 0) {
         return TRUE;
       } else {
diff --git a/ai/default/daicity.c b/ai/default/daicity.c
index 5b09ad4223..c8ede15e77 100644
--- a/ai/default/daicity.c
+++ b/ai/default/daicity.c
@@ -1280,6 +1280,7 @@ static int action_target_neg_util(action_id act_id,
   case ACTRES_PARADROP_CONQUER:
     /* Against the tile so potential city effects are overlooked for now. */
   case ACTRES_SPY_BRIBE_UNIT:
+  case ACTRES_SPY_BRIBE_STACK:
   case ACTRES_SPY_SABOTAGE_UNIT:
   case ACTRES_SPY_ATTACK:
   case ACTRES_EXPEL_UNIT:
diff --git a/ai/default/daidiplomacy.c b/ai/default/daidiplomacy.c
index 549267b636..7783ecbe06 100644
--- a/ai/default/daidiplomacy.c
+++ b/ai/default/daidiplomacy.c
@@ -2035,6 +2035,7 @@ void dai_incident(struct ai_type *ait, enum incident_type type,
       dai_incident_simple(receiver, violator, victim, scope, 3);
       break;
     case ACTRES_SPY_BRIBE_UNIT:
+    case ACTRES_SPY_BRIBE_STACK:
     case ACTRES_CAPTURE_UNITS:
     case ACTRES_BOMBARD:
     case ACTRES_ATTACK:
diff --git a/client/gui-gtk-3.22/action_dialog.c b/client/gui-gtk-3.22/action_dialog.c
index ac228f6866..84cca394b5 100644
--- a/client/gui-gtk-3.22/action_dialog.c
+++ b/client/gui-gtk-3.22/action_dialog.c
@@ -453,9 +453,9 @@ static void upgrade_callback(GtkWidget *w, gpointer data)
 }
 
 /**********************************************************************//**
-  User responded to bribe dialog
+  User responded to bribe unit dialog
 **************************************************************************/
-static void bribe_response(GtkWidget *w, gint response, gpointer data)
+static void bribe_unit_response(GtkWidget *w, gint response, gpointer data)
 {
   struct action_data *args = (struct action_data *)data;
 
@@ -471,11 +471,30 @@ static void bribe_response(GtkWidget *w, gint response, gpointer data)
   diplomat_queue_handle_secondary();
 }
 
+/**********************************************************************//**
+  User responded to bribe stack dialog
+**************************************************************************/
+static void bribe_stack_response(GtkWidget *w, gint response, gpointer data)
+{
+  struct action_data *args = (struct action_data *)data;
+
+  if (response == GTK_RESPONSE_YES) {
+    request_do_action(args->act_id, args->actor_unit_id,
+                      args->target_tile_id, 0, "");
+  }
+
+  gtk_widget_destroy(w);
+  free(args);
+
+  /* The user have answered the follow up question. Move on. */
+  diplomat_queue_handle_secondary();
+}
+
 /**********************************************************************//**
   Popup unit bribe dialog
 **************************************************************************/
-void popup_bribe_dialog(struct unit *actor, struct unit *punit, int cost,
-                        const struct action *paction)
+void popup_bribe_unit_dialog(struct unit *actor, struct unit *punit, int cost,
+                             const struct action *paction)
 {
   GtkWidget *shell;
   char buf[1024];
@@ -503,13 +522,52 @@ void popup_bribe_dialog(struct unit *actor, struct unit *punit, int cost,
     setup_dialog(shell, toplevel);
   }
   gtk_window_present(GTK_WINDOW(shell));
-  
-  g_signal_connect(shell, "response", G_CALLBACK(bribe_response),
+
+  g_signal_connect(shell, "response", G_CALLBACK(bribe_unit_response),
                    act_data(paction->id, actor->id,
                             0, punit->id, 0,
                             0, 0, 0));
 }
 
+/**********************************************************************//**
+  Popup stack bribe dialog
+**************************************************************************/
+void popup_bribe_stack_dialog(struct unit *actor, struct tile *ptile, int cost,
+                              const struct action *paction)
+{
+  GtkWidget *shell;
+  char buf[1024];
+
+  fc_snprintf(buf, ARRAY_SIZE(buf), PL_("Treasury contains %d gold.",
+                                        "Treasury contains %d gold.",
+                                        client_player()->economic.gold),
+              client_player()->economic.gold);
+
+  if (cost <= client_player()->economic.gold) {
+    shell = gtk_message_dialog_new(NULL, 0,
+      GTK_MESSAGE_QUESTION, GTK_BUTTONS_YES_NO,
+      /* TRANS: %s is pre-pluralised "Treasury contains %d gold." */
+      PL_("Bribe unit stack for %d gold?\n%s",
+          "Bribe unit stack for %d gold?\n%s", cost), cost, buf);
+    gtk_window_set_title(GTK_WINDOW(shell), _("Bribe Enemy Unit"));
+    setup_dialog(shell, toplevel);
+  } else {
+    shell = gtk_message_dialog_new(NULL, 0,
+      GTK_MESSAGE_INFO, GTK_BUTTONS_CLOSE,
+      /* TRANS: %s is pre-pluralised "Treasury contains %d gold." */
+      PL_("Bribing units costs %d gold.\n%s",
+          "Bribing units costs %d gold.\n%s", cost), cost, buf);
+    gtk_window_set_title(GTK_WINDOW(shell), _("Traitors Demand Too Much!"));
+    setup_dialog(shell, toplevel);
+  }
+  gtk_window_present(GTK_WINDOW(shell));
+
+  g_signal_connect(shell, "response", G_CALLBACK(bribe_stack_response),
+                   act_data(paction->id, actor->id,
+                            0, 0, ptile->index,
+                            0, 0, 0));
+}
+
 /**********************************************************************//**
   User responded to steal advances dialog
 **************************************************************************/
@@ -1251,10 +1309,10 @@ static const GCallback af_map[ACTION_COUNT] = {
 
   /* Unit acting against a unit target. */
   [ACTION_SPY_BRIBE_UNIT] = (GCallback)request_action_details_callback,
+  [ACTION_SPY_BRIBE_STACK] = (GCallback)request_action_details_callback,
 
   /* Unit acting against all units at a tile. */
-  /* No special callback functions needed for any unit stack targeted
-   * actions. */
+  [ACTION_SPY_BRIBE_STACK] = (GCallback)request_action_details_callback,
 
   /* Unit acting against a tile. */
   [ACTION_FOUND_CITY] = (GCallback)found_city_callback,
diff --git a/client/gui-gtk-4.0/action_dialog.c b/client/gui-gtk-4.0/action_dialog.c
index 22da3691d3..af12630eac 100644
--- a/client/gui-gtk-4.0/action_dialog.c
+++ b/client/gui-gtk-4.0/action_dialog.c
@@ -452,9 +452,9 @@ static void upgrade_callback(GtkWidget *w, gpointer data)
 }
 
 /**********************************************************************//**
-  User responded to bribe dialog
+  User responded to unit bribe dialog
 **************************************************************************/
-static void bribe_response(GtkWidget *w, gint response, gpointer data)
+static void bribe_unit_response(GtkWidget *w, gint response, gpointer data)
 {
   struct action_data *args = (struct action_data *)data;
 
@@ -470,11 +470,30 @@ static void bribe_response(GtkWidget *w, gint response, gpointer data)
   diplomat_queue_handle_secondary();
 }
 
+/**********************************************************************//**
+  User responded to stack bribe dialog
+**************************************************************************/
+static void bribe_stack_response(GtkWidget *w, gint response, gpointer data)
+{
+  struct action_data *args = (struct action_data *)data;
+
+  if (response == GTK_RESPONSE_YES) {
+    request_do_action(args->act_id, args->actor_unit_id,
+                      args->target_tile_id, 0, "");
+  }
+
+  gtk_window_destroy(GTK_WINDOW(w));
+  free(args);
+
+  /* The user have answered the follow up question. Move on. */
+  diplomat_queue_handle_secondary();
+}
+
 /**********************************************************************//**
   Popup unit bribe dialog
 **************************************************************************/
-void popup_bribe_dialog(struct unit *actor, struct unit *punit, int cost,
-                        const struct action *paction)
+void popup_bribe_unit_dialog(struct unit *actor, struct unit *punit, int cost,
+                             const struct action *paction)
 {
   GtkWidget *shell;
   char buf[1024];
@@ -503,12 +522,51 @@ void popup_bribe_dialog(struct unit *actor, struct unit *punit, int cost,
   }
   gtk_window_present(GTK_WINDOW(shell));
 
-  g_signal_connect(shell, "response", G_CALLBACK(bribe_response),
+  g_signal_connect(shell, "response", G_CALLBACK(bribe_unit_response),
                    act_data(paction->id, actor->id,
                             0, punit->id, 0,
                             0, 0, 0));
 }
 
+/**********************************************************************//**
+  Popup stack bribe dialog
+**************************************************************************/
+void popup_bribe_stack_dialog(struct unit *actor, struct tile *ptile, int cost,
+                              const struct action *paction)
+{
+  GtkWidget *shell;
+  char buf[1024];
+
+  fc_snprintf(buf, ARRAY_SIZE(buf), PL_("Treasury contains %d gold.",
+                                        "Treasury contains %d gold.",
+                                        client_player()->economic.gold),
+              client_player()->economic.gold);
+
+  if (cost <= client_player()->economic.gold) {
+    shell = gtk_message_dialog_new(NULL, 0,
+      GTK_MESSAGE_QUESTION, GTK_BUTTONS_YES_NO,
+      /* TRANS: %s is pre-pluralised "Treasury contains %d gold." */
+      PL_("Bribe unit stack for %d gold?\n%s",
+          "Bribe unit stack for %d gold?\n%s", cost), cost, buf);
+    gtk_window_set_title(GTK_WINDOW(shell), _("Bribe Enemy Unit"));
+    setup_dialog(shell, toplevel);
+  } else {
+    shell = gtk_message_dialog_new(NULL, 0,
+      GTK_MESSAGE_INFO, GTK_BUTTONS_CLOSE,
+      /* TRANS: %s is pre-pluralised "Treasury contains %d gold." */
+      PL_("Bribing units costs %d gold.\n%s",
+          "Bribing units costs %d gold.\n%s", cost), cost, buf);
+    gtk_window_set_title(GTK_WINDOW(shell), _("Traitors Demand Too Much!"));
+    setup_dialog(shell, toplevel);
+  }
+  gtk_window_present(GTK_WINDOW(shell));
+
+  g_signal_connect(shell, "response", G_CALLBACK(bribe_stack_response),
+                   act_data(paction->id, actor->id,
+                            0, 0, ptile->index,
+                            0, 0, 0));
+}
+
 /**********************************************************************//**
   User responded to steal advances dialog
 **************************************************************************/
@@ -1248,8 +1306,7 @@ static const GCallback af_map[ACTION_COUNT] = {
   [ACTION_SPY_BRIBE_UNIT] = (GCallback)request_action_details_callback,
 
   /* Unit acting against all units at a tile. */
-  /* No special callback functions needed for any unit stack targeted
-   * actions. */
+  [ACTION_SPY_BRIBE_STACK] = (GCallback)request_action_details_callback,
 
   /* Unit acting against a tile. */
   [ACTION_FOUND_CITY] = (GCallback)found_city_callback,
diff --git a/client/gui-gtk-5.0/action_dialog.c b/client/gui-gtk-5.0/action_dialog.c
index ee3f97a5bc..61c59e5dd7 100644
--- a/client/gui-gtk-5.0/action_dialog.c
+++ b/client/gui-gtk-5.0/action_dialog.c
@@ -452,9 +452,9 @@ static void upgrade_callback(GtkWidget *w, gpointer data)
 }
 
 /**********************************************************************//**
-  User responded to bribe dialog
+  User responded to bribe unit dialog
 **************************************************************************/
-static void bribe_response(GtkWidget *w, gint response, gpointer data)
+static void bribe_unit_response(GtkWidget *w, gint response, gpointer data)
 {
   struct action_data *args = (struct action_data *)data;
 
@@ -470,11 +470,30 @@ static void bribe_response(GtkWidget *w, gint response, gpointer data)
   diplomat_queue_handle_secondary();
 }
 
+/**********************************************************************//**
+  User responded to bribe stack dialog
+**************************************************************************/
+static void bribe_stack_response(GtkWidget *w, gint response, gpointer data)
+{
+  struct action_data *args = (struct action_data *)data;
+
+  if (response == GTK_RESPONSE_YES) {
+    request_do_action(args->act_id, args->actor_unit_id,
+                      args->target_tile_id, 0, "");
+  }
+
+  gtk_window_destroy(GTK_WINDOW(w));
+  free(args);
+
+  /* The user have answered the follow up question. Move on. */
+  diplomat_queue_handle_secondary();
+}
+
 /**********************************************************************//**
   Popup unit bribe dialog
 **************************************************************************/
-void popup_bribe_dialog(struct unit *actor, struct unit *punit, int cost,
-                        const struct action *paction)
+void popup_bribe_unit_dialog(struct unit *actor, struct unit *punit, int cost,
+                             const struct action *paction)
 {
   GtkWidget *shell;
   char buf[1024];
@@ -503,12 +522,51 @@ void popup_bribe_dialog(struct unit *actor, struct unit *punit, int cost,
   }
   gtk_window_present(GTK_WINDOW(shell));
 
-  g_signal_connect(shell, "response", G_CALLBACK(bribe_response),
+  g_signal_connect(shell, "response", G_CALLBACK(bribe_unit_response),
                    act_data(paction->id, actor->id,
                             0, punit->id, 0,
                             0, 0, 0));
 }
 
+/**********************************************************************//**
+  Popup stack bribe dialog
+**************************************************************************/
+void popup_bribe_stack_dialog(struct unit *actor, struct tile *ptile, int cost,
+                             const struct action *paction)
+{
+  GtkWidget *shell;
+  char buf[1024];
+
+  fc_snprintf(buf, ARRAY_SIZE(buf), PL_("Treasury contains %d gold.",
+                                        "Treasury contains %d gold.",
+                                        client_player()->economic.gold),
+              client_player()->economic.gold);
+
+  if (cost <= client_player()->economic.gold) {
+    shell = gtk_message_dialog_new(NULL, 0,
+      GTK_MESSAGE_QUESTION, GTK_BUTTONS_YES_NO,
+      /* TRANS: %s is pre-pluralised "Treasury contains %d gold." */
+      PL_("Bribe unit stack for %d gold?\n%s",
+          "Bribe unit stack for %d gold?\n%s", cost), cost, buf);
+    gtk_window_set_title(GTK_WINDOW(shell), _("Bribe Enemy Stack"));
+    setup_dialog(shell, toplevel);
+  } else {
+    shell = gtk_message_dialog_new(NULL, 0,
+      GTK_MESSAGE_INFO, GTK_BUTTONS_CLOSE,
+      /* TRANS: %s is pre-pluralised "Treasury contains %d gold." */
+      PL_("Bribing units costs %d gold.\n%s",
+          "Bribing units costs %d gold.\n%s", cost), cost, buf);
+    gtk_window_set_title(GTK_WINDOW(shell), _("Traitors Demand Too Much!"));
+    setup_dialog(shell, toplevel);
+  }
+  gtk_window_present(GTK_WINDOW(shell));
+
+  g_signal_connect(shell, "response", G_CALLBACK(bribe_stack_response),
+                   act_data(paction->id, actor->id,
+                            0, 0, ptile->index,
+                            0, 0, 0));
+}
+
 /**********************************************************************//**
   User responded to steal advances dialog
 **************************************************************************/
@@ -1248,8 +1306,7 @@ static const GCallback af_map[ACTION_COUNT] = {
   [ACTION_SPY_BRIBE_UNIT] = (GCallback)request_action_details_callback,
 
   /* Unit acting against all units at a tile. */
-  /* No special callback functions needed for any unit stack targeted
-   * actions. */
+  [ACTION_SPY_BRIBE_STACK] = (GCallback)request_action_details_callback,
 
   /* Unit acting against a tile. */
   [ACTION_FOUND_CITY] = (GCallback)found_city_callback,
diff --git a/client/gui-qt/dialogs.cpp b/client/gui-qt/dialogs.cpp
index 99a3971eca..48b6e9dbd5 100644
--- a/client/gui-qt/dialogs.cpp
+++ b/client/gui-qt/dialogs.cpp
@@ -110,7 +110,8 @@ static void spy_investigate(QVariant data1, QVariant data2);
 static void diplomat_investigate(QVariant data1, QVariant data2);
 static void diplomat_sabotage(QVariant data1, QVariant data2);
 static void diplomat_sabotage_esc(QVariant data1, QVariant data2);
-static void diplomat_bribe(QVariant data1, QVariant data2);
+static void diplomat_bribe_unit(QVariant data1, QVariant data2);
+static void diplomat_bribe_stack(QVariant data1, QVariant data2);
 static void caravan_marketplace(QVariant data1, QVariant data2);
 static void caravan_establish_trade(QVariant data1, QVariant data2);
 static void caravan_help_build(QVariant data1, QVariant data2);
@@ -249,7 +250,7 @@ static const QHash<action_id, pfcn_void> af_map_init(void)
   action_function[ACTION_NUKE_CITY] = nuke_city;
 
   // Unit acting against a unit target.
-  action_function[ACTION_SPY_BRIBE_UNIT] = diplomat_bribe;
+  action_function[ACTION_SPY_BRIBE_UNIT] = diplomat_bribe_unit;
   action_function[ACTION_SPY_SABOTAGE_UNIT] = spy_sabotage_unit;
   action_function[ACTION_SPY_SABOTAGE_UNIT_ESC] = spy_sabotage_unit_esc;
   action_function[ACTION_EXPEL_UNIT] = expel_unit;
@@ -269,6 +270,7 @@ static const QHash<action_id, pfcn_void> af_map_init(void)
   action_function[ACTION_TRANSPORT_EMBARK4] = transport_embark4;
 
   // Unit acting against all units at a tile.
+  action_function[ACTION_SPY_BRIBE_STACK] = diplomat_bribe_stack;
   action_function[ACTION_CAPTURE_UNITS] = capture_units;
   action_function[ACTION_BOMBARD] = bombard;
   action_function[ACTION_BOMBARD2] = bombard2;
@@ -2435,7 +2437,7 @@ static void homeless(QVariant data1, QVariant data2)
 /***********************************************************************//**
   Action bribe unit for choice dialog
 ***************************************************************************/
-static void diplomat_bribe(QVariant data1, QVariant data2)
+static void diplomat_bribe_unit(QVariant data1, QVariant data2)
 {
   int diplomat_id = data1.toInt();
   int diplomat_target_id = data2.toInt();
@@ -2450,6 +2452,23 @@ static void diplomat_bribe(QVariant data1, QVariant data2)
   }
 }
 
+/***********************************************************************//**
+  Action bribe stack for choice dialog
+***************************************************************************/
+static void diplomat_bribe_stack(QVariant data1, QVariant data2)
+{
+  int diplomat_id = data1.toInt();
+  int diplomat_target_id = data2.toInt();
+
+  if (game_unit_by_number(diplomat_id) != nullptr) {
+    // Wait for the server's reply before moving on to the next queued diplomat.
+    is_more_user_input_needed = TRUE;
+
+    request_action_details(ACTION_SPY_BRIBE_STACK, diplomat_id,
+                           diplomat_target_id);
+  }
+}
+
 /***********************************************************************//**
   Action sabotage unit for choice dialog
 ***************************************************************************/
@@ -3713,8 +3732,8 @@ void popup_incite_dialog(struct unit *actor, struct city *tcity, int cost,
   Popup a dialog asking a diplomatic unit if it wishes to bribe the
   given enemy unit.
 ***************************************************************************/
-void popup_bribe_dialog(struct unit *actor, struct unit *tunit, int cost,
-                        const struct action *paction)
+void popup_bribe_unit_dialog(struct unit *actor, struct unit *tunit, int cost,
+                             const struct action *paction)
 {
   hud_message_box *ask = new hud_message_box(gui()->central_wdg);
   char buf[1024];
@@ -3757,6 +3776,54 @@ void popup_bribe_dialog(struct unit *actor, struct unit *tunit, int cost,
   diplomat_queue_handle_secondary(diplomat_id);
 }
 
+/***********************************************************************//**
+  Popup a dialog asking a diplomatic unit if it wishes to bribe the
+  given enemy unit stack.
+***************************************************************************/
+void popup_bribe_stack_dialog(struct unit *actor, struct tile *ttile, int cost,
+                             const struct action *paction)
+{
+  hud_message_box *ask = new hud_message_box(gui()->central_wdg);
+  char buf[1024];
+  char buf2[1024];
+  int diplomat_id = actor->id;
+  int diplomat_target_id = ttile->index;
+  const int act_id = paction->id;
+
+  // Should be set before sending request to the server.
+  fc_assert(is_more_user_input_needed);
+
+  fc_snprintf(buf, ARRAY_SIZE(buf), PL_("Treasury contains %d gold.",
+                                        "Treasury contains %d gold.",
+                                        client_player()->economic.gold),
+              client_player()->economic.gold);
+
+  if (cost <= client_player()->economic.gold) {
+    fc_snprintf(buf2, ARRAY_SIZE(buf2), PL_("Bribe unit stack for %d gold?\n%s",
+                                            "Bribe unit stack for %d gold?\n%s",
+                                            cost), cost, buf);
+    ask->set_text_title(buf2, _("Bribe Enemy Stack"));
+    ask->setStandardButtons(QMessageBox::Cancel | QMessageBox::Ok);
+    ask->setDefaultButton(QMessageBox::Cancel);
+    ask->setAttribute(Qt::WA_DeleteOnClose);
+    QObject::connect(ask, &hud_message_box::accepted, [=]() {
+      request_do_action(act_id, diplomat_id, diplomat_target_id, 0, "");
+      diplomat_queue_handle_secondary(diplomat_id);
+    });
+    ask->show();
+    return;
+  } else {
+    fc_snprintf(buf2, ARRAY_SIZE(buf2),
+                PL_("Bribing the unit stack costs %d gold.\n%s",
+                    "Bribing the unit stack costs %d gold.\n%s", cost), cost, buf);
+    ask->set_text_title(buf2, _("Traitors Demand Too Much!"));
+    ask->setAttribute(Qt::WA_DeleteOnClose);
+    ask->show();
+  }
+
+  diplomat_queue_handle_secondary(diplomat_id);
+}
+
 /***********************************************************************//**
   Action pillage for choice dialog
 ***************************************************************************/
diff --git a/client/gui-qt/menu.cpp b/client/gui-qt/menu.cpp
index ea26cd7127..c0136ec2db 100644
--- a/client/gui-qt/menu.cpp
+++ b/client/gui-qt/menu.cpp
@@ -1422,7 +1422,7 @@ void mr_menu::setup_menus()
   action_vs_unit->addAction(act);
   connect(act, &QAction::triggered, this, &mr_menu::slot_action_vs_unit);
 
-  act = action_unit_menu->addAction(_("Bribe"));
+  act = action_unit_menu->addAction(_("Bribe Unit"));
   act->setCheckable(true);
   act->setChecked(false);
   act->setData(ACTION_SPY_BRIBE_UNIT);
diff --git a/client/gui-sdl2/action_dialog.c b/client/gui-sdl2/action_dialog.c
index b275666955..76f4709496 100644
--- a/client/gui-sdl2/action_dialog.c
+++ b/client/gui-sdl2/action_dialog.c
@@ -647,7 +647,7 @@ static int act_sel_wait_callback(struct widget *pwidget)
 /**********************************************************************//**
   Ask the server how much the bribe costs
 **************************************************************************/
-static int diplomat_bribe_callback(struct widget *pwidget)
+static int diplomat_bribe_unit_callback(struct widget *pwidget)
 {
   if (PRESSED_EVENT(main_data.event)) {
     if (NULL != game_unit_by_number(diplomat_dlg->actor_unit_id)
@@ -666,6 +666,26 @@ static int diplomat_bribe_callback(struct widget *pwidget)
   return -1;
 }
 
+/**********************************************************************//**
+  Ask the server how much the bribe costs
+**************************************************************************/
+static int diplomat_bribe_stack_callback(struct widget *pwidget)
+{
+  if (PRESSED_EVENT(main_data.event)) {
+    if (game_unit_by_number(diplomat_dlg->actor_unit_id) != nullptr) {
+      request_action_details(ACTION_SPY_BRIBE_STACK,
+                             diplomat_dlg->actor_unit_id,
+                             diplomat_dlg->target_ids[ATK_STACK]);
+      is_more_user_input_needed = TRUE;
+      popdown_diplomat_dialog();
+    } else {
+      popdown_diplomat_dialog();
+    }
+  }
+
+  return -1;
+}
+
 /**********************************************************************//**
   User clicked "Found City"
 **************************************************************************/
@@ -852,11 +872,10 @@ static const act_func af_map[ACTION_COUNT] = {
   [ACTION_STRIKE_BUILDING] = spy_strike_bld_request,
 
   /* Unit acting against a unit target. */
-  [ACTION_SPY_BRIBE_UNIT] = diplomat_bribe_callback,
+  [ACTION_SPY_BRIBE_UNIT] = diplomat_bribe_unit_callback,
 
   /* Unit acting against all units at a tile. */
-  /* No special callback functions needed for any unit stack targeted
-   * actions. */
+  [ACTION_SPY_BRIBE_STACK] = diplomat_bribe_stack_callback,
 
   /* Unit acting against a tile. */
   [ACTION_FOUND_CITY] = found_city_callback,
@@ -1719,7 +1738,7 @@ void popup_incite_dialog(struct unit *actor, struct city *pcity, int cost,
     area.h += buf->size.h;
     /* ------- */
     create_active_iconlabel(buf, pwindow->dst, pstr,
-                            _("No") , exit_incite_dlg_callback);
+                            _("No"), exit_incite_dlg_callback);
 
     set_wstate(buf, FC_WS_NORMAL);
     buf->key = SDLK_ESCAPE;
@@ -1821,9 +1840,9 @@ static int bribe_dlg_window_callback(struct widget *pwindow)
 }
 
 /**********************************************************************//**
-  User confirmed bribe.
+  User confirmed unit bribe.
 **************************************************************************/
-static int diplomat_bribe_yes_callback(struct widget *pwidget)
+static int diplomat_bribe_unit_yes_callback(struct widget *pwidget)
 {
   if (PRESSED_EVENT(main_data.event)) {
     if (NULL != game_unit_by_number(bribe_dlg->actor_unit_id)
@@ -1837,6 +1856,22 @@ static int diplomat_bribe_yes_callback(struct widget *pwidget)
   return -1;
 }
 
+/**********************************************************************//**
+  User confirmed stack bribe.
+**************************************************************************/
+static int diplomat_bribe_stack_yes_callback(struct widget *pwidget)
+{
+  if (PRESSED_EVENT(main_data.event)) {
+    if (NULL != game_unit_by_number(bribe_dlg->actor_unit_id)) {
+      request_do_action(bribe_dlg->act_id, bribe_dlg->actor_unit_id,
+                        bribe_dlg->target_id, 0, "");
+    }
+    popdown_bribe_dialog();
+  }
+
+  return -1;
+}
+
 /**********************************************************************//**
   Close bribe dialog.
 **************************************************************************/
@@ -1871,8 +1906,8 @@ void popdown_bribe_dialog(void)
   Popup a dialog asking a diplomatic unit if it wishes to bribe the
   given enemy unit.
 **************************************************************************/
-void popup_bribe_dialog(struct unit *actor, struct unit *punit, int cost,
-                        const struct action *paction)
+void popup_bribe_unit_dialog(struct unit *actor, struct unit *punit, int cost,
+                             const struct action *paction)
 {
   struct widget *pwindow = NULL, *buf = NULL;
   utf8_str *pstr;
@@ -1937,7 +1972,7 @@ void popup_bribe_dialog(struct unit *actor, struct unit *punit, int cost,
 
     /*------------*/
     create_active_iconlabel(buf, pwindow->dst, pstr,
-                            _("Yes"), diplomat_bribe_yes_callback);
+                            _("Yes"), diplomat_bribe_unit_yes_callback);
     buf->data.unit = punit;
     set_wstate(buf, FC_WS_NORMAL);
 
@@ -1947,7 +1982,7 @@ void popup_bribe_dialog(struct unit *actor, struct unit *punit, int cost,
     area.h += buf->size.h;
     /* ------- */
     create_active_iconlabel(buf, pwindow->dst, pstr,
-                            _("No") , exit_bribe_dlg_callback);
+                            _("No"), exit_bribe_dlg_callback);
 
     set_wstate(buf, FC_WS_NORMAL);
     buf->key = SDLK_ESCAPE;
@@ -1996,7 +2031,7 @@ void popup_bribe_dialog(struct unit *actor, struct unit *punit, int cost,
   }
   bribe_dlg->pdialog->begin_widget_list = buf;
 
-  /* setup window size and start position */
+  /* Setup window size and start position */
 
   resize_window(pwindow, NULL, NULL,
                 (pwindow->size.w - pwindow->area.w) + area.w,
@@ -2008,11 +2043,11 @@ void popup_bribe_dialog(struct unit *actor, struct unit *punit, int cost,
   put_window_near_map_tile(pwindow, pwindow->size.w, pwindow->size.h,
                            unit_tile(actor));
 
-  /* setup widget size and start position */
+  /* Setup widget size and start position */
   buf = pwindow;
 
   if (exit) {
-    /* exit button */
+    /* Exit button */
     buf = buf->prev;
     buf->size.x = area.x + area.w - buf->size.w - 1;
     buf->size.y = pwindow->size.y + adj_size(2);
@@ -2025,7 +2060,172 @@ void popup_bribe_dialog(struct unit *actor, struct unit *punit, int cost,
         bribe_dlg->pdialog->begin_widget_list, buf);
 
   /* --------------------- */
-  /* redraw */
+  /* Redraw */
+  redraw_group(bribe_dlg->pdialog->begin_widget_list, pwindow, 0);
+
+  widget_flush(pwindow);
+}
+
+
+/**********************************************************************//**
+  Popup a dialog asking a diplomatic unit if it wishes to bribe the
+  given enemy unit.
+**************************************************************************/
+void popup_bribe_stack_dialog(struct unit *actor, struct tile *ptile, int cost,
+                             const struct action *paction)
+{
+  struct widget *pwindow = NULL, *buf = NULL;
+  utf8_str *pstr;
+  char tBuf[255], cbuf[255];
+  bool exit = FALSE;
+  SDL_Rect area;
+
+  if (bribe_dlg) {
+    return;
+  }
+
+  /* Should be set before sending request to the server. */
+  fc_assert(is_more_user_input_needed);
+
+  if (!actor || !unit_can_do_action(actor, paction->id)) {
+    act_sel_done_secondary(actor ? actor->id : IDENTITY_NUMBER_ZERO);
+    return;
+  }
+
+  is_unit_move_blocked = TRUE;
+
+  bribe_dlg = fc_calloc(1, sizeof(struct small_diplomat_dialog));
+  bribe_dlg->act_id = paction->id;
+  bribe_dlg->actor_unit_id = actor->id;
+  bribe_dlg->target_id = ptile->index;
+  bribe_dlg->pdialog = fc_calloc(1, sizeof(struct small_dialog));
+
+  fc_snprintf(tBuf, ARRAY_SIZE(tBuf), PL_("Treasury contains %d gold.",
+                                          "Treasury contains %d gold.",
+                                          client_player()->economic.gold),
+              client_player()->economic.gold);
+
+  /* Window */
+  pstr = create_utf8_from_char_fonto(_("Bribe Enemy Stack"), FONTO_ATTENTION);
+
+  pstr->style |= TTF_STYLE_BOLD;
+
+  pwindow = create_window_skeleton(NULL, pstr, 0);
+
+  pwindow->action = bribe_dlg_window_callback;
+  set_wstate(pwindow, FC_WS_NORMAL);
+
+  add_to_gui_list(ID_BRIBE_DLG_WINDOW, pwindow);
+  bribe_dlg->pdialog->end_widget_list = pwindow;
+
+  area = pwindow->area;
+  area.w = MAX(area.w, adj_size(8));
+  area.h = MAX(area.h, adj_size(2));
+
+  if (cost <= client_player()->economic.gold) {
+    fc_snprintf(cbuf, sizeof(cbuf),
+                /* TRANS: %s is pre-pluralised "Treasury contains %d gold." */
+                PL_("Bribe unit stack for %d gold?\n%s",
+                    "Bribe unit stack for %d gold?\n%s", cost), cost, tBuf);
+
+    create_active_iconlabel(buf, pwindow->dst, pstr, cbuf, NULL);
+
+    add_to_gui_list(ID_LABEL, buf);
+
+    area.w = MAX(area.w, buf->size.w);
+    area.h += buf->size.h;
+
+    /*------------*/
+    create_active_iconlabel(buf, pwindow->dst, pstr,
+                            _("Yes"), diplomat_bribe_stack_yes_callback);
+    buf->data.tile = ptile;
+    set_wstate(buf, FC_WS_NORMAL);
+
+    add_to_gui_list(MAX_ID - actor->id, buf);
+
+    area.w = MAX(area.w, buf->size.w);
+    area.h += buf->size.h;
+    /* ------- */
+    create_active_iconlabel(buf, pwindow->dst, pstr,
+                            _("No"), exit_bribe_dlg_callback);
+
+    set_wstate(buf, FC_WS_NORMAL);
+    buf->key = SDLK_ESCAPE;
+
+    add_to_gui_list(ID_LABEL, buf);
+
+    area.w = MAX(area.w, buf->size.w);
+    area.h += buf->size.h;
+
+  } else {
+    /* Exit button */
+    buf = create_themeicon(current_theme->small_cancel_icon, pwindow->dst,
+                           WF_WIDGET_HAS_INFO_LABEL
+                           | WF_RESTORE_BACKGROUND);
+    buf->info_label = create_utf8_from_char_fonto(_("Close Dialog (Esc)"),
+                                                  FONTO_ATTENTION);
+    area.w += buf->size.w + adj_size(10);
+    buf->action = exit_bribe_dlg_callback;
+    set_wstate(buf, FC_WS_NORMAL);
+    buf->key = SDLK_ESCAPE;
+
+    add_to_gui_list(ID_BRIBE_DLG_EXIT_BUTTON, buf);
+    exit = TRUE;
+    /* --------------- */
+
+    fc_snprintf(cbuf, sizeof(cbuf),
+                /* TRANS: %s is pre-pluralised "Treasury contains %d gold." */
+                PL_("Bribing the unit stack costs %d gold.\n%s",
+                    "Bribing the unit stack costs %d gold.\n%s", cost), cost, tBuf);
+
+    create_active_iconlabel(buf, pwindow->dst, pstr, cbuf, NULL);
+
+    add_to_gui_list(ID_LABEL, buf);
+
+    area.w = MAX(area.w, buf->size.w);
+    area.h += buf->size.h;
+
+    /*------------*/
+    create_active_iconlabel(buf, pwindow->dst, pstr,
+                            _("Traitors Demand Too Much!"), NULL);
+
+    add_to_gui_list(ID_LABEL, buf);
+
+    area.w = MAX(area.w, buf->size.w);
+    area.h += buf->size.h;
+  }
+  bribe_dlg->pdialog->begin_widget_list = buf;
+
+  /* Setup window size and start position */
+
+  resize_window(pwindow, NULL, NULL,
+                (pwindow->size.w - pwindow->area.w) + area.w,
+                (pwindow->size.h - pwindow->area.h) + area.h);
+
+  area = pwindow->area;
+
+  auto_center_on_focus_unit();
+  put_window_near_map_tile(pwindow, pwindow->size.w, pwindow->size.h,
+                           unit_tile(actor));
+
+  /* Setup widget size and start position */
+  buf = pwindow;
+
+  if (exit) {
+    /* Exit button */
+    buf = buf->prev;
+    buf->size.x = area.x + area.w - buf->size.w - 1;
+    buf->size.y = pwindow->size.y + adj_size(2);
+  }
+
+  buf = buf->prev;
+  setup_vertical_widgets_position(1,
+        area.x,
+        area.y + 1, area.w, 0,
+        bribe_dlg->pdialog->begin_widget_list, buf);
+
+  /* --------------------- */
+  /* Redraw */
   redraw_group(bribe_dlg->pdialog->begin_widget_list, pwindow, 0);
 
   widget_flush(pwindow);
diff --git a/client/gui-sdl3/action_dialog.c b/client/gui-sdl3/action_dialog.c
index 52024589dd..ab9b899357 100644
--- a/client/gui-sdl3/action_dialog.c
+++ b/client/gui-sdl3/action_dialog.c
@@ -647,7 +647,7 @@ static int act_sel_wait_callback(struct widget *pwidget)
 /**********************************************************************//**
   Ask the server how much the bribe costs
 **************************************************************************/
-static int diplomat_bribe_callback(struct widget *pwidget)
+static int diplomat_bribe_unit_callback(struct widget *pwidget)
 {
   if (PRESSED_EVENT(main_data.event)) {
     if (NULL != game_unit_by_number(diplomat_dlg->actor_unit_id)
@@ -666,6 +666,26 @@ static int diplomat_bribe_callback(struct widget *pwidget)
   return -1;
 }
 
+/**********************************************************************//**
+  Ask the server how much the bribe costs
+**************************************************************************/
+static int diplomat_bribe_stack_callback(struct widget *pwidget)
+{
+  if (PRESSED_EVENT(main_data.event)) {
+    if (game_unit_by_number(diplomat_dlg->actor_unit_id) != nullptr) {
+      request_action_details(ACTION_SPY_BRIBE_STACK,
+                             diplomat_dlg->actor_unit_id,
+                             diplomat_dlg->target_ids[ATK_STACK]);
+      is_more_user_input_needed = TRUE;
+      popdown_diplomat_dialog();
+    } else {
+      popdown_diplomat_dialog();
+    }
+  }
+
+  return -1;
+}
+
 /**********************************************************************//**
   User clicked "Found City"
 **************************************************************************/
@@ -852,11 +872,10 @@ static const act_func af_map[ACTION_COUNT] = {
   [ACTION_STRIKE_BUILDING] = spy_strike_bld_request,
 
   /* Unit acting against a unit target. */
-  [ACTION_SPY_BRIBE_UNIT] = diplomat_bribe_callback,
+  [ACTION_SPY_BRIBE_UNIT] = diplomat_bribe_unit_callback,
 
   /* Unit acting against all units at a tile. */
-  /* No special callback functions needed for any unit stack targeted
-   * actions. */
+  [ACTION_SPY_BRIBE_STACK] = diplomat_bribe_stack_callback,
 
   /* Unit acting against a tile. */
   [ACTION_FOUND_CITY] = found_city_callback,
@@ -1719,7 +1738,7 @@ void popup_incite_dialog(struct unit *actor, struct city *pcity, int cost,
     area.h += buf->size.h;
     /* ------- */
     create_active_iconlabel(buf, pwindow->dst, pstr,
-                            _("No") , exit_incite_dlg_callback);
+                            _("No"), exit_incite_dlg_callback);
 
     set_wstate(buf, FC_WS_NORMAL);
     buf->key = SDLK_ESCAPE;
@@ -1821,9 +1840,9 @@ static int bribe_dlg_window_callback(struct widget *pwindow)
 }
 
 /**********************************************************************//**
-  User confirmed bribe.
+  User confirmed unit bribe.
 **************************************************************************/
-static int diplomat_bribe_yes_callback(struct widget *pwidget)
+static int diplomat_bribe_unit_yes_callback(struct widget *pwidget)
 {
   if (PRESSED_EVENT(main_data.event)) {
     if (NULL != game_unit_by_number(bribe_dlg->actor_unit_id)
@@ -1837,6 +1856,22 @@ static int diplomat_bribe_yes_callback(struct widget *pwidget)
   return -1;
 }
 
+/**********************************************************************//**
+  User confirmed stack bribe.
+**************************************************************************/
+static int diplomat_bribe_stack_yes_callback(struct widget *pwidget)
+{
+  if (PRESSED_EVENT(main_data.event)) {
+    if (NULL != game_unit_by_number(bribe_dlg->actor_unit_id)) {
+      request_do_action(bribe_dlg->act_id, bribe_dlg->actor_unit_id,
+                        bribe_dlg->target_id, 0, "");
+    }
+    popdown_bribe_dialog();
+  }
+
+  return -1;
+}
+
 /**********************************************************************//**
   Close bribe dialog.
 **************************************************************************/
@@ -1871,8 +1906,8 @@ void popdown_bribe_dialog(void)
   Popup a dialog asking a diplomatic unit if it wishes to bribe the
   given enemy unit.
 **************************************************************************/
-void popup_bribe_dialog(struct unit *actor, struct unit *punit, int cost,
-                        const struct action *paction)
+void popup_bribe_unit_dialog(struct unit *actor, struct unit *punit, int cost,
+                             const struct action *paction)
 {
   struct widget *pwindow = NULL, *buf = NULL;
   utf8_str *pstr;
@@ -1937,7 +1972,7 @@ void popup_bribe_dialog(struct unit *actor, struct unit *punit, int cost,
 
     /*------------*/
     create_active_iconlabel(buf, pwindow->dst, pstr,
-                            _("Yes"), diplomat_bribe_yes_callback);
+                            _("Yes"), diplomat_bribe_unit_yes_callback);
     buf->data.unit = punit;
     set_wstate(buf, FC_WS_NORMAL);
 
@@ -1947,7 +1982,7 @@ void popup_bribe_dialog(struct unit *actor, struct unit *punit, int cost,
     area.h += buf->size.h;
     /* ------- */
     create_active_iconlabel(buf, pwindow->dst, pstr,
-                            _("No") , exit_bribe_dlg_callback);
+                            _("No"), exit_bribe_dlg_callback);
 
     set_wstate(buf, FC_WS_NORMAL);
     buf->key = SDLK_ESCAPE;
@@ -1996,7 +2031,7 @@ void popup_bribe_dialog(struct unit *actor, struct unit *punit, int cost,
   }
   bribe_dlg->pdialog->begin_widget_list = buf;
 
-  /* setup window size and start position */
+  /* Setup window size and start position */
 
   resize_window(pwindow, NULL, NULL,
                 (pwindow->size.w - pwindow->area.w) + area.w,
@@ -2008,11 +2043,11 @@ void popup_bribe_dialog(struct unit *actor, struct unit *punit, int cost,
   put_window_near_map_tile(pwindow, pwindow->size.w, pwindow->size.h,
                            unit_tile(actor));
 
-  /* setup widget size and start position */
+  /* Setup widget size and start position */
   buf = pwindow;
 
   if (exit) {
-    /* exit button */
+    /* Exit button */
     buf = buf->prev;
     buf->size.x = area.x + area.w - buf->size.w - 1;
     buf->size.y = pwindow->size.y + adj_size(2);
@@ -2025,7 +2060,171 @@ void popup_bribe_dialog(struct unit *actor, struct unit *punit, int cost,
         bribe_dlg->pdialog->begin_widget_list, buf);
 
   /* --------------------- */
-  /* redraw */
+  /* Redraw */
+  redraw_group(bribe_dlg->pdialog->begin_widget_list, pwindow, 0);
+
+  widget_flush(pwindow);
+}
+
+/**********************************************************************//**
+  Popup a dialog asking a diplomatic unit if it wishes to bribe the
+  given enemy unit.
+**************************************************************************/
+void popup_bribe_stack_dialog(struct unit *actor, struct tile *ptile, int cost,
+                              const struct action *paction)
+{
+  struct widget *pwindow = NULL, *buf = NULL;
+  utf8_str *pstr;
+  char tBuf[255], cbuf[255];
+  bool exit = FALSE;
+  SDL_Rect area;
+
+  if (bribe_dlg) {
+    return;
+  }
+
+  /* Should be set before sending request to the server. */
+  fc_assert(is_more_user_input_needed);
+
+  if (!actor || !unit_can_do_action(actor, paction->id)) {
+    act_sel_done_secondary(actor ? actor->id : IDENTITY_NUMBER_ZERO);
+    return;
+  }
+
+  is_unit_move_blocked = TRUE;
+
+  bribe_dlg = fc_calloc(1, sizeof(struct small_diplomat_dialog));
+  bribe_dlg->act_id = paction->id;
+  bribe_dlg->actor_unit_id = actor->id;
+  bribe_dlg->target_id = ptile->index;
+  bribe_dlg->pdialog = fc_calloc(1, sizeof(struct small_dialog));
+
+  fc_snprintf(tBuf, ARRAY_SIZE(tBuf), PL_("Treasury contains %d gold.",
+                                          "Treasury contains %d gold.",
+                                          client_player()->economic.gold),
+              client_player()->economic.gold);
+
+  /* Window */
+  pstr = create_utf8_from_char_fonto(_("Bribe Enemy Stack"), FONTO_ATTENTION);
+
+  pstr->style |= TTF_STYLE_BOLD;
+
+  pwindow = create_window_skeleton(NULL, pstr, 0);
+
+  pwindow->action = bribe_dlg_window_callback;
+  set_wstate(pwindow, FC_WS_NORMAL);
+
+  add_to_gui_list(ID_BRIBE_DLG_WINDOW, pwindow);
+  bribe_dlg->pdialog->end_widget_list = pwindow;
+
+  area = pwindow->area;
+  area.w = MAX(area.w, adj_size(8));
+  area.h = MAX(area.h, adj_size(2));
+
+  if (cost <= client_player()->economic.gold) {
+    fc_snprintf(cbuf, sizeof(cbuf),
+                /* TRANS: %s is pre-pluralised "Treasury contains %d gold." */
+                PL_("Bribe unit stack for %d gold?\n%s",
+                    "Bribe unit stack for %d gold?\n%s", cost), cost, tBuf);
+
+    create_active_iconlabel(buf, pwindow->dst, pstr, cbuf, NULL);
+
+    add_to_gui_list(ID_LABEL, buf);
+
+    area.w = MAX(area.w, buf->size.w);
+    area.h += buf->size.h;
+
+    /*------------*/
+    create_active_iconlabel(buf, pwindow->dst, pstr,
+                            _("Yes"), diplomat_bribe_stack_yes_callback);
+    buf->data.tile = ptile;
+    set_wstate(buf, FC_WS_NORMAL);
+
+    add_to_gui_list(MAX_ID - actor->id, buf);
+
+    area.w = MAX(area.w, buf->size.w);
+    area.h += buf->size.h;
+    /* ------- */
+    create_active_iconlabel(buf, pwindow->dst, pstr,
+                            _("No"), exit_bribe_dlg_callback);
+
+    set_wstate(buf, FC_WS_NORMAL);
+    buf->key = SDLK_ESCAPE;
+
+    add_to_gui_list(ID_LABEL, buf);
+
+    area.w = MAX(area.w, buf->size.w);
+    area.h += buf->size.h;
+
+  } else {
+    /* Exit button */
+    buf = create_themeicon(current_theme->small_cancel_icon, pwindow->dst,
+                           WF_WIDGET_HAS_INFO_LABEL
+                           | WF_RESTORE_BACKGROUND);
+    buf->info_label = create_utf8_from_char_fonto(_("Close Dialog (Esc)"),
+                                                  FONTO_ATTENTION);
+    area.w += buf->size.w + adj_size(10);
+    buf->action = exit_bribe_dlg_callback;
+    set_wstate(buf, FC_WS_NORMAL);
+    buf->key = SDLK_ESCAPE;
+
+    add_to_gui_list(ID_BRIBE_DLG_EXIT_BUTTON, buf);
+    exit = TRUE;
+    /* --------------- */
+
+    fc_snprintf(cbuf, sizeof(cbuf),
+                /* TRANS: %s is pre-pluralised "Treasury contains %d gold." */
+                PL_("Bribing the unit stack costs %d gold.\n%s",
+                    "Bribing the unit stack costs %d gold.\n%s", cost), cost, tBuf);
+
+    create_active_iconlabel(buf, pwindow->dst, pstr, cbuf, NULL);
+
+    add_to_gui_list(ID_LABEL, buf);
+
+    area.w = MAX(area.w, buf->size.w);
+    area.h += buf->size.h;
+
+    /*------------*/
+    create_active_iconlabel(buf, pwindow->dst, pstr,
+                            _("Traitors Demand Too Much!"), NULL);
+
+    add_to_gui_list(ID_LABEL, buf);
+
+    area.w = MAX(area.w, buf->size.w);
+    area.h += buf->size.h;
+  }
+  bribe_dlg->pdialog->begin_widget_list = buf;
+
+  /* Setup window size and start position */
+
+  resize_window(pwindow, NULL, NULL,
+                (pwindow->size.w - pwindow->area.w) + area.w,
+                (pwindow->size.h - pwindow->area.h) + area.h);
+
+  area = pwindow->area;
+
+  auto_center_on_focus_unit();
+  put_window_near_map_tile(pwindow, pwindow->size.w, pwindow->size.h,
+                           unit_tile(actor));
+
+  /* Setup widget size and start position */
+  buf = pwindow;
+
+  if (exit) {
+    /* Exit button */
+    buf = buf->prev;
+    buf->size.x = area.x + area.w - buf->size.w - 1;
+    buf->size.y = pwindow->size.y + adj_size(2);
+  }
+
+  buf = buf->prev;
+  setup_vertical_widgets_position(1,
+        area.x,
+        area.y + 1, area.w, 0,
+        bribe_dlg->pdialog->begin_widget_list, buf);
+
+  /* --------------------- */
+  /* Redraw */
   redraw_group(bribe_dlg->pdialog->begin_widget_list, pwindow, 0);
 
   widget_flush(pwindow);
diff --git a/client/gui-stub/dialogs.c b/client/gui-stub/dialogs.c
index b5106782a0..23f420d268 100644
--- a/client/gui-stub/dialogs.c
+++ b/client/gui-stub/dialogs.c
@@ -142,8 +142,18 @@ void popup_incite_dialog(struct unit *actor, struct city *pcity, int cost,
   Popup a dialog asking a diplomatic unit if it wishes to bribe the
   given enemy unit.
 **************************************************************************/
-void popup_bribe_dialog(struct unit *actor, struct unit *punit, int cost,
-                        const struct action *paction)
+void popup_bribe_unit_dialog(struct unit *actor, struct unit *punit, int cost,
+                             const struct action *paction)
+{
+  /* PORTME */
+}
+
+/**********************************************************************//**
+  Popup a dialog asking a diplomatic unit if it wishes to bribe the
+  given enemy unit stack.
+**************************************************************************/
+void popup_bribe_stack_dialog(struct unit *actor, struct tile *ptile, int cost,
+                              const struct action *paction)
 {
   /* PORTME */
 }
diff --git a/client/include/dialogs_g.h b/client/include/dialogs_g.h
index f8484817de..3de18678cc 100644
--- a/client/include/dialogs_g.h
+++ b/client/include/dialogs_g.h
@@ -70,8 +70,10 @@ GUI_FUNC_PROTO(void, action_selection_no_longer_in_progress_gui_specific,
                int actor_unit_id)
 GUI_FUNC_PROTO(void, popup_incite_dialog, struct unit *actor,
                struct city *pcity, int cost, const struct action *paction)
-GUI_FUNC_PROTO(void, popup_bribe_dialog, struct unit *actor,
+GUI_FUNC_PROTO(void, popup_bribe_unit_dialog, struct unit *actor,
                struct unit *punit, int cost, const struct action *paction)
+GUI_FUNC_PROTO(void, popup_bribe_stack_dialog, struct unit *actor,
+               struct tile *ptile, int cost, const struct action *paction)
 GUI_FUNC_PROTO(void, popup_sabotage_dialog, struct unit *actor,
                struct city *pcity, const struct action *paction)
 GUI_FUNC_PROTO(void, popup_pillage_dialog, struct unit *punit, bv_extras extras)
diff --git a/client/packhand.c b/client/packhand.c
index 39491316d6..55f47204c9 100644
--- a/client/packhand.c
+++ b/client/packhand.c
@@ -176,7 +176,7 @@ const action_id auto_attack_blockers[] = {
   ACTION_SPY_INCITE_CITY, ACTION_SPY_INCITE_CITY_ESC,
   ACTION_TRADE_ROUTE, ACTION_MARKETPLACE,
   ACTION_HELP_WONDER,
-  ACTION_SPY_BRIBE_UNIT,
+  ACTION_SPY_BRIBE_UNIT, ACTION_SPY_BRIBE_STACK,
   ACTION_SPY_SABOTAGE_UNIT, ACTION_SPY_SABOTAGE_UNIT_ESC,
   ACTION_SPY_ATTACK,
   ACTION_FOUND_CITY,
@@ -4949,6 +4949,7 @@ void handle_unit_action_answer(int actor_id, int target_id, int cost,
 {
   struct city *pcity = game_city_by_number(target_id);
   struct unit *punit = game_unit_by_number(target_id);
+  struct tile *ptile = index_to_tile(&(wld.map), target_id);
   struct unit *pactor = player_unit_by_number(client_player(), actor_id);
   struct action *paction = action_by_number(action_type);
 
@@ -4980,13 +4981,13 @@ void handle_unit_action_answer(int actor_id, int target_id, int cost,
 
   switch ((enum gen_action)action_type) {
   case ACTION_SPY_BRIBE_UNIT:
-    if (punit && client.conn.playing
+    if (punit != nullptr && client.conn.playing
         && is_human(client.conn.playing)) {
       if (request_kind == REQEST_PLAYER_INITIATED) {
         /* Focus on the unit so the player knows where it is */
         unit_focus_set(pactor);
 
-        popup_bribe_dialog(pactor, punit, cost, paction);
+        popup_bribe_unit_dialog(pactor, punit, cost, paction);
       } else {
         /* Not in use (yet). */
         log_error("Unimplemented: received background unit bribe cost.");
@@ -5000,6 +5001,27 @@ void handle_unit_action_answer(int actor_id, int target_id, int cost,
       }
     }
     break;
+  case ACTION_SPY_BRIBE_STACK:
+    if (ptile != nullptr && client.conn.playing
+        && is_human(client.conn.playing)) {
+      if (request_kind == REQEST_PLAYER_INITIATED) {
+        /* Focus on the unit so the player knows where it is */
+        unit_focus_set(pactor);
+
+        popup_bribe_stack_dialog(pactor, ptile, cost, paction);
+      } else {
+        /* Not in use (yet). */
+        log_error("Unimplemented: received background stack bribe cost.");
+      }
+    } else {
+      log_debug("Bad target %d.", target_id);
+      if (request_kind == REQEST_PLAYER_INITIATED) {
+        action_selection_no_longer_in_progress(actor_id);
+        action_decision_clear_want(actor_id);
+        action_selection_next_in_focus(actor_id);
+      }
+    }
+    break;
   case ACTION_SPY_INCITE_CITY:
   case ACTION_SPY_INCITE_CITY_ESC:
     if (pcity && client.conn.playing
diff --git a/common/actions.c b/common/actions.c
index 4a4fe6c40a..f840f91e31 100644
--- a/common/actions.c
+++ b/common/actions.c
@@ -127,6 +127,11 @@ static void hard_code_actions(void)
                        * the forced move fails. */
                       MAK_FORCED,
                       0, 1, FALSE);
+  actions[ACTION_SPY_BRIBE_STACK] =
+      unit_action_new(ACTION_SPY_BRIBE_STACK, ACTRES_SPY_BRIBE_STACK,
+                      FALSE, TRUE,
+                      MAK_FORCED,
+                      0, 1, FALSE);
   actions[ACTION_SPY_SABOTAGE_CITY] =
       unit_action_new(ACTION_SPY_SABOTAGE_CITY, ACTRES_SPY_SABOTAGE_CITY,
                       FALSE, TRUE,
@@ -2506,6 +2511,7 @@ action_actor_utype_hard_reqs_ok_full(const struct action *paction,
   case ACTRES_MARKETPLACE:
   case ACTRES_HELP_WONDER:
   case ACTRES_SPY_BRIBE_UNIT:
+  case ACTRES_SPY_BRIBE_STACK:
   case ACTRES_SPY_SABOTAGE_UNIT:
   case ACTRES_CAPTURE_UNITS:
   case ACTRES_FOUND_CITY:
@@ -2698,6 +2704,7 @@ action_hard_reqs_actor(const struct civ_map *nmap,
   case ACTRES_MARKETPLACE:
   case ACTRES_HELP_WONDER:
   case ACTRES_SPY_BRIBE_UNIT:
+  case ACTRES_SPY_BRIBE_STACK:
   case ACTRES_SPY_SABOTAGE_UNIT:
   case ACTRES_CAPTURE_UNITS:
   case ACTRES_FOUND_CITY:
@@ -3847,6 +3854,7 @@ action_prob(const struct civ_map *nmap,
                                 paction);
     break;
   case ACTRES_SPY_BRIBE_UNIT:
+  case ACTRES_SPY_BRIBE_STACK:
     /* All uncertainty comes from potential diplomatic battles. */
     chance = ap_diplomat_battle(actor->unit, target->unit, target->tile,
                                 paction);
@@ -5762,6 +5770,8 @@ const char *action_ui_name_ruleset_var_name(int act)
     return "ui_name_sabotage_unit_escape";
   case ACTION_SPY_BRIBE_UNIT:
     return "ui_name_bribe_unit";
+  case ACTION_SPY_BRIBE_STACK:
+    return "ui_name_bribe_stack";
   case ACTION_SPY_SABOTAGE_CITY:
     return "ui_name_sabotage_city";
   case ACTION_SPY_SABOTAGE_CITY_ESC:
@@ -6062,6 +6072,9 @@ const char *action_ui_name_default(int act)
   case ACTION_SPY_BRIBE_UNIT:
     /* TRANS: Bribe Enemy _Unit (3% chance of success). */
     return N_("Bribe Enemy %sUnit%s");
+  case ACTION_SPY_BRIBE_STACK:
+    /* TRANS: Bribe Enemy _Stack (3% chance of success). */
+    return N_("Bribe Enemy %sStack%s");
   case ACTION_SPY_SABOTAGE_CITY:
     /* TRANS: _Sabotage City (3% chance of success). */
     return N_("%sSabotage City%s");
@@ -6411,6 +6424,7 @@ const char *action_min_range_ruleset_var_name(int act)
   case ACTION_SPY_SABOTAGE_UNIT:
   case ACTION_SPY_SABOTAGE_UNIT_ESC:
   case ACTION_SPY_BRIBE_UNIT:
+  case ACTION_SPY_BRIBE_STACK:
   case ACTION_SPY_SABOTAGE_CITY:
   case ACTION_SPY_SABOTAGE_CITY_ESC:
   case ACTION_SPY_TARGETED_SABOTAGE_CITY:
@@ -6587,6 +6601,7 @@ const char *action_max_range_ruleset_var_name(int act)
   case ACTION_SPY_SABOTAGE_UNIT:
   case ACTION_SPY_SABOTAGE_UNIT_ESC:
   case ACTION_SPY_BRIBE_UNIT:
+  case ACTION_SPY_BRIBE_STACK:
   case ACTION_SPY_SABOTAGE_CITY:
   case ACTION_SPY_SABOTAGE_CITY_ESC:
   case ACTION_SPY_TARGETED_SABOTAGE_CITY:
@@ -6772,6 +6787,7 @@ const char *action_target_kind_ruleset_var_name(int act)
   case ACTION_SPY_SABOTAGE_UNIT:
   case ACTION_SPY_SABOTAGE_UNIT_ESC:
   case ACTION_SPY_BRIBE_UNIT:
+  case ACTION_SPY_BRIBE_STACK:
   case ACTION_SPY_SABOTAGE_CITY:
   case ACTION_SPY_SABOTAGE_CITY_ESC:
   case ACTION_SPY_TARGETED_SABOTAGE_CITY:
@@ -6942,6 +6958,7 @@ const char *action_actor_consuming_always_ruleset_var_name(action_id act)
   case ACTION_SPY_SABOTAGE_UNIT:
   case ACTION_SPY_SABOTAGE_UNIT_ESC:
   case ACTION_SPY_BRIBE_UNIT:
+  case ACTION_SPY_BRIBE_STACK:
   case ACTION_SPY_SABOTAGE_CITY:
   case ACTION_SPY_SABOTAGE_CITY_ESC:
   case ACTION_SPY_TARGETED_SABOTAGE_CITY:
@@ -7178,6 +7195,7 @@ const char *action_blocked_by_ruleset_var_name(const struct action *act)
   case ACTION_SPY_SABOTAGE_UNIT:
   case ACTION_SPY_SABOTAGE_UNIT_ESC:
   case ACTION_SPY_BRIBE_UNIT:
+  case ACTION_SPY_BRIBE_STACK:
   case ACTION_SPY_SABOTAGE_CITY:
   case ACTION_SPY_SABOTAGE_CITY_ESC:
   case ACTION_SPY_TARGETED_SABOTAGE_CITY:
@@ -7302,6 +7320,7 @@ action_post_success_forced_ruleset_var_name(const struct action *act)
   fc_assert_ret_val(act != NULL, NULL);
 
   if (!(action_has_result(act, ACTRES_SPY_BRIBE_UNIT)
+        || action_has_result(act, ACTRES_SPY_BRIBE_STACK)
         || action_has_result(act, ACTRES_ATTACK)
         || action_has_result(act, ACTRES_WIPE_UNITS)
         || action_has_result(act, ACTRES_COLLECT_RANSOM))) {
@@ -7312,6 +7331,8 @@ action_post_success_forced_ruleset_var_name(const struct action *act)
   switch ((enum gen_action)action_number(act)) {
   case ACTION_SPY_BRIBE_UNIT:
     return "bribe_unit_post_success_forced_actions";
+  case ACTION_SPY_BRIBE_STACK:
+    return "bribe_stack_post_success_forced_actions";
   case ACTION_ATTACK:
     return "attack_post_success_forced_actions";
   case ACTION_ATTACK2:
diff --git a/common/actions.h b/common/actions.h
index a8d2bac491..17aff0d7e5 100644
--- a/common/actions.h
+++ b/common/actions.h
@@ -364,13 +364,14 @@ action_auto_perf_iterate(_act_perf_) {                                    \
 #define ACTION_AUTO_UPKEEP_GOLD          1
 #define ACTION_AUTO_UPKEEP_SHIELD        2
 #define ACTION_AUTO_MOVED_ADJ            3
-#define ACTION_AUTO_POST_BRIBE           4
-#define ACTION_AUTO_POST_ATTACK          5
-#define ACTION_AUTO_POST_ATTACK2         6
-#define ACTION_AUTO_POST_COLLECT_RANSOM  7
-#define ACTION_AUTO_ESCAPE_CITY          8
-#define ACTION_AUTO_ESCAPE_STACK         9
-#define ACTION_AUTO_POST_WIPE_UNITS     10
+#define ACTION_AUTO_POST_BRIBE_UNIT      4
+#define ACTION_AUTO_POST_BRIBE_STACK     5
+#define ACTION_AUTO_POST_ATTACK          6
+#define ACTION_AUTO_POST_ATTACK2         7
+#define ACTION_AUTO_POST_COLLECT_RANSOM  8
+#define ACTION_AUTO_ESCAPE_CITY          9
+#define ACTION_AUTO_ESCAPE_STACK        10
+#define ACTION_AUTO_POST_WIPE_UNITS     11
 
 /* Initialization */
 void actions_init(void);
diff --git a/common/actres.c b/common/actres.c
index 484f015fcb..ae95369ef7 100644
--- a/common/actres.c
+++ b/common/actres.c
@@ -73,6 +73,9 @@ static struct actres act_results[ACTRES_LAST] = {
   { ACT_TGT_COMPL_SIMPLE, ABK_DIPLOMATIC,    /* ACTRES_SPY_BRIBE_UNIT */
     TRUE, ACTIVITY_LAST, DRT_NONE,
     EC_NONE, ERM_NONE, ATK_UNIT },
+  { ACT_TGT_COMPL_SIMPLE, ABK_DIPLOMATIC,    /* ACTRES_SPY_BRIBE_STACK */
+    TRUE, ACTIVITY_LAST, DRT_NONE,
+    EC_NONE, ERM_NONE, ATK_STACK },
   { ACT_TGT_COMPL_SIMPLE, ABK_DIPLOMATIC,    /* ACTRES_SPY_SABOTAGE_UNIT */
     TRUE, ACTIVITY_LAST, DRT_NONE,
     EC_NONE, ERM_NONE, ATK_UNIT },
@@ -387,6 +390,7 @@ int actres_min_range_default(enum action_result result)
   case ACTRES_MARKETPLACE:
   case ACTRES_HELP_WONDER:
   case ACTRES_SPY_BRIBE_UNIT:
+  case ACTRES_SPY_BRIBE_STACK:
   case ACTRES_SPY_SABOTAGE_UNIT:
   case ACTRES_CAPTURE_UNITS:
   case ACTRES_FOUND_CITY:
@@ -475,6 +479,7 @@ int actres_max_range_default(enum action_result result)
   case ACTRES_TRADE_ROUTE:
   case ACTRES_MARKETPLACE:
   case ACTRES_SPY_BRIBE_UNIT:
+  case ACTRES_SPY_BRIBE_STACK:
   case ACTRES_SPY_SABOTAGE_UNIT:
   case ACTRES_CAPTURE_UNITS:
   case ACTRES_FOUND_CITY:
@@ -598,6 +603,7 @@ bool actres_legal_target_kind(enum action_result result,
   case ACTRES_TRANSPORT_BOARD:
   case ACTRES_TRANSPORT_EMBARK:
     return tgt_kind == ATK_UNIT;
+  case ACTRES_SPY_BRIBE_STACK:
   case ACTRES_CAPTURE_UNITS:
   case ACTRES_BOMBARD:
   case ACTRES_NUKE_UNITS:
@@ -699,6 +705,7 @@ actres_sub_target_kind_default(enum action_result result)
   case ACTRES_SPY_TARGETED_STEAL_TECH:
     return ASTK_TECH;
   case ACTRES_SPY_BRIBE_UNIT:
+  case ACTRES_SPY_BRIBE_STACK:
   case ACTRES_SPY_SABOTAGE_UNIT:
   case ACTRES_EXPEL_UNIT:
   case ACTRES_HEAL_UNIT:
@@ -854,6 +861,7 @@ enum fc_tristate actres_possible(const struct civ_map *nmap,
   switch (result) {
   case ACTRES_CAPTURE_UNITS:
   case ACTRES_SPY_BRIBE_UNIT:
+  case ACTRES_SPY_BRIBE_STACK:
     /* Why this is a hard requirement: Can't transfer a unique unit if the
      * actor player already has one. */
     /* Info leak: This is only checked for when the actor player can see
diff --git a/common/fc_types.h b/common/fc_types.h
index 44e9dd16c3..0fb25f0f4d 100644
--- a/common/fc_types.h
+++ b/common/fc_types.h
@@ -51,7 +51,7 @@ extern "C" {
 #define MAX_GOODS_TYPES 25
 #define MAX_DISASTER_TYPES 10
 #define MAX_ACHIEVEMENT_TYPES 40
-#define MAX_NUM_ACTION_AUTO_PERFORMERS 11
+#define MAX_NUM_ACTION_AUTO_PERFORMERS 12
 #define MAX_NUM_MULTIPLIERS 50
 #define MAX_NUM_LEADERS MAX_NUM_ITEMS /* Used in the network protocol. */
 #define MAX_NUM_NATION_SETS 32 /* Used in the network protocol.
diff --git a/common/unit.c b/common/unit.c
index 748e0250cd..7214ac8dd7 100644
--- a/common/unit.c
+++ b/common/unit.c
@@ -2418,6 +2418,26 @@ int unit_bribe_cost(const struct unit *punit, const struct player *briber,
   return ((float)cost / 2 * (1.0 + (float)punit->hp / default_hp));
 }
 
+/**********************************************************************//**
+  Calculate how expensive it is to bribe entire unit stack.
+
+  @param  ptile       Tile to bribe units from
+  @param  briber      Player that wants to bribe
+  @param  briber_unit Unit that does the bribing
+  @return             Bribe cost
+**************************************************************************/
+int stack_bribe_cost(const struct tile *ptile, const struct player *briber,
+                     const struct unit *briber_unit)
+{
+  int bribe_cost = 0;
+
+  unit_list_iterate(ptile->units, pbribed) {
+    bribe_cost += unit_bribe_cost(pbribed, briber, briber_unit);
+  } unit_list_iterate_end;
+
+  return bribe_cost;
+}
+
 /**********************************************************************//**
   Load pcargo onto ptrans. Returns TRUE on success.
 **************************************************************************/
diff --git a/common/unit.h b/common/unit.h
index f7c0953cbc..389e72484f 100644
--- a/common/unit.h
+++ b/common/unit.h
@@ -542,6 +542,8 @@ void unit_set_ai_data(struct unit *punit, const struct ai_type *ai,
 
 int unit_bribe_cost(const struct unit *punit, const struct player *briber,
                     const struct unit *briber_unit);
+int stack_bribe_cost(const struct tile *ptile, const struct player *briber,
+                     const struct unit *briber_unit);
 
 bool unit_transport_load(struct unit *pcargo, struct unit *ptrans,
                          bool force);
diff --git a/doc/README.actions b/doc/README.actions
index e0e354d199..64bf00f4e5 100644
--- a/doc/README.actions
+++ b/doc/README.actions
@@ -772,6 +772,14 @@ Actions done by a unit against another unit
 
 Actions done by a unit against all units at a tile
 ==================================================
+"Bribe Stack" - Make the target units join the actors owners side.
+ * UI name can be set using ui_name_bribe_stack
+ * forced actions after success can be set with
+   bribe_stack_post_success_forced_actions
+ * actor must be on the same tile as the target or on the tile next to it.
+ * target must be foreign. (!)
+ * target must be visible for the actor.
+
 "Capture Units" - steal the target units.
  * UI name can be set using ui_name_capture_units
  * actor must be on a tile next to the target.
diff --git a/gen_headers/enums/actions_enums.def b/gen_headers/enums/actions_enums.def
index 284837f954..0218145351 100644
--- a/gen_headers/enums/actions_enums.def
+++ b/gen_headers/enums/actions_enums.def
@@ -30,6 +30,7 @@ values
   ACTION_MARKETPLACE                      "Enter Marketplace"
   ACTION_HELP_WONDER                      "Help Wonder"
   ACTION_SPY_BRIBE_UNIT                   "Bribe Unit"
+  ACTION_SPY_BRIBE_STACK                  "Bribe Stack"
   ACTION_CAPTURE_UNITS                    "Capture Units"
   ACTION_SPY_SABOTAGE_UNIT                "Sabotage Unit"
   ACTION_SPY_SABOTAGE_UNIT_ESC            "Sabotage Unit Escape"
diff --git a/gen_headers/enums/fc_types_enums.def b/gen_headers/enums/fc_types_enums.def
index edf0d419d9..7839b73d70 100644
--- a/gen_headers/enums/fc_types_enums.def
+++ b/gen_headers/enums/fc_types_enums.def
@@ -141,6 +141,7 @@ values
   MARKETPLACE                  "Unit Enter Marketplace"
   HELP_WONDER                  "Unit Help Wonder"
   SPY_BRIBE_UNIT               "Unit Bribe Unit"
+  SPY_BRIBE_STACK              "Unit Bribe Stack"
   SPY_SABOTAGE_UNIT            "Unit Sabotage Unit"
   CAPTURE_UNITS                "Unit Capture Units"
   FOUND_CITY                   "Unit Found City"
diff --git a/server/advisors/advdata.c b/server/advisors/advdata.c
index f165b869a4..e6721bfc9a 100644
--- a/server/advisors/advdata.c
+++ b/server/advisors/advdata.c
@@ -853,9 +853,12 @@ adv_want adv_gov_action_immunity_want(struct government *gov)
       break;
     case ACTRES_CONQUER_EXTRAS:
     case ACTRES_PILLAGE:
-    case ACTRES_WIPE_UNITS:
       bonus += 0.2;
       break;
+    case ACTRES_WIPE_UNITS:
+    case ACTRES_SPY_BRIBE_STACK:
+      bonus += 0.3;
+      break;
     case ACTRES_HUT_ENTER:
     case ACTRES_HUT_FRIGHTEN:
       /* It is mine. My own. My precious. */
diff --git a/server/diplomats.c b/server/diplomats.c
index fc54f1c2b7..466be1fe1d 100644
--- a/server/diplomats.c
+++ b/server/diplomats.c
@@ -641,8 +641,8 @@ bool spy_sabotage_unit(struct player *pplayer, struct unit *pdiplomat,
   Returns TRUE iff action could be done, FALSE if it couldn't. Even if
   this returns TRUE, unit may have died during the action.
 ****************************************************************************/
-bool diplomat_bribe(struct player *pplayer, struct unit *pdiplomat,
-                    struct unit *pvictim, const struct action *paction)
+bool diplomat_bribe_unit(struct player *pplayer, struct unit *pdiplomat,
+                         struct unit *pvictim, const struct action *paction)
 {
   char victim_link[MAX_LEN_LINK];
   struct player *uplayer;
@@ -797,6 +797,125 @@ bool diplomat_bribe(struct player *pplayer, struct unit *pdiplomat,
   return TRUE;
 }
 
+/************************************************************************//**
+  Bribe an enemy unit stack.
+
+  - Can't bribe a unit if:
+    - Player doesn't have enough gold.
+  - Otherwise, the unit will be bribed.
+
+  - A successful briber will try to move onto the victim's square.
+
+  Returns TRUE iff action could be done, FALSE if it couldn't. Even if
+  this returns TRUE, unit may have died during the action.
+****************************************************************************/
+bool diplomat_bribe_stack(struct player *pplayer, struct unit *pdiplomat,
+                          struct tile *pvictim, const struct action *paction)
+{
+  int bribe_cost = 0;
+  int bribe_count = 0;
+  struct city *pcity;
+  bool bounce = FALSE;
+  int diplomat_id = pdiplomat->id;
+  const struct unit_type *act_utype;
+
+  unit_list_iterate(pvictim->units, pbribed) {
+    struct player *owner = unit_owner(pbribed);
+
+    if (!pplayers_at_war(pplayer, owner)) {
+      notify_player(pplayer, unit_tile(pdiplomat),
+                    E_MY_DIPLOMAT_FAILED, ftc_server,
+                    _("You are not in war with all the units in the stack."));
+      return FALSE;
+    }
+  } unit_list_iterate_end;
+
+  bribe_cost = stack_bribe_cost(pvictim, pplayer, pdiplomat);
+
+  /* If player doesn't have enough gold, can't bribe. */
+  if (pplayer->economic.gold < bribe_cost) {
+    notify_player(pplayer, unit_tile(pdiplomat),
+                  E_MY_DIPLOMAT_FAILED, ftc_server,
+                  _("You don't have enough gold to bribe the unit stack."));
+    log_debug("bribe-stack: not enough gold");
+    return FALSE;
+  }
+
+  pcity = tile_city(pvictim);
+  if (pcity != NULL && !pplayers_allied(city_owner(pcity), pplayer)) {
+    bounce = TRUE;
+  }
+
+  unit_list_iterate_safe(pvictim->units, pbribed) {
+    struct player *owner = unit_owner(pbribed);
+    struct unit *nunit = unit_change_owner(pbribed, pplayer,
+                                           pdiplomat->homecity, ULR_BRIBED);
+
+    notify_player(owner, pvictim, E_ENEMY_DIPLOMAT_BRIBE, ftc_server,
+                  /* TRANS: <unit> ... <Poles> */
+                  _("Your %s was bribed by the %s."),
+                  unit_link(nunit), nation_plural_for_player(pplayer));
+    bribe_count++;
+
+    if (bounce) {
+      bounce_unit(pbribed, TRUE);
+    }
+  } unit_list_iterate_safe_end;
+
+  if (!unit_is_alive(diplomat_id)) {
+    /* Destroyed by a script */
+    pdiplomat = NULL;
+  }
+
+  act_utype = unit_type_get(pdiplomat);
+
+  /* Notify everybody involved. */
+  notify_player(pplayer, pvictim, E_MY_DIPLOMAT_BRIBE, ftc_server,
+                /* TRANS: <diplomat> ... */
+                _("Your %s succeeded in bribing %d units."),
+                pdiplomat ? unit_link(pdiplomat)
+                : utype_name_translation(act_utype), bribe_count);
+  if (pdiplomat && maybe_make_veteran(pdiplomat, 100)) {
+    notify_unit_experience(pdiplomat);
+  }
+
+  /* This costs! */
+  pplayer->economic.gold -= bribe_cost;
+  if (pplayer->economic.gold < 0) {
+    /* Scripts have deprived us of too much gold before we paid */
+    log_normal("%s has bribed %d units but has not %d gold at payment time, "
+               "%d is the discount", player_name(pplayer),
+               bribe_count, bribe_cost,
+               -pplayer->economic.gold);
+    pplayer->economic.gold = 0;
+  }
+
+  if (!pdiplomat || !unit_is_alive(diplomat_id)) {
+    return TRUE;
+  }
+
+  /* Try to move the briber onto the victim's square unless the victim has
+   * been bounced because it couldn't share tile with a unit or city. */
+  if (!bounce
+      /* Try to perform post move forced actions. */
+      && (NULL == action_auto_perf_unit_do(AAPC_POST_ACTION, pdiplomat,
+                                           NULL, NULL, paction,
+                                           pvictim, pcity,
+                                           NULL, NULL))
+      /* May have died while trying to do forced actions. */
+      && unit_is_alive(diplomat_id)) {
+    pdiplomat->moves_left = 0;
+  }
+  if (NULL != player_unit_by_number(pplayer, diplomat_id)) {
+    send_unit_info(NULL, pdiplomat);
+  }
+
+  /* Update clients. */
+  send_player_all_c(pplayer, NULL);
+
+  return TRUE;
+}
+
 /************************************************************************//**
   Diplomatic battle.
 
diff --git a/server/diplomats.h b/server/diplomats.h
index eb09fbd53f..7f82fb6faa 100644
--- a/server/diplomats.h
+++ b/server/diplomats.h
@@ -1,4 +1,4 @@
-/********************************************************************** 
+/***********************************************************************
  Freeciv - Copyright (C) 1996 - A Kjeldberg, L Gregersen, P Unold
    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
@@ -13,6 +13,7 @@
 #ifndef FC__DIPLOMATS_H
 #define FC__DIPLOMATS_H
 
+/* common */
 #include "fc_types.h"
 
 bool diplomat_embassy(struct player *pplayer, struct unit *pdiplomat,
@@ -31,8 +32,10 @@ bool spy_spread_plague(struct player *act_player, struct unit *act_unit,
 bool spy_sabotage_unit(struct player *pplayer, struct unit *pdiplomat,
                        struct unit *pvictim,
                        const struct action *paction);
-bool diplomat_bribe(struct player *pplayer, struct unit *pdiplomat,
-                    struct unit *pvictim, const struct action *paction);
+bool diplomat_bribe_unit(struct player *pplayer, struct unit *pdiplomat,
+                         struct unit *pvictim, const struct action *paction);
+bool diplomat_bribe_stack(struct player *pplayer, struct unit *pdiplomat,
+                          struct tile *pvictim, const struct action *paction);
 bool spy_attack(struct player *act_player, struct unit *act_unit,
                 struct tile *tgt_tile, const struct action *paction);
 int diplomats_unignored_tech_stealings(struct unit *pdiplomat,
@@ -61,4 +64,4 @@ bool spy_escape(struct player *pplayer,
 
 int count_diplomats_on_tile(struct tile *ptile);
 
-#endif  /* FC__DIPLOMATS_H */
+#endif /* FC__DIPLOMATS_H */
diff --git a/server/ruleset/ruleload.c b/server/ruleset/ruleload.c
index 22b98fec06..584de814b2 100644
--- a/server/ruleset/ruleload.c
+++ b/server/ruleset/ruleload.c
@@ -7801,10 +7801,15 @@ static bool load_ruleset_actions(struct section_file *file,
     /* Forced actions after another action was successfully performed. */
 
     if (!load_action_post_success_force(file, filename,
-                                        ACTION_AUTO_POST_BRIBE,
+                                        ACTION_AUTO_POST_BRIBE_UNIT,
                                         action_by_number(
                                           ACTION_SPY_BRIBE_UNIT))) {
       ok = FALSE;
+    } else if (!load_action_post_success_force(file, filename,
+                                               ACTION_AUTO_POST_BRIBE_STACK,
+                                               action_by_number(
+                                                 ACTION_SPY_BRIBE_STACK))) {
+      ok = FALSE;
     } else if (!load_action_post_success_force(file, filename,
                                                ACTION_AUTO_POST_ATTACK,
                                                action_by_number(
diff --git a/server/unithand.c b/server/unithand.c
index 9ff036cdbc..114fc5b2c7 100644
--- a/server/unithand.c
+++ b/server/unithand.c
@@ -1116,6 +1116,7 @@ static struct player *need_war_player_hlp(const struct unit *actor,
   switch (paction->result) {
   case ACTRES_ATTACK:
   case ACTRES_WIPE_UNITS:
+  case ACTRES_SPY_BRIBE_STACK:
   case ACTRES_COLLECT_RANSOM:
     /* Target is a unit stack but a city can block it. */
     fc_assert_action(action_get_target_kind(paction) == ATK_STACK, break);
@@ -3196,6 +3197,7 @@ void handle_unit_action_query(struct connection *pc,
   struct action *paction = action_by_number(action_type);
   struct unit *punit = game_unit_by_number(target_id);
   struct city *pcity = game_city_by_number(target_id);
+  struct tile *ptile = index_to_tile(&(wld.map), target_id);
   const struct civ_map *nmap = &(wld.map);
 
   if (NULL == paction) {
@@ -3217,7 +3219,7 @@ void handle_unit_action_query(struct connection *pc,
 
   switch (paction->result) {
   case ACTRES_SPY_BRIBE_UNIT:
-    if (punit
+    if (punit != nullptr
         && is_action_enabled_unit_on_unit(nmap, action_type,
                                           pactor, punit)) {
       dsend_packet_unit_action_answer(pc,
@@ -3227,8 +3229,25 @@ void handle_unit_action_query(struct connection *pc,
                                       action_type, request_kind);
     } else {
       illegal_action(pplayer, pactor, action_type,
-                     punit ? unit_owner(punit) : NULL,
-                     NULL, NULL, punit, request_kind, ACT_REQ_PLAYER);
+                     punit ? unit_owner(punit) : nullptr,
+                     nullptr, nullptr, punit, request_kind, ACT_REQ_PLAYER);
+      unit_query_impossible(pc, actor_id, target_id, request_kind);
+      return;
+    }
+    break;
+  case ACTRES_SPY_BRIBE_STACK:
+    if (ptile != nullptr
+        && is_action_enabled_unit_on_stack(nmap, action_type,
+                                           pactor, ptile)) {
+      dsend_packet_unit_action_answer(pc,
+                                      actor_id, target_id,
+                                      stack_bribe_cost(ptile, pplayer,
+                                                       pactor),
+                                      action_type, request_kind);
+    } else {
+      illegal_action(pplayer, pactor, action_type,
+                     punit ? unit_owner(punit) : nullptr,
+                     nullptr, nullptr, punit, request_kind, ACT_REQ_PLAYER);
       unit_query_impossible(pc, actor_id, target_id, request_kind);
       return;
     }
@@ -3681,8 +3700,8 @@ bool unit_perform_action(struct player *pplayer,
   switch (paction->result) {
   case ACTRES_SPY_BRIBE_UNIT:
     ACTION_PERFORM_UNIT_UNIT(action_type, actor_unit, punit,
-                             diplomat_bribe(pplayer, actor_unit, punit,
-                                            paction));
+                             diplomat_bribe_unit(pplayer, actor_unit, punit,
+                                                 paction));
     break;
   case ACTRES_SPY_SABOTAGE_UNIT:
     /* Difference is caused by data in the action structure. */
@@ -3918,6 +3937,11 @@ bool unit_perform_action(struct player *pplayer,
     ACTION_PERFORM_UNIT_STACK(action_type, actor_unit, target_tile,
                               do_wipe_units(actor_unit, target_tile, paction));
     break;
+  case ACTRES_SPY_BRIBE_STACK:
+    ACTION_PERFORM_UNIT_STACK(action_type, actor_unit, target_tile,
+                              diplomat_bribe_stack(pplayer, actor_unit,
+                                                   target_tile, paction));
+    break;
   case ACTRES_NUKE_UNITS:
     ACTION_PERFORM_UNIT_STACK(action_type, actor_unit, target_tile,
                               unit_nuke(pplayer, actor_unit, target_tile,
diff --git a/tools/ruleutil/rulesave.c b/tools/ruleutil/rulesave.c
index 126acbecc8..8d85c9405a 100644
--- a/tools/ruleutil/rulesave.c
+++ b/tools/ruleutil/rulesave.c
@@ -1150,13 +1150,20 @@ static bool save_actions_ruleset(const char *filename, const char *name)
     return FALSE;
   }
 
-  if (!save_action_post_success_force(sfile, ACTION_AUTO_POST_BRIBE,
+  if (!save_action_post_success_force(sfile, ACTION_AUTO_POST_BRIBE_UNIT,
                                       action_by_number(
                                         ACTION_SPY_BRIBE_UNIT))) {
     log_error("Didn't save all post success forced actions.");
     return FALSE;
   }
 
+  if (!save_action_post_success_force(sfile, ACTION_AUTO_POST_BRIBE_STACK,
+                                      action_by_number(
+                                        ACTION_SPY_BRIBE_STACK))) {
+    log_error("Didn't save all post success forced actions.");
+    return FALSE;
+  }
+
   if (!save_action_post_success_force(sfile, ACTION_AUTO_POST_ATTACK,
                                       action_by_number(ACTION_ATTACK))) {
     log_error("Didn't save all post success forced actions.");
-- 
2.45.2

