#nullable enable
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma;
internal class DeathPrompt
{
private static CoroutineHandle? createPromptCoroutine;
private GUIFrame? deathPromptFrame;
private GUIComponent? skillPanel;
private GUIComponent? newCharacterPanel;
private GUIComponent? takeOverBotPanel;
private GUIComponent? content;
private static GUIComponent? takeOverBotPanelFrame;
///
/// Private constructor, because these should only be created using the Show method
///
private DeathPrompt() { }
public static void Create(float delay)
{
if (!RespawnManager.UseDeathPrompt) { return; }
if (GameMain.GameSession.DeathPrompt != null)
{
return;
}
if (createPromptCoroutine != null && CoroutineManager.IsCoroutineRunning(createPromptCoroutine)) { return; }
if ((GameMain.GameSession is not { IsRunning: true })) { return; }
createPromptCoroutine = CoroutineManager.Invoke(() =>
{
if (GameMain.GameSession != null)
{
GameMain.GameSession.DeathPrompt = new DeathPrompt();
GameMain.GameSession.DeathPrompt.CreatePrompt();
SoundPlayer.OverrideMusicType = "crewdead".ToIdentifier();
SoundPlayer.OverrideMusicDuration = 25.0f;
}
}, delay);
}
public void AddToGUIUpdateList()
{
content?.AddToGUIUpdateList();
}
private void CreatePrompt()
{
const float FadeInInterval = 1.0f;
const float FadeInDuration = 1.0f;
bool permadeath = GameMain.NetworkMember is { ServerSettings.RespawnMode: RespawnMode.Permadeath };
bool ironman = GameMain.NetworkMember is { ServerSettings.IronmanModeActive: true };
var background = new GUICustomComponent(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), onDraw: DrawBackground)
{
UserData = this
};
background.FadeIn(wait: 0, duration: 5.0f);
var foreground = new GUIImage(new RectTransform(new Vector2(1.0f, GUI.RelativeHorizontalAspectRatio), background.RectTransform, Anchor.BottomCenter) { AbsoluteOffset = new Point(0, GUI.IntScale(-20)) }, "DeathScreenForeground")
{
Color = Color.White
};
foreground.FadeIn(wait: 0, duration: 5.0f);
foreground.Pulsate(startScale: Vector2.One, Vector2.One * 0.8f, duration: 25.0f);
deathPromptFrame = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.3f), background.RectTransform, Anchor.Center))
{
UserData = this
};
deathPromptFrame.FadeIn(wait: 0, duration: FadeInDuration);
new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.1f), background.RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.2f) }, string.Empty, font: GUIStyle.LargeFont, textAlignment: Alignment.TopCenter)
{
TextGetter = () =>
{
return GameMain.Client.EndRoundTimeRemaining > 0.0f ?
TextManager.GetWithVariable("endinground", "[time]", ToolBox.SecondsToReadableTime(GameMain.Client.EndRoundTimeRemaining))
.Fallback(ToolBox.SecondsToReadableTime(GameMain.Client.EndRoundTimeRemaining), useDefaultLanguageIfFound: false) :
string.Empty;
}
};
var content = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.8f), deathPromptFrame.RectTransform, Anchor.Center))
{
Stretch = true,
RelativeSpacing = 0.05f
};
//"you have died" header
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), TextManager.Get("deathprompt.header"), font: GUIStyle.LargeFont, textAlignment: Alignment.Center)
.FadeIn(wait: 0, duration: FadeInDuration);
var causeOfDeath = GameMain.Client?.Character?.CauseOfDeath;
if (causeOfDeath != null && causeOfDeath.Type != CauseOfDeathType.Unknown)
{
var causeOfDeathDescription = causeOfDeath.Affliction != null ?
causeOfDeath.Affliction.SelfCauseOfDeathDescription :
TextManager.Get("Self_CauseOfDeathDescription." + causeOfDeath.Type.ToString(), "Self_CauseOfDeathDescription.Damage");
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), causeOfDeathDescription)
.FadeIn(wait: FadeInInterval * 2, duration: FadeInDuration);
}
if (permadeath)
{
if (ironman)
{
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform),
TextManager.Get("deathprompt.permadeathnotification") + "\n\n" + TextManager.Get("deathprompt.ironmanexplanation"), wrap: true)
.FadeIn(wait: FadeInInterval * 3, duration: FadeInDuration);
}
else
{
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform),
TextManager.Get("deathprompt.permadeathnotification") + '\n' + TextManager.Get("deathprompt.takeoverbotexplanation"), wrap: true)
.FadeIn(wait: FadeInInterval * 3, duration: FadeInDuration);
}
}
else if (RespawnManager.SkillLossPercentageOnDeath > 0)
{
string skillLossAmount = ((int)RespawnManager.SkillLossPercentageOnDeath).ToString();
string skillLossText = $"‖color: { XMLExtensions.ToStringHex(GUIStyle.Red)}‖{skillLossAmount}‖end‖";
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform),
RichString.Rich(TextManager.GetWithVariable("respawnskillpenalty", "[percentage]", skillLossText)))
.FadeIn(wait: FadeInInterval * 3, duration: FadeInDuration);
};
//"what do you want to do" buttons in the middle
//-------------------------------------------------------------------------------------------------------
var decisionButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), content.RectTransform), isHorizontal: true)
{
Stretch = true,
RelativeSpacing = 0.05f
};
if (ironman)
{
// The only option is to spectate
var buttonContainerMiddle = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), decisionButtonContainer.RectTransform), childAnchor: Anchor.Center);
new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), buttonContainerMiddle.RectTransform), TextManager.Get("spectatebutton"))
{
OnClicked = (btn, userdata) =>
{
GameMain.Client?.SendRespawnPromptResponse(waitForNextRoundRespawn: true);
Close();
return true;
}
}.FadeIn(wait: FadeInInterval * 4, duration: FadeInDuration, alsoChildren: true);
}
else
{
var buttonContainerLeft = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), decisionButtonContainer.RectTransform));
var buttonContainerRight = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), decisionButtonContainer.RectTransform));
// The default "I'll wait" button
new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), buttonContainerLeft.RectTransform), TextManager.Get("respawnquestionpromptwait"))
{
OnClicked = (btn, userdata) =>
{
GameMain.Client?.SendRespawnPromptResponse(waitForNextRoundRespawn: true);
Close();
return true;
}
}.FadeIn(wait: FadeInInterval * 4, duration: FadeInDuration, alsoChildren: true);
if (permadeath)
{
if (GameMain.Client != null && GameMain.Client.ServerSettings.AllowBotTakeoverOnPermadeath)
{
new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), buttonContainerRight.RectTransform), TextManager.Get("deathprompt.takeoverbot"))
{
Enabled = false,
OnAddedToGUIUpdateList = (component) =>
{
component.Enabled = GetAvailableBots().Any();
},
OnClicked = (btn, userdata) =>
{
if (takeOverBotPanel == null)
{
CreateTakeOverBotPanel(deathPromptFrame, this);
}
else
{
takeOverBotPanel.Parent?.RemoveChild(takeOverBotPanel);
takeOverBotPanel = null;
}
return true;
}
}.FadeIn(wait: FadeInInterval * 4, duration: FadeInDuration, alsoChildren: true);
}
}
else
{
var respawnNowButton = new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), buttonContainerRight.RectTransform), TextManager.Get("deathprompt.respawnnow"))
{
OnClicked = (btn, userdata) =>
{
GameMain.Client?.SendRespawnPromptResponse(waitForNextRoundRespawn: false);
Close();
return true;
},
Enabled = GameMain.NetworkMember is { ServerSettings.RespawnMode: RespawnMode.MidRound }
};
if (GameMain.NetworkMember is { ServerSettings.RespawnMode: RespawnMode.BetweenRounds })
{
respawnNowButton.ToolTip = TextManager.Get("respawnnotavailable.respawnmode.betweenrounds");
}
respawnNowButton.FadeIn(wait: FadeInInterval * 4, duration: FadeInDuration, alsoChildren: true);
}
//"info buttons" at the bottom
//-------------------------------------------------------------------------------------------------------
var infoButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.2f), content.RectTransform), childAnchor: Anchor.TopRight)
{
Stretch = true,
RelativeSpacing = 0.025f
};
if (permadeath)
{
if (Level.IsLoadedFriendlyOutpost)
{
new GUIButton(new RectTransform(new Vector2(0.6f, 1.0f), infoButtonContainer.RectTransform), TextManager.Get("npctitle.hrmanager"), style: "GUIButtonSmall")
{
OnClicked = (btn, userdata) =>
{
if (GameMain.GameSession?.Campaign is { } campaign)
{
campaign.ShowCampaignUI = true;
campaign.CampaignUI?.SelectTab(CampaignMode.InteractionType.Crew);
}
Close();
return true;
}
}.FadeIn(wait: FadeInInterval * 5, duration: FadeInDuration, alsoChildren: true);
}
}
else
{
new GUIButton(new RectTransform(new Vector2(0.6f, 1.0f), infoButtonContainer.RectTransform), TextManager.Get("deathprompt.showskills"), style: "GUIButtonSmall")
{
OnClicked = (btn, userdata) =>
{
if (skillPanel == null)
{
CreateSkillPanel(deathPromptFrame, GameMain.Client?.Character?.Info ?? GameMain.Client?.CharacterInfo);
}
else
{
skillPanel.Parent?.RemoveChild(skillPanel);
skillPanel = null;
}
return true;
}
}.FadeIn(wait: FadeInInterval * 5, duration: FadeInDuration, alsoChildren: true);
new GUIButton(new RectTransform(new Vector2(0.6f, 1.0f), infoButtonContainer.RectTransform), TextManager.Get("deathprompt.newcharacter"), style: "GUIButtonSmall")
{
OnClicked = (btn, userdata) =>
{
if (newCharacterPanel == null)
{
CreateNewCharacterPanel(deathPromptFrame);
}
else
{
newCharacterPanel.Parent?.RemoveChild(newCharacterPanel);
newCharacterPanel = null;
}
return true;
}
}.FadeIn(wait: FadeInInterval * 5, duration: FadeInDuration, alsoChildren: true);
}
}
this.content = background;
}
private void CreateSkillPanel(GUIComponent parent, CharacterInfo? characterInfo)
{
if (characterInfo == null) { return; }
var frame = new GUIFrame(new RectTransform(new Vector2(1.0f, 1.0f), parent.RectTransform, Anchor.CenterRight, Pivot.CenterLeft));
var content = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.8f), frame.RectTransform, Anchor.Center), isHorizontal: true)
{
Stretch = true
};
var leftColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 1.0f), content.RectTransform))
{
RelativeSpacing = 0.05f
};
var middleColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1.0f), content.RectTransform))
{
RelativeSpacing = 0.05f
};
var rightColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1.0f), content.RectTransform))
{
RelativeSpacing = 0.05f
};
var leftHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), leftColumn.RectTransform), TextManager.Get("Skills"), font: GUIStyle.SubHeadingFont, textColor: GUIStyle.TextColorBright);
var middleHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), middleColumn.RectTransform), TextManager.Get("deathprompt.SkillsLostHeader"), font: GUIStyle.SubHeadingFont, textColor: GUIStyle.TextColorBright);
var rightHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), rightColumn.RectTransform), TextManager.Get("deathprompt.respawnnow"), font: GUIStyle.SubHeadingFont, textColor: GUIStyle.TextColorBright);
GUITextBlock.AutoScaleAndNormalize(leftHeader, middleHeader, rightHeader);
foreach (var skill in characterInfo.Job.GetSkills().OrderByDescending(s => s.Level))
{
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), leftColumn.RectTransform), skill.DisplayName);
int previousSkill = (int)skill.HighestLevelDuringRound;
int reducedSkill = (int)RespawnManager.GetReducedSkill(characterInfo, skill, RespawnManager.SkillLossPercentageOnDeath);
int reducedSkillOnImmediateRespawn = (int)RespawnManager.GetReducedSkill(characterInfo, skill, RespawnManager.SkillLossPercentageOnImmediateRespawn, currentSkillLevel: reducedSkill);
int skillLoss = reducedSkill - previousSkill;
int skillLossOnImmediateRespawn = reducedSkillOnImmediateRespawn - previousSkill;
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), middleColumn.RectTransform),
RichString.Rich($"{reducedSkill} (‖color:{XMLExtensions.ToStringHex(GUIStyle.Red)}‖{skillLoss}‖end‖)"));
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), rightColumn.RectTransform),
RichString.Rich($"{reducedSkillOnImmediateRespawn} (‖color:{XMLExtensions.ToStringHex(GUIStyle.Red)}‖{skillLossOnImmediateRespawn}‖end‖)"));
}
new GUIButton(new RectTransform(new Vector2(1.0f, 0.15f), leftColumn.RectTransform, Anchor.BottomLeft), TextManager.Get("Close"), style: "GUIButtonSmall")
{
IgnoreLayoutGroups = true,
OnClicked = (btn, userdata) =>
{
frame.Parent?.RemoveChild(frame);
skillPanel = null;
return true;
}
};
skillPanel = frame;
}
private void CreateNewCharacterPanel(GUIComponent parent)
{
var frame = new GUIFrame(new RectTransform(new Vector2(1.0f, 1.5f), parent.RectTransform, Anchor.CenterRight, Pivot.CenterLeft));
var content = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), frame.RectTransform, Anchor.Center), isHorizontal: false)
{
Stretch = true,
RelativeSpacing = 0.05f
};
GameMain.NetLobbyScreen.CreatePlayerFrame(content, alwaysAllowEditing: true, createPendingText: false);
var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.15f), content.RectTransform), isHorizontal: true)
{
RelativeSpacing = 0.05f,
Stretch = true
};
new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), buttonContainer.RectTransform, Anchor.BottomLeft), TextManager.Get("Cancel"), style: "GUIButtonSmall")
{
OnClicked = (btn, userdata) =>
{
frame.Parent?.RemoveChild(frame);
newCharacterPanel = null;
return true;
}
};
new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), buttonContainer.RectTransform, Anchor.BottomLeft), TextManager.Get("ApplySettingsYes"), style: "GUIButtonSmall")
{
OnClicked = (btn, userdata) =>
{
GameMain.NetLobbyScreen.TryDiscardCampaignCharacter(onYes: () =>
{
GameMain.Client?.SendCharacterInfo(GameMain.Client.PendingName);
GameMain.NetLobbyScreen.CampaignCharacterDiscarded = false;
frame.Parent?.RemoveChild(frame);
newCharacterPanel = null;
});
return true;
}
};
newCharacterPanel = frame;
}
public static void CreateTakeOverBotPanel()
{
var panelHolder = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.3f), GUI.Canvas, Anchor.Center));
var takeOverBotPanel = CreateTakeOverBotPanel(panelHolder, deathPrompt: null);
if (takeOverBotPanel != null)
{
takeOverBotPanel.RectTransform.SetPosition(Anchor.Center);
GUIMessageBox.MessageBoxes.Add(panelHolder);
}
}
///
/// Static because the "take over bot" panel can be accessed outside the death prompt too
///
private static GUIComponent? CreateTakeOverBotPanel(GUIComponent parent, DeathPrompt? deathPrompt)
{
if (GameMain.GameSession?.CrewManager == null) { return null; }
if (GameMain.GameSession?.Campaign is not MultiPlayerCampaign campaign) { return null; }
if (campaign.CampaignUI == null) { campaign.InitCampaignUI(); }
var frame = new GUIFrame(new RectTransform(new Vector2(1.0f, 1.0f), parent.RectTransform, Anchor.CenterRight, Pivot.CenterLeft));
takeOverBotPanelFrame = frame;
var content = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), frame.RectTransform, Anchor.Center), isHorizontal: false)
{
Stretch = true,
RelativeSpacing = 0.05f
};
var botList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.9f), content.RectTransform));
foreach (CharacterInfo c in GetAvailableBots())
{
var characterFrame = campaign.CampaignUI?.HRManagerUI.CreateCharacterFrame(c, botList, hideSalary: true);
if (characterFrame != null)
{
characterFrame.UserData = c;
}
}
botList.UpdateScrollBarSize();
var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.15f), content.RectTransform), isHorizontal: true)
{
RelativeSpacing = 0.05f,
Stretch = true
};
new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), buttonContainer.RectTransform, Anchor.BottomLeft), TextManager.Get("Cancel"), style: "GUIButtonSmall")
{
OnClicked = (btn, userdata) =>
{
GUIMessageBox.MessageBoxes.Remove(frame.Parent);
frame.Parent?.RemoveChild(frame);
if (deathPrompt != null)
{
deathPrompt.takeOverBotPanel = null;
}
return true;
}
};
new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), buttonContainer.RectTransform, Anchor.BottomLeft), TextManager.Get("inputtype.select"), style: "GUIButtonSmall")
{
Enabled = false,
OnAddedToGUIUpdateList = (component) =>
{
component.Enabled = botList.SelectedData is CharacterInfo;
},
OnClicked = (btn, userdata) =>
{
if (botList.SelectedData is CharacterInfo selectedCharacter && GameMain.Client is GameClient client)
{
if (!GetAvailableBots().Contains(selectedCharacter)) // Someone may have taken over the bot while the list was open, etc
{
CreateTakeOverBotPanel(frame, deathPrompt); // Update
return true;
}
client.SendTakeOverBotRequest(selectedCharacter);
GUIMessageBox.MessageBoxes.Remove(frame.Parent);
deathPrompt?.Close();
return true;
}
else
{
DebugConsole.ThrowError($"Conditions for sending bot takeover request not met");
return false;
}
}
};
if (deathPrompt != null)
{
deathPrompt.takeOverBotPanel = frame;
}
return frame;
}
public void UpdateBotList()
{
if (deathPromptFrame != null && takeOverBotPanelFrame != null)
{
CloseBotPanel();
CreateTakeOverBotPanel(deathPromptFrame, deathPrompt: this);
}
}
private static IEnumerable GetAvailableBots()
{
if (GameMain.GameSession?.CrewManager is { } crewManager)
{
return crewManager.GetCharacterInfos(includeReserveBench: true).Where(c =>
// a bot on reserve bench
c.IsOnReserveBench ||
// an alive bot
(c.Character != null && c.Character is { IsBot: true, IsDead: false }) ||
// a newly hired bot that hasn't spawned yet
(c.Character == null && c.IsNewHire));
}
else
{
return Enumerable.Empty();
}
}
private void DrawBackground(SpriteBatch spriteBatch, GUICustomComponent guiCustomComponent)
{
var background = GUIStyle.GetComponentStyle("DeathScreenBackground");
if (background != null)
{
GUI.DrawBackgroundSprite(spriteBatch, background.GetDefaultSprite(), Color.White * (guiCustomComponent.Color.A / 255.0f));
}
}
public void Close()
{
if (GameMain.GameSession != null)
{
GameMain.GameSession.DeathPrompt = null;
}
}
public static void CloseBotPanel()
{
if (takeOverBotPanelFrame is GUIComponent frame)
{
GUIMessageBox.MessageBoxes.Remove(frame.Parent);
frame.Parent?.RemoveChild(frame);
}
takeOverBotPanelFrame = null;
}
}