4 using Microsoft.Xna.Framework;
5 using Microsoft.Xna.Framework.Graphics;
7 using System.Collections.Generic;
8 using System.Collections.Immutable;
10 using System.Xml.Linq;
14 partial class EventManager
16 private Graph intensityGraph;
17 private Graph targetIntensityGraph;
18 private Graph monsterStrengthGraph;
19 private const float intensityGraphUpdateInterval = 10;
20 private float lastIntensityUpdate;
22 private Vector2 pinnedPosition =
new Vector2(256, 128);
23 private bool isDragging;
27 private bool isGraphSelected;
31 foreach (
Event ev
in activeEvents)
34 drawPos.Y = -drawPos.Y;
36 var textOffset =
new Vector2(-150, 0);
37 spriteBatch.DrawCircle(drawPos, 600, 6, Color.White, thickness: 20);
38 GUI.DrawString(spriteBatch, drawPos + textOffset, ev.
ToString(), Color.White, Color.Black, 0, GUIStyle.LargeFont);
46 DrawEventTargetTags(spriteBatch, scriptedEvent);
49 float theoreticalMaxMonsterStrength = 10000;
51 float absoluteMonsterStrength = monsterStrength / theoreticalMaxMonsterStrength;
52 float relativeMonsterStrength = monsterStrength / relativeMaxMonsterStrength;
54 GUI.DrawString(spriteBatch,
new Vector2(10, y),
"EventManager", Color.White, backgroundColor: Color.Black * 0.6f, font: GUIStyle.SmallFont);
55 DrawString(
"Event cooldown", Math.Max(eventCoolDown, 0), Color.White, spacing: 20);
56 DrawString(
"Current intensity", Math.Round(currentIntensity * 100), Color.Lerp(Color.White, GUIStyle.Red, currentIntensity));
57 DrawString(
"Target intensity", Math.Round(targetIntensity * 100), Color.Lerp(Color.White, GUIStyle.Red, targetIntensity));
58 DrawString(
"Crew health", Math.Round(avgCrewHealth * 100), Color.Lerp(GUIStyle.Red, GUIStyle.Green, avgCrewHealth));
59 DrawString(
"Hull integrity", Math.Round(avgHullIntegrity * 100), Color.Lerp(GUIStyle.Red, GUIStyle.Green, avgHullIntegrity));
60 DrawString(
"Flooding amount", Math.Round(floodingAmount * 100), Color.Lerp(GUIStyle.Green, GUIStyle.Red, floodingAmount));
61 DrawString(
"Fire amount", Math.Round(fireAmount * 100), Color.Lerp(GUIStyle.Green, GUIStyle.Red, fireAmount));
62 DrawString(
"Enemy danger", Math.Round(enemyDanger * 100), Color.Lerp(GUIStyle.Green, GUIStyle.Red, enemyDanger));
63 DrawString(
"Current monster strength (total)", Math.Round(monsterStrength), Color.Lerp(GUIStyle.Green, GUIStyle.Red, relativeMonsterStrength));
68 void DrawString(
string text,
double value, Color textColor,
int spacing = 15)
70 y += GUI.AdjustForTextScale(spacing);
71 GUI.DrawString(spriteBatch,
new Vector2(15, y), $
"{text}: {(int)value}", textColor, backgroundColor: Color.Black * 0.6f, font: GUIStyle.SmallFont);
82 if (intensityGraph ==
null)
84 int graphDensity = 360;
85 intensityGraph =
new Graph(graphDensity);
86 targetIntensityGraph =
new Graph(graphDensity);
87 monsterStrengthGraph =
new Graph(graphDensity);
90 if (Timing.TotalTime > lastIntensityUpdate + intensityGraphUpdateInterval)
92 intensityGraph.Update(currentIntensity);
93 targetIntensityGraph.Update(targetIntensity);
94 monsterStrengthGraph.Update(relativeMonsterStrength);
95 lastIntensityUpdate = (float)Timing.TotalTime;
98 Rectangle graphRect =
new Rectangle(15, (
int)(y + GUI.AdjustForTextScale(55)), (
int)(200 * GUI.xScale), (
int)(100 * GUI.yScale));
102 if (!isGraphSelected && isGraphHovered && leftMousePressed)
104 isGraphSelected =
true;
106 if (isGraphSelected && rightMousePressed)
108 isGraphSelected =
false;
110 Color intensityColor = Color.Lerp(Color.White, GUIStyle.Red, currentIntensity);
111 if (isGraphHovered || isGraphSelected)
116 intensityColor = Color.Red;
117 GUI.DrawRectangle(spriteBatch, graphRect, Color.Black * 0.95f, isFilled:
true);
121 GUI.DrawRectangle(spriteBatch, graphRect, Color.Black * 0.6f, isFilled:
true);
123 intensityGraph.Draw(spriteBatch, graphRect, maxValue: 1.0f, xOffset: 0, intensityColor, (sBatch, value, order, pos) =>
125 if (isGraphHovered || isGraphSelected)
127 Vector2 bottomPoint =
new Vector2(pos.X, graphRect.Bottom);
128 float height = 3 * GUI.yScale;
132 string text = (order / 6).ToString();
133 var font = GUIStyle.SmallFont;
134 Vector2 textSize = font.MeasureString(text);
135 Vector2 textPos =
new Vector2(bottomPoint.X - textSize.X / 2, bottomPoint.Y + height * 1.5f);
136 GUI.DrawString(sBatch, textPos, text, Color.White, font: font);
138 GUI.DrawLine(sBatch, bottomPoint, bottomPoint + Vector2.UnitY * height, Color.White, width: Math.Max(GUI.Scale, 1));
139 DrawTimeStamps(sBatch, Color.Red, pos, order);
142 targetIntensityGraph.Draw(spriteBatch, graphRect, maxValue: 1.0f, xOffset: 0, intensityColor * 0.5f);
143 if (isGraphHovered || isGraphSelected)
146 Color color = Color.White;
147 if (relativeMonsterStrength > 1)
150 color = Color.Yellow;
152 monsterStrengthGraph.Draw(spriteBatch, graphRect, maxValue, color: color, doForEachValue: (sBatch, value, order, pos) => DrawTimeStamps(sBatch, color, pos, order));
155 void DrawTimeStamps(SpriteBatch sBatch, Color color, Vector2 pos,
int order)
157 if (isGraphHovered || isGraphSelected)
159 foreach (var timeStamp
in timeStamps)
161 int t = (int)Math.Abs(Math.Round((timeStamp.Time - lastIntensityUpdate) / intensityGraphUpdateInterval));
165 Vector2 p =
new Vector2(pos.X - size / 2, pos.Y - size / 2);
166 ShapeExtensions.DrawPoint(sBatch, p, color, size);
173 GUI.DrawLine(spriteBatch,
174 new Vector2(graphRect.Right, graphRect.Y + graphRect.Height * (1.0f - eventThreshold)),
175 new Vector2(graphRect.Right + 5, graphRect.Y + graphRect.Height * (1.0f - eventThreshold)), Color.Orange, width: 3);
177 int yStep = (int)(20 * GUI.yScale);
178 y = graphRect.Bottom + yStep;
179 if (isGraphHovered || isGraphSelected)
184 float adjustedYStep = GUI.AdjustForTextScale(15);
187 GUI.DrawString(spriteBatch,
new Vector2(x, y),
"Events frozen (crew away from sub): " + ToolBox.SecondsToReadableTime(settings.
FreezeDurationWhenCrewAway - crewAwayDuration), Color.LightGreen * 0.8f,
null, 0, GUIStyle.SmallFont);
190 else if (crewAwayResetTimer > 0.0f)
192 GUI.DrawString(spriteBatch,
new Vector2(x, y),
"Events frozen (crew just returned to the sub): " + ToolBox.SecondsToReadableTime(crewAwayResetTimer), Color.LightGreen * 0.8f,
null, 0, GUIStyle.SmallFont);
195 else if (eventCoolDown > 0.0f)
197 GUI.DrawString(spriteBatch,
new Vector2(x, y),
"Event cooldown active: " + ToolBox.SecondsToReadableTime(eventCoolDown), Color.LightGreen * 0.8f,
null, 0, GUIStyle.SmallFont);
200 else if (currentIntensity > eventThreshold)
202 GUI.DrawString(spriteBatch,
new Vector2(x, y),
203 "Intensity too high for new events: " + (int)(currentIntensity * 100) +
"%/" + (int)(eventThreshold * 100) +
"%", Color.LightGreen * 0.8f,
null, 0, GUIStyle.SmallFont);
207 adjustedYStep = GUI.AdjustForTextScale(12);
208 foreach (
EventSet eventSet
in pendingEventSets)
212 GUI.DrawString(spriteBatch,
new Vector2(x, y),
"New event (ID " + eventSet.
Identifier +
") after: ", Color.Orange * 0.8f,
null, 0, GUIStyle.SmallFont);
217 GUI.DrawString(spriteBatch,
new Vector2(x, y),
" submarine near cave", Color.Orange * 0.8f,
null, 0, GUIStyle.SmallFont);
222 GUI.DrawString(spriteBatch,
new Vector2(x, y),
" submarine near the wreck", Color.Orange * 0.8f,
null, 0, GUIStyle.SmallFont);
227 GUI.DrawString(spriteBatch,
new Vector2(x, y),
" submarine near the ruins", Color.Orange * 0.8f,
null, 0, GUIStyle.SmallFont);
232 GUI.DrawString(spriteBatch,
new Vector2(x, y),
233 " " + (int) (eventSet.
MinDistanceTraveled * 100.0f) +
"% travelled (current: " + (int) (distanceTraveled * 100.0f) +
" %)",
238 if (CurrentIntensity < eventSet.MinIntensity || CurrentIntensity > eventSet.MaxIntensity)
240 GUI.DrawString(spriteBatch,
new Vector2(x, y),
241 " intensity between " + eventSet.
MinIntensity.FormatDoubleDecimal() +
" and " + eventSet.MaxIntensity.FormatDoubleDecimal(),
242 Color.Orange * 0.8f,
null, 0, GUIStyle.SmallFont);
248 GUI.DrawString(spriteBatch,
new Vector2(x, y),
250 Color.Lerp(GUIStyle.Yellow, GUIStyle.Red, (eventSet.
MinMissionTime - roundDuration)),
null, 0, GUIStyle.SmallFont);
253 y += GUI.AdjustForTextScale(15);
257 y = graphRect.Bottom + yStep * 2;
262 GUI.DrawString(spriteBatch,
new Vector2(x, y),
"Current events: ", Color.White * 0.9f,
null, 0, GUIStyle.SmallFont);
265 adjustedYStep = GUI.AdjustForTextScale(18);
268 GUI.DrawString(spriteBatch,
new Vector2(x + 5, y), ev.
ToString(), (!ev.
IsFinished ? Color.White : Color.Red) * 0.8f,
null, 0, GUIStyle.SmallFont);
273 outlineRect.Inflate(4, 4);
275 if (
PinnedEvent == ev) { GUI.DrawRectangle(spriteBatch, outlineRect, Color.White); }
280 GUI.DrawRectangle(spriteBatch, outlineRect, Color.White);
283 DrawEvent(spriteBatch, ev, rect);
285 else if (rightMousePressed)
289 if (leftMousePressed)
298 y = graphRect.Bottom + yStep * 2;
340 private static void DrawEventTargetTags(SpriteBatch spriteBatch,
ScriptedEvent scriptedEvent)
345 Dictionary<Entity, List<Identifier>> tagsDictionary =
new Dictionary<Entity, List<Identifier>>();
346 foreach ((Identifier key, List<Entity> value) in scriptedEvent.
Targets)
348 foreach (
Entity entity
in value)
350 if (tagsDictionary.ContainsKey(entity))
352 tagsDictionary[entity].Add(key);
356 tagsDictionary.Add(entity,
new List<Identifier> { key });
363 foreach ((Entity entity, List<Identifier> tags) in tagsDictionary)
365 if (entity.Removed) {
continue; }
367 string text = tags.Aggregate(
"Tags:\n", (current, tag) => current + $
" {tag.ColorizeObject()}\n").TrimEnd(
'\r',
'\n');
368 if (!identifier.IsEmpty) { text = $
"Event: {identifier.ColorizeObject()}\n{text}"; }
370 ImmutableArray<RichTextData>? richTextData = RichTextData.GetRichTextData(text, out text);
373 Vector2 infoSize = GUIStyle.SmallFont.MeasureString(text);
375 Vector2 infoPos = entityPos +
new Vector2(128 * cam.
Zoom, -(128 * cam.
Zoom));
376 infoPos.Y -= infoSize.Y / 2;
379 infoRect.Inflate(4, 4);
381 GUI.DrawRectangle(spriteBatch, infoRect, Color.Black * 0.8f, isFilled:
true);
382 GUI.DrawRectangle(spriteBatch, infoRect, Color.White, isFilled:
false);
384 GUI.DrawStringWithColors(spriteBatch, infoPos, text, Color.White, richTextData, font: GUIStyle.SmallFont);
386 GUI.DrawLine(spriteBatch, entityPos,
new Vector2(infoRect.Location.X, infoRect.Location.Y + infoRect.Height / 2), Color.White);
391 private readonly
struct DebugLine
393 public readonly Vector2 Position;
394 public readonly Color Color;
396 public DebugLine(Vector2 position, Color color)
403 private Rectangle DrawEvent(SpriteBatch spriteBatch, Event ev, Rectangle? parentRect =
null)
407 ScriptedEvent scriptedEvent => DrawScriptedEvent(spriteBatch, scriptedEvent, parentRect),
408 ArtifactEvent artifactEvent => DrawArtifactEvent(spriteBatch, artifactEvent, parentRect),
409 MonsterEvent monsterEvent => DrawMonsterEvent(spriteBatch, monsterEvent, parentRect),
414 private Rectangle DrawScriptedEvent(SpriteBatch spriteBatch, ScriptedEvent scriptedEvent, Rectangle? parentRect =
null)
416 List<DebugLine> positions =
new List<DebugLine>();
418 string text = scriptedEvent.GetDebugInfo();
419 if (scriptedEvent.Targets !=
null)
421 foreach ((_, List<Entity> entities) in scriptedEvent.Targets)
423 if (entities ==
null || !entities.Any()) {
continue; }
425 foreach (var entity
in entities)
427 positions.Add(
new DebugLine(entity.WorldPosition, Color.White));
432 return DrawInfoRectangle(spriteBatch, scriptedEvent, text, parentRect, positions);
435 private readonly List<DebugLine> debugPositions =
new List<DebugLine>();
437 private Rectangle DrawArtifactEvent(SpriteBatch spriteBatch, ArtifactEvent artifactEvent, Rectangle? parentRect =
null)
439 debugPositions.Clear();
441 string text = artifactEvent.GetDebugInfo();
443 if (artifactEvent.Item !=
null && !artifactEvent.Item.Removed)
445 Vector2 pos = artifactEvent.Item.WorldPosition;
446 debugPositions.Add(
new DebugLine(pos, Color.White));
449 return DrawInfoRectangle(spriteBatch, artifactEvent, text, parentRect, debugPositions);
452 private Rectangle DrawMonsterEvent(SpriteBatch spriteBatch, MonsterEvent monsterEvent, Rectangle? parentRect =
null)
454 debugPositions.Clear();
456 string text = monsterEvent.GetDebugInfo();
458 if (monsterEvent.SpawnPos !=
null &&
Submarine.MainSub !=
null)
460 Vector2 pos = monsterEvent.SpawnPos.Value;
461 text += $
"Distance from submarine: {Vector2.Distance(pos, Submarine.MainSub.WorldPosition).ColorizeObject()}\n";
462 debugPositions.Add(
new DebugLine(pos, Color.White));
465 if (monsterEvent.Monsters !=
null)
467 text += !monsterEvent.Monsters.Any() ? $
"Monsters: {"None".ColorizeObject()}" :
"Monsters:\n";
469 foreach (Character monster
in monsterEvent.Monsters)
471 text += $
" {monster.ColorizeObject()} -> (Dead: {monster.IsDead.ColorizeObject()}, Health: {monster.HealthPercentage.ColorizeObject()}%, AIState: {(monster.AIController is EnemyAIController enemyAI ? enemyAI.State : AIState.Idle ).ColorizeObject()})\n";
472 if (monster.Removed) {
continue; }
473 debugPositions.Add(
new DebugLine(monster.WorldPosition, Color.Red));
476 return DrawInfoRectangle(spriteBatch, monsterEvent, text, parentRect, debugPositions);
479 private Rectangle DrawInfoRectangle(SpriteBatch spriteBatch, Event @event,
string text, Rectangle? parentRect =
null, List<DebugLine>? drawPoints =
null)
481 text = text.TrimEnd(
'\r',
'\n');
483 Identifier identifier = @
event.Prefab.Identifier;
484 if (!identifier.IsEmpty)
486 text = $
"Identifier: {identifier.ColorizeObject()}\n{text}";
489 ImmutableArray<RichTextData>? richTextData = RichTextData.GetRichTextData(text, out text);
491 Vector2 size = GUIStyle.SmallFont.MeasureString(text);
492 Vector2 pos = pinnedPosition;
496 if (parentRect !=
null)
499 pos =
new Vector2(350, GameMain.GraphicsHeight / 2.0f - size.Y / 2);
500 infoRect =
new Rectangle(pos.ToPoint(), size.ToPoint());
501 infoRect.Inflate(8, 8);
503 GUI.DrawLine(spriteBatch,
new Vector2(rect.Right, rect.Y + rect.Height / 2),
new Vector2(infoRect.X, infoRect.Y + infoRect.Height / 2), Color.White);
507 infoRect =
new Rectangle(pos.ToPoint(), size.ToPoint());
508 infoRect.Inflate(8, 8);
510 Rectangle barRect =
new Rectangle(infoRect.Left, infoRect.Top - 32, infoRect.Width, 32);
511 const string titleHeader =
"Pinned event";
513 GUI.DrawRectangle(spriteBatch, barRect, Color.DarkGray * 0.8f, isFilled:
true);
514 GUI.DrawString(spriteBatch, barRect.Location.ToVector2() + barRect.Size.ToVector2() / 2 - GUIStyle.SubHeadingFont.MeasureString(titleHeader) / 2, titleHeader, Color.White);
515 GUI.DrawRectangle(spriteBatch, barRect, Color.White);
516 infoBarRect = barRect;
519 if (drawPoints !=
null && drawPoints.Any() && Screen.Selected?.Cam !=
null)
521 foreach (DebugLine line
in drawPoints)
523 if (line.Position != Vector2.Zero)
525 float xPos = infoRect.Right;
527 if (parentRect ==
null && pinnedPosition.X + infoRect.Width / 2.0f > GameMain.GraphicsWidth / 2.0f)
529 xPos = infoRect.Left;
532 GUI.DrawLine(spriteBatch,
new Vector2(xPos, infoRect.Top + infoRect.Height / 2), Screen.Selected.Cam.WorldToScreen(line.Position), line.Color);
537 GUI.DrawRectangle(spriteBatch, infoRect, Color.Black * 0.8f, isFilled:
true);
538 GUI.DrawRectangle(spriteBatch, infoRect, Color.White);
540 if (richTextData.HasValue && richTextData.Value.Length > 0)
542 GUI.DrawStringWithColors(spriteBatch, pos, text, Color.White, richTextData.Value,
null, 0, GUIStyle.SmallFont);
546 GUI.DrawString(spriteBatch, pos, text, Color.White,
null, 0, GUIStyle.SmallFont);
548 return infoBarRect ?? infoRect;
560 List<Entity> targets =
new List<Entity>();
561 for (
int i = 0; i < targetCount; i++)
565 if (target !=
null) { targets.Add(target); }
569 if (eventPrefab ==
null) {
return; }
571 foreach (var element
in eventPrefab.ConfigElement.Descendants())
573 if (j != actionIndex)
578 foreach (var subElement
in element.Elements())
580 if (!subElement.Name.ToString().Equals(
"statuseffect", StringComparison.OrdinalIgnoreCase)) {
continue; }
582 foreach (
Entity target
in targets)
584 if (target is
Item item)
586 effect.
Apply(effect.type, 1.0f, item, item.AllPropertyObjects);
607 List<string> options =
new List<string>();
608 for (
int i = 0; i < optionCount; i++)
614 int[] endings =
new int[endCount];
615 for (
int i = 0; i < endCount; i++)
620 if (
string.IsNullOrEmpty(text) && optionCount == 0)
626 (mb as GUIMessageBox)?.Close();
637 speaker.SetCustomInteract(
null,
null);
644 int selectedOption = msg.
ReadByte() - 1;
658 new GUIMessageBox(
string.Empty, TextManager.GetWithVariable(
"missionunlocked",
"[missionname]", missionName),
665 Location location = map.Locations[locationIndex];
666 map.Discover(location, checkTalents:
false);
669 if (destinationIndex != locationIndex && destinationIndex >= 0 && destinationIndex < map.Locations.Count)
671 Location destination = map.Locations[destinationIndex];
672 connection = map.Connections.FirstOrDefault(c => c.Locations.Contains(location) && c.Locations.Contains(destination));
674 if (connection !=
null)
692 DebugConsole.ThrowError($
"Failed to unlock a path on the campaign map. Connection index out of bounds (index: {connectionIndex}, number of connections: {GameMain.GameSession.Map.Connections.Count})");
697 new GUIMessageBox(
string.Empty, TextManager.Get(
"pathunlockedgeneric"),
698 Array.Empty<
LocalizedString>(), type:
GUIMessageBox.
Type.InGame, iconStyle:
"UnlockPathIcon", relativeSize:
new Vector2(0.3f, 0.15f), minSize:
new Point(512, 128));
713 NetEventLogEntry entry = INetSerializableStruct.Read<NetEventLogEntry>(msg);
714 EventLog.AddEntry(entry.EventPrefabId, entry.LogEntryId, entry.Text.Replace(
"\\n",
"\n"));
718 NetEventObjective entry = INetSerializableStruct.Read<NetEventObjective>(msg);
723 entry.ParentObjectiveId,
725 entry.CanBeCompleted);
Vector2 WorldToScreen(Vector2 coords)
Triggers a "conversation popup" with text and support for different branching options.
static void SelectOption(ushort actionId, int option)
static void CreateDialog(LocalizedString text, Character speaker, IEnumerable< string > options, int[] closingOptions, string eventSprite, UInt16 actionId, bool fadeToBlack, DialogTypes dialogType, bool continueConversation=false)
static Entity FindEntityByID(ushort ID)
Find an entity based on the ID
override string ToString()
virtual Vector2 DebugDrawPos
Used to store logs of scripted events (a sort of "quest log")
float CumulativeMonsterStrengthWrecks
void DebugDraw(SpriteBatch spriteBatch)
float CumulativeMonsterStrengthMain
void DrawPinnedEvent(SpriteBatch spriteBatch)
void ClientRead(IReadMessage msg)
void DebugDrawHUD(SpriteBatch spriteBatch, float y)
float CumulativeMonsterStrengthRuins
readonly float FreezeDurationWhenCrewAway
Event sets are sets of random events that occur within a level (most commonly, monster spawns and scr...
static EventPrefab GetEventPrefab(Identifier identifier)
readonly bool PerWreck
The set is applied once per each wreck in the level. Can be used to ensure there's a consistent amoun...
readonly bool PerCave
The set is applied once per each cave in the level. Can be used to ensure there's a consistent amount...
readonly bool PerRuin
The set is applied once per each ruin in the level. Can be used to ensure there's a consistent amount...
readonly float MinIntensity
readonly float MinMissionTime
The event set won't become active until the round has lasted at least this many seconds.
readonly float MinDistanceTraveled
The event set won't become active until the submarine has travelled at least this far....
static readonly List< GUIComponent > MessageBoxes
static GameSession?? GameSession
static NetLobbyScreen NetLobbyScreen
static int GraphicsHeight
void UnlockMission(MissionPrefab missionPrefab, LocationConnection connection)
List< Location > Locations
List< LocationConnection > Connections
static readonly PrefabCollection< MissionPrefab > Prefabs
readonly Identifier Identifier
Dictionary< Identifier, List< Entity > > Targets
StatusEffects can be used to execute various kinds of effects: modifying the state of some entity in ...
static StatusEffect Load(ContentXElement element, string parentDebugName)
virtual void Apply(ActionType type, float deltaTime, Entity entity, ISerializableEntity target, Vector2? worldPosition=null)
static void Trigger(SegmentActionType Type, Identifier Identifier, Identifier ObjectiveTag, Identifier ParentObjectiveId, Identifier TextTag, bool CanBeCompleted, bool autoPlayVideo=false, string videoFile="", int width=450, int height=80)
Identifier ReadIdentifier()