diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs index e994f5954..480a5fd26 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs @@ -1,5 +1,4 @@ -using Barotrauma.Sounds; -using Barotrauma.Particles; +using Barotrauma.Particles; using Microsoft.Xna.Framework; using System.Xml.Linq; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index 0135133fe..31fc357b6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -21,7 +21,6 @@ namespace Barotrauma public static bool DebugDrawInteract; protected float soundTimer; - protected float soundInterval; protected float hudInfoTimer = 1.0f; protected bool hudInfoVisible = false; @@ -130,7 +129,7 @@ namespace Barotrauma } public static bool IsMouseOnUI => GUI.MouseOn != null || - (CharacterInventory.IsMouseOnInventory() && !CharacterInventory.DraggingItemToWorld); + (CharacterInventory.IsMouseOnInventory && !CharacterInventory.DraggingItemToWorld); public class ObjectiveEntity { @@ -161,8 +160,7 @@ namespace Barotrauma partial void InitProjSpecific(XElement mainElement) { - soundInterval = mainElement.GetAttributeFloat("soundinterval", 10.0f); - soundTimer = Rand.Range(0.0f, soundInterval); + soundTimer = Rand.Range(0.0f, Params.SoundInterval); sounds = new List(); Params.Sounds.ForEach(s => sounds.Add(new CharacterSound(s))); @@ -390,12 +388,7 @@ namespace Barotrauma { if (attackResult.Damage <= 1.0f) { return; } } - - if (soundTimer < soundInterval * 0.5f) - { - PlaySound(CharacterSound.SoundType.Damage); - soundTimer = soundInterval; - } + PlaySound(CharacterSound.SoundType.Damage, maxInterval: 2); } partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool log) @@ -470,9 +463,9 @@ namespace Barotrauma } - private List debugInteractablesInRange = new List(); - private List debugInteractablesAtCursor = new List(); - private List> debugInteractablesNearCursor = new List>(); + private readonly List debugInteractablesInRange = new List(); + private readonly List debugInteractablesAtCursor = new List(); + private readonly List<(Item item, float dist)> debugInteractablesNearCursor = new List<(Item item, float dist)>(); /// /// Finds the front (lowest depth) interactable item at a position. "Interactable" in this case means that the character can "reach" the item. @@ -568,7 +561,7 @@ namespace Barotrauma if (distanceToItem > closestItemDistance) { continue; } if (!CanInteractWith(item)) { continue; } - debugInteractablesNearCursor.Add(new Pair(item, 1.0f - distanceToItem / (100.0f * aimAssistModifier))); + debugInteractablesNearCursor.Add((item, 1.0f - distanceToItem / (100.0f * aimAssistModifier))); closestItem = item; closestItemDistance = distanceToItem; } @@ -579,31 +572,20 @@ namespace Barotrauma private Character FindCharacterAtPosition(Vector2 mouseSimPos, float maxDist = 150.0f) { Character closestCharacter = null; - float closestDist = 0.0f; maxDist = ConvertUnits.ToSimUnits(maxDist); - + float closestDist = maxDist * maxDist; foreach (Character c in CharacterList) { if (!CanInteractWith(c, checkVisibility: false) || (c.AnimController?.SimplePhysicsEnabled ?? true)) { continue; } float dist = Vector2.DistanceSquared(mouseSimPos, c.SimPosition); - if (dist < maxDist * maxDist && (closestCharacter == null || dist < closestDist)) + if (dist < closestDist || + (c.CampaignInteractionType != CampaignMode.InteractionType.None && closestCharacter?.CampaignInteractionType == CampaignMode.InteractionType.None && dist * 0.9f < closestDist)) { closestCharacter = c; closestDist = dist; } - - /*FarseerPhysics.Common.Transform transform; - c.AnimController.Collider.FarseerBody.GetTransform(out transform); - for (int i = 0; i < c.AnimController.Collider.FarseerBody.FixtureList.Count; i++) - { - if (c.AnimController.Collider.FarseerBody.FixtureList[i].Shape.TestPoint(ref transform, ref mouseSimPos)) - { - Console.WriteLine("Hit: " + i); - closestCharacter = c; - } - }*/ } return closestCharacter; @@ -638,7 +620,7 @@ namespace Barotrauma if (!enabled) { return; } - if (!IsDead && !IsIncapacitated) + if (!IsIncapacitated) { if (soundTimer > 0) { @@ -649,7 +631,14 @@ namespace Barotrauma switch (enemyAI.State) { case AIState.Attack: - PlaySound(CharacterSound.SoundType.Attack); + if (Rand.Value() > 0.5f) + { + PlaySound(CharacterSound.SoundType.Attack); + } + else + { + PlaySound(CharacterSound.SoundType.Idle); + } break; default: var petBehavior = enemyAI.PetBehavior; @@ -660,7 +649,6 @@ namespace Barotrauma else { PlaySound(CharacterSound.SoundType.Idle); - } break; } @@ -827,12 +815,12 @@ namespace Barotrauma GUI.DrawLine(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y), new Vector2(item.DrawPosition.X, -item.DrawPosition.Y), Color.White * 0.1f, width: 4); } - foreach (Pair item in debugInteractablesNearCursor) + foreach ((Item item, float dist) in debugInteractablesNearCursor) { GUI.DrawLine(spriteBatch, cursorPos, - new Vector2(item.First.DrawPosition.X, -item.First.DrawPosition.Y), - ToolBox.GradientLerp(item.Second, GUI.Style.Red, GUI.Style.Orange, GUI.Style.Green), width: 2); + new Vector2(item.DrawPosition.X, -item.DrawPosition.Y), + ToolBox.GradientLerp(dist, GUI.Style.Red, GUI.Style.Orange, GUI.Style.Green), width: 2); } } return; @@ -856,6 +844,7 @@ namespace Barotrauma Vector2 nameSize = GUI.Font.MeasureString(name); Vector2 namePos = new Vector2(pos.X, pos.Y - 10.0f - (5.0f / cam.Zoom)) - nameSize * 0.5f / cam.Zoom; + Color nameColor = GetNameColor(); Vector2 screenSize = new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight); Vector2 viewportSize = new Vector2(cam.WorldView.Width, cam.WorldView.Height); @@ -865,18 +854,6 @@ namespace Barotrauma namePos *= viewportSize / screenSize; namePos.X += cam.WorldView.X; namePos.Y -= cam.WorldView.Y; - Color nameColor = Color.White; - if (Controlled != null && TeamID != Controlled.TeamID) - { - if (TeamID == CharacterTeamType.FriendlyNPC) - { - nameColor = UniqueNameColor ?? Color.SkyBlue; - } - else - { - nameColor = GUI.Style.Red; - } - } if (CampaignInteractionType != CampaignMode.InteractionType.None && AllowCustomInteract) { var iconStyle = GUI.Style.GetComponentStyle("CampaignInteractionBubble." + CampaignInteractionType); @@ -931,6 +908,40 @@ namespace Barotrauma } } + public Color GetNameColor() + { + CharacterTeamType team = teamID; + if (Info?.IsDisguisedAsAnother != null) + { + var idCard = Inventory.GetItemInLimbSlot(InvSlotType.Card)?.GetComponent(); + if (idCard != null) + { + if (team == CharacterTeamType.Team2 && idCard.TeamID != CharacterTeamType.Team2) + { + team = CharacterTeamType.Team1; + } + else if (team == CharacterTeamType.Team1 && idCard.TeamID == CharacterTeamType.Team2) + { + team = CharacterTeamType.Team2; + } + } + } + + Color nameColor = GUI.Style.TextColor; + if (Controlled != null && team != Controlled.TeamID) + { + if (TeamID == CharacterTeamType.FriendlyNPC) + { + nameColor = UniqueNameColor ?? Color.SkyBlue; + } + else + { + nameColor = GUI.Style.Red; + } + } + return nameColor; + } + /// /// Creates a progress bar that's "linked" to the specified object (or updates an existing one if there's one already linked to the object) /// The progress bar will automatically fade out after 1 sec if the method hasn't been called during that time @@ -958,12 +969,13 @@ namespace Barotrauma private readonly List matchingSounds = new List(); private SoundChannel soundChannel; - public void PlaySound(CharacterSound.SoundType soundType, float soundIntervalFactor = 1.0f) + public void PlaySound(CharacterSound.SoundType soundType, float soundIntervalFactor = 1.0f, float maxInterval = 0) { if (sounds == null || sounds.Count == 0) { return; } if (soundChannel != null && soundChannel.IsPlaying) { return; } if (GameMain.SoundManager?.Disabled ?? true) { return; } - if (soundTimer > soundInterval * soundIntervalFactor) { return; } + if (soundTimer > Params.SoundInterval * soundIntervalFactor) { return; } + if (Params.SoundInterval - soundTimer < maxInterval) { return; } matchingSounds.Clear(); foreach (var s in sounds) { @@ -975,7 +987,7 @@ namespace Barotrauma var selectedSound = matchingSounds.GetRandom(); if (selectedSound?.Sound == null) { return; } soundChannel = SoundPlayer.PlaySound(selectedSound.Sound, AnimController.WorldPosition, selectedSound.Volume, selectedSound.Range, hullGuess: CurrentHull, ignoreMuffling: selectedSound.IgnoreMuffling); - soundTimer = soundInterval; + soundTimer = Params.SoundInterval; } public void AddActiveObjectiveEntity(Entity entity, Sprite sprite, Color? color = null) @@ -1028,5 +1040,20 @@ namespace Barotrauma Rand.Range(50.0f, 500.0f), null); } } + + partial void OnMoneyChanged(int prevAmount, int newAmount) + { + if (newAmount > prevAmount) + { + int increase = newAmount - prevAmount; + GUI.AddMessage( + "+" + TextManager.GetWithVariable("currencyformat", "[credits]", increase.ToString()), + GUI.Style.Yellow, + Position + Vector2.UnitY * 150.0f, + Vector2.UnitY * 10.0f, + playSound: true, + subId: Submarine?.ID ?? -1);; + } + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index b208ae945..89859485a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -391,7 +391,26 @@ namespace Barotrauma if (npc.CampaignInteractionType == CampaignMode.InteractionType.None || npc.Submarine != character.Submarine || npc.IsDead || npc.IsIncapacitated) { continue; } var iconStyle = GUI.Style.GetComponentStyle("CampaignInteractionIcon." + npc.CampaignInteractionType); - GUI.DrawIndicator(spriteBatch, npc.WorldPosition, cam, npc.CurrentHull == character.CurrentHull ? 500.0f : 100.0f, iconStyle.GetDefaultSprite(), iconStyle.Color); + Range visibleRange = new Range(npc.CurrentHull == Character.Controlled.CurrentHull ? 500.0f : 100.0f, float.PositiveInfinity); + if (npc.CampaignInteractionType == CampaignMode.InteractionType.Examine) + { + //TODO: we could probably do better than just hardcoding + //a check for InteractionType.Examine here. + + if (Vector2.DistanceSquared(character.Position, npc.Position) > 500f * 500f) { continue; } + + var body = Submarine.CheckVisibility(character.SimPosition, npc.SimPosition, ignoreLevel: true); + if (body != null && body.UserData as Character != npc) { continue; } + + visibleRange = new Range(-100f, 500f); + } + GUI.DrawIndicator( + spriteBatch, + npc.WorldPosition, + cam, + visibleRange, + iconStyle.GetDefaultSprite(), + iconStyle.Color); } foreach (Item item in Item.ItemList) @@ -400,7 +419,7 @@ namespace Barotrauma if (Vector2.DistanceSquared(character.Position, item.Position) > 500f*500f) { continue; } var body = Submarine.CheckVisibility(character.SimPosition, item.SimPosition, ignoreLevel: true); if (body != null && body.UserData as Item != item) { continue; } - GUI.DrawIndicator(spriteBatch, item.WorldPosition + new Vector2(0f, item.RectHeight * 0.65f), cam, new Vector2(-100f, 500.0f), item.IconStyle.GetDefaultSprite(), item.IconStyle.Color, createOffset: false); + GUI.DrawIndicator(spriteBatch, item.WorldPosition + new Vector2(0f, item.RectHeight * 0.65f), cam, new Range(-100f, 500.0f), item.IconStyle.GetDefaultSprite(), item.IconStyle.Color, createOffset: false); } } @@ -525,12 +544,7 @@ namespace Barotrauma textPos -= new Vector2(textSize.X / 2, textSize.Y); - Color nameColor = GUI.Style.TextColor; - if (character.TeamID != character.FocusedCharacter.TeamID) - { - nameColor = character.FocusedCharacter.TeamID == CharacterTeamType.FriendlyNPC ? Color.SkyBlue : GUI.Style.Red; - } - + Color nameColor = character.FocusedCharacter.GetNameColor(); GUI.DrawString(spriteBatch, textPos, focusName, nameColor, Color.Black * 0.7f, 2, GUI.SubHeadingFont); textPos.X += 10.0f * GUI.Scale; textPos.Y += GUI.SubHeadingFont.MeasureString(focusName).Y; @@ -544,11 +558,14 @@ namespace Barotrauma if (character.FocusedCharacter.CanBeDragged) { - GUI.DrawString(spriteBatch, textPos, GetCachedHudText("GrabHint", GameMain.Config.KeyBindText(InputType.Grab)), + string text = character.CanEat ? "EatHint" : "GrabHint"; + GUI.DrawString(spriteBatch, textPos, GetCachedHudText(text, GameMain.Config.KeyBindText(InputType.Grab)), GUI.Style.Green, Color.Black, 2, GUI.SmallFont); textPos.Y += largeTextSize.Y; } + if (!character.DisableHealthWindow && + character.IsFriendly(character.FocusedCharacter) && character.FocusedCharacter.CharacterHealth.UseHealthWindow && character.CanInteractWith(character.FocusedCharacter, 160f, false)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index 1d2892d72..c27aab975 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -170,15 +170,37 @@ namespace Barotrauma if (TeamID == CharacterTeamType.FriendlyNPC) { return; } if (Character.Controlled != null && Character.Controlled.TeamID != TeamID) { return; } + // if we increased by more than 1 in one increase, then display special color (for talents) + bool specialIncrease = Math.Abs(newLevel - prevLevel) >= 1.0f; + if ((int)newLevel > (int)prevLevel) { int increase = Math.Max((int)newLevel - (int)prevLevel, 1); GUI.AddMessage( - string.Format("+{0} {1}", increase, TextManager.Get("SkillName." + skillIdentifier)), - GUI.Style.Green, + string.Format("+{0} {1}", increase, TextManager.Get("SkillName." + skillIdentifier)), + specialIncrease ? GUI.Style.Orange : GUI.Style.Green, textPopupPos, Vector2.UnitY * 10.0f, - playSound: false, + playSound: specialIncrease, + subId: Character?.Submarine?.ID ?? -1); + } + } + + partial void OnExperienceChanged(int prevAmount, int newAmount, Vector2 textPopupPos) + { + if (Character.Controlled != null && Character.Controlled.TeamID != TeamID) { return; } + + GameSession.TabMenuInstance?.OnExperienceChanged(Character); + + if (newAmount > prevAmount) + { + int increase = newAmount - prevAmount; + GUI.AddMessage( + string.Format("+{0} {1}", increase, TextManager.Get("experienceshort")), + GUI.Style.Blue, + textPopupPos, + Vector2.UnitY * 10.0f, + playSound: true, subId: Character?.Submarine?.ID ?? -1); } } @@ -591,6 +613,17 @@ namespace Barotrauma } ch.Job.Skills.RemoveAll(s => !skillLevels.ContainsKey(s.Identifier)); } + + byte savedStatValueCount = inc.ReadByte(); + for (int i = 0; i < savedStatValueCount; i++) + { + int statType = inc.ReadByte(); + string statIdentifier = inc.ReadString(); + float statValue = inc.ReadSingle(); + bool removeOnDeath = inc.ReadBoolean(); + ch.ChangeSavedStatValue((StatTypes)statType, statValue, statIdentifier, removeOnDeath); + } + return ch; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index 0798a00d3..34a5e6378 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -119,15 +119,23 @@ namespace Barotrauma switch ((NetEntityEvent.Type)extraData[0]) { case NetEntityEvent.Type.InventoryState: - msg.WriteRangedInteger(0, 0, 3); + msg.WriteRangedInteger(0, 0, 4); Inventory.ClientWrite(msg, extraData); break; case NetEntityEvent.Type.Treatment: - msg.WriteRangedInteger(1, 0, 3); + msg.WriteRangedInteger(1, 0, 4); msg.Write(AnimController.Anim == AnimController.Animation.CPR); break; case NetEntityEvent.Type.Status: - msg.WriteRangedInteger(2, 0, 3); + msg.WriteRangedInteger(2, 0, 4); + break; + case NetEntityEvent.Type.UpdateTalents: + msg.WriteRangedInteger(3, 0, 4); + msg.Write((ushort)characterTalents.Count); + foreach (var unlockedTalent in characterTalents) + { + msg.Write(unlockedTalent.Prefab.UIntIdentifier); + } break; } } @@ -258,7 +266,7 @@ namespace Barotrauma if (readStatus) { ReadStatus(msg); - (AIController as EnemyAIController)?.PetBehavior?.ClientRead(msg); + AIController?.ClientRead(msg); } msg.ReadPadBits(); @@ -291,7 +299,7 @@ namespace Barotrauma break; case ServerNetObject.ENTITY_EVENT: - int eventType = msg.ReadRangedInteger(0, 9); + int eventType = msg.ReadRangedInteger(0, 12); switch (eventType) { case 0: //NetEntityEvent.Type.InventoryState @@ -387,6 +395,7 @@ namespace Barotrauma if (eventType == 4) { SetAttackTarget(attackLimb, targetEntity, targetSimPos); + PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3); } else { @@ -450,6 +459,23 @@ namespace Barotrauma } } break; + case 10: //NetEntityEvent.Type.UpdateExperience + int experienceAmount = msg.ReadInt32(); + info?.SetExperience(experienceAmount); + break; + case 11: //NetEntityEvent.Type.UpdateTalents: + ushort talentCount = msg.ReadUInt16(); + for (int i = 0; i < talentCount; i++) + { + UInt32 talentIdentifier = msg.ReadUInt32(); + GiveTalent(talentIdentifier); + } + break; + case 12: //NetEntityEvent.Type.UpdateMoney: + int moneyAmount = msg.ReadInt32(); + SetMoney(moneyAmount); + break; + } msg.ReadPadBits(); break; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index 4dc901c4f..d7b75bbd3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -543,7 +543,7 @@ namespace Barotrauma else { var causeOfDeath = GetCauseOfDeath(); - Character.Controlled.Kill(causeOfDeath.First, causeOfDeath.Second); + Character.Controlled.Kill(causeOfDeath.type, causeOfDeath.affliction); Character.Controlled = null; } } @@ -683,19 +683,33 @@ namespace Barotrauma float particleMaxScale = emitter?.Prefab.Properties.ScaleMax ?? 1; float severity = Math.Min(affliction.Strength / affliction.Prefab.MaxStrength * Character.Params.BleedParticleMultiplier, 1); float bloodParticleSize = MathHelper.Lerp(particleMinScale, particleMaxScale, severity); + + Vector2 velocity = Rand.Vector(affliction.Strength * 0.1f); if (!inWater) { bloodParticleSize *= 2.0f; + velocity = targetLimb.LinearVelocity * 100.0f; } // TODO: use the blood emitter? var blood = GameMain.ParticleManager.CreateParticle( inWater ? Character.Params.BleedParticleWater : Character.Params.BleedParticleAir, - targetLimb.WorldPosition, Rand.Vector(affliction.Strength), 0.0f, Character.AnimController.CurrentHull); + targetLimb.WorldPosition, velocity, 0.0f, Character.AnimController.CurrentHull); - if (blood != null) + if (blood != null && !inWater) { blood.Size *= bloodParticleSize; + if (!string.IsNullOrEmpty(Character.BloodDecalName) && Rand.Range(0.0f, 1.0f) < 0.05f) + { + blood.OnCollision += (Vector2 pos, Hull hull) => + { + var decal = hull?.AddDecal(Character.BloodDecalName, pos, Rand.Range(1.0f, 2.0f), isNetworkEvent: true); + if (decal != null) + { + decal.FadeTimer = decal.LifeTime - decal.FadeOutTime * 2; + } + }; + } } bloodParticleTimer = MathHelper.Lerp(2, 0.5f, severity); } @@ -1968,9 +1982,9 @@ namespace Barotrauma healthBarHolder.Visible = value; } - private readonly List> newAfflictions = new List>(); - private readonly List> newLimbAfflictions = new List>(); - private readonly List> newPeriodicEffects = new List>(); + private readonly List<(AfflictionPrefab afflictionPrefab, float strength)> newAfflictions = new List<(AfflictionPrefab afflictionPrefab, float strength)>(); + private readonly List<(LimbHealth limb, AfflictionPrefab afflictionPrefab, float strength)> newLimbAfflictions = new List<(LimbHealth limb, AfflictionPrefab afflictionPrefab, float strength)>(); + private readonly List<(AfflictionPrefab.PeriodicEffect effect, float timer)> newPeriodicEffects = new List<(AfflictionPrefab.PeriodicEffect effect, float timer)>(); public void ClientRead(IReadMessage inc) { @@ -1997,41 +2011,41 @@ namespace Barotrauma for (int j = 0; j < periodicAfflictionCount; j++) { float periodicAfflictionTimer = inc.ReadRangedSingle(afflictionPrefab.PeriodicEffects[j].MinInterval, afflictionPrefab.PeriodicEffects[j].MaxInterval, 8); - newPeriodicEffects.Add(new Pair(afflictionPrefab.PeriodicEffects[j], periodicAfflictionTimer)); + newPeriodicEffects.Add((afflictionPrefab.PeriodicEffects[j], periodicAfflictionTimer)); } - newAfflictions.Add(new Pair(afflictionPrefab, afflictionStrength)); + newAfflictions.Add((afflictionPrefab, afflictionStrength)); } foreach (Affliction affliction in afflictions) { //deactivate afflictions that weren't included in the network message - if (!newAfflictions.Any(a => a.First == affliction.Prefab)) + if (!newAfflictions.Any(a => a.afflictionPrefab == affliction.Prefab)) { affliction.Strength = 0.0f; } } - foreach (Pair newAffliction in newAfflictions) + foreach (var (afflictionPrefab, strength) in newAfflictions) { - Affliction existingAffliction = afflictions.Find(a => a.Prefab == newAffliction.First); + Affliction existingAffliction = afflictions.Find(a => a.Prefab == afflictionPrefab); if (existingAffliction == null) { - existingAffliction = newAffliction.First.Instantiate(newAffliction.Second); + existingAffliction = afflictionPrefab.Instantiate(strength); afflictions.Add(existingAffliction); } - existingAffliction.SetStrength(newAffliction.Second); + existingAffliction.SetStrength(strength); if (existingAffliction == stunAffliction) { Character.SetStun(existingAffliction.Strength, true, true); } foreach (var periodicEffect in newPeriodicEffects) { - if (!existingAffliction.Prefab.PeriodicEffects.Contains(periodicEffect.First)) { continue; } + if (!existingAffliction.Prefab.PeriodicEffects.Contains(periodicEffect.effect)) { continue; } //timer has wrapped around, apply the effect - if (periodicEffect.Second - existingAffliction.PeriodicEffectTimers[periodicEffect.First] > periodicEffect.First.MinInterval / 2) + if (periodicEffect.timer - existingAffliction.PeriodicEffectTimers[periodicEffect.effect] > periodicEffect.effect.MinInterval / 2) { - existingAffliction.PeriodicEffectTimers[periodicEffect.First] = periodicEffect.Second; - foreach (StatusEffect effect in periodicEffect.First.StatusEffects) + existingAffliction.PeriodicEffectTimers[periodicEffect.effect] = periodicEffect.timer; + foreach (StatusEffect effect in periodicEffect.effect.StatusEffects) { existingAffliction.ApplyStatusEffect(ActionType.OnActive, effect, deltaTime: 1.0f, this, targetLimb: null); } @@ -2063,9 +2077,9 @@ namespace Barotrauma for (int j = 0; j < periodicAfflictionCount; j++) { float periodicAfflictionTimer = inc.ReadRangedSingle(afflictionPrefab.PeriodicEffects[j].MinInterval, afflictionPrefab.PeriodicEffects[j].MaxInterval, 8); - newPeriodicEffects.Add(new Pair(afflictionPrefab.PeriodicEffects[j], periodicAfflictionTimer)); + newPeriodicEffects.Add((afflictionPrefab.PeriodicEffects[j], periodicAfflictionTimer)); } - newLimbAfflictions.Add(new Triplet(limbHealths[limbIndex], afflictionPrefab, afflictionStrength)); + newLimbAfflictions.Add((limbHealths[limbIndex], afflictionPrefab, afflictionStrength)); } foreach (LimbHealth limbHealth in limbHealths) @@ -2073,33 +2087,33 @@ namespace Barotrauma foreach (Affliction affliction in limbHealth.Afflictions) { //deactivate afflictions that weren't included in the network message - if (!newLimbAfflictions.Any(a => a.First == limbHealth && a.Second == affliction.Prefab)) + if (!newLimbAfflictions.Any(a => a.limb == limbHealth && a.afflictionPrefab == affliction.Prefab)) { affliction.Strength = 0.0f; } } - foreach (Triplet newAffliction in newLimbAfflictions) + foreach (var (limb, afflictionPrefab, strength) in newLimbAfflictions) { - if (newAffliction.First != limbHealth) { continue; } - Affliction existingAffliction = limbHealth.Afflictions.Find(a => a.Prefab == newAffliction.Second); + if (limb != limbHealth) { continue; } + Affliction existingAffliction = limbHealth.Afflictions.Find(a => a.Prefab == afflictionPrefab); if (existingAffliction == null) { - existingAffliction = newAffliction.Second.Instantiate(newAffliction.Third); + existingAffliction = afflictionPrefab.Instantiate(strength); limbHealth.Afflictions.Add(existingAffliction); } - existingAffliction.SetStrength(newAffliction.Third); + existingAffliction.SetStrength(strength); foreach (var periodicEffect in newPeriodicEffects) { - if (!existingAffliction.Prefab.PeriodicEffects.Contains(periodicEffect.First)) { continue; } + if (!existingAffliction.Prefab.PeriodicEffects.Contains(periodicEffect.effect)) { continue; } //timer has wrapped around, apply the effect - if (periodicEffect.Second - existingAffliction.PeriodicEffectTimers[periodicEffect.First] > periodicEffect.First.MinInterval / 2) + if (periodicEffect.timer - existingAffliction.PeriodicEffectTimers[periodicEffect.effect] > periodicEffect.effect.MinInterval / 2) { - existingAffliction.PeriodicEffectTimers[periodicEffect.First] = periodicEffect.Second; - foreach (StatusEffect effect in periodicEffect.First.StatusEffects) + existingAffliction.PeriodicEffectTimers[periodicEffect.effect] = periodicEffect.timer; + foreach (StatusEffect effect in periodicEffect.effect.StatusEffects) { - Limb targetLimb = Character.AnimController.Limbs.FirstOrDefault(l => l.HealthIndex == limbHealths.IndexOf(newAffliction.First)); + Limb targetLimb = Character.AnimController.Limbs.FirstOrDefault(l => l.HealthIndex == limbHealths.IndexOf(limb)); existingAffliction.ApplyStatusEffect(ActionType.OnActive, effect, deltaTime: 1.0f, this, targetLimb: targetLimb); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index 89805c561..69c8a1836 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -418,14 +418,14 @@ namespace Barotrauma ShowQuestionPrompt("The automatic hull generation may not work correctly if your submarine uses curved walls. Do you want to continue? Y/N", (option2) => { - if (option2.ToLower() == "y") { GameMain.SubEditorScreen.AutoHull(); } + if (option2.ToLowerInvariant() == "y") { GameMain.SubEditorScreen.AutoHull(); } }); }); } else { ShowQuestionPrompt("The automatic hull generation may not work correctly if your submarine uses curved walls. Do you want to continue? Y/N", - (option) => { if (option.ToLower() == "y") GameMain.SubEditorScreen.AutoHull(); }); + (option) => { if (option.ToLowerInvariant() == "y") GameMain.SubEditorScreen.AutoHull(); }); } })); @@ -608,7 +608,7 @@ namespace Barotrauma ShowQuestionPrompt($"Some keybinds may render the game unusable, are you sure you want to make these keybinds persistent? ({Keybinds.Count} keybind(s) assigned) Y/N", (option2) => { - if (option2.ToLower() != "y") + if (option2.ToLowerInvariant() != "y") { NewMessage("Aborted.", GUI.Style.Red); return; @@ -689,6 +689,9 @@ namespace Barotrauma AssignRelayToServer("setskill", true); AssignRelayToServer("readycheck", true); + AssignRelayToServer("givetalent", true); + AssignRelayToServer("giveexperience", true); + AssignOnExecute("control", (string[] args) => { if (args.Length < 1) return; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index 57e963b91..e3141a7b8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -163,6 +163,7 @@ namespace Barotrauma public static ScalableFont SubHeadingFont => Style?.SubHeadingFont; public static ScalableFont DigitalFont => Style?.DigitalFont; public static ScalableFont HotkeyFont => Style?.HotkeyFont; + public static ScalableFont MonospacedFont => Style?.MonospacedFont; public static ScalableFont CJKFont { get; private set; } @@ -306,7 +307,7 @@ namespace Barotrauma }); SubmarineIcon = new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(452, 385, 182, 81), new Vector2(0.5f, 0.5f)); - arrow = new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(392, 393, 49, 45), new Vector2(0.5f, 0.5f)); + arrow = new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(393, 393, 49, 45), new Vector2(0.5f, 0.5f)); SpeechBubbleIcon = new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(385, 449, 66, 60), new Vector2(0.5f, 0.5f)); BrokenIcon = new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(898, 386, 123, 123), new Vector2(0.5f, 0.5f)); } @@ -672,6 +673,12 @@ namespace Barotrauma spriteBatch.End(); spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: SamplerStateClamp, rasterizerState: GameMain.ScissorTestEnable); + if (GameMain.GameSession?.CrewManager is { DraggedOrder: { SymbolSprite: { } orderSprite, Color: var color }, DragOrder: true }) + { + float spriteSize = Math.Max(orderSprite.size.X, orderSprite.size.Y); + orderSprite.Draw(spriteBatch, PlayerInput.LatestMousePosition, color, orderSprite.size / 2f, scale: 32f / spriteSize * Scale); + } + var sprite = MouseCursorSprites[(int)MouseCursor] ?? MouseCursorSprites[(int)CursorState.Default]; sprite.Draw(spriteBatch, PlayerInput.LatestMousePosition, Color.White, sprite.Origin, 0f, Scale / 1.5f); @@ -927,13 +934,14 @@ namespace Barotrauma GUIComponent prevMouseOn = MouseOn; MouseOn = null; int inventoryIndex = -1; - - if (Inventory.IsMouseOnInventory()) + + Inventory.RefreshMouseOnInventory(); + if (Inventory.IsMouseOnInventory) { inventoryIndex = updateList.IndexOf(CharacterHUD.HUDFrame); } - if (!PlayerInput.PrimaryMouseButtonHeld() && !PlayerInput.PrimaryMouseButtonClicked()) + if ((!PlayerInput.PrimaryMouseButtonHeld() && !PlayerInput.PrimaryMouseButtonClicked()) || prevMouseOn == null) { for (var i = updateList.Count - 1; i > inventoryIndex; i--) { @@ -941,10 +949,9 @@ namespace Barotrauma if (!c.CanBeFocused) { continue; } if (c.MouseRect.Contains(PlayerInput.MousePosition)) { - if ((!PlayerInput.PrimaryMouseButtonHeld() && !PlayerInput.PrimaryMouseButtonClicked()) || c == prevMouseOn) + if ((!PlayerInput.PrimaryMouseButtonHeld() && !PlayerInput.PrimaryMouseButtonClicked()) || c == prevMouseOn || prevMouseOn == null) { MouseOn = c; - var sakdjfnsjkd = c.MouseRect; } break; } @@ -958,7 +965,6 @@ namespace Barotrauma MouseCursor = UpdateMouseCursorState(MouseOn); return MouseOn; } - } private static CursorState UpdateMouseCursorState(GUIComponent c) @@ -1050,7 +1056,7 @@ namespace Barotrauma { if (listBox.DraggedElement != null) { return CursorState.Dragging; } if (listBox.CanDragElements) { return CursorState.Move; } - + var hoverParent = c; while (true) { @@ -1059,14 +1065,14 @@ namespace Barotrauma hoverParent = hoverParent.Parent; } } - + if (parent != null && parent.CanBeFocused) { if (!parent.Rect.Equals(monitorRect)) { return parent.HoverCursor; } } } - - if (Inventory.IsMouseOnInventory()) { return Inventory.GetInventoryMouseCursor(); } + + if (Inventory.IsMouseOnInventory) { return Inventory.GetInventoryMouseCursor(); } var character = Character.Controlled; // ReSharper disable once InvertIf @@ -1343,7 +1349,7 @@ namespace Barotrauma /// Should the indicator move based on the camera position? /// Override the distance-based alpha value with the specified alpha value - public static void DrawIndicator(SpriteBatch spriteBatch, in Vector2 worldPosition, Camera cam, in Vector2 visibleRange, Sprite sprite, in Color color, + public static void DrawIndicator(SpriteBatch spriteBatch, in Vector2 worldPosition, Camera cam, in Range visibleRange, Sprite sprite, in Color color, bool createOffset = true, float scaleMultiplier = 1.0f, float? overrideAlpha = null) { Vector2 diff = worldPosition - cam.WorldViewCenter; @@ -1351,9 +1357,9 @@ namespace Barotrauma float symbolScale = Math.Min(64.0f / sprite.size.X, 1.0f) * scaleMultiplier * Scale; - if (overrideAlpha.HasValue || (dist > visibleRange.X && dist < visibleRange.Y)) + if (overrideAlpha.HasValue || (dist > visibleRange.Start && dist < visibleRange.End)) { - float alpha = overrideAlpha ?? MathUtils.Min((dist - visibleRange.X) / 100.0f, 1.0f - ((dist - visibleRange.Y + 100f) / 100.0f), 1.0f); + float alpha = overrideAlpha ?? MathUtils.Min((dist - visibleRange.Start) / 100.0f, 1.0f - ((dist - visibleRange.End + 100f) / 100.0f), 1.0f); Vector2 targetScreenPos = cam.WorldToScreen(worldPosition); if (!createOffset) @@ -1417,7 +1423,7 @@ namespace Barotrauma public static void DrawIndicator(SpriteBatch spriteBatch, Vector2 worldPosition, Camera cam, float hideDist, Sprite sprite, Color color, bool createOffset = true, float scaleMultiplier = 1.0f, float? overrideAlpha = null) { - DrawIndicator(spriteBatch, worldPosition, cam, new Vector2(hideDist, float.PositiveInfinity), sprite, color, createOffset, scaleMultiplier, overrideAlpha); + DrawIndicator(spriteBatch, worldPosition, cam, new Range(hideDist, float.PositiveInfinity), sprite, color, createOffset, scaleMultiplier, overrideAlpha); } public static void DrawLine(SpriteBatch sb, Vector2 start, Vector2 end, Color clr, float depth = 0.0f, float width = 1) @@ -1520,6 +1526,11 @@ namespace Barotrauma } } + public static void DrawFilledRectangle(SpriteBatch sb, RectangleF rect, Color clr, float depth = 0.0f) + { + DrawFilledRectangle(sb, rect.Location, rect.Size, clr, depth); + } + public static void DrawFilledRectangle(SpriteBatch sb, Vector2 start, Vector2 size, Color clr, float depth = 0.0f) { if (size.X < 0) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs index ecc75885a..34e99bc03 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs @@ -52,13 +52,13 @@ namespace Barotrauma public GUIComponent GetChild(int index) { - if (index < 0 || index >= CountChildren) return null; + if (index < 0 || index >= CountChildren) { return null; } return RectTransform.GetChild(index).GUIComponent; } public int GetChildIndex(GUIComponent child) { - if (child == null) return -1; + if (child == null) { return -1; } return RectTransform.GetChildIndex(child.RectTransform); } @@ -66,7 +66,7 @@ namespace Barotrauma { foreach (GUIComponent child in Children) { - if (child.UserData == obj || (child.userData != null && child.userData.Equals(obj))) return child; + if (child.UserData == obj || (child.userData != null && child.userData.Equals(obj))) { return child; } } return null; } @@ -175,6 +175,8 @@ namespace Barotrauma public bool GlowOnSelect { get; set; } + public Vector2 UVOffset { get; set; } + private CoroutineHandle pulsateCoroutine; protected Color flashColor; @@ -256,9 +258,9 @@ namespace Barotrauma protected Rectangle ClampRect(Rectangle r) { - if (Parent == null || !ClampMouseRectToParent) return r; + if (Parent == null || !ClampMouseRectToParent) { return r; } Rectangle parentRect = Parent.ClampRect(Parent.Rect); - if (parentRect.Width <= 0 || parentRect.Height <= 0) return Rectangle.Empty; + if (parentRect.Width <= 0 || parentRect.Height <= 0) { return Rectangle.Empty; } if (parentRect.X > r.X) { int diff = parentRect.X - r.X; @@ -281,7 +283,7 @@ namespace Barotrauma int diff = (r.Y + r.Height) - (parentRect.Y + parentRect.Height); r.Height -= diff; } - if (r.Width <= 0 || r.Height <= 0) return Rectangle.Empty; + if (r.Width <= 0 || r.Height <= 0) { return Rectangle.Empty; } return r; } @@ -295,7 +297,7 @@ namespace Barotrauma { get { - if (!CanBeFocused) return Rectangle.Empty; + if (!CanBeFocused) { return Rectangle.Empty; } return ClampMouseRectToParent ? ClampRect(Rect) : Rect; } } @@ -431,7 +433,7 @@ namespace Barotrauma #region Updating public virtual void AddToGUIUpdateList(bool ignoreChildren = false, int order = 0) { - if (!Visible) return; + if (!Visible) { return; } UpdateOrder = order; GUI.AddToUpdateList(this); @@ -463,7 +465,7 @@ namespace Barotrauma /// public void UpdateManually(float deltaTime, bool alsoChildren = false, bool recursive = true) { - if (!Visible) return; + if (!Visible) { return; } AutoUpdate = false; Update(deltaTime); @@ -475,7 +477,7 @@ namespace Barotrauma protected virtual void Update(float deltaTime) { - if (!Visible) return; + if (!Visible) { return; } if (CanBeFocused && OnSecondaryClicked != null) { @@ -555,7 +557,7 @@ namespace Barotrauma /// public virtual void DrawManually(SpriteBatch spriteBatch, bool alsoChildren = false, bool recursive = true) { - if (!Visible) return; + if (!Visible) { return; } AutoDraw = false; Draw(spriteBatch); @@ -598,7 +600,7 @@ namespace Barotrauma protected virtual void Draw(SpriteBatch spriteBatch) { - if (!Visible) return; + if (!Visible) { return; } var rect = Rect; GetBlendedColor(GetColor(State), ref _currentColor); @@ -653,7 +655,7 @@ namespace Barotrauma ? MathUtils.InverseLerp(0, SpriteCrossFadeTime, ToolBox.GetEasing(uiSprite.TransitionMode, spriteFadeTimer)) : 0; if (alphaMultiplier > 0) { - uiSprite.Draw(spriteBatch, rect, previousColor * alphaMultiplier, SpriteEffects); + uiSprite.Draw(spriteBatch, rect, previousColor * alphaMultiplier, SpriteEffects, uvOffset: UVOffset); } } } @@ -667,7 +669,11 @@ namespace Barotrauma ? MathUtils.InverseLerp(SpriteCrossFadeTime, 0, ToolBox.GetEasing(uiSprite.TransitionMode, spriteFadeTimer)) : (_currentColor.A / 255.0f); if (alphaMultiplier > 0) { - uiSprite.Draw(spriteBatch, rect, _currentColor * alphaMultiplier, SpriteEffects); + // * (rect.Location.Y - PlayerInput.MousePosition.Y) / rect.Height + Vector2 offset = new Vector2( + MathUtils.PositiveModulo((int)-UVOffset.X, uiSprite.Sprite.SourceRect.Width), + MathUtils.PositiveModulo((int)-UVOffset.Y, uiSprite.Sprite.SourceRect.Height)); + uiSprite.Draw(spriteBatch, rect, _currentColor * alphaMultiplier, SpriteEffects, uvOffset: offset); } } } @@ -708,7 +714,7 @@ namespace Barotrauma /// public void DrawToolTip(SpriteBatch spriteBatch) { - if (!Visible) return; + if (!Visible) { return; } DrawToolTip(spriteBatch, ToolTip, GUI.MouseOn.Rect, TooltipRichTextData); } @@ -1048,7 +1054,7 @@ namespace Barotrauma { case "language": string[] languages = element.GetAttributeStringArray(attribute.Name.ToString(), new string[0]); - if (!languages.Any(l => GameMain.Config.Language.ToLower() == l.ToLower())) { return false; } + if (!languages.Any(l => GameMain.Config.Language.Equals(l, StringComparison.OrdinalIgnoreCase))) { return false; } break; case "gameversion": var version = new Version(attribute.Value); @@ -1213,8 +1219,7 @@ namespace Barotrauma private static GUIImage LoadGUIImage(XElement element, RectTransform parent) { - Sprite sprite = null; - + Sprite sprite; string url = element.GetAttributeString("url", ""); if (!string.IsNullOrEmpty(url)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIFrame.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIFrame.cs index 0e5d5ca42..8e5b77664 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIFrame.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIFrame.cs @@ -28,7 +28,7 @@ namespace Barotrauma if (OutlineColor != Color.Transparent) { - GUI.DrawRectangle(spriteBatch, Rect, OutlineColor * (OutlineColor.A/255.0f), false, thickness: OutlineThickness); + GUI.DrawRectangle(spriteBatch, Rect, OutlineColor, false, thickness: OutlineThickness); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs index 9761bf213..ff2d3c4a7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs @@ -33,7 +33,7 @@ namespace Barotrauma public GUIFrame Content { get; private set; } public GUIScrollBar ScrollBar { get; private set; } - private Dictionary childVisible = new Dictionary(); + private readonly Dictionary childVisible = new Dictionary(); private int totalSize; private bool childrenNeedsRecalculation; @@ -224,7 +224,7 @@ namespace Barotrauma { if (value == false && canDragElements && draggedElement != null) { - draggedElement = null; + DraggedElement = null; } canDragElements = value; } @@ -233,8 +233,21 @@ namespace Barotrauma private GUIComponent draggedElement; private Rectangle draggedReferenceRectangle; private Point draggedReferenceOffset; + public bool HasDraggedElementIndexChanged { get; private set; } - public GUIComponent DraggedElement => draggedElement; + public GUIComponent DraggedElement + { + get + { + return draggedElement; + } + set + { + if (value == draggedElement) { return; } + draggedElement = value; + HasDraggedElementIndexChanged = false; + } + } private readonly bool isHorizontal; @@ -472,7 +485,7 @@ namespace Barotrauma if (!PlayerInput.PrimaryMouseButtonHeld()) { OnRearranged?.Invoke(this, draggedElement.UserData); - draggedElement = null; + DraggedElement = null; RepositionChildren(); } else @@ -518,6 +531,7 @@ namespace Barotrauma if (currIndex != index) { draggedElement.RectTransform.RepositionChildInHierarchy(currIndex); + HasDraggedElementIndexChanged = true; } return; @@ -577,7 +591,7 @@ namespace Barotrauma if (CanDragElements && PlayerInput.PrimaryMouseButtonDown() && GUI.MouseOn == child) { - draggedElement = child; + DraggedElement = child; draggedReferenceRectangle = child.Rect; draggedReferenceOffset = child.RectTransform.AbsoluteOffset; } @@ -750,7 +764,7 @@ namespace Barotrauma } } - if ((GUI.IsMouseOn(this) || GUI.IsMouseOn(ScrollBar)) && AllowMouseWheelScroll && PlayerInput.ScrollWheelSpeed != 0) + if (PlayerInput.ScrollWheelSpeed != 0 && AllowMouseWheelScroll && (FindScrollableParentListBox(GUI.MouseOn) == this || GUI.IsMouseOn(ScrollBar))) { if (SmoothScroll) { @@ -773,7 +787,6 @@ namespace Barotrauma ScrollBar.BarScroll -= (PlayerInput.ScrollWheelSpeed / 500.0f) * BarSize; } } - ScrollBar.Enabled = ScrollBarEnabled && BarSize < 1.0f; if (AutoHideScrollBar) @@ -785,6 +798,13 @@ namespace Barotrauma UpdateDimensions(); } } + + private static GUIListBox FindScrollableParentListBox(GUIComponent target) + { + if (target is GUIListBox listBox && listBox.ScrollBarEnabled && listBox.BarSize < 1.0f) { return listBox; } + if (target?.Parent == null) { return null; } + return FindScrollableParentListBox(target.Parent); + } public void SelectNext(bool force = false, bool autoScroll = true, bool takeKeyBoardFocus = false) { @@ -982,7 +1002,7 @@ namespace Barotrauma if (child == null) { return; } child.RectTransform.Parent = null; if (selected.Contains(child)) { selected.Remove(child); } - if (draggedElement == child) { draggedElement = null; } + if (draggedElement == child) { DraggedElement = null; } UpdateScrollBarSize(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScissorComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScissorComponent.cs new file mode 100644 index 000000000..07119e38a --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScissorComponent.cs @@ -0,0 +1,87 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace Barotrauma +{ + public class GUIScissorComponent: GUIComponent + { + public GUIComponent Content; + + public GUIScissorComponent(RectTransform rectT) : base(null, rectT) + { + Content = new GUIFrame(new RectTransform(Vector2.One, rectT), style: null) + { + CanBeFocused = false + }; + } + + protected override void Update(float deltaTime) + { + base.Update(deltaTime); + + foreach (GUIComponent child in Children) + { + if (child == Content) { continue; } + throw new InvalidOperationException($"Children were found in {nameof(GUIScissorComponent)}, Add them to {nameof(GUIScissorComponent)}.{nameof(Content)} instead."); + } + + ClampChildMouseRects(Content); + } + + protected override void Draw(SpriteBatch spriteBatch) + { + if (!Visible) { return; } + + Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle; + RasterizerState prevRasterizerState = spriteBatch.GraphicsDevice.RasterizerState; + + spriteBatch.End(); + spriteBatch.GraphicsDevice.ScissorRectangle = Rectangle.Intersect(prevScissorRect, Rect); + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); + + foreach (GUIComponent child in Content.Children) + { + if (!child.Visible) { continue; } + child.DrawManually(spriteBatch, alsoChildren: true, recursive: true); + } + + spriteBatch.End(); + spriteBatch.GraphicsDevice.ScissorRectangle = prevScissorRect; + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: prevRasterizerState); + } + + private void ClampChildMouseRects(GUIComponent child) + { + child.ClampMouseRectToParent = true; + + if (child is GUIListBox) { return; } + + foreach (GUIComponent grandChild in child.Children) + { + ClampChildMouseRects(grandChild); + } + } + + public override void AddToGUIUpdateList(bool ignoreChildren = false, int order = 0) + { + if (!Visible) { return; } + + UpdateOrder = order; + GUI.AddToUpdateList(this); + + if (ignoreChildren) + { + OnAddedToGUIUpdateList?.Invoke(this); + return; + } + + foreach (GUIComponent child in Content.Children) + { + if (!child.Visible) { continue; } + child.AddToGUIUpdateList(false, order); + } + OnAddedToGUIUpdateList?.Invoke(this); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs index 169b4d758..cd921c935 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs @@ -25,6 +25,7 @@ namespace Barotrauma public ScalableFont SubHeadingFont { get; private set; } public ScalableFont DigitalFont { get; private set; } public ScalableFont HotkeyFont { get; private set; } + public ScalableFont MonospacedFont { get; private set; } public Dictionary ForceFontUpperCase { @@ -40,11 +41,16 @@ namespace Barotrauma public SpriteSheet SavingIndicator { get; private set; } public UISprite UIGlow { get; private set; } + + public UISprite PingCircle { get; private set; } + public UISprite UIGlowCircular { get; private set; } public UISprite ButtonPulse { get; private set; } public SpriteSheet FocusIndicator { get; private set; } + + public UISprite IconOverflowIndicator { get; private set; } /// /// General green color used for elements whose colors are set from code @@ -235,6 +241,9 @@ namespace Barotrauma case "uiglow": UIGlow = new UISprite(subElement); break; + case "pingcircle": + PingCircle = new UISprite(subElement); + break; case "radiation": RadiationSprite = new UISprite(subElement); break; @@ -247,6 +256,9 @@ namespace Barotrauma case "endroundbuttonpulse": ButtonPulse = new UISprite(subElement); break; + case "iconoverflowindicator": + IconOverflowIndicator = new UISprite(subElement); + break; case "focusindicator": FocusIndicator = new SpriteSheet(subElement); break; @@ -277,6 +289,10 @@ namespace Barotrauma DigitalFont = LoadFont(subElement, graphicsDevice); ForceFontUpperCase[DigitalFont] = subElement.GetAttributeBool("forceuppercase", false); break; + case "monospacedfont": + MonospacedFont = LoadFont(subElement, graphicsDevice); + ForceFontUpperCase[MonospacedFont] = subElement.GetAttributeBool("forceuppercase", false); + break; case "hotkeyfont": HotkeyFont = LoadFont(subElement, graphicsDevice); ForceFontUpperCase[HotkeyFont] = subElement.GetAttributeBool("forceuppercase", false); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs index 407236e08..69f18c269 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs @@ -185,7 +185,7 @@ namespace Barotrauma if (GUI.MouseOn != null) { return false; } //don't close when hovering over an inventory element - if (Inventory.IsMouseOnInventory()) { return false; } + if (Inventory.IsMouseOnInventory) { return false; } bool input = PlayerInput.PrimaryMouseButtonDown() || PlayerInput.SecondaryMouseButtonClicked(); return input && !rect.Contains(PlayerInput.MousePosition); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs index 23b0f9036..5a84a234f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs @@ -193,7 +193,7 @@ namespace Barotrauma if (LoadState == 100.0f) { #if DEBUG - if (GameMain.Config.AutomaticQuickStartEnabled || GameMain.Config.AutomaticCampaignLoadEnabled && GameMain.FirstLoad) + if (GameMain.Config.AutomaticQuickStartEnabled || GameMain.Config.AutomaticCampaignLoadEnabled || GameMain.Config.TestScreenEnabled && GameMain.FirstLoad) { loadText = "QUICKSTARTING ..."; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ShapeExtensions.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ShapeExtensions.cs index 8bf8adea1..afb00b206 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ShapeExtensions.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ShapeExtensions.cs @@ -55,14 +55,47 @@ namespace Barotrauma var texture = GetTexture(spriteBatch); for (var i = 0; i < points.Count - 1; i++) - DrawPolygonEdge(spriteBatch, texture, points[i] + offset, points[i + 1] + offset, color, thickness); + DrawPolygonEdge(spriteBatch, points[i] + offset, points[i + 1] + offset, color, thickness); - DrawPolygonEdge(spriteBatch, texture, points[points.Count - 1] + offset, points[0] + offset, color, + DrawPolygonEdge(spriteBatch, points[points.Count - 1] + offset, points[0] + offset, color, thickness); } + + /// + /// Draws a closed polygon from an array of points + /// + public static void DrawPolygonInner(this SpriteBatch spriteBatch, Vector2 offset, IReadOnlyList points, Color color, float thickness = 1f) + { + if (points.Count == 0) { return; } - private static void DrawPolygonEdge(SpriteBatch spriteBatch, Texture2D texture, Vector2 point1, Vector2 point2, - Color color, float thickness) + if (points.Count == 1) + { + DrawPoint(spriteBatch, points[0], color, (int)thickness); + return; + } + + for (var i = 0; i < points.Count - 1; i++) + { + Vector2 point1 = points[i] + offset, + point2 = points[i + 1] + offset; + + DrawPolygonEdgeInner(spriteBatch, point1, point2, color, thickness); + } + + DrawPolygonEdgeInner(spriteBatch, points[^1] + offset, points[0] + offset, color, thickness); + } + + private static void DrawPolygonEdgeInner(SpriteBatch spriteBatch, Vector2 point1, Vector2 point2, Color color, float thickness) + { + var length = Vector2.Distance(point1, point2) + thickness; + var angle = (float)Math.Atan2(point2.Y - point1.Y, point2.X - point1.X); + var scale = new Vector2(length, thickness); + Vector2 middle = new Vector2((point1.X + point2.X) / 2f, (point1.Y + point2.Y) / 2f); + Texture2D tex = GetTexture(spriteBatch); + spriteBatch.Draw(GetTexture(spriteBatch), middle, null, color, angle, new Vector2(tex.Width / 2f, tex.Height / 2f), scale, SpriteEffects.None, 0); + } + + private static void DrawPolygonEdge(SpriteBatch spriteBatch, Vector2 point1, Vector2 point2, Color color, float thickness) { var length = Vector2.Distance(point1, point2); var angle = (float)Math.Atan2(point2.Y - point1.Y, point2.X - point1.X); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index 5c97c009d..934f6b001 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -37,7 +37,7 @@ namespace Barotrauma private Color storeSpecialColor; private GUIListBox shoppingCrateBuyList, shoppingCrateSellList, shoppingCrateSellFromSubList; - private GUITextBlock shoppingCrateTotal; + private GUITextBlock relevantBalanceName, shoppingCrateTotal; private GUIButton clearAllButton, confirmButton; private bool needsRefresh, needsBuyingRefresh, needsSellingRefresh, needsItemsToSellRefresh, needsSellingFromSubRefresh, needsItemsToSellFromSubRefresh; @@ -58,7 +58,6 @@ namespace Barotrauma StoreTab.SellFromSub => false, _ => throw new NotImplementedException() }; - private bool IsSelling => !IsBuying; private GUIListBox ActiveShoppingCrateList => activeTab switch { StoreTab.Buy => shoppingCrateBuyList, @@ -222,16 +221,8 @@ namespace Barotrauma TextScale = 1.1f, TextGetter = () => { - if (CurrentLocation != null) - { - merchantBalanceBlock.TextColor = CurrentLocation.BalanceColor; - return GetCurrencyFormatted(CurrentLocation.StoreCurrentBalance); - } - else - { - merchantBalanceBlock.TextColor = Color.Red; - return GetCurrencyFormatted(0); - } + merchantBalanceBlock.TextColor = CurrentLocation?.BalanceColor ?? Color.Red; + return GetMerchantBalanceText(); } }; @@ -375,28 +366,37 @@ namespace Barotrauma //don't show categories with no buyable items itemCategories.RemoveAll(c => !ItemPrefab.Prefabs.Any(ep => ep.Category.HasFlag(c) && ep.CanBeBought)); itemCategoryButtons.Clear(); + var categoryButton = new GUIButton(new RectTransform(new Point(categoryButtonContainer.Rect.Width, categoryButtonContainer.Rect.Width), categoryButtonContainer.RectTransform), style: "CategoryButton.All") + { + ToolTip = TextManager.Get("MapEntityCategory.All"), + OnClicked = OnClickedCategoryButton + }; + itemCategoryButtons.Add(categoryButton); foreach (MapEntityCategory category in itemCategories) { - var categoryButton = new GUIButton(new RectTransform(new Point(categoryButtonContainer.Rect.Width, categoryButtonContainer.Rect.Width), categoryButtonContainer.RectTransform), + categoryButton = new GUIButton(new RectTransform(new Point(categoryButtonContainer.Rect.Width, categoryButtonContainer.Rect.Width), categoryButtonContainer.RectTransform), style: "CategoryButton." + category) { ToolTip = TextManager.Get("MapEntityCategory." + category), UserData = category, - OnClicked = (btn, userdata) => - { - MapEntityCategory? newCategory = !btn.Selected ? (MapEntityCategory?)userdata : null; - if (newCategory.HasValue) { searchBox.Text = ""; } - if (newCategory != selectedItemCategory) { tabLists[activeTab].ScrollBar.BarScroll = 0f; } - FilterStoreItems(newCategory, searchBox.Text); - return true; - } + OnClicked = OnClickedCategoryButton }; itemCategoryButtons.Add(categoryButton); - categoryButton.RectTransform.SizeChanged += () => + } + bool OnClickedCategoryButton(GUIButton button, object userData) + { + MapEntityCategory? newCategory = !button.Selected ? (MapEntityCategory?)userData : null; + if (newCategory.HasValue) { searchBox.Text = ""; } + if (newCategory != selectedItemCategory) { tabLists[activeTab].ScrollBar.BarScroll = 0f; } + FilterStoreItems(newCategory, searchBox.Text); + return true; + } + foreach (var btn in itemCategoryButtons) + { + btn.RectTransform.SizeChanged += () => { - var sprite = categoryButton.Frame.sprites[GUIComponent.ComponentState.None].First(); - categoryButton.RectTransform.NonScaledSize = - new Point(categoryButton.Rect.Width, (int)(categoryButton.Rect.Width * ((float)sprite.Sprite.SourceRect.Height / sprite.Sprite.SourceRect.Width))); + var sprite = btn.Frame.sprites[GUIComponent.ComponentState.None].First(); + btn.RectTransform.NonScaledSize = new Point(btn.Rect.Width, (int)(btn.Rect.Width * ((float)sprite.Sprite.SourceRect.Height / sprite.Sprite.SourceRect.Width))); }; } @@ -503,11 +503,11 @@ namespace Barotrauma ForceUpperCase = true }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), playerBalanceContainer.RectTransform), - "", font: GUI.SubHeadingFont, textAlignment: Alignment.TopRight) + "", textColor: Color.White, font: GUI.SubHeadingFont, textAlignment: Alignment.TopRight) { AutoScaleVertical = true, TextScale = 1.1f, - TextGetter = () => GetCurrencyFormatted(PlayerMoney) + TextGetter = GetPlayerBalanceText }; // Divider ------------------------------------------------ @@ -523,7 +523,7 @@ namespace Barotrauma RelativeSpacing = 0.015f, Stretch = true }; - var shoppingCrateListContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.85f), shoppingCrateInventoryContainer.RectTransform), style: null); + var shoppingCrateListContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.8f), shoppingCrateInventoryContainer.RectTransform), style: null); shoppingCrateBuyList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false }; shoppingCrateSellList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false }; if (GameMain.IsSingleplayer) @@ -531,6 +531,21 @@ namespace Barotrauma shoppingCrateSellFromSubList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false }; } + var relevantBalanceContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), shoppingCrateInventoryContainer.RectTransform), isHorizontal: true) + { + Stretch = true + }; + relevantBalanceName = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), relevantBalanceContainer.RectTransform), "", font: GUI.Font) + { + CanBeFocused = false + }; + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), relevantBalanceContainer.RectTransform), "", textColor: Color.White, font: GUI.SubHeadingFont, textAlignment: Alignment.Right) + { + CanBeFocused = false, + TextScale = 1.1f, + TextGetter = () => IsBuying ? GetPlayerBalanceText() : GetMerchantBalanceText() + }; + var totalContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), shoppingCrateInventoryContainer.RectTransform), isHorizontal: true) { Stretch = true @@ -576,6 +591,10 @@ namespace Barotrauma resolutionWhenCreated = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); } + private string GetMerchantBalanceText() => GetCurrencyFormatted(CurrentLocation?.StoreCurrentBalance ?? 0); + + private string GetPlayerBalanceText() => GetCurrencyFormatted(PlayerMoney); + private GUILayoutGroup CreateDealsGroup(GUIListBox parentList) { var elementHeight = (int)(GUI.yScale * 80); @@ -604,17 +623,14 @@ namespace Barotrauma { prevLocation.Reputation.OnReputationValueChanged = null; } - - foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) + if (ItemPrefab.Prefabs.Any(p => p.CanBeBoughtAtLocation(CurrentLocation, out PriceInfo _))) { - if (itemPrefab.CanBeBoughtAtLocation(CurrentLocation, out PriceInfo _)) + selectedItemCategory = null; + searchBox.Text = ""; + ChangeStoreTab(StoreTab.Buy); + if (newLocation?.Reputation != null) { - ChangeStoreTab(StoreTab.Buy); - if (newLocation?.Reputation != null) - { - newLocation.Reputation.OnReputationValueChanged += () => { needsRefresh = true; }; - } - return; + newLocation.Reputation.OnReputationValueChanged += () => { needsRefresh = true; }; } } } @@ -628,6 +644,7 @@ namespace Barotrauma tabButton.Selected = (StoreTab)tabButton.UserData == activeTab; } sortingDropDown.SelectItem(tabSortingMethods[tab]); + relevantBalanceName.Text = IsBuying ? TextManager.Get("campaignstore.balance") : TextManager.Get("campaignstore.storebalance"); SetShoppingCrateTotalText(); SetClearAllButtonStatus(); SetConfirmButtonBehavior(); @@ -697,7 +714,7 @@ namespace Barotrauma } foreach (GUIButton btn in itemCategoryButtons) { - btn.Selected = category.HasValue && (MapEntityCategory)btn.UserData == selectedItemCategory; + btn.Selected = (MapEntityCategory?)btn.UserData == selectedItemCategory; } list.UpdateScrollBarSize(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index a5239b1b6..57c248466 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -18,7 +18,7 @@ namespace Barotrauma private static UISprite spectateIcon, disconnectedIcon; private static Sprite ownerIcon, moderatorIcon; - private enum InfoFrameTab { Crew, Mission, Reputation, MyCharacter, Traitor, Submarine }; + private enum InfoFrameTab { Crew, Mission, Reputation, MyCharacter, Traitor, Submarine, Talents }; private static InfoFrameTab selectedTab; private GUIFrame infoFrame, contentFrame; @@ -258,6 +258,8 @@ namespace Barotrauma { var myCharacterButton = createTabButton(InfoFrameTab.MyCharacter, "tabmenu.character"); } + + var talentsButton = createTabButton(InfoFrameTab.Talents, "tabmenu.talents"); } private bool SelectInfoFrameTab(GUIButton button, object userData) @@ -296,6 +298,9 @@ namespace Barotrauma case InfoFrameTab.Submarine: CreateSubmarineInfo(infoFrameHolder, Submarine.MainSub); break; + case InfoFrameTab.Talents: + CreateTalentInfo(infoFrameHolder); + break; } return true; @@ -1159,5 +1164,319 @@ namespace Barotrauma sub.Info.CreateSpecsWindow(specsListBox, GUI.Font, includeTitle: false, includeClass: false, includeDescription: true); } } + private Color unselectedColor = new Color(240, 255, 255, 225); + private Color selectedColor = new Color(220, 255, 220, 225); + private Color ownedColor = new Color(140, 180, 140, 225); + private Color unselectableColor = new Color(100, 100, 100, 225); + private Color pressedColor = new Color(60, 60, 60, 225); + + private readonly List<(GUIButton button, GUIImage background, GUIImage icon)> talentButtons = new List<(GUIButton button, GUIImage background, GUIImage icon)>(); + private List selectedTalents = new List(); + private GUITextBlock talentTitleText; + private GUITextBlock talentDescriptionText; + private GUITextBlock talentPointsText; + + private GUITextBlock experienceText; + private Color experienceBackgroundColor = new Color(255, 255, 255, 155); + + private GUIProgressBar experienceBar; + + private void CreateTalentInfo(GUIFrame infoFrame) + { + infoFrame.ClearChildren(); + talentButtons.Clear(); + + GUIFrame talentFrameBackground = new GUIFrame(new RectTransform(Vector2.One, infoFrame.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox"); + int padding = GUI.IntScale(15); + GUIFrame talentFrameContent = new GUIFrame(new RectTransform(new Point(talentFrameBackground.Rect.Width - padding, talentFrameBackground.Rect.Height - padding), infoFrame.RectTransform, Anchor.Center), style: null); + + Character controlledCharacter = Character.Controlled; + + if (controlledCharacter.Info == null) + { + DebugConsole.ThrowError("No character info found for talent UI"); + return; + } + + selectedTalents = controlledCharacter.Info.UnlockedTalents.ToList(); + + GUILayoutGroup talentFrameLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), talentFrameContent.RectTransform, anchor: Anchor.Center), childAnchor: Anchor.TopCenter) + { + AbsoluteSpacing = GUI.IntScale(5) + }; + + GUILayoutGroup talentInfoLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.325f), talentFrameLayoutGroup.RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter); + + GUIFrame talentTitleFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.25f), talentInfoLayoutGroup.RectTransform, Anchor.TopCenter), style: null); + + talentTitleText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), talentTitleFrame.RectTransform, Anchor.TopLeft), "", font: GUI.LargeFont); + talentPointsText = new GUITextBlock(new RectTransform(new Vector2(0.25f, 1.0f), talentTitleFrame.RectTransform, Anchor.TopRight), "", font: GUI.Font, textAlignment: Alignment.Center); + + GUIFrame talentDescriptionFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.4f), talentInfoLayoutGroup.RectTransform, Anchor.TopCenter), style: null); + + talentDescriptionText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), talentDescriptionFrame.RectTransform, Anchor.TopLeft), "", font: GUI.Font, textAlignment: Alignment.TopLeft, wrap: true); + + GUIFrame characterInfoFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.3f), talentInfoLayoutGroup.RectTransform, Anchor.TopLeft), style: null); + GUILayoutGroup characterInfoColumn = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), characterInfoFrame.RectTransform, anchor: Anchor.TopLeft), childAnchor: Anchor.TopLeft, isHorizontal: true); + + CreateCharacterSheet(characterInfoColumn); + + if (!TalentTree.JobTalentTrees.TryGetValue(controlledCharacter.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return; } + + GUIListBox talentTreeListBox = new GUIListBox(new RectTransform(new Vector2(1f, 0.6f), talentFrameLayoutGroup.RectTransform, Anchor.TopCenter), isHorizontal: true, style: null); + + int spacing = GUI.IntScale(5); + + foreach (var subTree in talentTree.TalentSubTrees) + { + GUIFrame subTreeFrame = new GUIFrame(new RectTransform(new Vector2(0.333f, 1f), talentTreeListBox.Content.RectTransform, anchor: Anchor.TopLeft), style: null); + GUILayoutGroup subTreeLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 1f), subTreeFrame.RectTransform, Anchor.Center), false, childAnchor: Anchor.TopCenter); + + GUIFrame subtreeTitleFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.111f), subTreeLayoutGroup.RectTransform, anchor: Anchor.TopCenter), style: "SubtreeHeader"); + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), subtreeTitleFrame.RectTransform, anchor: Anchor.TopCenter), subTree.Identifier, font: GUI.LargeFont, textAlignment: Alignment.Center); + + for (int i = 0; i < 4; i++) + { + GUIFrame talentOptionFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.222f), subTreeLayoutGroup.RectTransform, anchor: Anchor.TopCenter), style: "TalentOptionFrame"); + GUIImage talentBackground = new GUIImage(new RectTransform(Vector2.One, talentOptionFrame.RectTransform, anchor: Anchor.Center), style: "TalentBackground") + { + CanBeFocused = false, + Color = unselectableColor, + }; + + if (subTree.TalentOptionStages.Count > i) + { + TalentOption talentOption = subTree.TalentOptionStages[i]; + + GUILayoutGroup talentOptionLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 1f), talentOptionFrame.RectTransform, Anchor.CenterLeft), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true, + }; + + foreach (Talent talent in talentOption.Talents) + { + int optionPadding = GUI.IntScale(10); + GUIFrame talentFrame = new GUIFrame(new RectTransform(new Point(talentOptionFrame.Rect.Width, talentOptionFrame.Rect.Height - optionPadding), talentOptionLayoutGroup.RectTransform), style: null) + { + CanBeFocused = false, + }; + new GUIImage(new RectTransform(Vector2.One, talentFrame.RectTransform, anchor: Anchor.Center), style: "TalentFrameBackground") + { + CanBeFocused = false, + }; + GUIImage iconImage = null; + + GUIButton talentButton = new GUIButton(new RectTransform(Vector2.One, talentFrame.RectTransform, anchor: Anchor.Center), style: "TalentFrame") + { + ToolTip = $"{TextManager.Get("talentname." + talent.Identifier, returnNull: true) ?? talent.Identifier} \n\n{TextManager.Get("talentdescription." + talent.Identifier, returnNull: true) ?? string.Empty}", + UserData = talent.Identifier, + PressedColor = pressedColor, + OnClicked = (button, userData) => + { + talentTitleText.Text = TextManager.Get("talentname." + talent.Identifier, returnNull: true) ?? string.Empty; + talentDescriptionText.Text = TextManager.Get("talentdescription." + talent.Identifier, returnNull: true) ?? string.Empty; + + // deselect other buttons in tier by removing their selected talents from pool + foreach (GUIButton guiButton in talentOptionLayoutGroup.GetAllChildren()) + { + if (guiButton.UserData is string otherTalentIdentifier && guiButton != button) + { + if (!controlledCharacter.HasTalent(otherTalentIdentifier)) + { + selectedTalents.Remove(otherTalentIdentifier); + } + } + } + string talentIdentifier = userData as string; + + if (TalentTree.IsViableTalentForCharacter(controlledCharacter, talentIdentifier, selectedTalents)) + { + if (!selectedTalents.Contains(talentIdentifier)) + { + selectedTalents.Add(talentIdentifier); + } + } + else if (!controlledCharacter.HasTalent(talentIdentifier)) + { + selectedTalents.Remove(talentIdentifier); + } + + UpdateTalentButtons(); + return true; + }, + }; + + + int iconPadding = GUI.IntScale(15); + iconImage = new GUIImage(new RectTransform(new Point(talentFrame.Rect.Width - iconPadding, talentFrame.Rect.Height - iconPadding), talentFrame.RectTransform, anchor: Anchor.Center), sprite: talent.Icon, scaleToFit: true) + { + PressedColor = unselectableColor, + CanBeFocused = false, + }; + + talentButtons.Add((talentButton, talentBackground, iconImage)); + } + } + } + } + + GUIFrame talentBottomFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.07f), talentFrameLayoutGroup.RectTransform, Anchor.TopCenter), style: null); + + GUIFrame experienceBarFrame = new GUIFrame(new RectTransform(new Vector2(0.775f, 0.75f), talentBottomFrame.RectTransform, Anchor.TopCenter), style: null); + + experienceBar = new GUIProgressBar(new RectTransform(new Vector2(1f, 1f), experienceBarFrame.RectTransform, Anchor.CenterLeft), + barSize: controlledCharacter.Info.GetProgressTowardsNextLevel(), color: Color.White, style: "ExperienceBar") + { + IsHorizontal = true + }; + + GUIImage experienceTextBackground = new GUIImage(new RectTransform(new Vector2(0.2f, 0.475f), experienceBarFrame.RectTransform, anchor: Anchor.Center), style: "ExperienceTextBackground"); + + experienceText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), experienceTextBackground.RectTransform, anchor: Anchor.Center), "", font: GUI.Font, textColor: Color.White, textAlignment: Alignment.Center); + + new GUIButton(new RectTransform(new Vector2(0.1f, 0.3f), talentBottomFrame.RectTransform, anchor: Anchor.TopRight), text: TextManager.Get("applysettingsbutton")) + { + OnClicked = ApplyTalentSelection, + }; + + new GUIButton(new RectTransform(new Vector2(0.1f, 0.3f), talentBottomFrame.RectTransform, anchor: Anchor.TopLeft), text: TextManager.Get("reset")) + { + OnClicked = ResetTalentSelection, + }; + + UpdateTalentButtons(); + } + + private void UpdateTalentButtons() + { + Character controlledCharacter = Character.Controlled; + + talentPointsText.Text = $"{TextManager.Get("talentpointsleft")}{controlledCharacter.Info.GetAvailableTalentPoints()}"; + experienceText.Text = $"{controlledCharacter.Info.ExperiencePoints - controlledCharacter.Info.GetExperienceRequiredForCurrentLevel()} / {controlledCharacter.Info.GetExperienceRequiredToLevelUp() - controlledCharacter.Info.GetExperienceRequiredForCurrentLevel()}"; + experienceBar.BarSize = controlledCharacter.Info.GetProgressTowardsNextLevel(); + //experienceBar.ToolTip = $"{controlledCharacter.Info.ExperiencePoints - controlledCharacter.Info.GetExperienceRequiredForCurrentLevel()} / {controlledCharacter.Info.GetExperienceRequiredToLevelUp() - controlledCharacter.Info.GetExperienceRequiredForCurrentLevel()}"; + + selectedTalents = TalentTree.CheckTalentSelection(controlledCharacter, selectedTalents); + + foreach (var talentButton in talentButtons) + { + talentButton.background.Color = unselectableColor; + } + + foreach (var talentButton in talentButtons) + { + string talentIdentifier = talentButton.button.UserData as string; + bool unselectable = !TalentTree.IsViableTalentForCharacter(controlledCharacter, talentIdentifier, selectedTalents) || controlledCharacter.HasTalent(talentIdentifier); + Color newTalentColor = unselectable ? unselectableColor : unselectedColor; + + if (controlledCharacter.HasTalent(talentIdentifier)) + { + newTalentColor = ownedColor; + } + else if (selectedTalents.Contains(talentIdentifier)) + { + newTalentColor = selectedColor; + } + + talentButton.button.Color = newTalentColor; + talentButton.button.SelectedColor = newTalentColor; + talentButton.button.HoverColor = newTalentColor; + talentButton.button.DisabledColor = newTalentColor; + + talentButton.icon.Color = newTalentColor; + + // update background color as well, if not defined yet + if (talentButton.background.Color == unselectableColor) + { + talentButton.background.Color = newTalentColor; + } + } + } + + + private void ApplyTalents(Character controlledCharacter) + { + selectedTalents = TalentTree.CheckTalentSelection(controlledCharacter, selectedTalents); + foreach (string talent in selectedTalents) + { + controlledCharacter.GiveTalent(talent); + if (GameMain.Client != null) + { + GameMain.Client.CreateEntityEvent(controlledCharacter, new object[] { NetEntityEvent.Type.UpdateTalents }); + } + } + UpdateTalentButtons(); + } + + private bool ApplyTalentSelection(GUIButton guiButton, object userData) + { + Character controlledCharacter = Character.Controlled; + ApplyTalents(controlledCharacter); + return true; + } + + private bool ResetTalentSelection(GUIButton guiButton, object userData) + { + Character controlledCharacter = Character.Controlled; + selectedTalents = controlledCharacter.Info.UnlockedTalents.ToList(); + UpdateTalentButtons(); + return true; + } + + public void OnExperienceChanged(Character character) + { + if (character != Character.Controlled) { return; } + UpdateTalentButtons(); + } + + private readonly StatTypes[] basicStats = new StatTypes[] + { + StatTypes.MaximumHealthMultiplier, + StatTypes.MovementSpeed, + StatTypes.SwimmingSpeed, + StatTypes.RepairSpeed, + }; + + private readonly StatTypes[] combatStats = new StatTypes[] + { + StatTypes.MaximumHealthMultiplier, + StatTypes.MovementSpeed, + StatTypes.SwimmingSpeed, + StatTypes.RepairSpeed, + }; + + private readonly StatTypes[] miscStats = new StatTypes[] + { + StatTypes.ReputationGainMultiplier, + StatTypes.MissionMoneyGainMultiplier, + StatTypes.ExperienceGainMultiplier, + StatTypes.MissionExperienceGainMultiplier, + }; + + private void CreateCharacterSheet(GUILayoutGroup characterInfoColumn) + { + Character controlledCharacter = Character.Controlled; + + CreateRow(basicStats); + CreateRow(combatStats); + CreateRow(miscStats); + + void CreateRow(StatTypes[] statTypes) + { + GUILayoutGroup characterInfoRow = new GUILayoutGroup(new RectTransform(new Vector2(0.33f, 1.0f), characterInfoColumn.RectTransform, anchor: Anchor.TopLeft), childAnchor: Anchor.TopCenter); + foreach (StatTypes statType in statTypes) + { + ShowStat(statType, characterInfoRow); + } + } + + void ShowStat(StatTypes statType, GUILayoutGroup characterInfoRow) + { + GUIFrame textInfoFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.33f), characterInfoRow.RectTransform, Anchor.TopCenter), style: null); + new GUITextBlock(new RectTransform(new Vector2(1f, 1f), textInfoFrame.RectTransform, Anchor.TopLeft), statType.ToString(), font: GUI.SmallFont, textAlignment: Alignment.TopLeft); + new GUITextBlock(new RectTransform(new Vector2(1f, 1f), textInfoFrame.RectTransform, Anchor.TopLeft), (int)(100f * (1 + controlledCharacter.GetStatValue(statType))) + "%", font: GUI.Font, textAlignment: Alignment.TopRight); + } + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs index a6b3ca8dc..4d79d57fd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs @@ -115,8 +115,9 @@ namespace Barotrauma return MathHelper.Clamp(Math.Min(Math.Min(scale.X, scale.Y), GUI.SlicedSpriteScale), minBorderScale, maxBorderScale); } - public void Draw(SpriteBatch spriteBatch, Rectangle rect, Color color, SpriteEffects spriteEffects = SpriteEffects.None) + public void Draw(SpriteBatch spriteBatch, Rectangle rect, Color color, SpriteEffects spriteEffects = SpriteEffects.None, Vector2? uvOffset = null) { + uvOffset ??= Vector2.Zero; if (Sprite.Texture == null) { GUI.DrawRectangle(spriteBatch, rect, Color.Magenta); @@ -157,7 +158,7 @@ namespace Barotrauma else if (Tile) { Vector2 startPos = new Vector2(rect.X, rect.Y); - Sprite.DrawTiled(spriteBatch, startPos, new Vector2(rect.Width, rect.Height), color); + Sprite.DrawTiled(spriteBatch, startPos, new Vector2(rect.Width, rect.Height), color, startOffset: uvOffset); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index 0dbbe81f8..e22ee76cb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -43,6 +43,7 @@ namespace Barotrauma public static SteamWorkshopScreen SteamWorkshopScreen; public static SubEditorScreen SubEditorScreen; + public static TestScreen TestScreen; public static ParticleEditorScreen ParticleEditorScreen; public static LevelEditorScreen LevelEditorScreen; public static SpriteEditorScreen SpriteEditorScreen; @@ -89,7 +90,16 @@ namespace Barotrauma public static ParticleManager ParticleManager; public static DecalManager DecalManager; - public static World World; + private static World world; + public static World World + { + get + { + if (world == null) { world = new World(new Vector2(0, -9.82f)); } + return world; + } + set { world = value; } + } public static LoadingScreen TitleScreen; private bool loadingScreenOpen; @@ -239,7 +249,6 @@ namespace Barotrauma GameMain.ResetFrameTime(); fixedTime = new GameTime(); - World = new World(new Vector2(0, -9.82f)); FarseerPhysics.Settings.AllowSleep = true; FarseerPhysics.Settings.ContinuousPhysics = false; FarseerPhysics.Settings.VelocityIterations = 1; @@ -567,6 +576,8 @@ namespace Barotrauma ItemPrefab.LoadAll(GetFilesOfType(ContentType.Item)); AfflictionPrefab.LoadAll(GetFilesOfType(ContentType.Afflictions)); SkillSettings.Load(GetFilesOfType(ContentType.SkillSettings)); + TalentPrefab.LoadAll(GetFilesOfType(ContentType.Talents)); + TalentTree.LoadAll(GetFilesOfType(ContentType.TalentTrees)); Order.Init(); EventManagerSettings.Init(); BallastFloraPrefab.LoadAll(GetFilesOfType(ContentType.MapCreature)); @@ -620,6 +631,7 @@ namespace Barotrauma #endif SubEditorScreen = new SubEditorScreen(); + TestScreen = new TestScreen(); TitleScreen.LoadState = 75.0f; yield return CoroutineStatus.Running; @@ -792,12 +804,16 @@ namespace Barotrauma } #if DEBUG - if (TitleScreen.LoadState >= 100.0f && !TitleScreen.PlayingSplashScreen && (Config.AutomaticQuickStartEnabled || Config.AutomaticCampaignLoadEnabled) && FirstLoad && !PlayerInput.KeyDown(Keys.LeftShift)) + if (TitleScreen.LoadState >= 100.0f && !TitleScreen.PlayingSplashScreen && (Config.AutomaticQuickStartEnabled || Config.AutomaticCampaignLoadEnabled || Config.TestScreenEnabled) && FirstLoad && !PlayerInput.KeyDown(Keys.LeftShift)) { loadingScreenOpen = false; FirstLoad = false; - if (Config.AutomaticQuickStartEnabled) + if (Config.TestScreenEnabled) + { + TestScreen.Select(); + } + else if (Config.AutomaticQuickStartEnabled) { MainMenuScreen.QuickStart(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs index cffadc3ee..3010c608a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs @@ -48,11 +48,7 @@ namespace Barotrauma var equipmentSlots = new List() { InvSlotType.Head, InvSlotType.InnerClothes, InvSlotType.OuterClothes, InvSlotType.Headset, InvSlotType.Card }; return character.Inventory.FindAllItems(item => { - if (item.SpawnedInOutpost) { return false; } - if (!item.Prefab.AllowSellingWhenBroken && item.ConditionPercentage < 90.0f) { return false; } - if (confirmedSoldEntities.Any(it => it.Item == item)) { return false; } - // There must be no contained items or the contained items must be confirmed as sold - if (!item.ContainedItems.All(it => confirmedSoldEntities.Any(se => se.Item == it))) { return false; } + if (!IsItemSellable(item, confirmedSoldEntities)) { return false; } // Item must be in a non-equipment slot if possible if (!item.AllowedSlots.All(s => equipmentSlots.Contains(s)) && IsInEquipmentSlot(item)) { return false; } // Item must not be contained inside an item in an equipment slot @@ -76,15 +72,10 @@ namespace Barotrauma var confirmedSoldEntities = GetConfirmedSoldEntities(); return Submarine.MainSub.GetItems(true).FindAll(item => { - if (!item.Prefab.CanBeSold) { return false; } - if (item.SpawnedInOutpost) { return false; } - if (!item.Prefab.AllowSellingWhenBroken && item.ConditionPercentage < 90.0f) { return false; } + if (!IsItemSellable(item, confirmedSoldEntities)) { return false; } if (!item.Components.All(c => !(c is Holdable h) || !h.Attachable || !h.Attached)) { return false; } if (!item.Components.All(c => !(c is Wire w) || w.Connections.All(c => c == null))) { return false; } if (!ItemAndAllContainersInteractable(item)) { return false; } - if (confirmedSoldEntities.Any(it => it.Item == item)) { return false; } - // There must be no contained items or the contained items must be confirmed as sold - if (!item.ContainedItems.All(it => confirmedSoldEntities.Any(se => se.Item == it))) { return false; } return true; }).Distinct(); @@ -107,6 +98,24 @@ namespace Barotrauma return SoldEntities.Where(se => se.Status != SoldEntity.SellStatus.Unconfirmed); } + private bool IsItemSellable(Item item, IEnumerable confirmedSoldEntities) + { + if (!item.Prefab.CanBeSold) { return false; } + if (item.SpawnedInOutpost) { return false; } + if (!item.Prefab.AllowSellingWhenBroken && item.ConditionPercentage < 90.0f) { return false; } + if (confirmedSoldEntities.Any(it => it.Item == item)) { return false; } + if (item.OwnInventory?.Container is ItemContainer itemContainer) + { + var containedItems = item.ContainedItems; + if (containedItems.None()) { return true; } + // Allow selling the item if contained items are unsellable and set to be removed on deconstruct + if (itemContainer.RemoveContainedItemsOnDeconstruct && containedItems.All(it => !it.Prefab.CanBeSold)) { return true; } + // Otherwise there must be no contained items or the contained items must be confirmed as sold + if (!containedItems.All(it => confirmedSoldEntities.Any(se => se.Item == it))) { return false; } + } + return true; + } + public void SetItemsInBuyCrate(List items) { ItemsInBuyCrate.Clear(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index 5cc69eb70..ac4b7ed69 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -17,6 +17,13 @@ namespace Barotrauma { private Point screenResolution; + public Order DraggedOrder; + public bool DragOrder; + private bool dropOrder; + private int framesToSkip = 2; + private float dragOrderTreshold; + private Vector2 dragPoint = Vector2.Zero; + #region UI public GUIComponent ReportButtonFrame { get; set; } @@ -92,10 +99,11 @@ namespace Barotrauma crewList = new GUIListBox(new RectTransform(Vector2.One, crewArea.RectTransform), style: null, isScrollBarOnDefaultSide: false) { AutoHideScrollBar = false, - CanBeFocused = false, + CanDragElements = true, OnSelected = (component, userData) => false, SelectMultiple = false, - Spacing = (int)(GUI.Scale * 10) + Spacing = (int)(GUI.Scale * 10), + OnRearranged = OnCrewListRearranged }; jobIndicatorBackground = new Sprite("Content/UI/CommandUIAtlas.png", new Rectangle(0, 512, 128, 128)); @@ -180,7 +188,7 @@ namespace Barotrauma }; } - var reports = Order.PrefabList.FindAll(o => o.IsReport && o.SymbolSprite != null && !o.Hidden); + List reports = Order.PrefabList.FindAll(o => o.IsReport && o.SymbolSprite != null && !o.Hidden); if (reports.None()) { DebugConsole.ThrowError("No valid orders for report buttons found! Cannot create report buttons. The orders for the report buttons must have 'targetallcharacters' attribute enabled and a valid 'symbolsprite' defined."); @@ -198,19 +206,36 @@ namespace Barotrauma ReportButtonFrame.RectTransform.AbsoluteOffset = new Point(0, -chatBox.ToggleButton.Rect.Height); + CreateReports(this, ReportButtonFrame, reports, false); + + #endregion + + screenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); + prevUIScale = GUI.Scale; + _isCrewMenuOpen = GameMain.Config.CrewMenuOpen; + dismissedOrderPrefab ??= Order.GetPrefab("dismissed"); + } + + public static void CreateReports(CrewManager crewManager, GUIComponent parent, List reports, bool isHorizontal) + { //report buttons foreach (Order order in reports) { if (!order.IsReport || order.SymbolSprite == null || order.Hidden) { continue; } - var btn = new GUIButton(new RectTransform(new Point(ReportButtonFrame.Rect.Width), ReportButtonFrame.RectTransform), style: null) + var btn = new GUIButton(new RectTransform(new Point(isHorizontal ? parent.Rect.Height : parent.Rect.Width), parent.RectTransform), style: null) { - OnClicked = (GUIButton button, object userData) => + OnClicked = (button, userData) => { if (!CanIssueOrders) { return false; } var sub = Character.Controlled.Submarine; if (sub == null || sub.TeamID != Character.Controlled.TeamID || sub.Info.IsWreck) { return false; } - SetCharacterOrder(null, order, null, CharacterInfo.HighestManualOrderPriority, Character.Controlled); - if (IsSinglePlayer) { HumanAIController.ReportProblem(Character.Controlled, order); } + + if (crewManager != null) + { + crewManager.SetCharacterOrder(null, order, null, CharacterInfo.HighestManualOrderPriority, Character.Controlled); + + if (crewManager.IsSinglePlayer) { HumanAIController.ReportProblem(Character.Controlled, order); } + } return true; }, UserData = order, @@ -218,6 +243,19 @@ namespace Barotrauma ClampMouseRectToParent = false }; + if (crewManager != null) + { + btn.OnButtonDown = () => + { + crewManager.dragOrderTreshold = Math.Max(btn.Rect.Width, btn.Rect.Height) / 2f; + crewManager.DraggedOrder = order; + crewManager.dropOrder = false; + crewManager.framesToSkip = 2; + crewManager.dragPoint = btn.Rect.Center.ToVector2(); + return true; + }; + } + new GUIFrame(new RectTransform(new Vector2(1.5f), btn.RectTransform, Anchor.Center), "OuterGlowCircular") { Color = GUI.Style.Red * 0.8f, @@ -236,13 +274,6 @@ namespace Barotrauma SpriteEffects = SpriteEffects.FlipHorizontally }; } - - #endregion - - screenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); - prevUIScale = GUI.Scale; - _isCrewMenuOpen = GameMain.Config.CrewMenuOpen; - dismissedOrderPrefab ??= Order.GetPrefab("dismissed"); } #endregion @@ -291,8 +322,19 @@ namespace Barotrauma new RectTransform(crewListEntrySize, parent: crewList.Content.RectTransform, anchor: Anchor.TopRight), style: "CrewListBackground") { - UserData = character + UserData = character, + OnSecondaryClicked = (comp, data) => + { + if (data == null) { return false; } + if (GameMain.NetworkMember?.ConnectedClients?.Find(c => c.Character == data) is Client client) + { + CreateModerationContextMenu(PlayerInput.MousePosition.ToPoint(), client); + return true; + } + return false; + } }; + SetCharacterComponentTooltip(background); var iconRelativeWidth = (float)crewListEntrySize.Y / background.Rect.Width; @@ -310,7 +352,10 @@ namespace Barotrauma var paddingRelativeWidth = 0.35f * commandButtonAbsoluteHeight / background.Rect.Width; // "Padding" to prevent member-specific command button from overlapping job indicator - new GUIFrame(new RectTransform(new Vector2(paddingRelativeWidth, 1.0f), layoutGroup.RectTransform), style: null); + new GUIFrame(new RectTransform(new Vector2(paddingRelativeWidth, 1.0f), layoutGroup.RectTransform), style: null) + { + CanBeFocused = false + }; var jobIconBackground = new GUIImage( new RectTransform(new Vector2(0.8f * iconRelativeWidth, 0.8f), layoutGroup.RectTransform), @@ -320,7 +365,6 @@ namespace Barotrauma CanBeFocused = false, UserData = "job" }; - if (character?.Info?.Job.Prefab?.Icon != null) { new GUIImage( @@ -362,36 +406,6 @@ namespace Barotrauma }; nameBlock.Text = ToolBox.LimitString(character.Name, font, (int)nameBlock.Rect.Width); - var nameActualRealtiveWidth = Math.Min(nameRelativeWidth * background.Rect.Width, 150) / background.Rect.Width; - var characterButton = new GUIButton( - new RectTransform( - new Vector2(paddingRelativeWidth + 0.8f * iconRelativeWidth + nameActualRealtiveWidth + 2 * layoutGroup.RelativeSpacing, 1.0f), - background.RectTransform), - style: null) - { - UserData = character, - OnSecondaryClicked = (comp, data) => - { - if (data == null) { return false; } - if (GameMain.NetworkMember?.ConnectedClients?.Find(c => c.Character == data) is Client client) - { - CreateModerationContextMenu(PlayerInput.MousePosition.ToPoint(), client); - return true; - } - return false; - } - }; - SetCharacterButtonTooltip(characterButton); - - if (IsSinglePlayer) - { - characterButton.OnClicked = CharacterClicked; - } - else - { - characterButton.CanBeSelected = false; - } - new GUIImage( new RectTransform(new Vector2(0.1f * iconRelativeWidth, 0.5f), layoutGroup.RectTransform), style: "VerticalLine") @@ -487,15 +501,15 @@ namespace Barotrauma }; } - private void SetCharacterButtonTooltip(GUIButton characterButton) + private void SetCharacterComponentTooltip(GUIComponent characterComponent) { - var character = (Character)characterButton.UserData; - if (character?.Info?.Job?.Prefab == null) { return; } + if (!(characterComponent?.UserData is Character character)) { return; } + if (character.Info?.Job?.Prefab == null) { return; } string color = XMLExtensions.ColorToString(character.Info.Job.Prefab.UIColor); string tooltip = $"‖color:{color}‖{character.Name} ({character.Info.Job.Name})‖color:end‖"; var richTextData = RichTextData.GetRichTextData(tooltip, out string sanitizedTooltip); - characterButton.ToolTip = sanitizedTooltip; - characterButton.TooltipRichTextData = richTextData; + characterComponent.ToolTip = sanitizedTooltip; + characterComponent.TooltipRichTextData = richTextData; } /// @@ -564,10 +578,18 @@ namespace Barotrauma partial void RenameCharacterProjSpecific(CharacterInfo characterInfo) { if (!(crewList.Content.GetChildByUserData(characterInfo?.Character) is GUIComponent characterComponent)) { return; } + SetCharacterComponentTooltip(characterComponent); if (!(characterComponent.FindChild("name", recursive: true) is GUITextBlock nameBlock)) { return; } nameBlock.Text = ToolBox.LimitString(characterInfo.Name, nameBlock.Font, nameBlock.Rect.Width); - if (!(characterComponent.FindChild(c => c is GUIButton && c.UserData == characterInfo?.Character) is GUIButton characterButton)) { return; } - SetCharacterButtonTooltip(characterButton); + } + + private void OnCrewListRearranged(GUIListBox crewList, object draggedElementData) + { + if (crewList != this.crewList) { return; } + if (!(draggedElementData is Character)) { return; } + if (crewList.HasDraggedElementIndexChanged) { return; } + if (!IsSinglePlayer) { return; } + CharacterClicked(crewList.DraggedElement, draggedElementData); } #endregion @@ -682,15 +704,15 @@ namespace Barotrauma /// Sets the character's current order (if it's close enough to receive messages from orderGiver) and /// displays the order in the crew UI /// - public void SetCharacterOrder(Character character, Order order, string option, int priority, Character orderGiver) + public void SetCharacterOrder(Character character, Order order, string option, int priority, Character orderGiver, Hull targetHull = null) { if (order != null && order.TargetAllCharacters) { - Hull hull = null; + Hull hull = targetHull; if (order.IsReport) { - if (orderGiver?.CurrentHull == null) { return; } - hull = orderGiver.CurrentHull; + if (orderGiver?.CurrentHull == null && hull == null) { return; } + hull ??= orderGiver.CurrentHull; AddOrder(new Order(order.Prefab ?? order, hull, null, orderGiver), order.FadeOutTime); } else if(order.IsIgnoreOrder) @@ -748,7 +770,8 @@ namespace Barotrauma if (IsSinglePlayer) { character.SetOrder(order, option, priority, orderGiver, speak: orderGiver != character); - orderGiver?.Speak(order?.GetChatMessage(character.Name, orderGiver.CurrentHull?.DisplayName, givingOrderToSelf: character == orderGiver, orderOption: option)); + string message = order?.GetChatMessage(character.Name, orderGiver.CurrentHull?.DisplayName, givingOrderToSelf: character == orderGiver, orderOption: option, priority: priority); + orderGiver?.Speak(message); } else if (orderGiver != null) { @@ -1299,6 +1322,80 @@ namespace Barotrauma return 0; } + private bool CreateOrder(Order order, Hull targetHull = null) + { + var sub = Character.Controlled.Submarine; + + if (sub == null || sub.TeamID != Character.Controlled.TeamID || sub.Info.IsWreck) { return false; } + + SetCharacterOrder(null, order, null, CharacterInfo.HighestManualOrderPriority, Character.Controlled, targetHull); + + if (IsSinglePlayer) + { + HumanAIController.ReportProblem(Character.Controlled, order); + } + + return true; + } + + private void UpdateOrderDrag() + { + if (DraggedOrder is { } order) + { + if (dropOrder) + { + // stinky workaround + if (framesToSkip > 0) + { + framesToSkip--; + } + else + { + Hull hull = null; + + if (GUI.MouseOn is GUIFrame frame) + { + if (frame.UserData is Hull data) + { + hull = data; + } + else if (frame.Parent?.UserData is Hull parentData) + { + hull = parentData; + } + } + + framesToSkip = 2; + dropOrder = false; + DraggedOrder = null; + + if (hull is null && GUI.MouseOn is { Visible: true, CanBeFocused: true }) { return; } + + hull ??= Hull.hullList.FirstOrDefault(h => h.WorldRect.ContainsWorld(Screen.Selected.Cam.ScreenToWorld(PlayerInput.MousePosition))); + CreateOrder(order, hull); + } + } + else + { + DragOrder = DragOrder || Vector2.DistanceSquared(dragPoint, PlayerInput.MousePosition) > dragOrderTreshold * dragOrderTreshold; + + if (!PlayerInput.PrimaryMouseButtonHeld()) + { + if (DragOrder) + { + dropOrder = true; + } + else + { + DraggedOrder = null; + } + dragPoint = Vector2.Zero; + DragOrder = false; + } + } + } + } + partial void UpdateProjectSpecific(float deltaTime) { // Quick selection @@ -1316,6 +1413,8 @@ namespace Barotrauma if (GUI.DisableHUD) { return; } + UpdateOrderDrag(); + #region Command UI WasCommandInterfaceDisabledThisUpdate = false; @@ -1756,7 +1855,8 @@ namespace Barotrauma } } private GUIFrame commandFrame, targetFrame; - private GUIButton centerNode, returnNode, expandNode, shortcutCenterNode; + private GUIButton centerNode, returnNode, expandNode; + private GUIFrame shortcutCenterNode; private readonly List> optionNodes = new List>(); private Keys returnNodeHotkey = Keys.None, expandNodeHotkey = Keys.None; private readonly List shortcutNodes = new List(); @@ -1790,7 +1890,7 @@ namespace Barotrauma private bool isContextual; private readonly List contextualOrders = new List(); private Point shorcutCenterNodeOffset; - private const int maxShortCutNodeCount = 4; + private const int maxShortcutNodeCount = 4; private bool WasCommandInterfaceDisabledThisUpdate { get; set; } public static bool CanIssueOrders @@ -2101,6 +2201,8 @@ namespace Barotrauma shortcutNodes.Remove(node); }; RemoveOptionNodes(); + bool wasMinimapVisible = targetFrame != null && targetFrame.Visible; + HideMinimap(); if (returnNode != null) { @@ -2111,12 +2213,9 @@ namespace Barotrauma } // When the mini map is shown, always position the return node on the bottom - List matchingItems = null; - if (node?.UserData is Order order) - { - matchingItems = order.GetMatchingItems(true, interactableFor: characterContext ?? Character.Controlled); - } - var offset = matchingItems != null && matchingItems.Count > 1 ? + bool placeReturnNodeOnTheBottom = wasMinimapVisible || + (node?.UserData is Order order && order.GetMatchingItems(true, interactableFor: characterContext ?? Character.Controlled).Count > 1); + var offset = placeReturnNodeOnTheBottom ? new Point(0, (int)(returnNodeDistanceModifier * nodeDistance)) : node.RectTransform.AbsoluteOffset.Multiply(-returnNodeDistanceModifier); SetReturnNode(centerNode, offset); @@ -2137,12 +2236,7 @@ namespace Barotrauma { if (commandFrame == null) { return false; } RemoveOptionNodes(); - if (targetFrame != null) - { - targetFrame.Visible = false; - nodeConnectors.RectTransform.Parent = commandFrame.RectTransform; - nodeConnectors.RectTransform.RepositionChildInHierarchy(1); - } + HideMinimap(); // TODO: Center node could move to option node instead of being removed commandFrame.RemoveChild(centerNode); SetCenterNode(node); @@ -2163,6 +2257,15 @@ namespace Barotrauma return true; } + private void HideMinimap() + { + if (targetFrame == null || !targetFrame.Visible) { return; } + targetFrame.Visible = false; + // Reset the node connectors to their original parent + nodeConnectors.RectTransform.Parent = commandFrame.RectTransform; + nodeConnectors.RectTransform.RepositionChildInHierarchy(1); + } + private void CreateReturnNodeHotkey() { if (returnNode != null && returnNode.Visible) @@ -2203,6 +2306,7 @@ namespace Barotrauma } node.OnClicked = null; node.OnSecondaryClicked = null; + node.CanBeFocused = false; centerNode = node; } @@ -2219,6 +2323,7 @@ namespace Barotrauma } node.OnClicked = NavigateBackward; node.OnSecondaryClicked = null; + node.CanBeFocused = true; returnNode = node; } @@ -2240,9 +2345,33 @@ namespace Barotrauma { CreateOrderNodes(category); } - else if (userData is Order order) + else if (userData is Order nodeOrder) { - CreateOrderOptions(order); + Submarine submarine = GetTargetSubmarine(); + List matchingItems = null; + if (itemContext == null && nodeOrder.MustSetTarget) + { + matchingItems = nodeOrder.GetMatchingItems(submarine, true, interactableFor: characterContext ?? Character.Controlled); + } + //more than one target item -> create a minimap-like selection with a pic of the sub + if (itemContext == null && !(nodeOrder.TargetEntity is Item) && matchingItems != null && matchingItems.Count > 1) + { + CreateMinimapNodes(nodeOrder, submarine, matchingItems); + } + //only one target (or an order with no particular targets), just show options + else + { + CreateOrderOptionNodes(nodeOrder, itemContext ?? nodeOrder.TargetEntity as Item ?? matchingItems?.FirstOrDefault()); + } + } + else if (userData is (Order minimapOrder, string option) && minimapOrder.HasOptions && string.IsNullOrEmpty(option)) + { + CreateOrderOptionNodes(minimapOrder, minimapOrder.TargetEntity as Item); + } + else + { + DebugConsole.ThrowError($"Unexpected node user data of type {userData.GetType()} when creating command interface nodes"); + return false; } return true; } @@ -2291,7 +2420,7 @@ namespace Barotrauma node.RectTransform.MoveOverTime(offset, CommandNodeAnimDuration); if (Order.OrderCategoryIcons.TryGetValue(category, out Tuple sprite)) { - var tooltip = TextManager.Get("ordercategorytitle." + category.ToString().ToLower()); + var tooltip = TextManager.Get("ordercategorytitle." + category.ToString().ToLowerInvariant()); var categoryDescription = TextManager.Get("ordercategorydescription." + category.ToString(), true); if (!string.IsNullOrWhiteSpace(categoryDescription)) { tooltip += "\n" + categoryDescription; } CreateNodeIcon(Vector2.One, node.RectTransform, sprite.Item1, sprite.Item2, tooltip: tooltip); @@ -2302,99 +2431,91 @@ namespace Barotrauma private void CreateShortcutNodes() { - bool HasAppropriateJob(Character c, string jobId) => c.Info?.Job != null && c.Info.Job.Prefab.AppropriateOrders.Contains(jobId); - - Submarine sub = GetTargetSubmarine(); - + var sub = GetTargetSubmarine(); if (sub == null) { return; } - shortcutNodes.Clear(); - - if (shortcutNodes.Count < maxShortCutNodeCount && - sub.GetItems(false).Find(i => i.HasTag("reactor") && i.IsPlayerTeamInteractable)?.GetComponent() is Reactor reactor) + if (CanFitMoreNodes() && sub.GetItems(false).Find(i => i.HasTag("reactor") && i.IsPlayerTeamInteractable)?.GetComponent() is Reactor reactor) { - var reactorOutput = -reactor.CurrPowerConsumption; + float reactorOutput = -reactor.CurrPowerConsumption; // If player is not an engineer AND the reactor is not powered up AND nobody is using the reactor // ---> Create shortcut node for "Operate Reactor" order's "Power Up" option - if ((Character.Controlled == null || !HasAppropriateJob(Character.Controlled, "operatereactor")) && - reactorOutput < float.Epsilon && characters.None(c => c.SelectedConstruction == reactor.Item)) + if (ShouldDelegateOrder("operatereactor") && reactorOutput < float.Epsilon && characters.None(c => c.SelectedConstruction == reactor.Item)) { var order = new Order(Order.GetPrefab("operatereactor"), reactor.Item, reactor, Character.Controlled); - var option = order.Prefab.Options[0]; - shortcutNodes.Add( - CreateOrderOptionNode(shortcutNodeSize, null, Point.Zero, order, option, order.Prefab.GetOptionName(option), -1)); - } - } - - // TODO: Reconsider the conditions as bot captain can have the nav term selected without operating it - // If player is not a captain AND nobody is using the nav terminal AND the nav terminal is powered up - // --> Create shortcut node for Steer order - if (shortcutNodes.Count < maxShortCutNodeCount && (Character.Controlled == null || !HasAppropriateJob(Character.Controlled, "steer")) && - sub.GetItems(false).Find(i => i.HasTag("navterminal") && i.IsPlayerTeamInteractable) is Item nav && characters.None(c => c.SelectedConstruction == nav) && - nav.GetComponent() is Steering steering && steering.Voltage > steering.MinVoltage) - { - shortcutNodes.Add( - CreateOrderNode(shortcutNodeSize, null, Point.Zero, Order.GetPrefab("steer"), -1)); - } - - // If player is not a security officer AND invaders are reported - // --> Create shorcut node for Fight Intruders order - if (shortcutNodes.Count < maxShortCutNodeCount && (Character.Controlled == null || !HasAppropriateJob(Character.Controlled, "fightintruders")) && - Order.GetPrefab("reportintruders") is Order reportIntruders && ActiveOrders.Any(o => o.First.Prefab == reportIntruders)) - { - shortcutNodes.Add( - CreateOrderNode(shortcutNodeSize, null, Point.Zero, Order.GetPrefab("fightintruders"), -1)); - } - - // If player is not a mechanic AND a breach has been reported - // --> Create shorcut node for Fix Leaks order - if (shortcutNodes.Count < maxShortCutNodeCount && (Character.Controlled == null || !HasAppropriateJob(Character.Controlled, "fixleaks")) && - Order.GetPrefab("reportbreach") is Order reportBreach && ActiveOrders.Any(o => o.First.Prefab == reportBreach)) - { - shortcutNodes.Add( - CreateOrderNode(shortcutNodeSize, null, Point.Zero, Order.GetPrefab("fixleaks"), -1)); - } - - // --> Create shortcut nodes for the Repair orders - if (shortcutNodes.Count < maxShortCutNodeCount && Order.GetPrefab("reportbrokendevices") is Order reportBrokenDevices && ActiveOrders.Any(o => o.First.Prefab == reportBrokenDevices)) - { - // TODO: Doesn't work for player issued reports, because they don't have a target. - int repairNodes = 0; - string tag = "repairelectrical"; - if (shortcutNodes.Count < maxShortCutNodeCount && (Character.Controlled == null || !HasAppropriateJob(Character.Controlled, tag)) && ActiveOrders.Any(o => o.First.Prefab == reportBrokenDevices && o.First.TargetItemComponent is Repairable r && r.requiredSkills.Any(s => s.Identifier == "electrical"))) - { - shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, Order.GetPrefab(tag), -1)); - repairNodes++; - } - tag = "repairmechanical"; - if (shortcutNodes.Count < maxShortCutNodeCount && (Character.Controlled == null || !HasAppropriateJob(Character.Controlled, tag)) && ActiveOrders.Any(o => o.First.Prefab == reportBrokenDevices && o.First.TargetItemComponent is Repairable r && r.requiredSkills.Any(s => s.Identifier == "mechanical"))) - { - shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, Order.GetPrefab(tag), -1)); - repairNodes++; - } - if (repairNodes == 0 && shortcutNodes.Count < maxShortCutNodeCount) - { - tag = "repairsystems"; - if (Character.Controlled == null || !HasAppropriateJob(Character.Controlled, tag)) + string option = order.Prefab.Options[0]; + if (IsNonDuplicateOrder(order, option)) { - shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, Order.GetPrefab(tag), -1)); + shortcutNodes.Add(CreateOrderOptionNode(shortcutNodeSize, null, Point.Zero, order, option, order.Prefab.GetOptionName(option), -1)); } } } - + // TODO: Reconsider the conditions as bot captain can have the nav term selected without operating it + // If player is not a captain AND nobody is using the nav terminal AND the nav terminal is powered up + // --> Create shortcut node for Steer order + if (CanFitMoreNodes() && ShouldDelegateOrder("steer") && Order.GetPrefab("steer") is Order steerOrder && IsNonDuplicateOrder(steerOrder) && + sub.GetItems(false).Find(i => i.HasTag("navterminal") && i.IsPlayerTeamInteractable) is Item nav && characters.None(c => c.SelectedConstruction == nav) && + nav.GetComponent() is Steering steering && steering.Voltage > steering.MinVoltage) + { + shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, steerOrder, -1)); + } + // If player is not a security officer AND invaders are reported + // --> Create shorcut node for Fight Intruders order + if (CanFitMoreNodes() && ShouldDelegateOrder("fightintruders") && + Order.GetPrefab("reportintruders") is Order reportIntruders && ActiveOrders.Any(o => o.First.Prefab == reportIntruders) && + Order.GetPrefab("fightintruders") is Order fightOrder && IsNonDuplicateOrder(fightOrder)) + { + shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, fightOrder, -1)); + } + // If player is not a mechanic AND a breach has been reported + // --> Create shorcut node for Fix Leaks order + if (CanFitMoreNodes() && ShouldDelegateOrder("fixleaks") && Order.GetPrefab("fixleaks") is Order fixLeaksOrder && IsNonDuplicateOrder(fixLeaksOrder) && + Order.GetPrefab("reportbreach") is Order reportBreach && ActiveOrders.Any(o => o.First.Prefab == reportBreach)) + { + shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, fixLeaksOrder, -1)); + } + // --> Create shortcut nodes for the Repair orders + if (CanFitMoreNodes() && Order.GetPrefab("reportbrokendevices") is Order reportBrokenDevices && ActiveOrders.Any(o => o.First.Prefab == reportBrokenDevices)) + { + // TODO: Doesn't work for player issued reports, because they don't have a target. + bool useSpecificRepairOrder = false; + string tag = "repairelectrical"; + if (CanFitMoreNodes() && ShouldDelegateOrder(tag) && + ActiveOrders.Any(o => o.First.Prefab == reportBrokenDevices && o.First.TargetItemComponent is Repairable r && r.requiredSkills.Any(s => s.Identifier == "electrical"))) + { + if (Order.GetPrefab(tag) is Order repairElectricalOrder && IsNonDuplicateOrder(repairElectricalOrder)) + { + shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, repairElectricalOrder, -1)); + } + useSpecificRepairOrder = true; + } + tag = "repairmechanical"; + if (CanFitMoreNodes() && ShouldDelegateOrder(tag) && + ActiveOrders.Any(o => o.First.Prefab == reportBrokenDevices && o.First.TargetItemComponent is Repairable r && r.requiredSkills.Any(s => s.Identifier == "mechanical"))) + { + if (Order.GetPrefab(tag) is Order repairMechanicalOrder && IsNonDuplicateOrder(repairMechanicalOrder)) + { + shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, repairMechanicalOrder, -1)); + } + useSpecificRepairOrder = true; + } + tag = "repairsystems"; + if (!useSpecificRepairOrder && CanFitMoreNodes() && ShouldDelegateOrder(tag) && Order.GetPrefab(tag) is Order repairOrder && IsNonDuplicateOrder(repairOrder)) + { + shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, repairOrder, -1)); + } + } // If fire is reported // --> Create shortcut node for Extinguish Fires order - if (shortcutNodes.Count < maxShortCutNodeCount && ActiveOrders.Any(o => o.First.Prefab == Order.GetPrefab("reportfire"))) + if (CanFitMoreNodes() && Order.GetPrefab("extinguishfires") is Order extinguishOrder && IsNonDuplicateOrder(extinguishOrder) && + ActiveOrders.Any(o => o.First.Prefab == Order.GetPrefab("reportfire"))) { - shortcutNodes.Add( - CreateOrderNode(shortcutNodeSize, null, Point.Zero, Order.GetPrefab("extinguishfires"), -1)); + shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, extinguishOrder, -1)); } - - if (shortcutNodes.Count < maxShortCutNodeCount && characterContext?.Info?.Job?.Prefab?.AppropriateOrders != null) + if (CanFitMoreNodes() && characterContext?.Info?.Job?.Prefab?.AppropriateOrders != null) { foreach (string orderIdentifier in characterContext.Info.Job.Prefab.AppropriateOrders) { - if (Order.GetPrefab(orderIdentifier) is Order orderPrefab && + if (Order.GetPrefab(orderIdentifier) is Order orderPrefab && IsNonDuplicateOrder(orderPrefab) && shortcutNodes.None(n => (n.UserData is Order order && order.Identifier == orderIdentifier) || (n.UserData is Tuple orderWithOption && orderWithOption.Item1.Identifier == orderIdentifier)) && !orderPrefab.IsReport && orderPrefab.Category != null) @@ -2403,22 +2524,19 @@ namespace Barotrauma { shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, orderPrefab, -1)); } - if (shortcutNodes.Count >= maxShortCutNodeCount) { break; } + if (!CanFitMoreNodes()) { break; } } } } - - if (shortcutNodes.Count < maxShortCutNodeCount && characterContext != null && !characterContext.IsDismissed) + if (CanFitMoreNodes() && characterContext != null && !characterContext.IsDismissed) { - shortcutNodes.Add( - CreateOrderNode(shortcutNodeSize, null, Point.Zero, dismissedOrderPrefab, -1)); + shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, dismissedOrderPrefab, -1)); } - if (shortcutNodes.Count < 1) { return; } - - shortcutCenterNode = new GUIButton( - new RectTransform(shortcutCenterNodeSize, parent: commandFrame.RectTransform, anchor: Anchor.Center), - style: null); + shortcutCenterNode = new GUIFrame(new RectTransform(shortcutCenterNodeSize, parent: commandFrame.RectTransform, anchor: Anchor.Center), style: null) + { + CanBeFocused = false + }; CreateNodeIcon(shortcutCenterNode.RectTransform, "CommandShortcutNode"); foreach (GUIComponent c in shortcutCenterNode.Children) { @@ -2427,15 +2545,29 @@ namespace Barotrauma c.SelectedColor = c.Color; } shortcutCenterNode.RectTransform.MoveOverTime(shorcutCenterNodeOffset, CommandNodeAnimDuration); - - var nodeCountForCalculations = shortcutNodes.Count * 2 + 2; - var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, 0.75f * nodeDistance, nodeCountForCalculations); - var firstOffsetIndex = nodeCountForCalculations / 2 - 1; + int nodeCountForCalculations = shortcutNodes.Count * 2 + 2; + Vector2[] offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, 0.75f * nodeDistance, nodeCountForCalculations); + int firstOffsetIndex = nodeCountForCalculations / 2 - 1; for (int i = 0; i < shortcutNodes.Count; i++) { shortcutNodes[i].RectTransform.Parent = commandFrame.RectTransform; shortcutNodes[i].RectTransform.MoveOverTime(shorcutCenterNodeOffset + offsets[firstOffsetIndex - i].ToPoint(), CommandNodeAnimDuration); } + + bool CanFitMoreNodes() + { + return shortcutNodes.Count < maxShortcutNodeCount; + } + static bool ShouldDelegateOrder(string orderIdentifier) + { + return !(Character.Controlled is Character c) || !(c?.Info?.Job != null && c.Info.Job.Prefab.AppropriateOrders.Contains(orderIdentifier)); + } + bool IsNonDuplicateOrder(Order orderPrefab, string option = null) + { + return characterContext == null || (string.IsNullOrEmpty(option) ? + characterContext.CurrentOrders.None(oi => oi.Order?.Identifier == orderPrefab?.Identifier) : + characterContext.CurrentOrders.None(oi => oi.Order?.Identifier == orderPrefab?.Identifier && oi.OrderOption == option)); + } } private void CreateOrderNodes(OrderCategory orderCategory) @@ -2621,6 +2753,7 @@ namespace Barotrauma item.GetConnectedComponents(recursive: true).Any(c => c.Item.HasTag(operateWeaponsPrefab.TargetItems))); } + /// Use a negative value (e.g. -1) if there should be no hotkey associated with the node private GUIButton CreateOrderNode(Point size, RectTransform parent, Point offset, Order order, int hotkey, bool disableNode = false, bool checkIfOrderCanBeHeard = true) { var node = new GUIButton( @@ -2694,7 +2827,7 @@ namespace Barotrauma if (disableNode) { node.CanBeFocused = icon.CanBeFocused = false; - CreateBlockIcon(node.RectTransform); + CreateBlockIcon(node.RectTransform, tooltip: TextManager.Get("nocharactercanhear")); } else if (hotkey >= 0) { @@ -2703,195 +2836,137 @@ namespace Barotrauma return node; } - private void CreateOrderOptions(Order order) + private void CreateMinimapNodes(Order order, Submarine submarine, List matchingItems) { - Submarine submarine = GetTargetSubmarine(); - var matchingItems = (itemContext == null && order.MustSetTarget) ? order.GetMatchingItems(submarine, true, interactableFor: characterContext ?? Character.Controlled) : new List(); - - //more than one target item -> create a minimap-like selection with a pic of the sub - if (itemContext == null && matchingItems.Count > 1) + // TODO: Further adjustments to frameSize calculations + // I just divided the existing sizes by 2 to get it working quickly without it overlapping too much + Point frameSize; + Rectangle subBorders = submarine.GetDockedBorders(); + if (subBorders.Width > subBorders.Height) { - // TODO: Further adjustments to frameSize calculations - // I just divided the existing sizes by 2 to get it working quickly without it overlapping too much - Point frameSize; - Rectangle subBorders = submarine.GetDockedBorders(); - if (subBorders.Width > subBorders.Height) + frameSize.X = Math.Min(GameMain.GraphicsWidth / 2, GameMain.GraphicsWidth - 50) / 2; + //height depends on the dimensions of the sub + frameSize.Y = (int)(frameSize.X * (subBorders.Height / (float)subBorders.Width)); + } + else + { + frameSize.Y = Math.Min((int)(GameMain.GraphicsHeight * 0.6f), GameMain.GraphicsHeight - 50) / 2; + //width depends on the dimensions of the sub + frameSize.X = (int)(frameSize.Y * (subBorders.Width / (float)subBorders.Height)); + } + + // TODO: Use the old targetFrame if possible + targetFrame = new GUIFrame( + new RectTransform(frameSize, parent: commandFrame.RectTransform, anchor: Anchor.Center) { - frameSize.X = Math.Min(GameMain.GraphicsWidth / 2, GameMain.GraphicsWidth - 50) / 2; - //height depends on the dimensions of the sub - frameSize.Y = (int)(frameSize.X * (subBorders.Height / (float)subBorders.Width)); - } - else + AbsoluteOffset = new Point(0, -150), + Pivot = Pivot.BottomCenter + }, + style: "InnerFrame"); + + submarine.CreateMiniMap(targetFrame, pointsOfInterest: matchingItems); + + new GUICustomComponent(new RectTransform(Vector2.One, targetFrame.RectTransform), onDraw: DrawMiniMapOverlay) + { + CanBeFocused = false, + UserData = submarine + }; + + List optionElements = new List(); + foreach (Item item in matchingItems) + { + var itemTargetFrame = targetFrame.Children.First().FindChild(item); + if (itemTargetFrame == null) { continue; } + + var anchor = Anchor.TopLeft; + if (itemTargetFrame.RectTransform.RelativeOffset.X < 0.5f) { - frameSize.Y = Math.Min((int)(GameMain.GraphicsHeight * 0.6f), GameMain.GraphicsHeight - 50) / 2; - //width depends on the dimensions of the sub - frameSize.X = (int)(frameSize.Y * (subBorders.Width / (float)subBorders.Height)); - } - - // TODO: Use the old targetFrame if possible - targetFrame = new GUIFrame( - new RectTransform(frameSize, parent: commandFrame.RectTransform, anchor: Anchor.Center) + if (itemTargetFrame.RectTransform.RelativeOffset.Y < 0.5f) { - AbsoluteOffset = new Point(0, -150), - Pivot = Pivot.BottomCenter - }, - style: "InnerFrame"); - - submarine.CreateMiniMap(targetFrame, pointsOfInterest: matchingItems); - - new GUICustomComponent(new RectTransform(Vector2.One, targetFrame.RectTransform), onDraw: DrawMiniMapOverlay) - { - CanBeFocused = false, - UserData = submarine - }; - - List optionElements = new List(); - foreach (Item item in matchingItems) - { - var itemTargetFrame = targetFrame.Children.First().FindChild(item); - if (itemTargetFrame == null) { continue; } - - var anchor = Anchor.TopLeft; - if (itemTargetFrame.RectTransform.RelativeOffset.X < 0.5f) - { - if (itemTargetFrame.RectTransform.RelativeOffset.Y < 0.5f) - { - anchor = Anchor.BottomRight; - } - else - { - anchor = Anchor.TopRight; - } - } - else if (itemTargetFrame.RectTransform.RelativeOffset.Y < 0.5f) - { - anchor = Anchor.BottomLeft; - } - - GUIComponent optionElement; - if (order.Options.Length > 1) - { - optionElement = new GUIFrame( - new RectTransform( - new Point((int)(250 * GUI.Scale), (int)((40 + order.Options.Length * 40) * GUI.Scale)), - parent: itemTargetFrame.RectTransform, - anchor: anchor), - style: "InnerFrame"); - - new GUIFrame( - new RectTransform(Vector2.One, optionElement.RectTransform, anchor: Anchor.Center), - style: "OuterGlow", - color: Color.Black * 0.7f); - - var optionContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.9f), optionElement.RectTransform, anchor: Anchor.Center)) - { - RelativeSpacing = 0.05f, - Stretch = true - }; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), optionContainer.RectTransform), - item?.Name ?? GetOrderNameBasedOnContextuality(order)); - - for (int i = 0; i < order.Options.Length; i++) - { - var optionButton = new GUIButton( - new RectTransform(new Vector2(1.0f, 0.2f), optionContainer.RectTransform), - text: order.GetOptionName(i), style: "GUITextBox") - { - UserData = new Tuple( - item == null ? order : new Order(order, item, order.GetTargetItemComponent(item)), - order.Options[i]), - Font = GUI.SmallFont, - OnClicked = (_, userData) => - { - if (!CanIssueOrders) { return false; } - var o = userData as Tuple; - SetCharacterOrder(characterContext ?? GetCharacterForQuickAssignment(o.Item1), o.Item1, o.Item2, CharacterInfo.HighestManualOrderPriority, Character.Controlled); - DisableCommandUI(); - return true; - } - }; - if (CanOpenManualAssignment(optionButton)) - { - optionButton.OnSecondaryClicked = (button, _) => CreateAssignmentNodes(button); - } - optionNodes.Add(new Tuple(optionButton, Keys.None)); - } + anchor = Anchor.BottomRight; } else { - var userData = new Tuple(item == null ? order : new Order(order, item, order.GetTargetItemComponent(item)), ""); - optionElement = new GUIButton( - new RectTransform( - new Point((int)(50 * GUI.Scale)), - parent: itemTargetFrame.RectTransform, - anchor: anchor), - style: null) - { - UserData = userData, - Font = GUI.SmallFont, - OnClicked = (_, userData) => - { - if (!CanIssueOrders) { return false; } - var o = userData as Tuple; - SetCharacterOrder(characterContext ?? GetCharacterForQuickAssignment(o.Item1), o.Item1, o.Item2, CharacterInfo.HighestManualOrderPriority, Character.Controlled); - DisableCommandUI(); - return true; - } - }; - if (CanOpenManualAssignment(optionElement)) - { - optionElement.OnSecondaryClicked = (button, _) => CreateAssignmentNodes(button); - } - var colorMultiplier = characters.Any(c => c.CurrentOrders.Any(o => o.Order != null && - o.Order.Identifier == userData.Item1.Identifier && - o.Order.TargetEntity == userData.Item1.TargetEntity)) ? 0.5f : 1f; - CreateNodeIcon(Vector2.One, optionElement.RectTransform, item.Prefab.MinimapIcon ?? order.SymbolSprite, order.Color * colorMultiplier, tooltip: item.Name); - optionNodes.Add(new Tuple(optionElement, Keys.None)); + anchor = Anchor.TopRight; } - optionElements.Add(optionElement); + } + else if (itemTargetFrame.RectTransform.RelativeOffset.Y < 0.5f) + { + anchor = Anchor.BottomLeft; } - Rectangle clampArea = new Rectangle(10, 10, GameMain.GraphicsWidth - 20, GameMain.GraphicsHeight - 20); - if (order.Identifier == "operateweapons") + var userData = new Tuple(item == null ? order : new Order(order, item, order.GetTargetItemComponent(item)), ""); + var optionElement = new GUIButton( + new RectTransform( + new Point((int)(50 * GUI.Scale)), + parent: itemTargetFrame.RectTransform, + anchor: anchor), + style: null) { - Rectangle disallowedArea = targetFrame.GetChild().Rect; - Point originalSize = disallowedArea.Size; - disallowedArea.Size = disallowedArea.MultiplySize(0.9f); - disallowedArea.X += (originalSize.X - disallowedArea.Size.X) / 2; - disallowedArea.Y += (originalSize.Y - disallowedArea.Size.Y) / 2; - GUI.PreventElementOverlap(optionElements, new List() { disallowedArea }, clampArea); - nodeConnectors.RectTransform.Parent = targetFrame.RectTransform; - nodeConnectors.RectTransform.SetAsFirstChild(); - } - else + UserData = userData, + Font = GUI.SmallFont, + OnClicked = (button, userData) => + { + if (!CanIssueOrders) { return false; } + var o = userData as Tuple; + if (o.Item1.HasOptions) + { + NavigateForward(button, userData); + } + else + { + SetCharacterOrder(characterContext ?? GetCharacterForQuickAssignment(o.Item1), o.Item1, o.Item2, CharacterInfo.HighestManualOrderPriority, Character.Controlled); + DisableCommandUI(); + } + return true; + } + }; + if (CanOpenManualAssignment(optionElement)) { - GUI.PreventElementOverlap(optionElements, clampArea: clampArea); + optionElement.OnSecondaryClicked = (button, _) => CreateAssignmentNodes(button); } - - var shadow = new GUIFrame( - new RectTransform(targetFrame.Rect.Size + new Point((int)(200 * GUI.Scale)), targetFrame.RectTransform, anchor: Anchor.Center), - style: "OuterGlow", - color: matchingItems.Count > 1 ? Color.Black * 0.9f : Color.Black * 0.7f); - shadow.SetAsFirstChild(); + var colorMultiplier = characters.Any(c => c.CurrentOrders.Any(o => o.Order != null && + o.Order.Identifier == userData.Item1.Identifier && + o.Order.TargetEntity == userData.Item1.TargetEntity)) ? 0.5f : 1f; + CreateNodeIcon(Vector2.One, optionElement.RectTransform, item.Prefab.MinimapIcon ?? order.SymbolSprite, order.Color * colorMultiplier, tooltip: item.Name); + optionNodes.Add(new Tuple(optionElement, Keys.None)); + optionElements.Add(optionElement); } - //only one target (or an order with no particular targets), just show options - else + + Rectangle clampArea = new Rectangle(10, 10, GameMain.GraphicsWidth - 20, GameMain.GraphicsHeight - 20); + Rectangle disallowedArea = targetFrame.GetChild().Rect; + Point originalSize = disallowedArea.Size; + disallowedArea.Size = disallowedArea.MultiplySize(0.9f); + disallowedArea.X += (originalSize.X - disallowedArea.Size.X) / 2; + disallowedArea.Y += (originalSize.Y - disallowedArea.Size.Y) / 2; + GUI.PreventElementOverlap(optionElements, new List() { disallowedArea }, clampArea); + nodeConnectors.RectTransform.Parent = targetFrame.RectTransform; + nodeConnectors.RectTransform.SetAsFirstChild(); + + var shadow = new GUIFrame( + new RectTransform(targetFrame.Rect.Size + new Point((int)(200 * GUI.Scale)), targetFrame.RectTransform, anchor: Anchor.Center), + style: "OuterGlow", + color: matchingItems.Count > 1 ? Color.Black * 0.9f : Color.Black * 0.7f); + shadow.SetAsFirstChild(); + } + + private void CreateOrderOptionNodes(Order order, Item targetItem) + { + if (itemContext != null) { - var item = itemContext != null ? - (order.UseController ? itemContext.GetConnectedComponents().FirstOrDefault()?.Item ?? itemContext.GetConnectedComponents(recursive: true).FirstOrDefault()?.Item : itemContext) : - (matchingItems.Count > 0 ? matchingItems[0] : null); - var o = item == null || !order.IsPrefab ? order : new Order(order, item, order.GetTargetItemComponent(item)); - var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, nodeDistance, - GetCircumferencePointCount(order.Options.Length), - GetFirstNodeAngle(order.Options.Length)); - var offsetIndex = 0; - for (int i = 0; i < order.Options.Length; i++) - { - optionNodes.Add(new Tuple( - CreateOrderOptionNode(nodeSize, commandFrame.RectTransform, offsets[offsetIndex++].ToPoint(), o, order.Options[i], order.GetOptionName(i), (i + 1) % 10), - Keys.D0 + (i + 1) % 10)); - } + targetItem = !order.UseController ? itemContext : + itemContext.GetConnectedComponents().FirstOrDefault()?.Item ?? itemContext.GetConnectedComponents(recursive: true).FirstOrDefault()?.Item; + } + var o = (targetItem == null || !order.IsPrefab) ? order : new Order(order, targetItem, order.GetTargetItemComponent(targetItem)); + var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, nodeDistance, + GetCircumferencePointCount(order.Options.Length), + GetFirstNodeAngle(order.Options.Length)); + var offsetIndex = 0; + for (int i = 0; i < order.Options.Length; i++) + { + optionNodes.Add(new Tuple( + CreateOrderOptionNode(nodeSize, commandFrame.RectTransform, offsets[offsetIndex++].ToPoint(), o, order.Options[i], order.GetOptionName(i), (i + 1) % 10), + Keys.D0 + (i + 1) % 10)); } } @@ -2928,7 +3003,7 @@ namespace Barotrauma { node.CanBeFocused = false; if (icon != null) { icon.CanBeFocused = false; } - CreateBlockIcon(node.RectTransform); + CreateBlockIcon(node.RectTransform, tooltip: TextManager.Get("nocharactercanhear")); } else if (hotkey >= 0) { @@ -2990,9 +3065,7 @@ namespace Barotrauma SetCenterNode(clickedOptionNode); node = null; } - targetFrame.Visible = false; - nodeConnectors.RectTransform.Parent = commandFrame.RectTransform; - nodeConnectors.RectTransform.RepositionChildInHierarchy(1); + HideMinimap(); } if (shortcutCenterNode != null) { @@ -3178,7 +3251,7 @@ namespace Barotrauma if (!canHear) { node.CanBeFocused = orderIcon.CanBeFocused = false; - CreateBlockIcon(node.RectTransform); + CreateBlockIcon(node.RectTransform, tooltip: TextManager.Get("thischaractercanthear")); } if (hotkey >= 0) { @@ -3270,14 +3343,23 @@ namespace Barotrauma }; } - private void CreateBlockIcon(RectTransform parent) + private void CreateBlockIcon(RectTransform parent, string tooltip = null) { - new GUIImage(new RectTransform(new Vector2(0.9f), parent, anchor: Anchor.Center), cancelIcon, scaleToFit: true) + var icon = new GUIImage(new RectTransform(new Vector2(0.9f), parent, anchor: Anchor.Center), cancelIcon, scaleToFit: true) { CanBeFocused = false, Color = GUI.Style.Red * nodeColorMultiplier, HoverColor = GUI.Style.Red }; + if (!string.IsNullOrEmpty(tooltip)) + { + icon.ToolTip = tooltip; + string color = XMLExtensions.ColorToString(GUI.Style.Red); + tooltip = $"‖color:{color}‖{tooltip}‖color:end‖"; + var richTextData = RichTextData.GetRichTextData(tooltip, out _); + icon.TooltipRichTextData = richTextData; + icon.CanBeFocused = true; + } } private int GetCircumferencePointCount(int nodes) @@ -3383,15 +3465,15 @@ namespace Barotrauma private bool CanOpenManualAssignment(GUIComponent node) { if (node == null || characterContext != null) { return false; } - if (node.UserData is Tuple orderInfo) + if (node.UserData is (Order minimapOrder, string option)) { - return !orderInfo.Item1.TargetAllCharacters; + return !minimapOrder.TargetAllCharacters && (!minimapOrder.HasOptions || !string.IsNullOrEmpty(option)); } - if (node.UserData is Order order) + if (node.UserData is Order nodeOrder) { - return !order.TargetAllCharacters && !order.HasOptions && - (!order.MustSetTarget || itemContext != null || - order.GetMatchingItems(GetTargetSubmarine(), true, interactableFor: Character.Controlled).Count < 2); + return !nodeOrder.TargetAllCharacters && !nodeOrder.HasOptions && + (!nodeOrder.MustSetTarget || itemContext != null || + nodeOrder.GetMatchingItems(GetTargetSubmarine(), true, interactableFor: Character.Controlled).Count < 2); } return false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index e2a74ba9a..99526244b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -761,7 +761,7 @@ namespace Barotrauma Faction faction = campaign.Factions.FirstOrDefault(f => f.Prefab.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase)); if (faction?.Reputation != null) { - faction.Reputation.Value = rep; + faction.Reputation.SetReputation(rep); } else { @@ -771,7 +771,7 @@ namespace Barotrauma if (reputation.HasValue) { - campaign.Map.CurrentLocation.Reputation.Value = reputation.Value; + campaign.Map.CurrentLocation.Reputation.SetReputation(reputation.Value); campaign?.CampaignUI?.UpgradeStore?.RefreshAll(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs index 90aae527a..86db15f21 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs @@ -24,7 +24,7 @@ namespace Barotrauma if (GameMain.NetLobbyScreen.HeadSelectionList != null) { GameMain.NetLobbyScreen.HeadSelectionList.Visible = false; } if (GameMain.NetLobbyScreen.JobSelectionFrame != null) { GameMain.NetLobbyScreen.JobSelectionFrame.Visible = false; } } - if (tabMenu == null && GameMode is TutorialMode == false) + if (tabMenu == null && !(GameMode is TutorialMode) && !ConversationAction.IsDialogOpen) { tabMenu = new TabMenu(); HintManager.OnShowTabMenu(); @@ -34,7 +34,6 @@ namespace Barotrauma tabMenu = null; NetLobbyScreen.JobInfoFrame = null; } - return true; } @@ -44,7 +43,7 @@ namespace Barotrauma private GUIComponent respawnInfoFrame, respawnButtonContainer; private GUITextBlock respawnInfoText; private GUITickBox respawnTickBox; - private GUILayoutGroup TopLeftButtonGroup; + private void CreateTopLeftButtons() { if (topLeftButtonGroup != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs index 200d39720..00f03cf5b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs @@ -1518,6 +1518,12 @@ namespace Barotrauma "Automatic quickstart enabled", "Will the game automatically move on to Quickstart when the game is launched"); + addDebugTickBox( + TestScreenEnabled, + (b) => TestScreenEnabled = b, + "Test screen enabled", + "Will the game automatically move on to a test screen when the game is launched"); + addDebugTickBox( AutomaticCampaignLoadEnabled, (b) => AutomaticCampaignLoadEnabled = b, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index 516f62488..953594247 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -161,7 +161,7 @@ namespace Barotrauma public override void CreateSlots() { - if (visualSlots == null) { visualSlots = new VisualSlot[capacity]; } + visualSlots ??= new VisualSlot[capacity]; float multiplier = !GUI.IsFourByThree() ? UIScale : UIScale * 0.925f; @@ -359,7 +359,8 @@ namespace Barotrauma int personalSlotX = HUDLayoutSettings.InventoryAreaLower.Right - SlotSize.X - Spacing; for (int i = 0; i < visualSlots.Length; i++) { - if (HideSlot(i)) continue; + if (HideSlot(i)) { continue; } + if (SlotTypes[i] == InvSlotType.RightHand || SlotTypes[i] == InvSlotType.LeftHand) { continue; } if (PersonalSlots.HasFlag(SlotTypes[i])) { //upperX -= slotSize.X + spacing; @@ -371,10 +372,18 @@ namespace Barotrauma } int lowerX = x; + int handSlotX = x; int personalSlotY = GameMain.GraphicsHeight - bottomOffset * 2 - Spacing * 2 - (int)(!GUI.IsFourByThree() ? UnequippedIndicator.size.Y * UIScale * IndicatorScaleAdjustment : UnequippedIndicator.size.Y * UIScale * IndicatorScaleAdjustment * 2f); for (int i = 0; i < SlotPositions.Length; i++) { - if (HideSlot(i)) continue; + if (SlotTypes[i] == InvSlotType.RightHand || SlotTypes[i] == InvSlotType.LeftHand) + { + SlotPositions[i] = new Vector2(handSlotX, personalSlotY); + handSlotX += visualSlots[i].Rect.Width + Spacing; + continue; + } + + if (HideSlot(i)) { continue; } if (PersonalSlots.HasFlag(SlotTypes[i])) { SlotPositions[i] = new Vector2(personalSlotX, personalSlotY); @@ -390,7 +399,8 @@ namespace Barotrauma x = lowerX; for (int i = 0; i < SlotPositions.Length; i++) { - if (!HideSlot(i)) continue; + if (!HideSlot(i)) { continue; } + if (SlotTypes[i] == InvSlotType.RightHand || SlotTypes[i] == InvSlotType.LeftHand) { continue; } x -= visualSlots[i].Rect.Width + Spacing; SlotPositions[i] = new Vector2(x, GameMain.GraphicsHeight - bottomOffset); } @@ -404,7 +414,8 @@ namespace Barotrauma for (int i = 0; i < SlotPositions.Length; i++) { - if (HideSlot(i)) continue; + if (HideSlot(i)) { continue; } + if (SlotTypes[i] == InvSlotType.RightHand || SlotTypes[i] == InvSlotType.LeftHand) { continue; } if (PersonalSlots.HasFlag(SlotTypes[i])) { SlotPositions[i] = new Vector2(personalSlotX, personalSlotY); @@ -416,9 +427,16 @@ namespace Barotrauma x += visualSlots[i].Rect.Width + Spacing; } } + int handSlotX = x - visualSlots[0].Rect.Width - Spacing; for (int i = 0; i < SlotPositions.Length; i++) { - if (!HideSlot(i)) continue; + if (SlotTypes[i] == InvSlotType.RightHand || SlotTypes[i] == InvSlotType.LeftHand) + { + bool rightSlot = SlotTypes[i] == InvSlotType.RightHand; + SlotPositions[i] = new Vector2(rightSlot ? handSlotX : handSlotX - visualSlots[0].Rect.Width - Spacing, personalSlotY); + continue; + } + if (!HideSlot(i)) { continue; } SlotPositions[i] = new Vector2(x, GameMain.GraphicsHeight - bottomOffset); x += visualSlots[i].Rect.Width + Spacing; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs index ebe4c63e5..08ffe00cf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using Barotrauma.IO; using System.Text; using System.Xml.Linq; +using Barotrauma.Sounds; namespace Barotrauma.Items.Components { @@ -18,7 +19,11 @@ namespace Barotrauma.Items.Components protected float currentCrossHairScale, currentCrossHairPointerScale; + private RoundSound chargeSound; + private SoundChannel chargeSoundChannel; + private readonly List particleEmitters = new List(); + private readonly List particleEmitterCharges = new List(); [Serialize(1.0f, false, description: "The scale of the crosshair sprite (if there is one).")] public float CrossHairScale @@ -48,6 +53,12 @@ namespace Barotrauma.Items.Components case "particleemitter": particleEmitters.Add(new ParticleEmitter(subElement)); break; + case "particleemittercharge": + particleEmitterCharges.Add(new ParticleEmitter(subElement)); + break; + case "chargesound": + chargeSound = Submarine.LoadRoundSound(subElement, false); + break; } } } @@ -84,6 +95,51 @@ namespace Barotrauma.Items.Components crosshairPointerPos = PlayerInput.MousePosition; } + partial void UpdateProjSpecific(float deltaTime) + { + float chargeRatio = currentChargeTime / MaxChargeTime; + + switch (currentChargingState) + { + case ChargingState.WindingUp: + case ChargingState.WindingDown: + Vector2 particlePos = item.WorldPosition + ConvertUnits.ToDisplayUnits(TransformedBarrelPos); + float sizeMultiplier = Math.Clamp(chargeRatio, 0.1f, 1f); + foreach (ParticleEmitter emitter in particleEmitterCharges) + { + emitter.Emit(deltaTime, particlePos, hullGuess: null, sizeMultiplier: sizeMultiplier, colorMultiplier: emitter.Prefab.Properties.ColorMultiplier); + } + + if (chargeSoundChannel == null || !chargeSoundChannel.IsPlaying) + { + if (chargeSound != null) + { + chargeSoundChannel = SoundPlayer.PlaySound(chargeSound.Sound, item.WorldPosition, chargeSound.Volume, chargeSound.Range, ignoreMuffling: chargeSound.IgnoreMuffling); + if (chargeSoundChannel != null) chargeSoundChannel.Looping = true; + } + } + else if (chargeSoundChannel != null) + { + chargeSoundChannel.FrequencyMultiplier = MathHelper.Lerp(0.5f, 1.5f, chargeRatio); + } + break; + default: + if (chargeSoundChannel != null) + { + if (chargeSoundChannel.IsPlaying) + { + chargeSoundChannel.FadeOutAndDispose(); + chargeSoundChannel.Looping = false; + } + else + { + chargeSoundChannel = null; + } + } + break; + } + } + public override void DrawHUD(SpriteBatch spriteBatch, Character character) { if (character == null || !character.IsKeyDown(InputType.Aim)) { return; } @@ -92,7 +148,7 @@ namespace Barotrauma.Items.Components if (character.ViewTarget != null && (character.ViewTarget is Item item) && item.Prefab.FocusOnSelected) { return; } GUI.HideCursor = (crosshairSprite != null || crosshairPointerSprite != null) && - GUI.MouseOn == null && !Inventory.IsMouseOnInventory() && !GameMain.Instance.Paused; + GUI.MouseOn == null && !Inventory.IsMouseOnInventory && !GameMain.Instance.Paused; if (GUI.HideCursor) { crosshairSprite?.Draw(spriteBatch, crosshairPos, Color.White, 0, currentCrossHairScale); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index cc6ef2654..faaa76c27 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -246,6 +246,7 @@ namespace Barotrauma.Items.Components public void PlaySound(ActionType type, Character user = null) { if (!hasSoundsOfType[(int)type]) { return; } + if (GameMain.Client?.MidRoundSyncing ?? false) { return; } if (loopingSound != null) { @@ -429,7 +430,7 @@ namespace Barotrauma.Items.Components } foreach (ItemComponent component in item.Components) { - if (component.name.ToLower() == LinkUIToComponent.ToLower()) + if (component.name.Equals(LinkUIToComponent, StringComparison.OrdinalIgnoreCase)) { linkToUIComponent = component; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs index 711376751..24149ae69 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs @@ -126,7 +126,8 @@ namespace Barotrauma.Items.Components private void DrawOverLay(SpriteBatch spriteBatch, GUICustomComponent overlayComponent) { overlayComponent.RectTransform.SetAsLastChild(); - var lastSlot = inputContainer.Inventory.visualSlots.Last(); + if (!(inputContainer?.Inventory?.visualSlots is { } visualSlots)) { return; } + var lastSlot = visualSlots.Last(); GUI.DrawRectangle(spriteBatch, new Rectangle( diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index e63a0fb84..f85f13a2a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -255,12 +255,15 @@ namespace Barotrauma.Items.Components var item1 = c1.GUIComponent.UserData as FabricationRecipe; var item2 = c2.GUIComponent.UserData as FabricationRecipe; - bool hasSkills1 = FabricationDegreeOfSuccess(character, item1.RequiredSkills) >= 0.5f; - bool hasSkills2 = FabricationDegreeOfSuccess(character, item2.RequiredSkills) >= 0.5f; + int itemPlacement1 = FabricationDegreeOfSuccess(character, item1.RequiredSkills) >= 0.5f ? 0 : -1; + int itemPlacement2 = FabricationDegreeOfSuccess(character, item2.RequiredSkills) >= 0.5f ? 0 : -1; - if (hasSkills1 != hasSkills2) + itemPlacement1 += item1.RequiresRecipe && !character.HasRecipeForItem(item1.TargetItem.Identifier) ? -2 : 0; + itemPlacement2 += item2.RequiresRecipe && !character.HasRecipeForItem(item2.TargetItem.Identifier) ? -2 : 0; + + if (itemPlacement1 != itemPlacement2) { - return hasSkills1 ? -1 : 1; + return itemPlacement1 > itemPlacement2 ? -1 : 1; } return string.Compare(item1.DisplayName, item2.DisplayName); @@ -285,6 +288,18 @@ namespace Barotrauma.Items.Components { insufficientSkillsText.RectTransform.RepositionChildInHierarchy(itemList.Content.RectTransform.GetChildIndex(firstinSufficient.RectTransform)); } + + var requiresRecipeText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), itemList.Content.RectTransform), + TextManager.Get("fabricatorrequiresrecipe", returnNull: true) ?? "Requires recipe to fabricate", textColor: Color.Red, font: GUI.SubHeadingFont) + { + AutoScaleHorizontal = true, + CanBeFocused = false + }; + var firstRequiresRecipe = itemList.Content.Children.FirstOrDefault(c => c.UserData is FabricationRecipe fabricableItem && (fabricableItem.RequiresRecipe && !character.HasRecipeForItem(fabricableItem.TargetItem.Identifier))); + if (firstRequiresRecipe != null) + { + requiresRecipeText.RectTransform.RepositionChildInHierarchy(itemList.Content.RectTransform.GetChildIndex(firstRequiresRecipe.RectTransform)); + } } private void DrawInputOverLay(SpriteBatch spriteBatch, GUICustomComponent overlayComponent) @@ -297,6 +312,7 @@ namespace Barotrauma.Items.Components int slotIndex = 0; var missingItems = new List(); + foreach (FabricationRecipe.RequiredItem requiredItem in targetItem.RequiredItems) { for (int i = 0; i < requiredItem.Amount; i++) @@ -308,6 +324,8 @@ namespace Barotrauma.Items.Components { missingItems.Remove(missingItems.FirstOrDefault(mi => mi.ItemPrefabs.Contains(item.prefab))); } + var missingCounts = missingItems.GroupBy(missingItem => missingItem).ToDictionary(x => x.Key, x => x.Count()); + missingItems = missingItems.Distinct().ToList(); var availableIngredients = GetAvailableIngredients(); @@ -318,30 +336,30 @@ namespace Barotrauma.Items.Components slotIndex++; } - //highlight suitable ingredients in linked inventories - foreach (Item item in availableIngredients) - { - if (item.ParentInventory != inputContainer.Inventory && IsItemValidIngredient(item, requiredItem)) - { - int availableSlotIndex = item.ParentInventory.FindIndex(item); - //slots are null if the inventory has never been displayed - //(linked item, but the UI is not set to be displayed at the same time) - if (item.ParentInventory.visualSlots != null) - { - if (item.ParentInventory.visualSlots[availableSlotIndex].HighlightTimer <= 0.0f) - { - item.ParentInventory.visualSlots[availableSlotIndex].ShowBorderHighlight(GUI.Style.Green, 0.5f, 0.5f, 0.2f); - if (slotIndex < inputContainer.Capacity) + requiredItem.ItemPrefabs + .Where(requiredPrefab => availableIngredients.ContainsKey(requiredPrefab.Identifier)) + .ForEach(requiredPrefab => { + var availablePrefabs = availableIngredients[requiredPrefab.Identifier]; + + availablePrefabs + .Where(availablePrefab => availablePrefab.ParentInventory != inputContainer.Inventory) + .Where(availablePrefab => availablePrefab.ParentInventory.visualSlots != null) //slots are null if the inventory has never been displayed + .ForEach(availablePrefab => { //(linked item, but the UI is not set to be displayed at the same time) + int availableSlotIndex = availablePrefab.ParentInventory.FindIndex(availablePrefab); + + if (availablePrefab.ParentInventory.visualSlots[availableSlotIndex].HighlightTimer <= 0.0f) { - inputContainer.Inventory.visualSlots[slotIndex].ShowBorderHighlight(GUI.Style.Green, 0.5f, 0.5f, 0.2f); + availablePrefab.ParentInventory.visualSlots[availableSlotIndex].ShowBorderHighlight(GUI.Style.Green, 0.5f, 0.5f, 0.2f); + if (slotIndex < inputContainer.Capacity) + { + inputContainer.Inventory.visualSlots[slotIndex].ShowBorderHighlight(GUI.Style.Green, 0.5f, 0.5f, 0.2f); + } } - } - } - } - } + }); + }); if (slotIndex >= inputContainer.Capacity) { break; } - + var itemIcon = requiredItem.ItemPrefabs.First().InventoryIcon ?? requiredItem.ItemPrefabs.First().sprite; Rectangle slotRect = inputContainer.Inventory.visualSlots[slotIndex].Rect; itemIcon.Draw( @@ -350,6 +368,16 @@ namespace Barotrauma.Items.Components color: requiredItem.ItemPrefabs.First().InventoryIconColor * 0.3f, scale: Math.Min(slotRect.Width / itemIcon.size.X, slotRect.Height / itemIcon.size.Y)); + + if (missingCounts[requiredItem] > 1) + { + Vector2 stackCountPos = new Vector2(slotRect.Right, slotRect.Bottom); + string stackCountText = "x" + missingCounts[requiredItem]; + stackCountPos -= GUI.SmallFont.MeasureString(stackCountText) + new Vector2(4, 2); + GUI.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos + Vector2.One, Color.Black); + GUI.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos, Color.White); + } + if (requiredItem.UseCondition && requiredItem.MinCondition < 1.0f) { GUI.DrawRectangle(spriteBatch, new Rectangle(slotRect.X, slotRect.Bottom - 8, slotRect.Width, 8), Color.Black * 0.8f, true); @@ -601,7 +629,7 @@ namespace Barotrauma.Items.Components var itemPrefab = child.UserData as FabricationRecipe; if (itemPrefab == null) continue; - bool canBeFabricated = CanBeFabricated(itemPrefab, availableIngredients); + bool canBeFabricated = CanBeFabricated(itemPrefab, availableIngredients, character); if (itemPrefab == selectedItem) { activateButton.Enabled = canBeFabricated; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index 0ec56396d..0e9e7b897 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -1,63 +1,379 @@ -using Barotrauma.Extensions; -using FarseerPhysics; +#nullable enable +using Barotrauma.Extensions; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; -using System.Xml.Linq; +using Microsoft.Xna.Framework.Input; namespace Barotrauma.Items.Components { + internal readonly struct MiniMapGUIComponent + { + public readonly GUIComponent Component; + public readonly GUIComponent BorderComponent; + + public MiniMapGUIComponent(GUIComponent component) + { + Component = component; + BorderComponent = component; + } + + public MiniMapGUIComponent(GUIComponent frame, GUIComponent linkedHullComponent) + { + Component = frame; + BorderComponent = linkedHullComponent; + } + + public void Deconstruct(out GUIComponent component, out GUIComponent borderComponent) + { + component = Component; + borderComponent = BorderComponent; + } + } + + internal readonly struct MiniMapSprite + { + public readonly Sprite Sprite; + public readonly Color Color; + + public MiniMapSprite(JobPrefab prefab) + { + Sprite = prefab.IconSmall; + Color = prefab.UIColor; + } + + public MiniMapSprite(Order order) + { + Sprite = order.SymbolSprite; + Color = order.Color; + } + } + + internal readonly struct MiniMapHullData + { + public readonly List> Polygon; + public readonly (RectangleF Rect, Hull Hull)[] RectDatas; + public readonly RectangleF Bounds; + public readonly Point ParentSize; + + public MiniMapHullData(List> polygon, RectangleF bounds, Point parentSize, ImmutableArray rects, ImmutableArray hulls) + { + ParentSize = parentSize; + Bounds = bounds; + Polygon = polygon; + int count = Math.Min(rects.Length, hulls.Length); + RectDatas = new (RectangleF Rect, Hull Hull)[count]; + for (int i = 0; i < count; i++) + { + RectDatas[i] = (rects[i], hulls[i]); + } + } + } + + internal enum MiniMapMode + { + None, + HullStatus, + ElectricalView, + HullCondition, + ItemFinder + } + + internal readonly struct RelativeEntityRect + { + public readonly Vector2 RelativePosition; + public readonly Vector2 RelativeSize; + + public RelativeEntityRect(RectangleF worldBorders, RectangleF entityRect) + { + RelativePosition = new Vector2((entityRect.X - worldBorders.X) / worldBorders.Width, (worldBorders.Y - entityRect.Y) / worldBorders.Height); + RelativeSize = new Vector2(entityRect.Width / worldBorders.Width, entityRect.Height / worldBorders.Height); + } + + public Vector2 PositionRelativeTo(RectangleF frame, bool skipOffset = false) + { + if (skipOffset) + { + return RelativePosition * frame.Size; + } + + return frame.Location + RelativePosition * frame.Size; + } + + public Vector2 SizeRelativeTo(RectangleF frame) + { + return RelativeSize * frame.Size; + } + + public RectangleF RectangleRelativeTo(RectangleF frame, bool skipOffset = false) + { + return new RectangleF(PositionRelativeTo(frame, skipOffset), SizeRelativeTo(frame)); + } + + public void Deconstruct(out float posX, out float posY, out float sizeX, out float sizeY) + { + posX = RelativePosition.X; + posY = RelativePosition.Y; + sizeX = RelativeSize.X; + sizeY = RelativeSize.Y; + } + } + + internal readonly struct MiniMapSettings + { + public static MiniMapSettings Default = new MiniMapSettings + ( + ignoreOutposts: false, + createHullElements: true, + elementColor: MiniMap.MiniMapBaseColor + ); + + public readonly bool IgnoreOutposts; + public readonly bool CreateHullElements; + public readonly Color ElementColor; + + public MiniMapSettings(bool ignoreOutposts = false, bool createHullElements = false, Color? elementColor = null) + { + IgnoreOutposts = ignoreOutposts; + CreateHullElements = createHullElements; + ElementColor = elementColor ?? MiniMap.MiniMapBaseColor; + } + } + partial class MiniMap : Powered { private GUIFrame submarineContainer; private GUIFrame hullInfoFrame; + private GUIScissorComponent scissorComponent; + private GUIComponent miniMapContainer; + private GUIComponent miniMapFrame; + private GUIComponent electricalFrame; + private GUILayoutGroup reportFrame; + private GUILayoutGroup searchBarFrame; + private GUITextBox searchBar; + private GUIComponent searchAutoComplete; - private GUITextBlock hullNameText, hullBreachText, hullAirQualityText, hullWaterText; + private ItemPrefab? searchedPrefab; - private string noPowerTip = ""; + private GUITextBlock tooltipHeader, tooltipFirstLine, tooltipSecondLine, tooltipThirdLine; + + private string noPowerTip = string.Empty; private readonly List displayedSubs = new List(); private Point prevResolution; + private float cardRefreshTimer; + private const float cardRefreshDelay = 3f; - partial void InitProjSpecific(XElement element) + private readonly HashSet cardsToDraw = new HashSet(); + + private List subEntities = new List(); + + private Texture2D? submarinePreview; + + private MiniMapMode currentMode; + private ImmutableArray modeSwitchButtons; + + private Point elementSize; + + private ImmutableDictionary hullStatusComponents; + private ImmutableDictionary electricalMapComponents; + private ImmutableDictionary electricalChildren; + + private ImmutableHashSet itemsFoundOnSub; + + private ImmutableHashSet? MiniMapBlips; + private float blipState; + private const float maxBlipState = 1f; + + private const float maxZoom = 2f, + minZoom = 0.5f, + defaultZoom = 1f; + + private float zoom = defaultZoom; + + private float Zoom { + get => zoom; + set => zoom = Math.Clamp(value, minZoom, maxZoom); + } + + private Vector2 mapOffset = Vector2.Zero; + private bool dragMap; + private Vector2? dragMapStart; + private const int dragTreshold = 8; + + private bool recalculate; + + public static readonly Color MiniMapBaseColor = Color.DarkCyan; + + private static readonly Color WetHullColor = new Color(9, 80, 159), + DefaultNeutralColor = MiniMapBaseColor * 0.8f, + HoverColor = Color.White, + BlueprintBlue = new Color(48, 87, 255), + HullWaterColor = new Color(85, 136, 147), + HullWaterLineColor = Color.LightBlue, + NoPowerColor = MiniMapBaseColor * 0.1f, + ElectricalBaseColor = GUI.Style.Orange, + NoPowerElectricalColor = ElectricalBaseColor * 0.1f; + + partial void InitProjSpecific() + { + SetDefaultMode(); + noPowerTip = TextManager.Get("SteeringNoPowerTip"); CreateGUI(); } + private void SetDefaultMode() + { + currentMode = true switch + { + true when EnableHullStatus => MiniMapMode.HullStatus, + true when EnableElectricalView => MiniMapMode.ElectricalView, + true when EnableHullCondition => MiniMapMode.HullCondition, + true when EnableItemFinder => MiniMapMode.ItemFinder, + _ => MiniMapMode.None + }; + } + protected override void CreateGUI() { GuiFrame.RectTransform.RelativeOffset = new Vector2(0.05f, 0.0f); GuiFrame.CanBeFocused = true; - new GUICustomComponent(new RectTransform(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin, GuiFrame.RectTransform, Anchor.Center) { AbsoluteOffset = GUIStyle.ItemFrameOffset }, - DrawHUDBack, null); - submarineContainer = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.9f), GuiFrame.RectTransform, Anchor.Center), style: null); + new GUICustomComponent(new RectTransform(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin, GuiFrame.RectTransform, Anchor.Center) { AbsoluteOffset = GUIStyle.ItemFrameOffset }, DrawHUDBack, null); + GUIFrame paddedContainer = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.9f), GuiFrame.RectTransform, Anchor.Center), style: null); + submarineContainer = new GUIFrame(new RectTransform(Vector2.One, paddedContainer.RectTransform, Anchor.Center), style: null); - new GUICustomComponent(new RectTransform(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin, GuiFrame.RectTransform, Anchor.Center) { AbsoluteOffset = GUIStyle.ItemFrameOffset }, - DrawHUDFront, null) + new GUICustomComponent(new RectTransform(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin, GuiFrame.RectTransform, Anchor.Center) { AbsoluteOffset = GUIStyle.ItemFrameOffset }, DrawHUDFront, null) { CanBeFocused = false }; - hullInfoFrame = new GUIFrame(new RectTransform(new Vector2(0.13f, 0.13f), GUI.Canvas, minSize: new Point(250, 150)), - style: "GUIToolTip") + GUILayoutGroup buttonLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 0.2f), paddedContainer.RectTransform), isHorizontal: true); + + modeSwitchButtons = ImmutableArray.Create + ( + new GUIButton(new RectTransform(new Vector2(0.25f, 0.5f), buttonLayout.RectTransform), string.Empty, style: "StatusMonitorButton.HullStatus") { UserData = MiniMapMode.HullStatus, Enabled = EnableHullStatus, ToolTip = TextManager.Get("StatusMonitorButton.HullStatus.Tooltip") }, + new GUIButton(new RectTransform(new Vector2(0.25f, 0.5f), buttonLayout.RectTransform), string.Empty, style: "StatusMonitorButton.ElectricalView") { UserData = MiniMapMode.ElectricalView, Enabled = EnableHullCondition, ToolTip = TextManager.Get("StatusMonitorButton.ElectricalView.Tooltip") }, + new GUIButton(new RectTransform(new Vector2(0.25f, 0.5f), buttonLayout.RectTransform), string.Empty, style: "StatusMonitorButton.HullCondition") { UserData = MiniMapMode.HullCondition, Enabled = EnableHullCondition, ToolTip = TextManager.Get("StatusMonitorButton.HullCondition.Tooltip") }, + new GUIButton(new RectTransform(new Vector2(0.25f, 0.5f), buttonLayout.RectTransform), string.Empty, style: "StatusMonitorButton.ItemFinder") { UserData = MiniMapMode.ItemFinder, Enabled = EnableItemFinder, ToolTip = TextManager.Get("StatusMonitorButton.ItemFinder.Tooltip") } + ); + + foreach (GUIButton button in modeSwitchButtons) + { + button.OnClicked = (btn, o) => + { + if (!(o is MiniMapMode m)) { return false; } + + currentMode = m; + Zoom = defaultZoom; + mapOffset = Vector2.Zero; + recalculate = true; + + foreach (GUIButton otherButton in modeSwitchButtons) + { + otherButton.Selected = false; + } + + btn.Selected = true; + return true; + }; + + if (button.UserData is MiniMapMode buttonMode) + { + button.Selected = currentMode == buttonMode; + } + } + + List reports = Order.PrefabList.FindAll(o => o.IsReport && o.SymbolSprite != null && !o.Hidden); + + GUIFrame bottomFrame = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.15f), paddedContainer.RectTransform, Anchor.BottomCenter), style: null) { CanBeFocused = false }; + + reportFrame = new GUILayoutGroup(new RectTransform(new Vector2(1), bottomFrame.RectTransform), isHorizontal: true) + { + AbsoluteSpacing = (int)(5 * GUI.Scale) + }; + + if (reports.Any()) + { + CrewManager.CreateReports(GameMain.GameSession?.CrewManager, reportFrame, reports, true); + } + + searchBarFrame = new GUILayoutGroup(new RectTransform(new Vector2(1), bottomFrame.RectTransform), isHorizontal: true, childAnchor: Anchor.Center); + searchBar = new GUITextBox(new RectTransform(new Vector2(1), searchBarFrame.RectTransform), string.Empty, createClearButton: true, createPenIcon: true) + { + OnEnterPressed = (box, text) => + { + SearchItems(text); + return true; + } + }; + + searchAutoComplete = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas), style: "GUIToolTip") + { + Visible = false, + CanBeFocused = false + }; + + SetTooltipPosition(searchAutoComplete, searchBar); + + GUIListBox listBox = new GUIListBox(new RectTransform(Vector2.One, searchAutoComplete.RectTransform)) + { + OnSelected = (component, o) => + { + if (o is ItemPrefab prefab) + { + searchedPrefab = prefab; + searchBar.TextBlock.Text = prefab.Name; + searchBar.Deselect(); + SearchItems(searchBar.Text); + } + return true; + } + }; + + foreach (ItemPrefab prefab in ItemPrefab.Prefabs.OrderBy(prefab => prefab.Name)) + { + CreateItemFrame(prefab, listBox.Content.RectTransform); + } + + searchBar.OnDeselected += (sender, key) => + { + searchAutoComplete.Visible = false; + }; + + searchBar.OnSelected += (sender, key) => + { + itemsFoundOnSub = Item.ItemList.Where(it => it.Submarine == item.Submarine && !it.NonInteractable && !it.HiddenInGame && it.Components.OfType().Any()).Select(it => it.Prefab).ToImmutableHashSet(); + }; + + searchBar.OnKeyHit += ControlSearchTooltip; + searchBar.OnTextChanged += UpdateSearchTooltip; + + hullInfoFrame = new GUIFrame(new RectTransform(new Vector2(0.13f, 0.13f), GUI.Canvas, minSize: new Point(250, 150)), style: "GUIToolTip") + { + CanBeFocused = false + + }; + var hullInfoContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), hullInfoFrame.RectTransform, Anchor.Center)) { Stretch = true, RelativeSpacing = 0.05f }; - hullNameText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), hullInfoContainer.RectTransform), "") { Wrap = true }; - hullBreachText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), hullInfoContainer.RectTransform), "") { Wrap = true }; - hullAirQualityText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), hullInfoContainer.RectTransform), "") { Wrap = true }; - hullWaterText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), hullInfoContainer.RectTransform), "") { Wrap = true }; + tooltipHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), hullInfoContainer.RectTransform), string.Empty) { Wrap = true }; + tooltipFirstLine = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), hullInfoContainer.RectTransform), string.Empty) { Wrap = true }; + tooltipSecondLine = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), hullInfoContainer.RectTransform), string.Empty) { Wrap = true }; + tooltipThirdLine = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), hullInfoContainer.RectTransform), string.Empty) { Wrap = true }; hullInfoFrame.Children.ForEach(c => { @@ -70,34 +386,141 @@ namespace Barotrauma.Items.Components { base.AddToGUIUpdateList(); hullInfoFrame.AddToGUIUpdateList(order: 1); + if (currentMode == MiniMapMode.ItemFinder && searchBar.Selected) + { + searchAutoComplete.AddToGUIUpdateList(order: 1); + } } private void CreateHUD() { + subEntities.Clear(); prevResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); - submarineContainer?.ClearChildren(); + submarineContainer.ClearChildren(); - if (item.Submarine == null) { return; } + if (item.Submarine is null) { return; } + + scissorComponent = new GUIScissorComponent(new RectTransform(Vector2.One, submarineContainer.RectTransform, Anchor.Center)); + miniMapContainer = new GUIFrame(new RectTransform(Vector2.One, scissorComponent.Content.RectTransform, Anchor.Center), style: null) { CanBeFocused = false }; + + miniMapFrame = CreateMiniMap(item.Submarine, miniMapContainer, MiniMapSettings.Default, null, out hullStatusComponents); + + IEnumerable pointsOfInterest = Item.ItemList.Where(it => it.Submarine == item.Submarine && !it.HiddenInGame && !it.NonInteractable && it.GetComponent() != null); + electricalFrame = CreateMiniMap(item.Submarine, miniMapContainer, new MiniMapSettings(createHullElements: false), pointsOfInterest, out electricalMapComponents); + + Dictionary electricChildren = new Dictionary(); + + foreach (var (entity, component) in electricalMapComponents) + { + GUIComponent parent = component.Component; + if (!(entity is Item it )) { continue; } + Sprite? sprite = it.Prefab.UpgradePreviewSprite; + if (sprite is null) { continue; } + + GUIImage child = new GUIImage(new RectTransform(Vector2.One, parent.RectTransform, Anchor.Center), sprite) + { + OutlineColor = ElectricalBaseColor, + Color = ElectricalBaseColor, + HoverCursor = CursorState.Hand, + SpriteEffects = item.Rotation > 90.0f && item.Rotation < 270.0f ? SpriteEffects.FlipVertically : SpriteEffects.None + }; + + electricChildren.Add(component, child); + } + + electricalChildren = electricChildren.ToImmutableDictionary(); + + Rectangle parentRect = miniMapFrame.Rect; - item.Submarine.CreateMiniMap(submarineContainer); displayedSubs.Clear(); displayedSubs.Add(item.Submarine); displayedSubs.AddRange(item.Submarine.DockedTo); + + subEntities = MapEntity.mapEntityList.Where(me => me.Submarine == item.Submarine && !me.HiddenInGame).OrderByDescending(w => w.SpriteDepth).ToList(); + + BakeSubmarine(item.Submarine, parentRect); + elementSize = GuiFrame.Rect.Size; } public override void UpdateHUD(Character character, float deltaTime, Camera cam) { //recreate HUD if the subs we should display have changed - if ((item.Submarine == null && displayedSubs.Count > 0) || //item not inside a sub anymore, but display is still showing subs - !displayedSubs.Contains(item.Submarine) || //current sub not displayer - prevResolution.X != GameMain.GraphicsWidth || prevResolution.Y != GameMain.GraphicsHeight || //resolution changed - item.Submarine.DockedTo.Any(s => !displayedSubs.Contains(s)) || //some of the docked subs not diplayed - !submarineContainer.Children.Any() || // We lack a GUI - displayedSubs.Any(s => s != item.Submarine && !item.Submarine.DockedTo.Contains(s))) //displaying a sub that shouldn't be displayed + if (item.Submarine == null && displayedSubs.Count > 0 || // item not inside a sub anymore, but display is still showing subs + item.Submarine is { } itemSub && + ( + !displayedSubs.Contains(itemSub) || // current sub not displayed + itemSub.DockedTo.Any(s => !displayedSubs.Contains(s)) || // some of the docked subs not displayed + displayedSubs.Any(s => s != itemSub && !itemSub.DockedTo.Contains(s)) // displaying a sub that shouldn't be displayed + ) || + prevResolution.X != GameMain.GraphicsWidth || prevResolution.Y != GameMain.GraphicsHeight || // resolution changed + !submarineContainer.Children.Any()) // We lack a GUI { CreateHUD(); } - + + if (PlayerInput.PrimaryMouseButtonDown()) + { + if (GUI.MouseOn == scissorComponent || scissorComponent.IsParentOf(GUI.MouseOn)) + { + dragMapStart = PlayerInput.MousePosition; + } + } + + float newZoom = Zoom; + + if (Math.Abs(PlayerInput.ScrollWheelSpeed) > 0 && (GUI.MouseOn == scissorComponent || scissorComponent.IsParentOf(GUI.MouseOn))) + { + newZoom = Math.Clamp(Zoom + PlayerInput.ScrollWheelSpeed / 1000.0f * Zoom, minZoom, maxZoom); + float distanceScale = newZoom / Zoom; + mapOffset *= distanceScale; + } + + recalculate |= !MathUtils.NearlyEqual(Zoom, newZoom); + Zoom = newZoom; + + Vector2 elementScale = new Vector2(Zoom); + + if (dragMapStart is { } dragStart) + { + if (dragMap || Vector2.DistanceSquared(dragStart, PlayerInput.MousePosition) > GUI.IntScale(dragTreshold * dragTreshold)) + { + mapOffset.X += PlayerInput.MouseSpeed.X; + mapOffset.Y += PlayerInput.MouseSpeed.Y; + + recalculate |= PlayerInput.MouseSpeed != Vector2.Zero; + dragMap = true; + } + } + + var (maxWidth, maxHeight) = miniMapContainer.Rect.Size.ToVector2() / 2f / Zoom; + + mapOffset.X = Math.Clamp(mapOffset.X, -maxWidth, maxWidth); + mapOffset.Y = Math.Clamp(mapOffset.Y, -maxHeight, maxHeight); + + if (!PlayerInput.PrimaryMouseButtonHeld()) + { + dragMapStart = null; + dragMap = false; + } + + if (recalculate) + { + miniMapContainer.RectTransform.LocalScale = elementScale; + miniMapContainer.RectTransform.RecalculateChildren(true, true); + miniMapContainer.RectTransform.AbsoluteOffset = mapOffset.ToPoint(); + recalculate = false; + } + + // is there a better way to do this? + if (GuiFrame.Rect.Size != elementSize) + { + if (item.Submarine is { } sub) + { + BakeSubmarine(sub, miniMapFrame.Rect); + } + elementSize = GuiFrame.Rect.Size; + } + float distort = 1.0f - item.Condition / item.MaxCondition; foreach (HullData hullData in hullDatas.Values) { @@ -107,223 +530,717 @@ namespace Barotrauma.Items.Components hullData.Distort = Rand.Range(0.0f, 1.0f) < distort * distort; if (hullData.Distort) { - hullData.Oxygen = Rand.Range(0.0f, 100.0f); - hullData.Water = Rand.Range(0.0f, 1.0f); + hullData.ReceivedOxygenAmount = Rand.Range(0.0f, 100.0f); + hullData.ReceivedWaterAmount = Rand.Range(0.0f, 1.0f); } hullData.DistortionTimer = Rand.Range(1.0f, 10.0f); } } + + UpdateHUDBack(); + + if (blipState > maxBlipState) + { + blipState = 0; + } + + blipState += deltaTime; + + if (currentMode == MiniMapMode.HullStatus && !EnableHullStatus || + currentMode == MiniMapMode.ElectricalView && !EnableElectricalView || + currentMode == MiniMapMode.HullCondition && !EnableHullCondition || + currentMode == MiniMapMode.ItemFinder && !EnableItemFinder) + { + SetDefaultMode(); + } + + modeSwitchButtons[0].Enabled = EnableHullStatus; + modeSwitchButtons[1].Enabled = EnableElectricalView; + modeSwitchButtons[2].Enabled = EnableHullCondition; + modeSwitchButtons[3].Enabled = EnableItemFinder; + } + + private void UpdateIDCards(Submarine sub) + { + if (hullDatas is null) { return; } + + foreach (HullData data in hullDatas.Values) + { + data.Cards.Clear(); + } + + foreach (Item it in sub.GetItems(true)) + { + if (it is { CurrentHull: { } hull } && it.GetComponent() is { } idCard && idCard.TeamID == sub.TeamID) + { + if (!hullDatas.ContainsKey(hull)) { continue; } + + hullDatas[hull].Cards.Add(idCard); + } + } } private void DrawHUDFront(SpriteBatch spriteBatch, GUICustomComponent container) { + // TODO remove + if (currentMode == MiniMapMode.HullCondition) + { + const string wipText = "work in progress"; + Vector2 textSize = GUI.LargeFont.MeasureString(wipText); + Vector2 textPos = GuiFrame.Rect.Center.ToVector2(); + + GUI.DrawString(spriteBatch, textPos - textSize / 2, wipText.ToUpper(), GUI.Style.Orange, Color.Black * 0.8f, backgroundPadding: 8, font: GUI.LargeFont); + } + if (Voltage < MinVoltage) { Vector2 textSize = GUI.Font.MeasureString(noPowerTip); Vector2 textPos = GuiFrame.Rect.Center.ToVector2(); + Color noPowerColor = GUI.Style.Orange * (float)Math.Abs(Math.Sin(Timing.TotalTime)); - GUI.DrawString(spriteBatch, textPos - textSize / 2, noPowerTip, - GUI.Style.Orange * (float)Math.Abs(Math.Sin(Timing.TotalTime)), Color.Black * 0.8f, font: GUI.SubHeadingFont); + GUI.DrawString(spriteBatch, textPos - textSize / 2, noPowerTip, noPowerColor, Color.Black * 0.8f, font: GUI.SubHeadingFont); return; } - if (!submarineContainer.Children.Any()) { return; } - foreach (GUIComponent child in submarineContainer.Children.FirstOrDefault()?.Children) + + if (currentMode == MiniMapMode.HullStatus) { - if (child.UserData is Hull hull) + Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle; + spriteBatch.End(); + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); + spriteBatch.GraphicsDevice.ScissorRectangle = submarineContainer.Rect; + + foreach (var (entity, component) in hullStatusComponents) { - if (hull.Submarine == null || !hull.Submarine.Info.IsOutpost) { continue; } - string text = TextManager.GetWithVariable("MiniMapOutpostDockingInfo", "[outpost]", hull.Submarine.Info.Name); - Vector2 textSize = GUI.Font.MeasureString(text); - Vector2 textPos = child.Center; - if (textPos.X + textSize.X / 2 > submarineContainer.Rect.Right) - textPos.X -= ((textPos.X + textSize.X / 2) - submarineContainer.Rect.Right) + 10 * GUI.xScale; - if (textPos.X - textSize.X / 2 < submarineContainer.Rect.X) - textPos.X += (submarineContainer.Rect.X - (textPos.X - textSize.X / 2)) + 10 * GUI.xScale; - GUI.DrawString(spriteBatch, textPos - textSize / 2, text, - GUI.Style.Orange * (float)Math.Abs(Math.Sin(Timing.TotalTime)), Color.Black * 0.8f); - break; + if (!(entity is Hull hull)) { continue; } + if (!hullDatas.TryGetValue(hull, out HullData? hullData) || hullData is null) { continue; } + DrawHullCards(spriteBatch, hull, hullData, component.Component); } - } + + spriteBatch.End(); + spriteBatch.GraphicsDevice.ScissorRectangle = prevScissorRect; + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); + } } - private void DrawHUDBack(SpriteBatch spriteBatch, GUICustomComponent container) + private void ControlSearchTooltip(GUITextBox sender, Keys key) { - Hull mouseOnHull = null; - hullInfoFrame.Visible = false; + if (!searchAutoComplete.Visible) { return; } + GUIListBox listBox = searchAutoComplete.GetChild(); + if (listBox is null) { return; } - foreach (Hull hull in Hull.hullList) + if (key == Keys.Down) { - var hullFrame = submarineContainer.Children.FirstOrDefault()?.FindChild(hull); - if (hullFrame == null) { continue; } - - if (GUI.MouseOn == hullFrame || hullFrame.IsParentOf(GUI.MouseOn)) - { - mouseOnHull = hull; - } - if (item.Submarine == null || !hasPower) - { - hullFrame.Color = Color.DarkCyan * 0.3f; - hullFrame.Children.First().Color = Color.DarkCyan * 0.3f; - } + listBox.SelectNext(true, autoScroll: true); } - - if (Voltage < MinVoltage) + else if (key == Keys.Up) { - return; + listBox.SelectPrevious(true, autoScroll: true); } - - float scale = 1.0f; - HashSet subs = new HashSet(); - foreach (Hull hull in Hull.hullList) + else if (key == Keys.Enter) { - if (hull.Submarine == null) { continue; } - var hullFrame = submarineContainer.Children.FirstOrDefault()?.FindChild(hull); - if (hullFrame == null) { continue; } + listBox.OnSelected?.Invoke(listBox, listBox.SelectedData); + searchBar.Deselect(); + } + } - hullFrame.Visible = true; - if (!submarineContainer.Rect.Contains(hullFrame.Rect)) + private bool UpdateSearchTooltip(GUITextBox box, string text) + { + MiniMapBlips = null; + searchedPrefab = null; + searchAutoComplete.Visible = true; + SetTooltipPosition(searchAutoComplete, box); + + GUIListBox listBox = searchAutoComplete.GetChild(); + if (listBox is null) { return false; } + + bool first = true; + + int i = 0; + + foreach (GUIComponent component in listBox.Content.Children) + { + component.Visible = false; + if (component.UserData is ItemPrefab prefab && itemsFoundOnSub.Contains(prefab)) { - if (hull.Submarine.Info.Type != SubmarineType.Player) + component.Visible = prefab.Name.ToLower().Contains(text.ToLower()); + + if (component.Visible && first) { - hullFrame.Visible = false; - continue; + listBox.Select(i, force: true, autoScroll: false); + first = false; } } - hullDatas.TryGetValue(hull, out HullData hullData); - if (hullData == null) + i++; + } + + listBox.BarScroll = 0f; + listBox.RecalculateChildren(); + + return true; + } + + private void SetTooltipPosition(GUIComponent tooltip, GUITextBox box) + { + int height = GuiFrame.Rect.Height / 2; + tooltip.RectTransform.NonScaledSize = new Point(box.Rect.Width, height); + tooltip.RectTransform.ScreenSpaceOffset = new Point(box.Rect.X, box.Rect.Y - height); + } + + private void CreateItemFrame(ItemPrefab prefab, RectTransform parent) + { + Sprite sprite = prefab.InventoryIcon ?? prefab.sprite; + if (sprite is null) { return; } + GUIFrame frame = new GUIFrame(new RectTransform(new Vector2(1f, 0.25f), parent), style: "ListBoxElement") + { + UserData = prefab + }; + + GUILayoutGroup layout = new GUILayoutGroup(new RectTransform(Vector2.One, frame.RectTransform), isHorizontal: true); + new GUIImage(new RectTransform(Vector2.One, layout.RectTransform, scaleBasis: ScaleBasis.BothHeight), sprite) + { + Color = prefab.InventoryIconColor + }; + + new GUITextBlock(new RectTransform(Vector2.One, layout.RectTransform), prefab.Name, font: GUI.SubHeadingFont); + layout.UserData = prefab; + } + + private void SearchItems(string text) + { + if (searchedPrefab is null) + { + Console.WriteLine("Bruh"); + ItemPrefab? first = ItemPrefab.Prefabs.FirstOrDefault(p => p.Name.ToLower().Equals(text.ToLower())); + + if (first is null) + { + searchBar.Flash(GUI.Style.Red); + return; + } + searchedPrefab = first; + } + + if (item.Submarine is null) { return; } + + HashSet foundItems = new HashSet(); + + foreach (Item it in Item.ItemList) + { + if (it.Submarine != item.Submarine) { continue; } + if (it.HiddenInGame || it.NonInteractable) { continue; } + if (it.GetComponent() is { Connections: { } conn} && conn.Any()) { continue; } + if (it.HasTag("traitormissionitem")) { continue; } + + if (it.Prefab == searchedPrefab) + { + // ignore items on players and hidden inventories + if (it.FindParentInventory(inv => inv is CharacterInventory || inv is ItemInventory { Owner: Item { HiddenInGame: true }}) is { }) { continue; } + + if (it.FindParentInventory(inventory => inventory is ItemInventory { Owner: Item { ParentInventory: null } }) is ItemInventory parent) + { + foundItems.Add((Item) parent.Owner); + } + else + { + foundItems.Add(it); + } + } + } + + + RectangleF dockedBorders = item.Submarine.GetDockedBorders(); + dockedBorders.Location += item.Submarine.WorldPosition; + RectangleF parentRect = miniMapFrame.Rect; + + HashSet positions = new HashSet(); + foreach (Item foundItem in foundItems) + { + RelativeEntityRect scaledRect = new RelativeEntityRect(dockedBorders, foundItem.WorldRect); + Vector2 pos = (scaledRect.PositionRelativeTo(parentRect, skipOffset: true) + scaledRect.SizeRelativeTo(parentRect) / 2f) / Zoom; + positions.Add(pos); + } + + MiniMapBlips = positions.ToImmutableHashSet(); + + searchAutoComplete.Visible = false; + } + + private void UpdateHUDBack() + { + hullInfoFrame.Visible = false; + electricalFrame.Visible = false; + miniMapFrame.Visible = false; + reportFrame.Visible = false; + searchBarFrame.Visible = false; + + switch (currentMode) + { + case MiniMapMode.HullStatus: + UpdateHullStatus(); + miniMapFrame.Visible = true; + reportFrame.Visible = true; + break; + case MiniMapMode.ElectricalView: + UpdateElectricalView(); + electricalFrame.Visible = true; + break; + case MiniMapMode.ItemFinder: + searchBarFrame.Visible = true; + break; + } + } + + private void UpdateHullStatus() + { + foreach (var (entity, (component, borderComponent)) in hullStatusComponents) + { + if (item.Submarine == null || !hasPower) + { + component.Color = NoPowerColor; + borderComponent.OutlineColor = NoPowerColor; + } + + if (Voltage < MinVoltage) { continue; } + + if (!component.Visible) { continue; } + if (!(entity is Hull hull)) { continue; } + + if (!submarineContainer.Rect.Contains(component.Rect)) + { + if (hull.Submarine.Info.Type != SubmarineType.Player) + { + component.Visible = borderComponent.Visible = false; + continue; + } + } + + hullDatas.TryGetValue(hull, out HullData? hullData); + if (hullData is null) { hullData = new HullData(); GetLinkedHulls(hull, hullData.LinkedHulls); hullDatas.Add(hull, hullData); } - - Color neutralColor = Color.DarkCyan; + + Color neutralColor = DefaultNeutralColor; + Color borderColor = neutralColor; + Color componentColor; + if (hull.IsWetRoom) { - neutralColor = new Color(9, 80, 159); + neutralColor = WetHullColor; } if (hullData.Distort) { - hullFrame.Children.First().Color = Color.Lerp(Color.Black, Color.DarkGray * 0.5f, Rand.Range(0.0f, 1.0f)); - hullFrame.Color = neutralColor * 0.5f; + borderComponent.OutlineColor = neutralColor * 0.5f; + component.Color = Color.Lerp(Color.Black, Color.DarkGray * 0.5f, Rand.Range(0.0f, 1.0f)); continue; } - - subs.Add(hull.Submarine); - scale = Math.Min( - hullFrame.Parent.Rect.Width / (float)hull.Submarine.Borders.Width, - hullFrame.Parent.Rect.Height / (float)hull.Submarine.Borders.Height); - - Color borderColor = neutralColor; - - float? gapOpenSum = 0.0f; + + hullData.HullOxygenAmount = RequireOxygenDetectors ? hullData.ReceivedOxygenAmount : hull.OxygenPercentage; + hullData.HullWaterAmount = RequireWaterDetectors ? hullData.ReceivedWaterAmount : Math.Min(hull.WaterVolume / hull.Volume, 1.0f); + + float gapOpenSum = 0.0f; + if (ShowHullIntegrity) { - gapOpenSum = hull.ConnectedGaps.Where(g => !g.IsRoomToRoom).Sum(g => g.Open); - borderColor = Color.Lerp(neutralColor, GUI.Style.Red, Math.Min((float)gapOpenSum, 1.0f)); + float amount = 1f + hullData.LinkedHulls.Count; + gapOpenSum = hull.ConnectedGaps.Concat(hullData.LinkedHulls.SelectMany(h => h.ConnectedGaps)).Where(g => !g.IsRoomToRoom).Sum(g => g.Open) / amount; + borderColor = Color.Lerp(neutralColor, GUI.Style.Red, Math.Min(gapOpenSum, 1.0f)); } - float? oxygenAmount = null; - if (!RequireOxygenDetectors || hullData?.Oxygen != null) + bool isHoveringOver = GUI.MouseOn == component; + + // When drawing tooltip we are only interested in the component we are hovering over + if (isHoveringOver) { - oxygenAmount = RequireOxygenDetectors ? hullData.Oxygen : hull.OxygenPercentage; - GUI.DrawRectangle( - spriteBatch, hullFrame.Rect, - Color.Lerp(GUI.Style.Red * 0.5f, GUI.Style.Green * 0.3f, (float)oxygenAmount / 100.0f), - true); - } + string header = hull.DisplayName; - float? waterAmount = null; - if (!RequireWaterDetectors || hullData.Water != null) - { - waterAmount = RequireWaterDetectors ? hullData.Water : Math.Min(hull.WaterVolume / hull.Volume, 1.0f); - if (hullFrame.Rect.Height * waterAmount > 3.0f) - { - Rectangle waterRect = new Rectangle( - hullFrame.Rect.X, (int)(hullFrame.Rect.Y + hullFrame.Rect.Height * (1.0f - waterAmount)), - hullFrame.Rect.Width, (int)(hullFrame.Rect.Height * waterAmount)); - - waterRect.Inflate(-3, -3); - - GUI.DrawRectangle(spriteBatch, waterRect, new Color(85, 136, 147), true); - GUI.DrawLine(spriteBatch, new Vector2(waterRect.X, waterRect.Y), new Vector2(waterRect.Right, waterRect.Y), Color.LightBlue); - } - } - - if (mouseOnHull == hull || - hullData.LinkedHulls.Contains(mouseOnHull)) - { - borderColor = Color.Lerp(borderColor, Color.White, 0.5f); - hullFrame.Children.First().Color = Color.White; - hullFrame.Color = borderColor; - } - else - { - hullFrame.Children.First().Color = neutralColor * 0.8f; - } - - if (mouseOnHull == hull) - { - hullInfoFrame.RectTransform.ScreenSpaceOffset = hullFrame.Rect.Center; - if (hullInfoFrame.Rect.Right > GameMain.GraphicsWidth) { hullInfoFrame.RectTransform.ScreenSpaceOffset -= new Point(hullInfoFrame.Rect.Width, 0); } - if (hullInfoFrame.Rect.Bottom > GameMain.GraphicsHeight) { hullInfoFrame.RectTransform.ScreenSpaceOffset -= new Point(0, hullInfoFrame.Rect.Height); } - - hullInfoFrame.Visible = true; - hullNameText.Text = hull.DisplayName; + float? oxygenAmount = hullData.HullOxygenAmount, + waterAmount = hullData.HullWaterAmount; foreach (Hull linkedHull in hullData.LinkedHulls) { - gapOpenSum += linkedHull.ConnectedGaps.Where(g => !g.IsRoomToRoom).Sum(g => g.Open); oxygenAmount += linkedHull.OxygenPercentage; waterAmount += Math.Min(linkedHull.WaterVolume / linkedHull.Volume, 1.0f); } + oxygenAmount /= (hullData.LinkedHulls.Count + 1); waterAmount /= (hullData.LinkedHulls.Count + 1); - hullBreachText.Text = gapOpenSum > 0.1f ? TextManager.Get("MiniMapHullBreach") : ""; - hullBreachText.TextColor = GUI.Style.Red; + string line1 = gapOpenSum > 0.1f ? TextManager.Get("MiniMapHullBreach") : string.Empty; + Color line1Color = GUI.Style.Red; - hullAirQualityText.Text = oxygenAmount == null ? TextManager.Get("MiniMapAirQualityUnavailable") : - TextManager.AddPunctuation(':', TextManager.Get("MiniMapAirQuality"), + (int)oxygenAmount + " %"); - hullAirQualityText.TextColor = oxygenAmount == null ? GUI.Style.Red : Color.Lerp(GUI.Style.Red, Color.LightGreen, (float)oxygenAmount / 100.0f); + string line2 = oxygenAmount == null ? TextManager.Get("MiniMapAirQualityUnavailable") : TextManager.AddPunctuation(':', TextManager.Get("MiniMapAirQuality"), +(int)oxygenAmount + " %"); + Color line2Color = oxygenAmount == null ? GUI.Style.Red : Color.Lerp(GUI.Style.Red, Color.LightGreen, (float)oxygenAmount / 100.0f); - hullWaterText.Text = waterAmount == null ? TextManager.Get("MiniMapWaterLevelUnavailable") : - TextManager.AddPunctuation(':', TextManager.Get("MiniMapWaterLevel"), (int)(waterAmount * 100.0f) + " %"); - hullWaterText.TextColor = waterAmount == null ? GUI.Style.Red : Color.Lerp(Color.LightGreen, GUI.Style.Red, (float)waterAmount); + string line3 = waterAmount == null ? TextManager.Get("MiniMapWaterLevelUnavailable") : TextManager.AddPunctuation(':', TextManager.Get("MiniMapWaterLevel"), (int)(waterAmount * 100.0f) + " %"); + Color line3Color = waterAmount == null ? GUI.Style.Red : Color.Lerp(Color.LightGreen, GUI.Style.Red, (float)waterAmount); + + SetTooltip(borderComponent.Rect.Center, header, line1, line2, line3, line1Color, line2Color, line3Color); } - - hullFrame.Color = borderColor; - } - - foreach (Submarine sub in subs) - { - if (sub.HullVertices == null || sub.Info.IsOutpost) { continue; } - - Rectangle worldBorders = sub.GetDockedBorders(); - worldBorders.Location += sub.WorldPosition.ToPoint(); - - scale = Math.Min( - submarineContainer.Rect.Width / (float)worldBorders.Width, - submarineContainer.Rect.Height / (float)worldBorders.Height) * 0.9f; - float displayScale = ConvertUnits.ToDisplayUnits(scale); - Vector2 offset = ConvertUnits.ToSimUnits(sub.WorldPosition - new Vector2(worldBorders.Center.X, worldBorders.Y - worldBorders.Height / 2)); - Vector2 center = container.Rect.Center.ToVector2(); - - for (int i = 0; i < sub.HullVertices.Count; i++) + // When setting the colors we want to know the linked hulls too or else the linked hull will not realize its being hovered over and reset the border color + foreach (Hull linkedHull in hullData.LinkedHulls) { - Vector2 start = (sub.HullVertices[i] + offset) * displayScale; - start.Y = -start.Y; - Vector2 end = (sub.HullVertices[(i + 1) % sub.HullVertices.Count] + offset) * displayScale; - end.Y = -end.Y; - GUI.DrawLine(spriteBatch, center + start, center + end, Color.DarkCyan * Rand.Range(0.3f, 0.35f), width: (int)(10 * GUI.Scale)); + if (!hullStatusComponents.ContainsKey(linkedHull)) { continue; } + + isHoveringOver |= hullStatusComponents[linkedHull].Component == GUI.MouseOn; + if (isHoveringOver) { break; } } + + if (isHoveringOver) + { + borderColor = Color.Lerp(borderColor, Color.White, 0.5f); + componentColor = HoverColor; + } + else + { + componentColor = neutralColor * 0.8f; + } + + borderComponent.OutlineColor = borderColor; + component.Color = componentColor; } } - private void GetLinkedHulls(Hull hull, List linkedHulls) + private void UpdateElectricalView() + { + foreach (var (entity, miniMapGuiComponent) in electricalMapComponents) + { + if (!(entity is Item it)) { continue; } + if (!electricalChildren.TryGetValue(miniMapGuiComponent, out GUIComponent component)) { continue; } + + if (item.Submarine == null || !hasPower) + { + component.Color = component.OutlineColor = NoPowerElectricalColor; + } + + if (Voltage < MinVoltage || !miniMapGuiComponent.Component.Visible) { continue; } + + int durability = (int)(it.Condition / it.MaxCondition * 100f); + Color color = ToolBox.GradientLerp(durability / 100f, GUI.Style.Red, GUI.Style.Orange, GUI.Style.Green, GUI.Style.Green); + + if (GUI.MouseOn == component) + { + string line1 = string.Empty; + string line2 = string.Empty; + + if (it.GetComponent() is { } battery) + { + int batteryCapacity = (int)(battery.Charge / battery.Capacity * 100f); + line2 = TextManager.GetWithVariable("statusmonitor.battery.tooltip", "[amount]", batteryCapacity.ToString()); + } + else if (it.GetComponent() is { } powerTransfer) + { + int current = (int) -powerTransfer.CurrPowerConsumption, + load = (int) powerTransfer.PowerLoad; + + line1 = TextManager.GetWithVariable("statusmonitor.junctioncurrent.tooltip", "[amount]", current.ToString()); + line2 = TextManager.GetWithVariable("statusmonitor.junctionload.tooltip", "[amount]", load.ToString()); + } + + string line3 = TextManager.GetWithVariable("statusmonitor.durability.tooltip", "[amount]", durability.ToString()); + SetTooltip(component.Rect.Center, it.Prefab.Name, line1, line2, line3, line3Color: color); + color = HoverColor; + } + + component.Color = component.OutlineColor = color; + } + } + + private void DrawHUDBack(SpriteBatch spriteBatch, GUICustomComponent container) + { + if (item.Submarine != null) + { + Rectangle parentRect = container.Rect; + if (miniMapFrame is { } miniMap) { parentRect = miniMap.Rect; } + + DrawSubmarine(spriteBatch, parentRect); + } + + if (Voltage < MinVoltage) { return; } + Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle; + spriteBatch.End(); + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); + spriteBatch.GraphicsDevice.ScissorRectangle = submarineContainer.Rect; + + if (currentMode == MiniMapMode.ItemFinder) + { + if (MiniMapBlips != null) + { + foreach (Vector2 blip in MiniMapBlips) + { + Vector2 parentSize = miniMapFrame.Rect.Size.ToVector2(); + Sprite pingCircle = GUI.Style.PingCircle.Sprite; + Vector2 targetSize = new Vector2(parentSize.X / 4f); + Vector2 spriteScale = targetSize / pingCircle.size; + float scale = Math.Min(blipState, maxBlipState / 2f); + float alpha = 1.0f - Math.Clamp((blipState - maxBlipState * 0.25f) * 2f, 0f, 1f); + pingCircle.Draw(spriteBatch, miniMapFrame.Rect.Location.ToVector2() + (blip * Zoom), GUI.Style.Red * alpha, pingCircle.Origin, 0f, spriteScale * scale, SpriteEffects.None); + } + } + } + else + { + bool hullsVisible = currentMode == MiniMapMode.HullStatus; + + foreach (var (entity, component) in hullStatusComponents) + { + if (!(entity is Hull hull)) { continue; } + if (!hullDatas.TryGetValue(hull, out HullData? hullData) || hullData is null) { continue; } + + if (hullData.Distort) { continue; } + + GUIComponent hullFrame = component.Component; + + if (hullsVisible && hullData.HullWaterAmount is { } waterAmount) + { + if (hullFrame.Rect.Height * waterAmount > 3.0f) + { + RectangleF waterRect = new RectangleF(hullFrame.Rect.X, hullFrame.Rect.Y + hullFrame.Rect.Height * (1.0f - waterAmount), hullFrame.Rect.Width, hullFrame.Rect.Height * waterAmount); + + GUI.DrawFilledRectangle(spriteBatch, waterRect, HullWaterColor); + GUI.DrawLine(spriteBatch, waterRect.Location, new Vector2(waterRect.Right, waterRect.Y), HullWaterLineColor); + } + } + + if (hullsVisible && hullData.HullOxygenAmount is { } oxygenAmount) + { + GUI.DrawRectangle(spriteBatch, hullFrame.Rect, Color.Lerp(GUI.Style.Red * 0.5f, GUI.Style.Green * 0.3f, oxygenAmount / 100.0f), true); + } + } + } + + spriteBatch.End(); + spriteBatch.GraphicsDevice.ScissorRectangle = prevScissorRect; + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); + } + + private void SetTooltip(Point pos, string header, string line1, string line2, string line3, Color? line1Color = null, Color? line2Color = null, Color? line3Color = null) + { + hullInfoFrame.RectTransform.ScreenSpaceOffset = pos; + + if (hullInfoFrame.Rect.Left > submarineContainer.Rect.Right) { hullInfoFrame.RectTransform.ScreenSpaceOffset = new Point(submarineContainer.Rect.Right, hullInfoFrame.RectTransform.ScreenSpaceOffset.Y); } + if (hullInfoFrame.Rect.Top > submarineContainer.Rect.Bottom) { hullInfoFrame.RectTransform.ScreenSpaceOffset = new Point(hullInfoFrame.RectTransform.ScreenSpaceOffset.X, submarineContainer.Rect.Bottom); } + + if (hullInfoFrame.Rect.Right > GameMain.GraphicsWidth) { hullInfoFrame.RectTransform.ScreenSpaceOffset -= new Point(hullInfoFrame.Rect.Width, 0); } + if (hullInfoFrame.Rect.Bottom > GameMain.GraphicsHeight) { hullInfoFrame.RectTransform.ScreenSpaceOffset -= new Point(0, hullInfoFrame.Rect.Height); } + + hullInfoFrame.Visible = true; + tooltipHeader.Text = header; + + tooltipFirstLine.Text = line1; + tooltipFirstLine.TextColor = line1Color ?? GUI.Style.TextColor; + + tooltipSecondLine.Text = line2; + tooltipSecondLine.TextColor = line2Color ?? GUI.Style.TextColor; + + tooltipThirdLine.Text = line3; + tooltipThirdLine.TextColor = line3Color ?? GUI.Style.TextColor; + } + + private void BakeSubmarine(Submarine sub, Rectangle container) + { + submarinePreview?.Dispose(); + Rectangle parentRect = new Rectangle(container.X, container.Y, container.Width, container.Height); + const int inflate = 128; + parentRect.Inflate(inflate, inflate); + RenderTarget2D rt = new RenderTarget2D(GameMain.Instance.GraphicsDevice, parentRect.Width, parentRect.Height, false, SurfaceFormat.Color, DepthFormat.None); + + using SpriteBatch spriteBatch = new SpriteBatch(GameMain.Instance.GraphicsDevice); + GameMain.Instance.GraphicsDevice.SetRenderTarget(rt); + GameMain.Instance.GraphicsDevice.Clear(Color.Transparent); + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); + Rectangle worldBorders = sub.GetDockedBorders(); + worldBorders.Location += sub.WorldPosition.ToPoint(); + + parentRect.Inflate(-inflate, -inflate); + + foreach (MapEntity entity in subEntities) + { + if (entity is Structure wall) + { + if (wall.IsPlatform) { continue; } + DrawStructure(spriteBatch, wall, parentRect, worldBorders, inflate); + } + + if (entity is Item it) + { + if (it.GetComponent() != null || it.ParentInventory != null) { continue; } + DrawItem(spriteBatch, it, parentRect, worldBorders, inflate); + } + } + + spriteBatch.End(); + GameMain.Instance.GraphicsDevice.SetRenderTarget(null); + submarinePreview = rt; + } + + private void DrawSubmarine(SpriteBatch spriteBatch, Rectangle parentRect) + { + Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle; + spriteBatch.End(); + if (submarinePreview is { } texture) + { + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, blendState: BlendState.NonPremultiplied, effect: GameMain.GameScreen.BlueprintEffect, rasterizerState: GameMain.ScissorTestEnable); + spriteBatch.GraphicsDevice.ScissorRectangle = submarineContainer.Rect; + + GameMain.GameScreen.BlueprintEffect.Parameters["width"].SetValue((float)texture.Width); + GameMain.GameScreen.BlueprintEffect.Parameters["height"].SetValue((float)texture.Height); + + Color blueprintBlue = BlueprintBlue * currentMode switch { MiniMapMode.HullStatus => 0.1f, MiniMapMode.ElectricalView => 0.1f, _ => 0.5f }; + + Vector2 origin = new Vector2(texture.Width / 2f, texture.Height / 2f); + spriteBatch.Draw(texture, parentRect.Center.ToVector2(), null, blueprintBlue, 0f, origin, Zoom, SpriteEffects.None, 0f); + + spriteBatch.End(); + } + spriteBatch.GraphicsDevice.ScissorRectangle = prevScissorRect; + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); + } + + private static void DrawItem(ISpriteBatch spriteBatch, Item item, Rectangle parent, Rectangle border, int inflate) + { + Sprite sprite = item.Sprite; + if (sprite is null) { return; } + + RectangleF entityRect = ScaleRectToUI(item, parent, border); + + Vector2 spriteScale = new Vector2(entityRect.Size.X / sprite.size.X, entityRect.Size.Y / sprite.size.Y); + Vector2 origin = new Vector2(sprite.Origin.X * spriteScale.X, sprite.Origin.Y * spriteScale.Y); + + if (item.GetComponent() is { } turret) + { + Vector2 drawPos = turret.GetDrawPos(); + drawPos.Y = -drawPos.Y; + if (turret.BarrelSprite is { } barrelSprite) + { + DrawAdditionalSprite(drawPos, barrelSprite, turret.Rotation + MathHelper.PiOver2); + } + } + + Vector2 pos = entityRect.Location + origin; + pos.X += inflate; + pos.Y += inflate; + + sprite.Draw(spriteBatch, pos, item.SpriteColor, sprite.Origin, MathHelper.ToRadians(item.Rotation), spriteScale, item.SpriteEffects); + + void DrawAdditionalSprite(Vector2 basePos, Sprite addSprite, float rotation) + { + RectangleF addRect = ScaleRectToUI(new RectangleF(basePos, addSprite.size * item.Scale), parent, border); + Vector2 addScale = new Vector2(addRect.Size.X / addSprite.size.X, addRect.Size.Y / addSprite.size.Y); + addSprite.Draw(spriteBatch, new Vector2(addRect.Location.X + inflate, addRect.Location.Y + inflate), item.SpriteColor, addSprite.Origin, rotation, addScale, item.SpriteEffects); + } + } + + private static void DrawStructure(ISpriteBatch spriteBatch, Structure structure, Rectangle parent, Rectangle border, int inflate) + { + Sprite sprite = structure.Sprite; + if (sprite is null) { return; } + + RectangleF entityRect = ScaleRectToUI(structure, parent, border); + Vector2 spriteScale = new Vector2(entityRect.Size.X / sprite.size.X, entityRect.Size.Y / sprite.size.Y); + sprite.Draw(spriteBatch, new Vector2(entityRect.Location.X + inflate, entityRect.Location.Y + inflate), structure.SpriteColor, Vector2.Zero, 0f, spriteScale, structure.SpriteEffects); + } + + private static RectangleF ScaleRectToUI(MapEntity entity, RectangleF parentRect, RectangleF worldBorders) + { + return ScaleRectToUI(entity.WorldRect, parentRect, worldBorders); + } + + private static RectangleF ScaleRectToUI(RectangleF rect, RectangleF parentRect, RectangleF worldBorders) + { + RelativeEntityRect relativeRect = new RelativeEntityRect(worldBorders, rect); + return relativeRect.RectangleRelativeTo(parentRect, skipOffset: true); + } + + private void DrawHullCards(SpriteBatch spriteBatch, Hull hull, HullData data, GUIComponent frame) + { + cardsToDraw.Clear(); + + if (GameMain.GameSession?.CrewManager is { ActiveOrders: { } orders }) + { + foreach (var pair in orders) + { + Order order = pair.First; + if (order is { SymbolSprite: { }, TargetEntity: Hull _ } && order.TargetEntity == hull) + { + cardsToDraw.Add(new MiniMapSprite(order)); + } + } + } + + foreach (IdCard card in data.Cards) + { + if (card.GetJob() is { Icon: { }} job) + { + cardsToDraw.Add(new MiniMapSprite(job)); + } + } + + if (!cardsToDraw.Any()) { return; } + + var (centerX, centerY) = frame.Center; + + const float padding = 8f; + float totalWidth = 0f; + + int i = 0; + foreach (MiniMapSprite info in cardsToDraw) + { + float spriteSize = info.Sprite.size.X * (frame.Rect.Height / info.Sprite.size.Y) + padding; + if (totalWidth + spriteSize > frame.Rect.Width) { break; } + + totalWidth += spriteSize; + i++; + } + + if (i > 0) { totalWidth -= padding; } + + float adjustedCenterX = centerX - totalWidth / 2f; + + float offset = 0; + int amount = 0; + foreach (MiniMapSprite info in cardsToDraw) + { + Sprite sprite = info.Sprite; + float scale = frame.Rect.Height / sprite.size.Y; + float spriteSize = sprite.size.X * scale; + float posX = adjustedCenterX + offset; + + if (posX + spriteSize > frame.Rect.X + frame.Rect.Width && amount > 0) + { + int amountLeft = cardsToDraw.Count - amount; + if (amountLeft > 0) + { + string text = $"+{amountLeft}"; // TODO localization + var (sizeX, sizeY) = GUI.SubHeadingFont.MeasureString(text); // TODO expensive, move to a global variable + float maxWidth = Math.Max(sizeX, sizeY); + Vector2 drawPos = new Vector2(frame.Rect.Right - sizeX, frame.Rect.Y - sizeY / 2f); + + UISprite icon = GUI.Style.IconOverflowIndicator; + + const int iconPadding = 4; + icon.Draw(spriteBatch, new Rectangle((int) drawPos.X - iconPadding, (int) drawPos.Y - iconPadding, (int) maxWidth + iconPadding * 2, (int) maxWidth + iconPadding * 2), Color.White, SpriteEffects.None); + + GUI.DrawString(spriteBatch, drawPos, text, GUI.Style.TextColor, font: GUI.SubHeadingFont); + } + break; + } + + float halfSize = spriteSize / 2f; + if (i > 0) { offset += halfSize; } + Vector2 pos = new Vector2(adjustedCenterX + offset, centerY); + sprite.Draw(spriteBatch, pos, info.Color, scale: scale, origin: sprite.size / 2f); + offset += halfSize + padding; + amount++; + } + } + + public static void GetLinkedHulls(Hull hull, List linkedHulls) { foreach (var linkedEntity in hull.linkedTo) { @@ -335,5 +1252,270 @@ namespace Barotrauma.Items.Components } } } + + public static GUIFrame CreateMiniMap(Submarine sub, GUIComponent parent, MiniMapSettings settings) + { + return CreateMiniMap(sub, parent, settings, null, out _); + } + + public static GUIFrame CreateMiniMap(Submarine sub, GUIComponent parent, MiniMapSettings settings, IEnumerable? pointsOfInterest, out ImmutableDictionary elements) + { + if (settings.Equals(default(MiniMapSettings))) + { + throw new ArgumentException($"Provided {nameof(MiniMapSettings)} is not valid, did you mean {nameof(MiniMapSettings)}.{nameof(MiniMapSettings.Default)}?", nameof(settings)); + } + + Dictionary pointsOfInterestCollection = new Dictionary(); + + RectangleF worldBorders = sub.GetDockedBorders(); + worldBorders.Location += sub.WorldPosition; + + // create a container that has the same "aspect ratio" as the sub + float aspectRatio = worldBorders.Width / worldBorders.Height; + float parentAspectRatio = parent.Rect.Width / (float)parent.Rect.Height; + + const float elementPadding = 0.9f; + + Vector2 containerScale = parentAspectRatio > aspectRatio ? new Vector2(aspectRatio / parentAspectRatio, 1.0f) : new Vector2(1.0f, parentAspectRatio / aspectRatio); + + GUIFrame hullContainer = new GUIFrame(new RectTransform(containerScale * elementPadding, parent.RectTransform, Anchor.Center), style: null); + + ImmutableHashSet connectedSubs = sub.GetConnectedSubs().ToImmutableHashSet(); + ImmutableHashSet hullList = ImmutableHashSet.Empty; + ImmutableDictionary> combinedHulls = ImmutableDictionary>.Empty; + + if (settings.CreateHullElements) + { + hullList = Hull.hullList.Where(IsPartofSub).ToImmutableHashSet(); + combinedHulls = CombinedHulls(hullList); + } + + // Make components for non-linked hulls + foreach (Hull hull in hullList.Where(IsStandaloneHull)) + { + RelativeEntityRect relativeRect = new RelativeEntityRect(worldBorders, hull.WorldRect); + + GUIFrame hullFrame = new GUIFrame(new RectTransform(relativeRect.RelativeSize, hullContainer.RectTransform) { RelativeOffset = relativeRect.RelativePosition }, style: "ScanLines", color: settings.ElementColor) + { + OutlineColor = settings.ElementColor, + OutlineThickness = 2, + UserData = hull + }; + + pointsOfInterestCollection.Add(hull, new MiniMapGUIComponent(hullFrame)); + } + + // Make components for linked hulls + foreach (var (mainHull, linkedHulls) in combinedHulls) + { + MiniMapHullData data = ConstructHullPolygon(mainHull, linkedHulls, hullContainer, worldBorders); + + RelativeEntityRect relativeRect = new RelativeEntityRect(worldBorders, data.Bounds); + + float highestY = 0f, + highestX = 0f; + + foreach (var (r, _) in data.RectDatas) + { + float y = r.Y - -r.Height, + x = r.X; + + if (y > highestY) { highestY = y; } + if (x > highestX) { highestX = x; } + } + + Dictionary hullsAndFrames = new Dictionary(); + + foreach (var (snappredRect, hull) in data.RectDatas) + { + RectangleF rect = snappredRect; + rect.Height = -rect.Height; + rect.Y -= rect.Height; + + var (parentW, parentH) = hullContainer.Rect.Size.ToVector2(); + Vector2 size = new Vector2(rect.Width / parentW, rect.Height / parentH); + Vector2 pos = new Vector2(rect.X / parentW, rect.Y / parentH); + + GUIFrame hullFrame = new GUIFrame(new RectTransform(size, hullContainer.RectTransform) { RelativeOffset = pos }, style: "ScanLinesSeamless", color: settings.ElementColor) + { + UserData = hull, + UVOffset = new Vector2(highestX - rect.X, highestY - rect.Y) + }; + + hullsAndFrames.Add(hull, hullFrame); + } + + /* + * This exists because the rectangle of GUIComponents still uses Rectangle instead of RectangleF + * and because of rounding sometimes it creates 1px gaps between which looks nasty so we snap + * the rectangles together if they are 2 pixels apart or less. + */ + foreach (var (hull1, frame1) in hullsAndFrames) + { + Rectangle rect1 = frame1.Rect; + foreach (var (hull2, frame2) in hullsAndFrames) + { + if (hull2 == hull1) { continue; } + + Rectangle rect2 = frame2.Rect; + Point size = frame1.RectTransform.NonScaledSize; + + const int treshold = 2; + + int diffY = rect2.Top - rect1.Bottom; + int diffX = rect2.Left - rect1.Right; + + if (diffY <= treshold && diffY > 0) + { + size.Y += diffY; + } + + if (diffX <= treshold && diffX > 0) + { + size.X += diffX; + } + + frame1.RectTransform.NonScaledSize = size; + } + } + + GUICustomComponent linkedHullFrame = new GUICustomComponent(new RectTransform(relativeRect.RelativeSize, hullContainer.RectTransform) { RelativeOffset = relativeRect.RelativePosition }, (spriteBatch, component) => + { + foreach (List list in data.Polygon) + { + spriteBatch.DrawPolygonInner(hullContainer.Rect.Location.ToVector2(), list, component.OutlineColor, 2f); + } + }, (deltaTime, component) => + { + if (component.Parent.Rect.Size != data.ParentSize) + { + data = ConstructHullPolygon(mainHull, linkedHulls, hullContainer, worldBorders); + } + }) + { + UserData = hullsAndFrames.Values.ToHashSet(), + OutlineColor = settings.ElementColor, + CanBeFocused = false + }; + + foreach (var (hull, component) in hullsAndFrames) + { + pointsOfInterestCollection.Add(hull, new MiniMapGUIComponent(component, linkedHullFrame)); + } + } + + if (pointsOfInterest != null) + { + foreach (MapEntity entity in pointsOfInterest) + { + RelativeEntityRect relativeRect = new RelativeEntityRect(worldBorders, entity.WorldRect); + + GUIFrame poiComponent = new GUIFrame(new RectTransform(relativeRect.RelativeSize, hullContainer.RectTransform) { RelativeOffset = relativeRect.RelativePosition }, style: null) + { + CanBeFocused = false, + UserData = entity + }; + + pointsOfInterestCollection.Add(entity, new MiniMapGUIComponent(poiComponent)); + } + } + + elements = pointsOfInterestCollection.ToImmutableDictionary(); + + return hullContainer; + + bool IsPartofSub(MapEntity entity) + { + if (entity.Submarine != sub && !connectedSubs.Contains(entity.Submarine)) { return false; } + return !settings.IgnoreOutposts || sub.IsEntityFoundOnThisSub(entity, true); + } + + bool IsStandaloneHull(Hull hull) + { + return !combinedHulls.ContainsKey(hull) && !combinedHulls.Values.Any(hh => hh.Contains(hull)); + } + } + + private static ImmutableDictionary> CombinedHulls(ImmutableHashSet hulls) + { + Dictionary> combinedHulls = new Dictionary>(); + + foreach (Hull hull in hulls) + { + if (combinedHulls.ContainsKey(hull) || combinedHulls.Values.Any(hh => hh.Contains(hull))) { continue; } + + List linkedHulls = new List(); + GetLinkedHulls(hull, linkedHulls); + + linkedHulls.Remove(hull); + + foreach (Hull linkedHull in linkedHulls) + { + if (!combinedHulls.ContainsKey(hull)) + { + combinedHulls.Add(hull, new HashSet()); + } + + combinedHulls[hull].Add(linkedHull); + } + } + + return combinedHulls.ToImmutableDictionary(pair => pair.Key, pair => pair.Value.ToImmutableHashSet()); + } + + private static MiniMapHullData ConstructHullPolygon(Hull mainHull, ImmutableHashSet linkedHulls, GUIComponent parent, RectangleF worldBorders) + { + Rectangle parentRect = parent.Rect; + + Dictionary rects = new Dictionary(); + Rectangle worldRect = mainHull.WorldRect; + worldRect.Y = -worldRect.Y; + + rects.Add(mainHull, worldRect); + + foreach (Hull hull in linkedHulls) + { + Rectangle rect = hull.WorldRect; + rect.Y = -rect.Y; + + worldRect = Rectangle.Union(worldRect, rect); + rects.Add(hull, rect); + } + + worldRect.Y = -worldRect.Y; + + List normalizedRects = new List(); + List hullRefs = new List(); + + foreach (var (hull, rect) in rects) + { + Rectangle wRect = rect; + wRect.Y = -wRect.Y; + + var (posX, posY, sizeX, sizeY) = new RelativeEntityRect(worldBorders, wRect); + + RectangleF newRect = new RectangleF(posX * parentRect.Width, posY * parentRect.Height, sizeX * parentRect.Width, sizeY * parentRect.Height); + + normalizedRects.Add(newRect); + hullRefs.Add(hull); + } + + ImmutableArray snappedRectangles = ToolBox.SnapRectangles(normalizedRects, treshold: 1); + + List> polygon = ToolBox.CombineRectanglesIntoShape(snappedRectangles); + + List> scaledPolygon = new List>(); + + foreach (List list in polygon) + { + var (polySizeX, polySizeY) = ToolBox.GetPolygonBoundingBoxSize(list); + float sizeX = polySizeX - 1f, + sizeY = polySizeY - 1f; + + scaledPolygon.Add(ToolBox.ScalePolygon(list, new Vector2(sizeX / polySizeX, sizeY / polySizeY))); + } + + return new MiniMapHullData(scaledPolygon, worldRect, parentRect.Size, snappedRectangles, hullRefs.ToImmutableArray()); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs index 3877a2ccf..a6b0b74b8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs @@ -142,6 +142,7 @@ namespace Barotrauma.Items.Components partial void UpdateProjSpecific(float deltaTime) { + float rotationRad = MathHelper.ToRadians(item.Rotation); if (FlowPercentage < 0.0f) { foreach (var (position, emitter) in pumpOutEmitters) @@ -149,12 +150,13 @@ namespace Barotrauma.Items.Components if (item.CurrentHull != null && item.CurrentHull.Surface < item.Rect.Location.Y + position.Y) { continue; } //only emit "pump out" particles when underwater - Vector2 relativeParticlePos = (item.WorldRect.Location.ToVector2() + position * item.Scale) - item.WorldPosition; - float angle = 0.0f; + Vector2 relativeParticlePos = (item.WorldRect.Location.ToVector2() + position * item.Scale) - item.WorldPosition; + relativeParticlePos = MathUtils.RotatePoint(relativeParticlePos, item.FlippedX ? rotationRad : -rotationRad); + float angle = -rotationRad; if (item.FlippedX) { relativeParticlePos.X = -relativeParticlePos.X; - angle = MathHelper.Pi; + angle += MathHelper.Pi; } if (item.FlippedY) { @@ -170,11 +172,12 @@ namespace Barotrauma.Items.Components foreach (var (position, emitter) in pumpInEmitters) { Vector2 relativeParticlePos = (item.WorldRect.Location.ToVector2() + position * item.Scale) - item.WorldPosition; - float angle = 0.0f; + relativeParticlePos = MathUtils.RotatePoint(relativeParticlePos, item.FlippedX ? rotationRad : -rotationRad); + float angle = -rotationRad; if (item.FlippedX) { relativeParticlePos.X = -relativeParticlePos.X; - angle = MathHelper.Pi; + angle += MathHelper.Pi; } if (item.FlippedY) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs index 1f32930b3..b66bcaa5a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs @@ -888,6 +888,7 @@ namespace Barotrauma.Items.Components maintainPosOriginIndicator?.Remove(); steeringIndicator?.Remove(); enterOutpostPrompt?.Close(); + pathFinder = null; } public void ClientWrite(IWriteMessage msg, object[] extraData = null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs index 4b6c91176..643b2042e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs @@ -71,7 +71,7 @@ namespace Barotrauma.Items.Components var chargeText = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1), textArea.RectTransform, Anchor.CenterRight), "", textColor: GUI.Style.TextColor, font: GUI.Font, textAlignment: Alignment.CenterRight) { - TextGetter = () => $"{(int)charge}/{(int)capacity} {kWmin} ({((int)MathUtils.Percentage(charge, capacity)).ToString()} %)" + TextGetter = () => $"{(int)Math.Round(charge)}/{(int)capacity} {kWmin} ({(int)Math.Round(MathUtils.Percentage(charge, capacity))} %)" }; if (chargeText.TextSize.X > chargeText.Rect.Width) { chargeText.Font = GUI.SmallFont; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs index 0099e8bfb..a770a45cb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs @@ -15,6 +15,8 @@ namespace Barotrauma.Items.Components public GUIButton SabotageButton { get; private set; } + public GUIButton TinkerButton { get; private set; } + private GUIProgressBar progressBar; private List particleEmitters = new List(); @@ -25,6 +27,7 @@ namespace Barotrauma.Items.Components private string repairButtonText, repairingText; private string sabotageButtonText, sabotagingText; + private string tinkerButtonText, tinkeringText; private FixActions requestStartFixAction; @@ -46,7 +49,7 @@ namespace Barotrauma.Items.Components public override bool ShouldDrawHUD(Character character) { if (!HasRequiredItems(character, false) || character.SelectedConstruction != item) return false; - return item.ConditionPercentage < RepairThreshold || character.IsTraitor && item.ConditionPercentage > MinSabotageCondition || (CurrentFixer == character && (!item.IsFullCondition || (character.IsTraitor && item.ConditionPercentage > MinSabotageCondition))); + return item.ConditionPercentage < RepairThreshold || character.IsTraitor && item.ConditionPercentage > MinSabotageCondition || (CurrentFixer == character && (!item.IsFullCondition || (character.IsTraitor && item.ConditionPercentage > MinSabotageCondition))) || CanTinker(character); } partial void InitProjSpecific(XElement element) @@ -148,6 +151,20 @@ namespace Barotrauma.Items.Components return true; } }; + + tinkerButtonText = "Tinker"; + tinkeringText = "Tinkering"; + TinkerButton = new GUIButton(new RectTransform(new Vector2(0.8f, 0.15f), paddedFrame.RectTransform, Anchor.BottomCenter), tinkerButtonText, style: "GUIButtonSmall") + { + IgnoreLayoutGroups = true, + Visible = false, + OnClicked = (btn, obj) => + { + requestStartFixAction = FixActions.Tinker; + item.CreateClientEvent(this); + return true; + } + }; } partial void UpdateProjSpecific(float deltaTime) @@ -176,6 +193,7 @@ namespace Barotrauma.Items.Components { case FixActions.Repair: case FixActions.Sabotage: + case FixActions.Tinker: StartRepairing(Character.Controlled, requestStartFixAction); requestStartFixAction = FixActions.None; break; @@ -226,6 +244,13 @@ namespace Barotrauma.Items.Components sabotageButtonText : sabotagingText + new string('.', ((int)(Timing.TotalTime * 2.0f) % 3) + 1); + TinkerButton.Visible = CanTinker(character); + TinkerButton.IgnoreLayoutGroups = !TinkerButton.Visible; + TinkerButton.Enabled = (currentFixerAction == FixActions.None || (CurrentFixer == character && currentFixerAction != FixActions.Tinker)) && CanTinker(character); + TinkerButton.Text = (currentFixerAction == FixActions.None || CurrentFixer != character || currentFixerAction != FixActions.Tinker && CanTinker(character)) ? + tinkerButtonText : + tinkeringText + new string('.', ((int)(Timing.TotalTime * 2.0f) % 3) + 1); + System.Diagnostics.Debug.Assert(GuiFrame.GetChild(0) is GUILayoutGroup, "Repair UI hierarchy has changed, could not find skill texts"); foreach (GUIComponent c in GuiFrame.GetChild(0).Children) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs index 353e4550b..fe20dca85 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs @@ -85,7 +85,7 @@ namespace Barotrauma.Items.Components GUITextBlock newBlock = new GUITextBlock( new RectTransform(new Vector2(1, 0), historyBox.Content.RectTransform, anchor: Anchor.TopCenter), "> " + input, - textColor: Color.LimeGreen, wrap: true) + textColor: Color.LimeGreen, wrap: true, font: UseMonospaceFont ? GUI.MonospacedFont : GUI.GlobalFont) { CanBeFocused = false }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs index f28cdf48b..e78bdf3b2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs @@ -534,20 +534,20 @@ namespace Barotrauma.Items.Components minRotationWidget.Draw(spriteBatch, (float)Timing.Step); maxRotationWidget.Draw(spriteBatch, (float)Timing.Step); - Vector2 GetDrawPos() - { - Vector2 drawPos = new Vector2(item.Rect.X + transformedBarrelPos.X, item.Rect.Y - transformedBarrelPos.Y); - if (item.Submarine != null) { drawPos += item.Submarine.DrawPosition; } - drawPos.Y = -drawPos.Y; - return drawPos; - } - void UpdateBarrel() { rotation = (minRotation + maxRotation) / 2; } } + public Vector2 GetDrawPos() + { + Vector2 drawPos = new Vector2(item.Rect.X + transformedBarrelPos.X, item.Rect.Y - transformedBarrelPos.Y); + if (item.Submarine != null) { drawPos += item.Submarine.DrawPosition; } + drawPos.Y = -drawPos.Y; + return drawPos; + } + private Widget GetWidget(string id, SpriteBatch spriteBatch, int size = 5, float thickness = 1f, Action initMethod = null) { Vector2 offset = new Vector2(size / 2 + 5, -10); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index e563f152d..21ac893a5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -478,7 +478,7 @@ namespace Barotrauma } if (container == null) { return false; } - return owner.SelectedCharacter != null|| (!(owner is Character character)) || !container.KeepOpenWhenEquippedBy(character) || !owner.HasEquippedItem(container.Item); + return owner.SelectedCharacter != null|| (!(owner is Character character)) || !container.KeepOpenWhenEquippedBy(character) || !owner.HasEquippedItem(container.Item); } protected virtual bool HideSlot(int i) @@ -667,6 +667,10 @@ namespace Barotrauma if (subInventory.visualSlots == null) { subInventory.CreateSlots(); } canMove = container.MovableFrame && !subInventory.IsInventoryHoverAvailable(Owner as Character, container) && subInventory.originalPos != Point.Zero; + if (this is CharacterInventory characterInventory && characterInventory.CurrentLayout != CharacterInventory.Layout.Default) + { + canMove = false; + } if (canMove) { @@ -826,11 +830,23 @@ namespace Barotrauma return rect.Contains(PlayerInput.MousePosition); } + public static bool IsMouseOnInventory + { + get; private set; + } + + /// + /// Refresh the value of IsMouseOnInventory + /// + public static void RefreshMouseOnInventory() + { + IsMouseOnInventory = DetermineMouseOnInventory(); + } + /// /// Is the mouse on any inventory element (slot, equip button, subinventory...) /// - /// - public static bool IsMouseOnInventory(bool ignoreDraggedItem = false) + private static bool DetermineMouseOnInventory(bool ignoreDraggedItem = false) { if (GameMain.GameSession?.Campaign != null && (GameMain.GameSession.Campaign.ShowCampaignUI || GameMain.GameSession.Campaign.ForceMapUI)) @@ -1112,7 +1128,7 @@ namespace Barotrauma { Character.Controlled.ClearInputs(); - if (!IsMouseOnInventory(ignoreDraggedItem: true) && + if (!DetermineMouseOnInventory(ignoreDraggedItem: true) && CharacterHealth.OpenHealthWindow != null) { bool dropSuccessful = false; @@ -1306,7 +1322,9 @@ namespace Barotrauma protected static Rectangle GetSubInventoryHoverArea(SlotReference subSlot) { Rectangle hoverArea; - if (!subSlot.Inventory.Movable() || Character.Controlled?.Inventory == subSlot.ParentInventory && !Character.Controlled.HasEquippedItem(subSlot.Item)) + if (!subSlot.Inventory.Movable() || + (Character.Controlled?.Inventory == subSlot.ParentInventory && !Character.Controlled.HasEquippedItem(subSlot.Item)) || + (subSlot.ParentInventory is CharacterInventory characterInventory && characterInventory.CurrentLayout != CharacterInventory.Layout.Default)) { hoverArea = subSlot.Slot.Rect; hoverArea.Location += subSlot.Slot.DrawOffset.ToPoint(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index d675203fd..237c062d3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -332,7 +332,7 @@ namespace Barotrauma foreach (var decorativeSprite in Prefab.DecorativeSprites) { if (!spriteAnimState[decorativeSprite].IsActive) { continue; } - Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, -rotationRad) * Scale; + Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, flippedX && Prefab.CanSpriteFlipX ? rotationRad : -rotationRad) * Scale; if (flippedX && Prefab.CanSpriteFlipX) { offset.X = -offset.X; } if (flippedY && Prefab.CanSpriteFlipY) { offset.Y = -offset.Y; } decorativeSprite.Sprite.DrawTiled(spriteBatch, @@ -368,7 +368,7 @@ namespace Barotrauma { if (!spriteAnimState[decorativeSprite].IsActive) { continue; } float rot = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState, spriteAnimState[decorativeSprite].RandomRotationFactor); - Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, -rotationRad) * Scale; + Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, flippedX && Prefab.CanSpriteFlipX ? rotationRad : -rotationRad) * Scale; if (flippedX && Prefab.CanSpriteFlipX) { offset.X = -offset.X; } if (flippedY && Prefab.CanSpriteFlipY) { offset.Y = -offset.Y; } decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + offset.X, -(DrawPosition.Y + offset.Y)), color, @@ -1623,6 +1623,10 @@ namespace Barotrauma { wifiComponent.TeamID = (CharacterTeamType)teamID; } + foreach (IdCard idCard in item.GetComponents()) + { + idCard.TeamID = (CharacterTeamType)teamID; + } if (descriptionChanged) { item.Description = itemDesc; } if (tagsChanged) { item.Tags = tags; } var nameTag = item.GetComponent(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs index 308010cac..57d7b3e44 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs @@ -8,6 +8,8 @@ namespace Barotrauma { partial void ExplodeProjSpecific(Vector2 worldPosition, Hull hull) { + if (GameMain.Client?.MidRoundSyncing ?? false) { return; } + if (shockwave) { GameMain.ParticleManager.CreateParticle("shockwave", worldPosition, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs index 2138e1109..13533eb7d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs @@ -676,7 +676,7 @@ namespace Barotrauma } else { - remoteBackgroundSections.Add(new BackgroundSection(new Rectangle(0, 0, 1, 1), i, colorStrength, color, 0)); + remoteBackgroundSections.Add(new BackgroundSection(new Rectangle(0, 0, 1, 1), (ushort)i, colorStrength, color, 0)); } } paintAmount = BackgroundSections.Sum(s => s.ColorStrength); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index bb8e93052..e4c73b52c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs @@ -238,13 +238,13 @@ namespace Barotrauma.Lights //draw a black rectangle on hulls to hide background lights behind subs //--------------------------------------------------------------------------------------------------- - if (backgroundObstructor != null) + /*if (backgroundObstructor != null) { spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied); spriteBatch.Draw(backgroundObstructor, new Rectangle(0, 0, (int)(GameMain.GraphicsWidth * currLightMapScale), (int)(GameMain.GraphicsHeight * currLightMapScale)), Color.Black); spriteBatch.End(); - } + }*/ spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Opaque, transformMatrix: spriteBatchTransform); Dictionary visibleHulls = GetVisibleHulls(cam); @@ -258,7 +258,7 @@ namespace Barotrauma.Lights spriteBatch.End(); SolidColorEffect.CurrentTechnique = SolidColorEffect.Techniques["SolidColor"]; - SolidColorEffect.Parameters["color"].SetValue(AmbientLight.ToVector4()); + SolidColorEffect.Parameters["color"].SetValue(AmbientLight.Opaque().ToVector4()); spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, transformMatrix: spriteBatchTransform, effect: SolidColorEffect); Submarine.DrawDamageable(spriteBatch, null); spriteBatch.End(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs index 28e2bbdb2..04701beb5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs @@ -7,6 +7,7 @@ using Microsoft.Xna.Framework.Graphics; using System; using System.Collections; using System.Collections.Generic; +using System.Collections.Immutable; using Barotrauma.IO; using System.Linq; using System.Xml.Linq; @@ -403,6 +404,8 @@ namespace Barotrauma } } + // TODO remove + [Obsolete("Use MiniMap.CreateMiniMap()")] public void CreateMiniMap(GUIComponent parent, IEnumerable pointsOfInterest = null, bool ignoreOutpost = false) { Rectangle worldBorders = GetDockedBorders(); @@ -417,24 +420,125 @@ namespace Barotrauma GUIFrame hullContainer = new GUIFrame(new RectTransform( (parentAspectRatio > aspectRatio ? new Vector2(aspectRatio / parentAspectRatio, 1.0f) : new Vector2(1.0f, parentAspectRatio / aspectRatio)) * scale, parent.RectTransform, Anchor.Center), - style: null); + style: null) + { + UserData = "hullcontainer" + }; var connectedSubs = GetConnectedSubs(); - foreach (Hull hull in Hull.hullList) - { - if (hull.Submarine != this && !connectedSubs.Contains(hull.Submarine)) { continue; } - if (ignoreOutpost && !IsEntityFoundOnThisSub(hull, true)) { continue; } + HashSet hullList = Hull.hullList.Where(hull => hull.Submarine == this || connectedSubs.Contains(hull.Submarine)).Where(hull => !ignoreOutpost || IsEntityFoundOnThisSub(hull, true)).ToHashSet(); + + Dictionary> combinedHulls = new Dictionary>(); + + foreach (Hull hull in hullList) + { + if (combinedHulls.ContainsKey(hull) || combinedHulls.Values.Any(hh => hh.Contains(hull))) { continue; } + + List linkedHulls = new List(); + MiniMap.GetLinkedHulls(hull, linkedHulls); + + linkedHulls.Remove(hull); + + foreach (Hull linkedHull in linkedHulls) + { + if (!combinedHulls.ContainsKey(hull)) + { + combinedHulls.Add(hull, new HashSet()); + } + + combinedHulls[hull].Add(linkedHull); + } + } + + foreach (Hull hull in hullList) + { Vector2 relativeHullPos = new Vector2( (hull.WorldRect.X - worldBorders.X) / (float)worldBorders.Width, (worldBorders.Y - hull.WorldRect.Y) / (float)worldBorders.Height); Vector2 relativeHullSize = new Vector2(hull.Rect.Width / (float)worldBorders.Width, hull.Rect.Height / (float)worldBorders.Height); - var hullFrame = new GUIFrame(new RectTransform(relativeHullSize, hullContainer.RectTransform) { RelativeOffset = relativeHullPos }, style: "MiniMapRoom", color: Color.DarkCyan * 0.8f) + bool hideHull = combinedHulls.ContainsKey(hull) || combinedHulls.Values.Any(hh => hh.Contains(hull)); + + if (hideHull) { continue; } + + Color color = Color.DarkCyan * 0.8f; + + var hullFrame = new GUIFrame(new RectTransform(relativeHullSize, hullContainer.RectTransform) { RelativeOffset = relativeHullPos }, style: "MiniMapRoom", color: color) { UserData = hull }; - new GUIFrame(new RectTransform(Vector2.One, hullFrame.RectTransform), style: "ScanLines", color: Color.DarkCyan * 0.8f); + + new GUIFrame(new RectTransform(Vector2.One, hullFrame.RectTransform), style: "ScanLines", color: color); + } + + foreach (var (mainHull, linkedHulls) in combinedHulls) + { + MiniMapHullData data = ConstructLinkedHulls(mainHull, linkedHulls, hullContainer, worldBorders); + + Vector2 relativeHullPos = new Vector2( + (data.Bounds.X - worldBorders.X) / worldBorders.Width, + (worldBorders.Y - data.Bounds.Y) / worldBorders.Height); + + Vector2 relativeHullSize = new Vector2(data.Bounds.Width / worldBorders.Width, data.Bounds.Height / worldBorders.Height); + + Color color = Color.DarkCyan * 0.8f; + + float highestY = 0f, + highestX = 0f; + + foreach (var (r, _) in data.RectDatas) + { + float y = r.Y - -r.Height, + x = r.X; + + if (y > highestY) { highestY = y; } + if (x > highestX) { highestX = x; } + } + + HashSet frames = new HashSet(); + + foreach (var (snappredRect, hull) in data.RectDatas) + { + RectangleF rect = snappredRect; + rect.Height = -rect.Height; + rect.Y -= rect.Height; + + var (parentW, parentH) = hullContainer.Rect.Size.ToVector2(); + Vector2 size = new Vector2(rect.Width / parentW, rect.Height / parentH); + // TODO this won't be required if we some day switch RectTransform to use RectangleF + const float padding = 0.001f; + size.X += padding; + size.Y += padding; + Vector2 pos = new Vector2(rect.X / parentW, rect.Y / parentH); + + GUIFrame hullFrame = new GUIFrame(new RectTransform(size, hullContainer.RectTransform) { RelativeOffset = pos }, style: "ScanLinesSeamless", color: color) + { + UserData = hull, + UVOffset = new Vector2(highestX - rect.X, highestY - rect.Y) + }; + + frames.Add(hullFrame); + } + + new GUICustomComponent(new RectTransform(relativeHullSize, hullContainer.RectTransform) { RelativeOffset = relativeHullPos }, (spriteBatch, component) => + { + foreach (List list in data.Polygon) + { + spriteBatch.DrawPolygonInner(hullContainer.Rect.Location.ToVector2(), list, component.Color, 2f); + } + }, (deltaTime, component) => + { + if (component.Parent.Rect.Size != data.ParentSize) + { + data = ConstructLinkedHulls(mainHull, linkedHulls, hullContainer, worldBorders); + } + }) + { + UserData = frames, + Color = color, + CanBeFocused = false + }; } if (pointsOfInterest != null) @@ -453,6 +557,64 @@ namespace Barotrauma } } + public static MiniMapHullData ConstructLinkedHulls(Hull mainHull, HashSet linkedHulls, GUIComponent parent, Rectangle worldBorders) + { + Rectangle parentRect = parent.Rect; + + Dictionary rects = new Dictionary(); + Rectangle worldRect = mainHull.WorldRect; + worldRect.Y = -worldRect.Y; + + rects.Add(mainHull, worldRect); + + foreach (Hull hull in linkedHulls) + { + Rectangle rect = hull.WorldRect; + rect.Y = -rect.Y; + + worldRect = Rectangle.Union(worldRect, rect); + rects.Add(hull, rect); + } + + worldRect.Y = -worldRect.Y; + + List normalizedRects = new List(); + List hullRefs = new List(); + foreach (var (hull, rect) in rects) + { + Rectangle wRect = rect; + wRect.Y = -wRect.Y; + + var (posX, posY) = new Vector2( + (wRect.X - worldBorders.X) / (float)worldBorders.Width, + (worldBorders.Y - wRect.Y) / (float)worldBorders.Height); + + var (scaleX, scaleY) = new Vector2(wRect.Width / (float)worldBorders.Width, wRect.Height / (float)worldBorders.Height); + + RectangleF newRect = new RectangleF(posX * parentRect.Width, posY * parentRect.Height, scaleX * parentRect.Width, scaleY * parentRect.Height); + + normalizedRects.Add(newRect); + hullRefs.Add(hull); + } + + ImmutableArray snappedRectangles = ToolBox.SnapRectangles(normalizedRects, treshold: 1); + + List> polygon = ToolBox.CombineRectanglesIntoShape(snappedRectangles); + + List> scaledPolygon = new List>(); + + foreach (List list in polygon) + { + var (polySizeX, polySizeY) = ToolBox.GetPolygonBoundingBoxSize(list); + float sizeX = polySizeX - 1f, + sizeY = polySizeY - 1f; + + scaledPolygon.Add(ToolBox.ScalePolygon(list, new Vector2(sizeX / polySizeX, sizeY / polySizeY))); + } + + return new MiniMapHullData(scaledPolygon, worldRect, parentRect.Size, snappedRectangles, hullRefs.ToImmutableArray()); + } + public void CheckForErrors() { List errorMsgs = new List(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs index feee46eb0..67c758aa6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs @@ -64,7 +64,10 @@ namespace Barotrauma.Networking string orderOption = orderMessageInfo.OrderOption; orderOption ??= orderMessageInfo.OrderOptionIndex.HasValue && orderMessageInfo.OrderOptionIndex >= 0 && orderMessageInfo.OrderOptionIndex < orderPrefab.Options.Length ? orderPrefab.Options[orderMessageInfo.OrderOptionIndex.Value] : ""; - txt = orderPrefab.GetChatMessage(orderMessageInfo.TargetCharacter?.Name, senderCharacter?.CurrentHull?.DisplayName, givingOrderToSelf: orderMessageInfo.TargetCharacter == senderCharacter, orderOption: orderOption); + txt = orderPrefab.GetChatMessage(orderMessageInfo.TargetCharacter?.Name, senderCharacter?.CurrentHull?.DisplayName, + givingOrderToSelf: orderMessageInfo.TargetCharacter == senderCharacter, + orderOption: orderOption, + priority: orderMessageInfo.Priority); if (GameMain.Client.GameStarted && Screen.Selected == GameMain.GameScreen) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 363d909a3..bc8cc1075 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -550,6 +550,13 @@ namespace Barotrauma.Networking okButton.OnClicked += msgBox.Close; var cancelButton = msgBox.Buttons[1]; cancelButton.OnClicked += msgBox.Close; + passwordBox.OnEnterPressed += (GUITextBox textBox, string text) => + { + msgBox.Close(); + clientPeer?.SendPassword(passwordBox.Text); + requiresPw = false; + return true; + }; okButton.OnClicked += (GUIButton button, object obj) => { @@ -565,6 +572,8 @@ namespace Barotrauma.Networking GameMain.ServerListScreen.Select(); return true; }; + yield return CoroutineStatus.Running; + passwordBox.Select(); while (GUIMessageBox.MessageBoxes.Contains(msgBox)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs index 931cd5d44..ee2ffa7c1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs @@ -110,12 +110,7 @@ namespace Barotrauma.Networking if (frame == null) { return; } - var previewContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.98f), frame.RectTransform, Anchor.Center)) - { - Stretch = true - }; - - var title = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), previewContainer.RectTransform, Anchor.CenterLeft), ServerName, font: GUI.LargeFont) + var title = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform), ServerName, font: GUI.LargeFont) { ToolTip = ServerName }; @@ -141,41 +136,30 @@ namespace Barotrauma.Networking } }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), previewContainer.RectTransform), + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform), TextManager.AddPunctuation(':', TextManager.Get("ServerListVersion"), string.IsNullOrEmpty(GameVersion) ? TextManager.Get("Unknown") : GameVersion)); - bool hidePlaystyleBanner = previewContainer.Rect.Height < 380 || !PlayStyle.HasValue; + bool hidePlaystyleBanner = !PlayStyle.HasValue; if (!hidePlaystyleBanner) { PlayStyle playStyle = PlayStyle ?? Networking.PlayStyle.Serious; Sprite playStyleBannerSprite = ServerListScreen.PlayStyleBanners[(int)playStyle]; float playStyleBannerAspectRatio = playStyleBannerSprite.SourceRect.Width / playStyleBannerSprite.SourceRect.Height; - var playStyleBanner = new GUIImage(new RectTransform(new Point(previewContainer.Rect.Width, (int)(previewContainer.Rect.Width / playStyleBannerAspectRatio)), previewContainer.RectTransform), + var playStyleBanner = new GUIImage(new RectTransform(new Point(frame.Rect.Width, (int)(frame.Rect.Width / playStyleBannerAspectRatio)), frame.RectTransform), playStyleBannerSprite, null, true); - var playStyleName = new GUITextBlock(new RectTransform(new Vector2(0.15f, 0.0f), playStyleBanner.RectTransform) { RelativeOffset = new Vector2(0.01f, 0.06f) }, + var playStyleName = new GUITextBlock(new RectTransform(new Vector2(0.15f, 0.0f), playStyleBanner.RectTransform) { RelativeOffset = new Vector2(0.0f, 0.06f) }, TextManager.AddPunctuation(':', TextManager.Get("serverplaystyle"), TextManager.Get("servertag."+ playStyle)), textColor: Color.White, font: GUI.SmallFont, textAlignment: Alignment.Center, color: ServerListScreen.PlayStyleColors[(int)playStyle], style: "GUISlopedHeader"); playStyleName.RectTransform.NonScaledSize = (playStyleName.Font.MeasureString(playStyleName.Text) + new Vector2(20, 5) * GUI.Scale).ToPoint(); playStyleName.RectTransform.IsFixedSize = true; - - var serverTypeContainer = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.2f), playStyleBanner.RectTransform, Anchor.BottomLeft, Pivot.BottomLeft), - "MainMenuNotifBackground", Color.Black) - { - CanBeFocused = false, - }; - - var serverType = new GUITextBlock(new RectTransform(Vector2.One, serverTypeContainer.RectTransform, Anchor.CenterLeft), - TextManager.Get((OwnerID != 0 || LobbyID != 0) ? "SteamP2PServer" : "DedicatedServer"), textAlignment: Alignment.CenterLeft); - } - else - { - var serverType = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), previewContainer.RectTransform, Anchor.CenterLeft), - TextManager.Get((OwnerID != 0 || LobbyID != 0) ? "SteamP2PServer" : "DedicatedServer"), textAlignment: Alignment.CenterLeft); } + var serverType = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform), + TextManager.Get((OwnerID != 0 || LobbyID != 0) ? "SteamP2PServer" : "DedicatedServer"), textAlignment: Alignment.TopLeft); + serverType.RectTransform.MinSize = new Point(0, (int)(serverType.Rect.Height * 1.5f)); - var content = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.6f), previewContainer.RectTransform)) + var content = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.6f), frame.RectTransform)) { Stretch = true }; @@ -285,7 +269,6 @@ namespace Barotrauma.Networking else usingWhiteList.Selected = UsingWhiteList.Value; - content.RectTransform.SizeChanged += () => { GUITextBlock.AutoScaleAndNormalize(allowSpectating.TextBlock, allowRespawn.TextBlock, usingWhiteList.TextBlock); @@ -294,7 +277,7 @@ namespace Barotrauma.Networking new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), TextManager.Get("ServerListContentPackages"), textAlignment: Alignment.Center, font: GUI.SubHeadingFont); - var contentPackageList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.2f), content.RectTransform)) { ScrollBarVisible = true }; + var contentPackageList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.3f), frame.RectTransform)) { ScrollBarVisible = true }; if (ContentPackageNames.Count == 0) { new GUITextBlock(new RectTransform(Vector2.One, contentPackageList.Content.RectTransform), TextManager.Get("Unknown"), textAlignment: Alignment.Center) @@ -309,7 +292,7 @@ namespace Barotrauma.Networking var packageText = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.15f), contentPackageList.Content.RectTransform) { MinSize = new Point(0, 15) }, ContentPackageNames[i]) { - Enabled = false + CanBeFocused = false }; if (i < ContentPackageHashes.Count) { @@ -322,7 +305,7 @@ namespace Barotrauma.Networking //workshop download link found if (i < ContentPackageWorkshopIds.Count && ContentPackageWorkshopIds[i] != 0) { - packageText.TextColor = Color.Yellow; + packageText.TextColor = GUI.Style.Yellow; packageText.ToolTip = TextManager.GetWithVariable("ServerListIncompatibleContentPackageWorkshopAvailable", "[contentpackage]", ContentPackageNames[i]); } else //no package or workshop download link found, tough luck diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs index d9647ac72..13604817e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs @@ -1207,7 +1207,7 @@ namespace Barotrauma.Steam foreach (string file in allPackageFiles) { if (file == metaDataFilePath) { continue; } - string relativePath = UpdaterUtil.GetRelativePath(file, item.Directory); + string relativePath = Path.GetRelativePath(item.Directory, file); string fullPath = Path.GetFullPath(relativePath); if (contentPackage.Files.Any(f => { string fp = Path.GetFullPath(f.Path); return fp == fullPath; })) { continue; } nonContentFiles.Add(relativePath); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs index 3a4852708..7747a7b16 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs @@ -3,7 +3,6 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; -using System.Reflection.Metadata; namespace Barotrauma.Particles { @@ -16,6 +15,8 @@ namespace Barotrauma.Particles public delegate void OnChangeHullHandler(Vector2 position, Hull currentHull); public OnChangeHullHandler OnChangeHull; + public OnChangeHullHandler OnCollision; + private Vector2 position; private Vector2 prevPosition; @@ -166,6 +167,7 @@ namespace Barotrauma.Particles HighQualityCollisionDetection = false; OnChangeHull = null; + OnCollision = null; subEmitters.Clear(); hasSubEmitters = false; @@ -340,12 +342,20 @@ namespace Barotrauma.Particles Vector2 collisionNormal = Vector2.Zero; if (velocity.Y < 0.0f && position.Y - prefab.CollisionRadius * size.Y < hullRect.Y - hullRect.Height) { - if (prefab.DeleteOnCollision) { return UpdateResult.Delete; } + if (prefab.DeleteOnCollision) + { + OnCollision?.Invoke(position, currentHull); + return UpdateResult.Delete; + } collisionNormal = new Vector2(0.0f, 1.0f); } else if (velocity.Y > 0.0f && position.Y + prefab.CollisionRadius * size.Y > hullRect.Y) { - if (prefab.DeleteOnCollision) { return UpdateResult.Delete; } + if (prefab.DeleteOnCollision) + { + OnCollision?.Invoke(position, currentHull); + return UpdateResult.Delete; + } collisionNormal = new Vector2(0.0f, -1.0f); } @@ -487,6 +497,8 @@ namespace Barotrauma.Particles velocity.Y = Math.Sign(collisionNormal.Y) * Math.Abs(velocity.Y) * prefab.Restitution; } + OnCollision?.Invoke(position, currentHull); + velocity += subVel; } @@ -523,6 +535,8 @@ namespace Barotrauma.Particles velocity.Y *= (1.0f - prefab.Friction); } + OnCollision?.Invoke(position, currentHull); + velocity *= prefab.Restitution; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs index f54793630..72d20579a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs @@ -138,6 +138,8 @@ namespace Barotrauma.Particles public void Emit(float deltaTime, Vector2 position, Hull hullGuess = null, float angle = 0.0f, float particleRotation = 0.0f, float velocityMultiplier = 1.0f, float sizeMultiplier = 1.0f, float amountMultiplier = 1.0f, Color? colorMultiplier = null, ParticlePrefab overrideParticle = null, Tuple tracerPoints = null) { + if (GameMain.Client?.MidRoundSyncing ?? false) { return; } + if (initialDelay < Prefab.Properties.InitialDelay) { initialDelay += deltaTime; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs index 9ce02308e..182e826f9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs @@ -133,22 +133,41 @@ namespace Barotrauma.Particles public Particle CreateParticle(ParticlePrefab prefab, Vector2 position, Vector2 velocity, float rotation = 0.0f, Hull hullGuess = null, bool drawOnTop = false, float collisionIgnoreTimer = 0f, Tuple tracerPoints = null) { - if (particleCount >= MaxParticles || prefab == null || prefab.Sprites.Count == 0) { return null; } - - // this should be optimized for tracers after prototyping - if (tracerPoints == null) + if (prefab == null || prefab.Sprites.Count == 0) { return null; } + + if (particleCount >= MaxParticles) { - Vector2 particleEndPos = prefab.CalculateEndPosition(position, velocity); - - Vector2 minPos = new Vector2(Math.Min(position.X, particleEndPos.X), Math.Min(position.Y, particleEndPos.Y)); - Vector2 maxPos = new Vector2(Math.Max(position.X, particleEndPos.X), Math.Max(position.Y, particleEndPos.Y)); - - Rectangle expandedViewRect = MathUtils.ExpandRect(cam.WorldView, MaxOutOfViewDist); - - if (minPos.X > expandedViewRect.Right || maxPos.X < expandedViewRect.X) { return null; } - if (minPos.Y > expandedViewRect.Y || maxPos.Y < expandedViewRect.Y - expandedViewRect.Height) { return null; } + for (int i = 0; i < particleCount; i++) + { + if (particles[i].Prefab.Priority < prefab.Priority) + { + RemoveParticle(i); + break; + } + } + if (particleCount >= MaxParticles) { return null; } } + Vector2 particleEndPos = prefab.CalculateEndPosition(position, velocity); + + Vector2 minPos = new Vector2(Math.Min(position.X, particleEndPos.X), Math.Min(position.Y, particleEndPos.Y)); + Vector2 maxPos = new Vector2(Math.Max(position.X, particleEndPos.X), Math.Max(position.Y, particleEndPos.Y)); + + if (tracerPoints != null) + { + minPos = new Vector2( + Math.Min(Math.Min(minPos.X, tracerPoints.Item1.X), tracerPoints.Item2.X), + Math.Min(Math.Min(minPos.Y, tracerPoints.Item1.Y), tracerPoints.Item2.Y)); + maxPos = new Vector2( + Math.Max(Math.Max(maxPos.X, tracerPoints.Item1.X), tracerPoints.Item2.X), + Math.Max(Math.Max(maxPos.Y, tracerPoints.Item1.Y), tracerPoints.Item2.Y)); + } + + Rectangle expandedViewRect = MathUtils.ExpandRect(cam.WorldView, MaxOutOfViewDist); + + if (minPos.X > expandedViewRect.Right || maxPos.X < expandedViewRect.X) { return null; } + if (minPos.Y > expandedViewRect.Y || maxPos.Y < expandedViewRect.Y - expandedViewRect.Height) { return null; } + if (particles[particleCount] == null) particles[particleCount] = new Particle(); particles[particleCount].Init(prefab, position, velocity, rotation, hullGuess, drawOnTop, collisionIgnoreTimer, tracerPoints: tracerPoints); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs index 104789a59..9882048e8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs @@ -180,16 +180,13 @@ namespace Barotrauma.Particles [Editable, Serialize("1.0,1.0", false, description: "The maximum initial size of the particle.")] public Vector2 StartSizeMax { get; private set; } - [Editable] - [Serialize("0.0,0.0", false, description: "How much the size of the particle changes per second. The rate of growth for each particle is randomize between SizeChangeMin and SizeChangeMax.")] + [Editable, Serialize("0.0,0.0", false, description: "How much the size of the particle changes per second. The rate of growth for each particle is randomize between SizeChangeMin and SizeChangeMax.")] public Vector2 SizeChangeMin { get; private set; } - [Editable] - [Serialize("0.0,0.0", false, description: "How much the size of the particle changes per second. The rate of growth for each particle is randomize between SizeChangeMin and SizeChangeMax.")] + [Editable, Serialize("0.0,0.0", false, description: "How much the size of the particle changes per second. The rate of growth for each particle is randomize between SizeChangeMin and SizeChangeMax.")] public Vector2 SizeChangeMax { get; private set; } - [Editable] - [Serialize(0.0f, false, description: "How many seconds it takes for the particle to grow to it's initial size.")] + [Editable, Serialize(0.0f, false, description: "How many seconds it takes for the particle to grow to it's initial size.")] public float GrowTime { get; private set; } //rendering ----------------------------------------- @@ -215,6 +212,9 @@ namespace Barotrauma.Particles [Editable, Serialize(ParticleBlendState.AlphaBlend, false, description: "The type of blending to use when rendering the particle.")] public ParticleBlendState BlendState { get; private set; } + [Editable, Serialize(0, false, description: "Particles with a higher priority can replace lower-priority ones if the maximum number of active particles has been reached.")] + public int Priority { get; private set; } + //animation ----------------------------------------- [Editable(0.0f, float.MaxValue), Serialize(1.0f, false, description: "The duration of the particle's animation cycle (if it's animated).")] diff --git a/Barotrauma/BarotraumaClient/ClientSource/Program.cs b/Barotrauma/BarotraumaClient/ClientSource/Program.cs index 0ee887f01..b8beb4aa6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Program.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Program.cs @@ -54,9 +54,11 @@ namespace Barotrauma executableDir = Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location); Directory.SetCurrentDirectory(executableDir); SteamManager.Initialize(); + EnableNvOptimus(); Game = new GameMain(args); Game.Run(); Game.Dispose(); + FreeNvOptimus(); CrossThread.ProcessTasks(); } @@ -263,6 +265,27 @@ namespace Barotrauma " if you'd like to help fix this bug, you may post it on Barotrauma's GitHub issue tracker: https://github.com/Regalis11/Barotrauma/issues/", filePath); } } - } + + private static IntPtr nvApi64Dll = IntPtr.Zero; + private static void EnableNvOptimus() + { +#if WINDOWS && X64 + // We force load nvapi64.dll so nvidia gives us the dedicated GPU on optimus laptops. + // This is not a method for getting optimus that is documented by nvidia, but it works, so... + if (NativeLibrary.TryLoad("nvapi64.dll", out nvApi64Dll)) + { + DebugConsole.Log("Loaded nvapi64.dll successfully"); + } #endif } + + private static void FreeNvOptimus() + { + #warning TODO: determine if we can do this safely + //NativeLibrary.Free(nvApi64Dll); + } + + } +#endif + + } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs index 7882cc77b..32a158105 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs @@ -466,7 +466,7 @@ namespace Barotrauma { var sub = child.UserData as SubmarineInfo; if (sub == null) { return; } - child.Visible = string.IsNullOrEmpty(filter) ? true : sub.DisplayName.ToLower().Contains(filter.ToLower()); + child.Visible = string.IsNullOrEmpty(filter) || sub.DisplayName.ToLower().Contains(filter.ToLower()); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index 947077154..27efd044d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -628,7 +628,7 @@ namespace Barotrauma { TextGetter = () => { - return TextManager.AddPunctuation(':', TextManager.Get("Missions"), $"{Campaign.NumberOfMissionsAtLocation(destination)}/{Campaign.Settings.MaxMissionCount}"); + return TextManager.AddPunctuation(':', TextManager.Get("Missions"), $"{Campaign.NumberOfMissionsAtLocation(destination)}/{Campaign.Settings.TotalMaxMissionCount}"); } }; @@ -735,7 +735,7 @@ namespace Barotrauma private void UpdateMaxMissions(Location location) { - hasMaxMissions = Campaign.NumberOfMissionsAtLocation(location) >= Campaign.Settings.MaxMissionCount; + hasMaxMissions = Campaign.NumberOfMissionsAtLocation(location) >= Campaign.Settings.TotalMaxMissionCount; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs index 6d6d44024..e5a343cbe 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs @@ -262,7 +262,7 @@ namespace Barotrauma.CharacterEditor { FileSelection.OnFileSelected = (file) => { - string relativePath = UpdaterUtil.GetRelativePath(Path.GetFullPath(file), Environment.CurrentDirectory); + string relativePath = Path.GetRelativePath(Environment.CurrentDirectory, Path.GetFullPath(file)); string destinationPath = relativePath; //copy file to XML path if it's not located relative to the game's files diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs index 75bbf5cfc..d85bf42ee 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs @@ -541,7 +541,7 @@ namespace Barotrauma private XElement? ExportXML() { - XElement mainElement = new XElement("ScriptedEvent", new XAttribute("identifier", projectName.RemoveWhitespace().ToLower())); + XElement mainElement = new XElement("ScriptedEvent", new XAttribute("identifier", projectName.RemoveWhitespace().ToLowerInvariant())); EditorNode? startNode = null; foreach (EditorNode eventNode in nodeList.Where(node => node is EventNode || node is SpecialNode)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index a179926ae..a2420730e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -25,6 +25,7 @@ namespace Barotrauma public Effect PostProcessEffect { get; private set; } public Effect GradientEffect { get; private set; } public Effect GrainEffect { get; private set; } + public Effect BlueprintEffect { get; set; } public GameScreen(GraphicsDevice graphics, ContentManager content) { @@ -43,12 +44,14 @@ namespace Barotrauma PostProcessEffect = content.Load("Effects/postprocess_opengl"); GradientEffect = content.Load("Effects/gradientshader_opengl"); GrainEffect = content.Load("Effects/grainshader_opengl"); + BlueprintEffect = content.Load("Effects/blueprintshader_opengl"); #else //var blurEffect = content.Load("Effects/blurshader"); damageEffect = content.Load("Effects/damageshader"); PostProcessEffect = content.Load("Effects/postprocess"); GradientEffect = content.Load("Effects/gradientshader"); GrainEffect = content.Load("Effects/grainshader"); + BlueprintEffect = content.Load("Effects/blueprintshader"); #endif damageStencil = TextureLoader.FromFile("Content/Map/walldamage.png"); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index 7e9f8c97a..071349710 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -2020,7 +2020,8 @@ namespace Barotrauma var playerFrame = (GUITextBlock)PlayerList.Content.FindChild(client); if (playerFrame == null) { return; } playerFrame.Text = client.Name; - + + playerFrame.ToolTip = ""; Color color = Color.White; if (SelectedMode == GameModePreset.PvP) { @@ -2028,15 +2029,28 @@ namespace Barotrauma { case CharacterTeamType.Team1: color = new Color(0, 110, 150, 255); + playerFrame.ToolTip = TextManager.GetWithVariable("teampreference", "[team]", TextManager.Get("teampreference.team1")); break; case CharacterTeamType.Team2: color = new Color(150, 110, 0, 255); + playerFrame.ToolTip = TextManager.GetWithVariable("teampreference", "[team]", TextManager.Get("teampreference.team2")); + break; + default: + playerFrame.ToolTip = TextManager.GetWithVariable("teampreference", "[team]", TextManager.Get("none")); break; } } - else if (JobPrefab.Prefabs.ContainsKey(client.PreferredJob)) + else { - color = JobPrefab.Prefabs[client.PreferredJob].UIColor; + if (JobPrefab.Prefabs.ContainsKey(client.PreferredJob)) + { + color = JobPrefab.Prefabs[client.PreferredJob].UIColor; + playerFrame.ToolTip = TextManager.GetWithVariable("jobpreference", "[job]", JobPrefab.Prefabs[client.PreferredJob].Name); + } + else + { + playerFrame.ToolTip = TextManager.GetWithVariable("jobpreference", "[job]", TextManager.Get("none")); + } } playerFrame.Color = color * 0.4f; playerFrame.HoverColor = color * 0.6f; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs index 5ac93c54e..4430efdb1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs @@ -24,7 +24,8 @@ namespace Barotrauma private GUIFrame menu; private GUIListBox serverList; - private GUIFrame serverPreview; + private GUIFrame serverPreviewContainer; + private GUIListBox serverPreview; private GUIButton joinButton; private ServerInfo selectedServer; @@ -340,11 +341,11 @@ namespace Barotrauma void RecalculateHolder() { float listContainerSubtract = filtersHolder.Visible ? sidebarWidth : 0.0f; - listContainerSubtract += serverPreview.Visible ? sidebarWidth : 0.0f; + listContainerSubtract += serverPreviewContainer.Visible ? sidebarWidth : 0.0f; float toggleButtonsSubtract = 1.1f * filterToggle.Rect.Width / serverListHolder.Rect.Width; listContainerSubtract += filterToggle.Visible ? toggleButtonsSubtract : 0.0f; - listContainerSubtract += serverPreviewToggleButton.Visible ? toggleButtonsSubtract : 0.0f; + listContainerSubtract += serverPreviewContainer.Visible ? toggleButtonsSubtract : 0.0f; serverListContainer.RectTransform.RelativeSize = new Vector2(1.0f - listContainerSubtract, 1.0f); serverListHolder.Recalculate(); @@ -567,17 +568,17 @@ namespace Barotrauma { joinButton.Enabled = true; selectedServer = serverInfo; - if (!serverPreview.Visible) + if (!serverPreviewContainer.Visible) { - serverPreview.RectTransform.RelativeSize = new Vector2(sidebarWidth, 1.0f); + serverPreviewContainer.RectTransform.RelativeSize = new Vector2(sidebarWidth, 1.0f); serverPreviewToggleButton.Visible = true; serverPreviewToggleButton.IgnoreLayoutGroups = false; - serverPreview.Visible = true; - serverPreview.IgnoreLayoutGroups = false; + serverPreviewContainer.Visible = true; + serverPreviewContainer.IgnoreLayoutGroups = false; RecalculateHolder(); } - serverInfo.CreatePreviewWindow(serverPreview); - btn.Children.ForEach(c => c.SpriteEffects = serverPreview.Visible ? SpriteEffects.None : SpriteEffects.FlipHorizontally); + serverInfo.CreatePreviewWindow(serverPreview.Content); + btn.Children.ForEach(c => c.SpriteEffects = serverPreviewContainer.Visible ? SpriteEffects.None : SpriteEffects.FlipHorizontally); } return true; } @@ -592,24 +593,28 @@ namespace Barotrauma Visible = false, OnClicked = (btn, userdata) => { - serverPreview.RectTransform.RelativeSize = new Vector2(0.2f, 1.0f); - serverPreview.Visible = !serverPreview.Visible; - serverPreview.IgnoreLayoutGroups = !serverPreview.Visible; + serverPreviewContainer.RectTransform.RelativeSize = new Vector2(0.2f, 1.0f); + serverPreviewContainer.Visible = !serverPreviewContainer.Visible; + serverPreviewContainer.IgnoreLayoutGroups = !serverPreviewContainer.Visible; RecalculateHolder(); - btn.Children.ForEach(c => c.SpriteEffects = serverPreview.Visible ? SpriteEffects.None : SpriteEffects.FlipHorizontally); + btn.Children.ForEach(c => c.SpriteEffects = serverPreviewContainer.Visible ? SpriteEffects.None : SpriteEffects.FlipHorizontally); return true; } }; - serverPreview = new GUIFrame(new RectTransform(new Vector2(sidebarWidth, 1.0f), serverListHolder.RectTransform, Anchor.Center), style: null) + serverPreviewContainer = new GUIFrame(new RectTransform(new Vector2(sidebarWidth, 1.0f), serverListHolder.RectTransform, Anchor.Center), style: null) { Color = new Color(12, 14, 15, 255) * 0.5f, OutlineColor = Color.Black, IgnoreLayoutGroups = true, Visible = false }; + serverPreview = new GUIListBox(new RectTransform(Vector2.One, serverPreviewContainer.RectTransform, Anchor.Center)) + { + Padding = Vector4.One * 10 * GUI.Scale + }; // Spacing new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), bottomRow.RectTransform), style: null); @@ -1697,7 +1702,7 @@ namespace Barotrauma UpdateFriendsList(); serverList.ClearChildren(); - serverPreview.ClearChildren(); + serverPreview.Content.ClearChildren(); joinButton.Enabled = false; selectedServer = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs index e40a03ece..350ff7ac5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs @@ -1604,7 +1604,7 @@ namespace Barotrauma if (string.IsNullOrEmpty(file) || !File.Exists(file)) { continue; } string modFolder = Path.GetDirectoryName(itemContentPackage.Path); - string filePathRelativeToModFolder = UpdaterUtil.GetRelativePath(file, Path.Combine(Environment.CurrentDirectory, modFolder)); + string filePathRelativeToModFolder = Path.GetRelativePath(Path.Combine(Environment.CurrentDirectory, modFolder), file); //file is not inside the mod folder, we need to move it if (filePathRelativeToModFolder.StartsWith("..") || diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs new file mode 100644 index 000000000..0deb694c4 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs @@ -0,0 +1,125 @@ +#nullable enable +using System; +using System.Linq; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +/* + * This screen only exists because I'm going mental without access to EnC on Linux. + * This is fucking stupid and horrible. + * Remember to remove this crap eventually. + * - Markus + */ +namespace Barotrauma +{ + class TestScreen : EditorScreen + { + public override Camera Cam { get; } + + private Item? miniMapItem; + + private Submarine? submarine; + private Character? dummyCharacter; + public static Effect BlueprintEffect; + + public TestScreen() + { + Cam = new Camera(); + BlueprintEffect = GameMain.GameScreen.BlueprintEffect; + + new GUIButton(new RectTransform(new Point(256, 256), Frame.RectTransform), "Reload shader") + { + OnClicked = (button, o) => + { + BlueprintEffect.Dispose(); + GameMain.Instance.Content.Unload(); + BlueprintEffect = GameMain.Instance.Content.Load("Effects/blueprintshader_opengl"); + GameMain.GameScreen.BlueprintEffect = BlueprintEffect; + return true; + } + }; + } + + public override void Select() + { + base.Select(); + + if (dummyCharacter is { Removed: false }) + { + dummyCharacter?.Remove(); + } + + // ???????? + submarine = new Submarine(SubmarineInfo.SavedSubmarines.FirstOrDefault(info => info.Name.Equals("Kastrull", StringComparison.OrdinalIgnoreCase))); + miniMapItem = new Item(ItemPrefab.Find(null, "statusmonitor"), Vector2.Zero, submarine); + MiniMap miniMap = miniMapItem.GetComponent(); + miniMap.PowerConsumption = 0; + + dummyCharacter = Character.Create(CharacterPrefab.HumanSpeciesName, Vector2.Zero, "", id: Entity.DummyID, hasAi: false); + dummyCharacter.Info.Name = "Galldren"; + dummyCharacter.Inventory.CreateSlots(); + + Character.Controlled = dummyCharacter; + GameMain.World.ProcessChanges(); + } + + public override void AddToGUIUpdateList() + { + Frame.AddToGUIUpdateList(); + CharacterHUD.AddToGUIUpdateList(dummyCharacter); + dummyCharacter?.SelectedConstruction?.AddToGUIUpdateList(); + } + + public override void Update(double deltaTime) + { + base.Update(deltaTime); + + if (dummyCharacter is { } dummy && miniMapItem is { } item) + { + if (dummy.SelectedConstruction != item) + { + dummy.SelectedConstruction = item; + } + dummy.SelectedConstruction?.UpdateHUD(Cam, dummy, (float)deltaTime); + Vector2 pos = FarseerPhysics.ConvertUnits.ToSimUnits(item.Position); + foreach (Limb limb in dummy.AnimController.Limbs) + { + limb.body.SetTransform(pos, 0.0f); + } + + if (dummy.AnimController?.Collider is { } collider) + { + collider.SetTransform(pos, 0); + } + + dummy.ControlLocalPlayer((float)deltaTime, Cam, false); + dummy.Control((float)deltaTime, Cam); + dummy.Submarine = submarine; + } + } + + public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) + { + base.Draw(deltaTime, graphics, spriteBatch); + graphics.Clear(BackgroundColor); + + spriteBatch.Begin(SpriteSortMode.BackToFront, transformMatrix: Cam.Transform); + miniMapItem?.Draw(spriteBatch, false); + if (dummyCharacter is { } dummy) + { + dummyCharacter.DrawFront(spriteBatch, Cam); + dummyCharacter.Draw(spriteBatch, Cam); + } + spriteBatch.End(); + + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState); + + GUI.Draw(Cam, spriteBatch); + + dummyCharacter?.DrawHUD(spriteBatch, Cam, false); + + spriteBatch.End(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index de7051846..f6e679850 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -38,6 +38,7 @@ namespace Barotrauma public readonly string File; public readonly string Type; public readonly bool DuckVolume; + public readonly float Volume; public readonly Vector2 IntensityRange; @@ -52,6 +53,7 @@ namespace Barotrauma this.Type = element.GetAttributeString("type", "").ToLowerInvariant(); this.IntensityRange = element.GetAttributeVector2("intensityrange", new Vector2(0.0f, 100.0f)); this.DuckVolume = element.GetAttributeBool("duckvolume", false); + this.Volume = element.GetAttributeFloat("volume", 1.0f); this.ContinueFromPreviousTime = element.GetAttributeBool("continuefromprevioustime", false); this.Element = element; } @@ -816,6 +818,8 @@ namespace Barotrauma } } + int noiseLoopIndex = 1; + updateMusicTimer -= deltaTime; if (updateMusicTimer <= 0.0f) { @@ -851,7 +855,6 @@ namespace Barotrauma } } - int noiseLoopIndex = 1; if (Level.Loaded?.Type == LevelData.LevelType.LocationConnection) { // Find background noise loop for the current biome @@ -917,7 +920,7 @@ namespace Barotrauma { //mute the channel musicChannel[i].Gain = MathHelper.Lerp(musicChannel[i].Gain, 0.0f, MusicLerpSpeed * deltaTime); - if (musicChannel[i].Gain < 0.01f) DisposeMusicChannel(i); + if (musicChannel[i].Gain < 0.01f) { DisposeMusicChannel(i); } } } //something should be playing, but the targetMusic is invalid @@ -932,7 +935,7 @@ namespace Barotrauma if (musicChannel[i] != null && musicChannel[i].IsPlaying) { musicChannel[i].Gain = MathHelper.Lerp(musicChannel[i].Gain, 0.0f, MusicLerpSpeed * deltaTime); - if (musicChannel[i].Gain < 0.01f) DisposeMusicChannel(i); + if (musicChannel[i].Gain < 0.01f) { DisposeMusicChannel(i); } } //channel free now, start playing the correct clip if (currentMusic[i] == null || (musicChannel[i] == null || !musicChannel[i].IsPlaying)) @@ -949,7 +952,7 @@ namespace Barotrauma targetMusic[i] = null; break; } - musicChannel[i] = currentMusic[i].Play(0.0f, "music"); + musicChannel[i] = currentMusic[i].Play(0.0f, i == noiseLoopIndex ? "" : "music"); if (targetMusic[i].ContinueFromPreviousTime) { musicChannel[i].StreamSeekPos = targetMusic[i].PreviousTime; @@ -963,13 +966,13 @@ namespace Barotrauma if (musicChannel[i] == null || !musicChannel[i].IsPlaying) { musicChannel[i]?.Dispose(); - musicChannel[i] = currentMusic[i].Play(0.0f, "music"); + musicChannel[i] = currentMusic[i].Play(0.0f, i == noiseLoopIndex ? "" : "music"); musicChannel[i].Looping = true; } - float targetGain = 1.0f; + float targetGain = targetMusic[i].Volume; if (targetMusic[i].DuckVolume) { - targetGain = (float)Math.Sqrt(1.0f / activeTrackCount); + targetGain *= (float)Math.Sqrt(1.0f / activeTrackCount); } musicChannel[i].Gain = MathHelper.Lerp(musicChannel[i].Gain, targetGain, MusicLerpSpeed * deltaTime); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs index d5847be30..327dac75a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs @@ -1,9 +1,10 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Text; -using System.Text.RegularExpressions; +using Color = Microsoft.Xna.Framework.Color; namespace Barotrauma { @@ -54,6 +55,225 @@ namespace Barotrauma return isInside; } + + public static Vector2 GetPolygonBoundingBoxSize(List verticess) + { + float minX = verticess[0].X; + float maxX = verticess[0].X; + float minY = verticess[0].Y; + float maxY = verticess[0].Y; + + foreach (var (vertX, vertY) in verticess) + { + minX = Math.Min(vertX, minX); + maxX = Math.Max(vertX, maxX); + minY = Math.Min(vertY, minY); + maxY = Math.Max(vertY, maxY); + } + + return new Vector2(maxX - minX, maxY - minY); + } + + public static List ScalePolygon(List vertices, Vector2 scale) + { + List newVertices = new List(); + + Vector2 center = GetPolygonCentroid(vertices); + + foreach (Vector2 vert in vertices) + { + Vector2 centerVector = vert - center; + Vector2 centerVectorScale = centerVector * scale; + Vector2 scaledVector = centerVectorScale + center; + newVertices.Add(scaledVector); + } + + return newVertices; + } + + public static Vector2 GetPolygonCentroid(List poly) + { + float accumulatedArea = 0.0f; + float centerX = 0.0f; + float centerY = 0.0f; + + for (int i = 0, j = poly.Count - 1; i < poly.Count; j = i++) + { + float temp = poly[i].X * poly[j].Y - poly[j].X * poly[i].Y; + accumulatedArea += temp; + centerX += (poly[i].X + poly[j].X) * temp; + centerY += (poly[i].Y + poly[j].Y) * temp; + } + + if (Math.Abs(accumulatedArea) < 1E-7f) { return Vector2.Zero; } // Avoid division by zero + + accumulatedArea *= 3f; + return new Vector2(centerX / accumulatedArea, centerY / accumulatedArea); + } + + public static List SnapVertices(List points, int treshold = 1) + { + Stack toCheck = new Stack(); + List newPoints = new List(); + + foreach (Vector2 point in points) + { + toCheck.Push(point); + } + + while (toCheck.TryPop(out Vector2 point)) + { + Vector2 newPoint = new Vector2(point.X, point.Y); + foreach (Vector2 otherPoint in toCheck.Concat(newPoints)) + { + float diffX = Math.Abs(newPoint.X - otherPoint.X), + diffY = Math.Abs(newPoint.Y - otherPoint.Y); + + if (diffX <= treshold) + { + newPoint.X = Math.Max(newPoint.X, otherPoint.X); + } + + if (diffY <= treshold) + { + newPoint.Y = Math.Max(newPoint.Y, otherPoint.Y); + } + } + newPoints.Add(newPoint); + } + + return newPoints; + } + + public static ImmutableArray SnapRectangles(IEnumerable rects, int treshold = 1) + { + List list = new List(); + + List points = new List(); + + foreach (RectangleF rect in rects) + { + points.Add(new Vector2(rect.Left, rect.Top)); + points.Add(new Vector2(rect.Right, rect.Top)); + points.Add(new Vector2(rect.Right, rect.Bottom)); + points.Add(new Vector2(rect.Left, rect.Bottom)); + } + + points = SnapVertices(points, treshold); + + for (int i = 0; i < points.Count; i += 4) + { + Vector2 topLeft = points[i]; + Vector2 bottomRight = points[i + 2]; + + list.Add(new RectangleF(topLeft, bottomRight - topLeft)); + } + + return list.ToImmutableArray(); + } + + public static List> CombineRectanglesIntoShape(IEnumerable rectangles) + { + List points = + (from point in rectangles.SelectMany(RectangleToPoints) + group point by point + into g + where g.Count() % 2 == 1 + select g.Key) + .ToList(); + + List sortedY = points.OrderBy(p => p.Y).ThenByDescending(p => p.X).ToList(); + List sortedX = points.OrderBy(p => p.X).ThenByDescending(p => p.Y).ToList(); + + Dictionary edgesH = new Dictionary(); + Dictionary edgesV = new Dictionary(); + + int i = 0; + while (i < points.Count) + { + float currY = sortedY[i].Y; + + while (i < points.Count && Math.Abs(sortedY[i].Y - currY) < 0.01f) + { + edgesH[sortedY[i]] = sortedY[i + 1]; + edgesH[sortedY[i + 1]] = sortedY[i]; + i += 2; + } + + } + + i = 0; + + while (i < points.Count) + { + float currX = sortedX[i].X; + while (i < points.Count && Math.Abs(sortedX[i].X - currX) < 0.01f) + { + edgesV[sortedX[i]] = sortedX[i + 1]; + edgesV[sortedX[i + 1]] = sortedX[i]; + i += 2; + } + } + + List> polygons = new List>(); + + while (edgesH.Any()) + { + var (key, _) = edgesH.First(); + List<(Vector2 Point, int Direction)> polygon = new List<(Vector2 Point, int Direction)> { (key, 0) }; + edgesH.Remove(key); + + while (true) + { + var (curr, direction) = polygon[^1]; + + if (direction == 0) + { + Vector2 nextVertex = edgesV[curr]; + edgesV.Remove(curr); + polygon.Add((nextVertex, 1)); + } + else + { + Vector2 nextVertex = edgesH[curr]; + edgesH.Remove(curr); + polygon.Add((nextVertex, 0)); + } + + if (polygon[^1] == polygon[0]) + { + polygon.Remove(polygon[^1]); + break; + } + } + + List poly = polygon.Select(t => t.Point).ToList(); + + foreach (Vector2 vertex in poly) + { + if (edgesH.ContainsKey(vertex)) + { + edgesH.Remove(vertex); + } + + if (edgesV.ContainsKey(vertex)) + { + edgesV.Remove(vertex); + } + } + + polygons.Add(poly); + } + + return polygons; + + static IEnumerable RectangleToPoints(RectangleF rect) + { + (float x1, float y1, float x2, float y2) = (rect.Left, rect.Top, rect.Right, rect.Bottom); + Vector2[] pts = { new Vector2(x1, y1), new Vector2(x2, y1), new Vector2(x2, y2), new Vector2(x1, y2) }; + return pts; + } + } // Convert an RGB value into an HLS value. public static Vector3 RgbToHLS(this Color color) diff --git a/Barotrauma/BarotraumaClient/Content/Effects/blueprintshader.xnb b/Barotrauma/BarotraumaClient/Content/Effects/blueprintshader.xnb new file mode 100644 index 000000000..8ef444671 Binary files /dev/null and b/Barotrauma/BarotraumaClient/Content/Effects/blueprintshader.xnb differ diff --git a/Barotrauma/BarotraumaClient/Content/Effects/blueprintshader_opengl.xnb b/Barotrauma/BarotraumaClient/Content/Effects/blueprintshader_opengl.xnb new file mode 100644 index 000000000..608cdb04c Binary files /dev/null and b/Barotrauma/BarotraumaClient/Content/Effects/blueprintshader_opengl.xnb differ diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 62c0cd40a..dac5130ed 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.14.9.0 + 0.1500.0.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index ac537775c..6d8757d5b 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.14.9.0 + 0.1500.0.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/Shaders/Content.mgcb b/Barotrauma/BarotraumaClient/Shaders/Content.mgcb index 901a2171c..e88794f28 100644 --- a/Barotrauma/BarotraumaClient/Shaders/Content.mgcb +++ b/Barotrauma/BarotraumaClient/Shaders/Content.mgcb @@ -67,3 +67,9 @@ /processorParam:DebugMode=Auto /build:grainshader.fx +#begin blueprintshader.fx +/importer:EffectImporter +/processor:EffectProcessor +/processorParam:DebugMode=Auto +/build:blueprintshader.fx + diff --git a/Barotrauma/BarotraumaClient/Shaders/Content_opengl.mgcb b/Barotrauma/BarotraumaClient/Shaders/Content_opengl.mgcb index cb119ed14..16d516848 100644 --- a/Barotrauma/BarotraumaClient/Shaders/Content_opengl.mgcb +++ b/Barotrauma/BarotraumaClient/Shaders/Content_opengl.mgcb @@ -67,3 +67,8 @@ /processorParam:DebugMode=Auto /build:grainshader_opengl.fx +#begin blueprintshader_opengl.fx +/importer:EffectImporter +/processor:EffectProcessor +/processorParam:DebugMode=Auto +/build:blueprintshader_opengl.fx diff --git a/Barotrauma/BarotraumaClient/Shaders/blueprintshader.fx b/Barotrauma/BarotraumaClient/Shaders/blueprintshader.fx new file mode 100644 index 000000000..87048562e --- /dev/null +++ b/Barotrauma/BarotraumaClient/Shaders/blueprintshader.fx @@ -0,0 +1,48 @@ +// vim:ft=hlsl +sampler TextureSampler : register(s0); + +float width; +float height; + +float3 sobel(float2 uv) +{ + float x = 0; + float y = 0; + + float w = 1.0 / width; + float h = 1.0 / height; + + x += tex2D(TextureSampler, uv + float2(-w, -h)) * -1.0; + x += tex2D(TextureSampler, uv + float2(-w, 0)) * -2.0; + x += tex2D(TextureSampler, uv + float2(-w, h)) * -1.0; + + x += tex2D(TextureSampler, uv + float2( w, -h)) * 1.0; + x += tex2D(TextureSampler, uv + float2( w, 0)) * 2.0; + x += tex2D(TextureSampler, uv + float2( w, h)) * 1.0; + + y += tex2D(TextureSampler, uv + float2(-w, -h)) * -1.0; + y += tex2D(TextureSampler, uv + float2( 0, -h)) * -2.0; + y += tex2D(TextureSampler, uv + float2( w, -h)) * -1.0; + + y += tex2D(TextureSampler, uv + float2(-w, h)) * 1.0; + y += tex2D(TextureSampler, uv + float2( 0, h)) * 2.0; + y += tex2D(TextureSampler, uv + float2( w, h)) * 1.0; + + return sqrt(x * x + y * y); +} + +float4 blueprint(float4 position : SV_POSITION, float4 clr : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +{ + float3 s = sobel(texCoord); + float a = tex2D(TextureSampler, texCoord).a; + a *= clr.a; + return float4(clr.r + s.r, clr.g + s.g, clr.b + s.b, a); +} + +technique Blueprint +{ + pass Pass1 + { + PixelShader = compile ps_4_0_level_9_1 blueprint(); + } +} diff --git a/Barotrauma/BarotraumaClient/Shaders/blueprintshader_opengl.fx b/Barotrauma/BarotraumaClient/Shaders/blueprintshader_opengl.fx new file mode 100644 index 000000000..4af0d4b36 --- /dev/null +++ b/Barotrauma/BarotraumaClient/Shaders/blueprintshader_opengl.fx @@ -0,0 +1,48 @@ +// vim:ft=hlsl +sampler TextureSampler : register(s0); + +float width; +float height; + +float3 sobel(float2 uv) +{ + float x = 0; + float y = 0; + + float w = 1.0 / width; + float h = 1.0 / height; + + x += tex2D(TextureSampler, uv + float2(-w, -h)) * -1.0; + x += tex2D(TextureSampler, uv + float2(-w, 0)) * -2.0; + x += tex2D(TextureSampler, uv + float2(-w, h)) * -1.0; + + x += tex2D(TextureSampler, uv + float2( w, -h)) * 1.0; + x += tex2D(TextureSampler, uv + float2( w, 0)) * 2.0; + x += tex2D(TextureSampler, uv + float2( w, h)) * 1.0; + + y += tex2D(TextureSampler, uv + float2(-w, -h)) * -1.0; + y += tex2D(TextureSampler, uv + float2( 0, -h)) * -2.0; + y += tex2D(TextureSampler, uv + float2( w, -h)) * -1.0; + + y += tex2D(TextureSampler, uv + float2(-w, h)) * 1.0; + y += tex2D(TextureSampler, uv + float2( 0, h)) * 2.0; + y += tex2D(TextureSampler, uv + float2( w, h)) * 1.0; + + return sqrt(x * x + y * y); +} + +float4 blueprint(float4 position : SV_POSITION, float4 clr : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +{ + float3 s = sobel(texCoord); + float a = tex2D(TextureSampler, texCoord).a; + a *= clr.a; + return float4(clr.r + s.r, clr.g + s.g, clr.b + s.b, a); +} + +technique Blueprint +{ + pass Pass1 + { + PixelShader = compile ps_3_0 blueprint(); + } +} diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index c597f6633..bc67e5f17 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.14.9.0 + 0.1500.0.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 4c90c5c2e..d563f8dfa 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.14.9.0 + 0.1500.0.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 403968026..79640545b 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.14.9.0 + 0.1500.0.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs index acf0f00de..f9f51b713 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs @@ -46,5 +46,10 @@ namespace Barotrauma } } } + + partial void OnMoneyChanged(int prevAmount, int newAmount) + { + GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.UpdateMoney }); + } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs index f4d7baa52..148bfeda3 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs @@ -2,6 +2,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Linq; namespace Barotrauma { @@ -22,6 +23,14 @@ namespace Barotrauma } } + partial void OnExperienceChanged(int prevAmount, int newAmount, Vector2 textPopupPos) + { + if (Math.Abs(prevAmount - newAmount) > 0) + { + GameMain.NetworkMember.CreateEntityEvent(Character, new object[] { NetEntityEvent.Type.UpdateExperience }); + } + } + public void ServerWrite(IWriteMessage msg) { msg.Write(ID); @@ -53,6 +62,17 @@ namespace Barotrauma msg.Write((byte)0); } // TODO: animations + msg.Write((byte)savedStatValues.SelectMany(s => s.Value).Count()); + foreach (var savedStatValuePair in savedStatValues) + { + foreach (var savedStatValue in savedStatValuePair.Value) + { + msg.Write((byte)savedStatValuePair.Key); + msg.Write(savedStatValue.StatIdentifier); + msg.Write(savedStatValue.StatValue); + msg.Write(savedStatValue.RemoveOnDeath); + } + } } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index bcd25ddf9..f5c86cae2 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -238,7 +238,7 @@ namespace Barotrauma break; case ClientNetObject.ENTITY_STATE: - int eventType = msg.ReadRangedInteger(0, 3); + int eventType = msg.ReadRangedInteger(0, 4); switch (eventType) { case 0: @@ -268,8 +268,35 @@ namespace Barotrauma if (IsIncapacitated) { var causeOfDeath = CharacterHealth.GetCauseOfDeath(); - Kill(causeOfDeath.First, causeOfDeath.Second); + Kill(causeOfDeath.type, causeOfDeath.affliction); } + break; + case 3: // NetEntityEvent.Type.UpdateTalents + if (c.Character != this) + { +#if DEBUG + DebugConsole.Log("Received a character update message from a client who's not controlling the character"); +#endif + return; + } + + // get the full list of talents from the player, only give the ones + // that are not already given (or otherwise not viable) + ushort talentCount = msg.ReadUInt16(); + List talentSelection = new List(); + for (int i = 0; i < talentCount; i++) + { + UInt32 talentIdentifier = msg.ReadUInt32(); + var prefab = TalentPrefab.TalentPrefabs.Find(p => p.UIntIdentifier == talentIdentifier); + if (prefab != null) { talentSelection.Add(prefab.Identifier); } + } + talentSelection = TalentTree.CheckTalentSelection(this, talentSelection); + + foreach (string talent in talentSelection) + { + GiveTalent(talent); + } + break; } break; @@ -283,7 +310,7 @@ namespace Barotrauma if (extraData != null) { - const int min = 0, max = 9; + const int min = 0, max = 12; switch ((NetEntityEvent.Type)extraData[0]) { case NetEntityEvent.Type.InventoryState: @@ -394,6 +421,22 @@ namespace Barotrauma msg.Write(inventoryItemIDs[i]); } break; + case NetEntityEvent.Type.UpdateExperience: + msg.WriteRangedInteger(10, min, max); + msg.Write(Info.ExperiencePoints); + break; + case NetEntityEvent.Type.UpdateTalents: + msg.WriteRangedInteger(11, min, max); + msg.Write((ushort)characterTalents.Count); + foreach (var unlockedTalent in characterTalents) + { + msg.Write(unlockedTalent.Prefab.UIntIdentifier); + } + break; + case NetEntityEvent.Type.UpdateMoney: + msg.WriteRangedInteger(12, min, max); + msg.Write(GameMain.GameSession.Campaign.Money); + break; default: DebugConsole.ThrowError("Invalid NetworkEvent type for entity " + ToString() + " (" + (NetEntityEvent.Type)extraData[0] + ")"); break; @@ -499,7 +542,7 @@ namespace Barotrauma if (writeStatus) { WriteStatus(tempBuffer); - (AIController as EnemyAIController)?.PetBehavior?.ServerWrite(tempBuffer); + AIController?.ServerWrite(tempBuffer); HealthUpdatePending = false; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index 4e69294a8..9401b8cfb 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -1187,6 +1187,7 @@ namespace Barotrauma NewMessage("*****************", Color.Lime); GameServer.Log("Console command \"restart\" executed: closing the server...", ServerLog.MessageType.ServerMessage); GameMain.Instance.CloseServer(); + GameMain.Instance.TryStartChildServerRelay(); GameMain.Instance.StartServer(); })); diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index 95b3ee358..ddaefc903 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -18,7 +18,17 @@ namespace Barotrauma { public static readonly Version Version = Assembly.GetEntryAssembly().GetName().Version; - public static World World; + + private static World world; + public static World World + { + get + { + if (world == null) { world = new World(new Vector2(0, -9.82f)); } + return world; + } + set { world = value; } + } public static GameSettings Config; public static GameServer Server; @@ -123,6 +133,8 @@ namespace Barotrauma ItemAssemblyPrefab.LoadAll(); LevelObjectPrefab.LoadAll(); BallastFloraPrefab.LoadAll(GetFilesOfType(ContentType.MapCreature)); + TalentPrefab.LoadAll(GetFilesOfType(ContentType.Talents)); + TalentTree.LoadAll(GetFilesOfType(ContentType.TalentTrees)); GameModePreset.Init(); DecalManager = new DecalManager(); @@ -179,6 +191,20 @@ namespace Barotrauma } } + public bool TryStartChildServerRelay() + { + for (int i = 0; i < CommandLineArgs.Length; i++) + { + switch (CommandLineArgs[i].Trim()) + { + case "-pipes": + ChildServerRelay.Start(CommandLineArgs[i + 2], CommandLineArgs[i + 1]); + return true; + } + } + return false; + } + public void StartServer() { string name = "Server"; @@ -264,7 +290,7 @@ namespace Barotrauma i++; break; case "-pipes": - ChildServerRelay.Start(CommandLineArgs[i + 2], CommandLineArgs[i + 1]); + //handled in TryStartChildServerRelay i += 2; break; } @@ -323,6 +349,7 @@ namespace Barotrauma Hyper.ComponentModel.HyperTypeDescriptionProvider.Add(typeof(Items.Components.ItemComponent)); Hyper.ComponentModel.HyperTypeDescriptionProvider.Add(typeof(Hull)); + TryStartChildServerRelay(); Init(); StartServer(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs index 94152023e..21c9f7c8e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs @@ -56,6 +56,6 @@ namespace Barotrauma public void ApplyOrderData(Character character) { CharacterInfo.ApplyOrderData(character, OrderData); - } + } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs index 7b29551ae..472cf14b9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs @@ -16,6 +16,12 @@ namespace Barotrauma.Items.Components set { unsentChanges = value; } } + protected override void RemoveComponentSpecific() + { + base.RemoveComponentSpecific(); + pathFinder = null; + } + public void ServerRead(ClientNetObject type, IReadMessage msg, Barotrauma.Networking.Client c) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs index ce354ecaf..c393668e2 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -289,6 +289,14 @@ namespace Barotrauma teamID = (byte)wifiComponent.TeamID; break; } + if (teamID == 0) + { + foreach (IdCard idCard in GetComponents()) + { + teamID = (byte)idCard.TeamID; + break; + } + } msg.Write(teamID); bool tagsChanged = tags.Count != prefab.Tags.Count || !tags.All(t => prefab.Tags.Contains(t)); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChildServerRelay.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChildServerRelay.cs index 6226d0b2c..584342950 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChildServerRelay.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChildServerRelay.cs @@ -1,12 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using Barotrauma.IO; -using System.IO.Pipes; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Win32.SafeHandles; +using System.IO.Pipes; namespace Barotrauma.Networking { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 232ecf417..2df0be84f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -2354,6 +2354,8 @@ namespace Barotrauma.Networking characterData.ApplyHealthData(spawnedCharacter); characterData.ApplyOrderData(spawnedCharacter); spawnedCharacter.GiveIdCardTags(mainSubWaypoints[i]); + spawnedCharacter.LoadTalents(); + characterData.HasSpawned = true; } spawnedCharacter.OwnerClientEndPoint = teamClients[i].Connection.EndPointString; @@ -2366,6 +2368,8 @@ namespace Barotrauma.Networking spawnedCharacter.TeamID = teamID; spawnedCharacter.GiveJobItems(mainSubWaypoints[i]); spawnedCharacter.GiveIdCardTags(mainSubWaypoints[i]); + // talents are only avilable for players in online sessions, but modders or someone else might want to have them loaded anyway + spawnedCharacter.LoadTalents(); } } @@ -2431,6 +2435,7 @@ namespace Barotrauma.Networking roundStartTime = DateTime.Now; + startGameCoroutine = null; yield return CoroutineStatus.Success; } @@ -2619,8 +2624,8 @@ namespace Barotrauma.Networking } } - Submarine.Unload(); entityEventManager.Clear(); + Submarine.Unload(); GameMain.NetLobbyScreen.Select(); Log("Round ended.", ServerLog.MessageType.ServerMessage); @@ -3145,28 +3150,19 @@ namespace Barotrauma.Networking public void SendOrderChatMessage(OrderChatMessage message) { if (message.Sender == null || message.Sender.SpeechImpediment >= 100.0f) { return; } - //ChatMessageType messageType = ChatMessage.CanUseRadio(message.Sender) ? ChatMessageType.Radio : ChatMessageType.Default; - //check which clients can receive the message and apply distance effects foreach (Client client in ConnectedClients) { - string modifiedMessage = message.Text; - - if (message.Sender != null && - client.Character != null && !client.Character.IsDead) + if (message.Sender != null && client.Character != null && !client.Character.IsDead) { //too far to hear the msg -> don't send if (!client.Character.CanHearCharacter(message.Sender)) { continue; } } - SendDirectChatMessage(new OrderChatMessage(message.Order, message.OrderOption, message.OrderPriority, message.TargetEntity, message.TargetCharacter, message.Sender), client); } - - string myReceivedMessage = message.Text; - - if (!string.IsNullOrWhiteSpace(myReceivedMessage)) + if (!string.IsNullOrWhiteSpace(message.Text)) { - AddChatMessage(new OrderChatMessage(message.Order, message.OrderOption, message.OrderPriority, myReceivedMessage, message.TargetEntity, message.TargetCharacter, message.Sender)); + AddChatMessage(new OrderChatMessage(message.Order, message.OrderOption, message.OrderPriority, message.Text, message.TargetEntity, message.TargetCharacter, message.Sender)); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs index dd851251d..95aa0e83f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs @@ -44,11 +44,11 @@ namespace Barotrauma.Networking class ServerEntityEventManager : NetEntityEventManager { - private List events; + private readonly List events; //list of unique events (i.e. !IsDuplicate) created during the round //used for syncing clients who join mid-round - private List uniqueEvents; + private readonly List uniqueEvents; private UInt16 lastSentToAll; private UInt16 lastSentToAnyone; @@ -90,11 +90,11 @@ namespace Barotrauma.Networking } } - private List bufferedEvents; + private readonly List bufferedEvents; private UInt16 ID; - private GameServer server; + private readonly GameServer server; private double lastEventCountHighWarning; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index 7f97a81d9..d162db6f0 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -284,6 +284,8 @@ namespace Barotrauma.Networking partial void RespawnCharactersProjSpecific(Vector2? shuttlePos) { + respawnedCharacters.Clear(); + var respawnSub = RespawnShuttle ?? Submarine.MainSub; MultiPlayerCampaign campaign = GameMain.GameSession.GameMode as MultiPlayerCampaign; @@ -300,7 +302,7 @@ namespace Barotrauma.Networking if (matchingData != null && !matchingData.HasSpawned) { c.CharacterInfo = matchingData.CharacterInfo; - } + } //all characters are in Team 1 in game modes/missions with only one team. //if at some point we add a game mode with multiple teams where respawning is possible, this needs to be reworked @@ -355,8 +357,21 @@ namespace Barotrauma.Networking characterInfos[i].ClearCurrentOrders(); - var character = Character.Create(characterInfos[i], shuttleSpawnPoints[i].WorldPosition, characterInfos[i].Name, isRemotePlayer: !bot, hasAi: bot); + bool forceSpawnInMainSub = false; + if (!bot && campaign != null) + { + var matchingData = campaign?.GetClientCharacterData(clients[i]); + if (matchingData != null && !matchingData.HasSpawned) + { + forceSpawnInMainSub = true; + } + } + + var character = Character.Create(characterInfos[i], (forceSpawnInMainSub ? mainSubSpawnPoints[i] : shuttleSpawnPoints[i]).WorldPosition, characterInfos[i].Name, isRemotePlayer: !bot, hasAi: bot); character.TeamID = CharacterTeamType.Team1; + character.LoadTalents(); + + respawnedCharacters.Add(character); if (bot) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFindItem.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFindItem.cs index 582a853fe..a95f200bc 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFindItem.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFindItem.cs @@ -185,7 +185,10 @@ namespace Barotrauma { existingItems.Add(item); } - Entity.Spawner.AddToSpawnQueue(targetPrefab, targetContainer.OwnInventory); + Entity.Spawner.AddToSpawnQueue(targetPrefab, targetContainer.OwnInventory, null, item => + { + item.AddTag("traitormissionitem"); + }); target = null; } else if (allowExisting) diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index d5b2ff207..879a52022 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.14.9.0 + 0.1500.0.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml index 19f21b7a6..f5c2f7d19 100644 --- a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml +++ b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml @@ -63,6 +63,11 @@ + + + + + @@ -147,6 +152,12 @@ + + + + + + @@ -261,4 +272,11 @@ + + + + + + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs index 4e3f6b91e..168edfa9e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs @@ -1,7 +1,8 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Items.Components; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using Barotrauma.Items.Components; using System.Linq; namespace Barotrauma @@ -328,5 +329,8 @@ namespace Barotrauma protected virtual void OnStateChanged(AIState from, AIState to) { } protected virtual void OnTargetChanged(AITarget previousTarget, AITarget newTarget) { } + + public virtual void ClientRead(IReadMessage msg) { } + public virtual void ServerWrite(IWriteMessage msg) { } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index ba65f0c2c..6a86228e6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -1,5 +1,6 @@ using Barotrauma.Extensions; using Barotrauma.Items.Components; +using Barotrauma.Networking; using FarseerPhysics; using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; @@ -37,6 +38,12 @@ namespace Barotrauma PreviousState = _state; OnStateChanged(_state, value); _state = value; + if (_state == AIState.Attack) + { +#if CLIENT + Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3); +#endif + } } } @@ -50,6 +57,8 @@ namespace Barotrauma private readonly float updateTargetsInterval = 1; private readonly float updateMemoriesInverval = 1; private readonly float attackLimbResetInterval = 2; + // Min priority for the memorized targets. The actual value fades gradually, unless kept fresh by selecting the target. + private const float minPriority = 10; private readonly float avoidLookAheadDistance; @@ -394,7 +403,7 @@ namespace Barotrauma public void SelectTarget(AITarget target, float priority) { SelectedAiTarget = target; - selectedTargetMemory = GetTargetMemory(target, true); + selectedTargetMemory = GetTargetMemory(target, addIfNotFound: true); selectedTargetMemory.Priority = priority; ignoredTargets.Remove(target); } @@ -641,7 +650,7 @@ namespace Barotrauma { Character c = a.Character; if (c.IsDead || c.Removed) { return false; } - if (!IsFriendly(Character, c)) { return true; } + if (!Character.IsFriendly(c)) { return true; } // Only apply the threshold to friendly characters return a.Damage >= selectedTargetingParams.DamageThreshold; } @@ -976,7 +985,7 @@ namespace Barotrauma Character owner = GetOwner(item); if (owner != null) { - if (IsFriendly(Character, owner)) + if (Character.IsFriendly(owner)) { ResetAITarget(); State = AIState.Idle; @@ -1337,7 +1346,7 @@ namespace Barotrauma } else { - canAttack = Character.CharacterList.All(c => c == Character || !IsFriendly(Character, c) || IsFarEnough(c)); + canAttack = Character.CharacterList.All(c => c == Character || !Character.IsFriendly(c) || IsFarEnough(c)); } if (canAttack) { @@ -1356,7 +1365,7 @@ namespace Barotrauma { hitTarget = limb.character; } - if (hitTarget != null && !hitTarget.IsDead && IsFriendly(Character, hitTarget)) + if (hitTarget != null && !hitTarget.IsDead && Character.IsFriendly(hitTarget)) { return true; } @@ -1764,7 +1773,7 @@ namespace Barotrauma Character.AnimController.ReleaseStuckLimbs(); LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 1); if (attacker == null || attacker.AiTarget == null || attacker.Removed || attacker.IsDead) { return; } - bool isFriendly = IsFriendly(Character, attacker); + bool isFriendly = Character.IsFriendly(attacker); if (wasLatched) { State = AIState.Escape; @@ -1840,7 +1849,7 @@ namespace Barotrauma } } - AITargetMemory targetMemory = GetTargetMemory(attacker.AiTarget, true); + AITargetMemory targetMemory = GetTargetMemory(attacker.AiTarget, addIfNotFound: true); targetMemory.Priority += GetRelativeDamage(attackResult.Damage, Character.Vitality) * AIParams.AggressionHurt; // Only allow to react once. Otherwise would attack the target with only a fraction of a cooldown @@ -1884,16 +1893,15 @@ namespace Barotrauma private bool UpdateLimbAttack(float deltaTime, Limb attackingLimb, Vector2 attackSimPos, float distance = -1, Limb targetLimb = null) { if (SelectedAiTarget?.Entity == null) { return false; } - - ActiveAttack = attackingLimb?.attack; - + if (attackingLimb?.attack == null) { return false; } + ActiveAttack = attackingLimb.attack; if (wallTarget != null) { // If the selected target is not the wall target, make the wall target the selected target. var aiTarget = wallTarget.Structure.AiTarget; if (aiTarget != null && SelectedAiTarget != aiTarget) { - SelectTarget(aiTarget, GetTargetMemory(SelectedAiTarget, true).Priority); + SelectTarget(aiTarget, GetTargetMemory(SelectedAiTarget, addIfNotFound: true).Priority); State = AIState.Attack; } } @@ -1902,23 +1910,35 @@ namespace Barotrauma { //simulate attack input to get the character to attack client-side Character.SetInput(InputType.Attack, true, true); -#if SERVER - GameMain.NetworkMember.CreateEntityEvent(Character, new object[] + if (!ActiveAttack.IsRunning) { +#if SERVER + GameMain.NetworkMember.CreateEntityEvent(Character, new object[] + { Networking.NetEntityEvent.Type.SetAttackTarget, attackingLimb, (damageTarget as Entity)?.ID ?? Entity.NullEntityID, damageTarget is Character character && targetLimb != null ? Array.IndexOf(character.AnimController.Limbs, targetLimb) : 0, SimPosition.X, SimPosition.Y - }); + }); +#else + Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3); #endif + } + if (attackingLimb.UpdateAttack(deltaTime, attackSimPos, damageTarget, out AttackResult attackResult, distance, targetLimb)) { if (damageTarget.Health > 0 && attackResult.Damage > 0) { // Managed to hit a living/non-destroyed target. Increase the priority more if the target is low in health -> dies easily/soon - selectedTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * AIParams.AggressionGreed; + float greed = AIParams.AggressionGreed; + if (!(damageTarget is Character)) + { + // Halve the greed for attacking non-characters. + greed /= 2; + } + selectedTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * greed; } else { @@ -2125,6 +2145,9 @@ namespace Barotrauma string targetingTag = null; if (targetCharacter != null) { + // ignore if target is tagged to be explicitly ignored (Feign Death) + if (targetCharacter.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI)) { continue; } + if (targetCharacter.IsDead) { targetingTag = "dead"; @@ -2139,7 +2162,7 @@ namespace Barotrauma } else { - if (IsFriendly(Character, targetCharacter)) + if (Character.IsFriendly(targetCharacter)) { continue; } @@ -2449,7 +2472,7 @@ namespace Barotrauma { if (otherCharacter == character) { continue; } if (otherCharacter.AIController?.SelectedAiTarget != aiTarget) { continue; } - if (!IsFriendly(character, otherCharacter)) { continue; } + if (!character.IsFriendly(otherCharacter)) { continue; } valueModifier /= 2; } } @@ -2469,7 +2492,7 @@ namespace Barotrauma // -> just ignore the distance and attack whatever has the highest priority dist = Math.Max(dist, 100.0f); - AITargetMemory targetMemory = GetTargetMemory(aiTarget, true); + AITargetMemory targetMemory = GetTargetMemory(aiTarget, addIfNotFound: true); if (Character.CurrentHull != null && Math.Abs(toTarget.Y) > Character.CurrentHull.Size.Y) { // Inside the sub, treat objects that are up or down, as they were farther away. @@ -2527,7 +2550,7 @@ namespace Barotrauma // Don't target items that we own. // This is a rare case, and almost entirely related to Humanhusks, so let's check it last to reduce unnecessary checks (although the check shouldn't be expensive) if (owner == character) { continue; } - if (owner != null && (IsFriendly(Character, owner) || owner.AiTarget != null && ignoredTargets.Contains(owner.AiTarget))) + if (owner != null && (Character.IsFriendly(owner) || owner.AiTarget != null && ignoredTargets.Contains(owner.AiTarget))) { continue; } @@ -2599,7 +2622,7 @@ namespace Barotrauma wall = wallTarget?.Structure; } // The target is not a wall or it's not the same as we are attached to -> release - bool releaseTarget = wall == null || (!wall.Bodies.Contains(LatchOntoAI.AttachJoints[0].BodyB) && wall.Submarine?.PhysicsBody?.FarseerBody != LatchOntoAI.AttachJoints[0].BodyB); + bool releaseTarget = wall?.Bodies == null || (!wall.Bodies.Contains(LatchOntoAI.AttachJoints[0].BodyB) && wall.Submarine?.PhysicsBody?.FarseerBody != LatchOntoAI.AttachJoints[0].BodyB); if (!releaseTarget) { for (int i = 0; i < wall.Sections.Length; i++) @@ -2847,10 +2870,15 @@ namespace Barotrauma { if (addIfNotFound) { - memory = new AITargetMemory(target, 10); + memory = new AITargetMemory(target, minPriority); targetMemories.Add(target, memory); } } + if (addIfNotFound) + { + // Keep the memory alive. + memory.Priority = Math.Max(memory.Priority, minPriority); + } return memory; } @@ -3014,7 +3042,7 @@ namespace Barotrauma { if (!onlyExisting && !tempParams.ContainsKey(tag)) { - if (AIParams.TryAddNewTarget(tag, state, priority ?? 100, out targetParams)) + if (AIParams.TryAddNewTarget(tag, state, priority ?? minPriority, out targetParams)) { tempParams.Add(tag, targetParams); } @@ -3051,6 +3079,7 @@ namespace Barotrauma ChangeParams(target.SpeciesName, state, priority); if (target.IsHuman) { + priority = GetTargetParams("human")?.Priority; // Target also items, because if we are blind and the target doesn't move, we can only perceive the target when it uses items if (state == AIState.Attack || state == AIState.Escape) { @@ -3061,20 +3090,19 @@ namespace Barotrauma { // If the target is shooting from the submarine, we might not perceive it because it doesn't move. // --> Target the submarine too. - if (target.Submarine != null && (canAttackDoors || canAttackWalls)) + if (target.Submarine != null && Character.Submarine == null && (canAttackDoors || canAttackWalls)) { - ChangeParams("room", state, priority); + ChangeParams("room", state, priority * 0.1f); if (canAttackWalls) { - ChangeParams("wall", state, priority); + ChangeParams("wall", state, priority * 0.1f); } if (canAttackDoors) { - ChangeParams("door", state, priority); + ChangeParams("door", state, priority * 0.1f); } } ChangeParams("provocative", state, priority, onlyExisting: true); - ChangeParams("light", state, priority, onlyExisting: true); } } } @@ -3306,7 +3334,17 @@ namespace Barotrauma return null; } - public static bool IsFriendly(Character me, Character other) => other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group); + public override void ServerWrite(IWriteMessage msg) + { + msg.Write((byte)State); + PetBehavior?.ServerWrite(msg); + } + + public override void ClientRead(IReadMessage msg) + { + State = (AIState)msg.ReadByte(); + PetBehavior?.ClientRead(msg); + } } //the "memory" of the Character diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 8c562b697..a67aa81f3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -915,10 +915,10 @@ namespace Barotrauma return false; } - public static void ReportProblem(Character reporter, Order order) + public static void ReportProblem(Character reporter, Order order, Hull targetHull = null) { if (reporter == null || order == null) { return; } - var visibleHulls = new List(reporter.GetVisibleHulls()); + var visibleHulls = targetHull is null ? new List(reporter.GetVisibleHulls()) : new List { targetHull }; foreach (var hull in visibleHulls) { PropagateHullSafety(reporter, hull); @@ -1415,7 +1415,7 @@ namespace Barotrauma if (GameMain.GameSession?.Campaign?.Map?.CurrentLocation != null) { var reputationLoss = damageAmount * Reputation.ReputationLossPerWallDamage; - GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.Value -= reputationLoss; + GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.AddReputation(-reputationLoss); } if (accumulatedDamage <= WarningThreshold) { return; } @@ -1510,7 +1510,7 @@ namespace Barotrauma var reputationLoss = MathHelper.Clamp( (item.Prefab.GetMinPrice() ?? 0) * Reputation.ReputationLossPerStolenItemPrice, Reputation.MinReputationLossPerStolenItem, Reputation.MaxReputationLossPerStolenItem); - GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.Value -= reputationLoss; + GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.AddReputation(-reputationLoss); } item.StolenDuringRound = true; otherCharacter.Speak(TextManager.Get("dialogstealwarning"), null, Rand.Range(0.5f, 1.0f), "thief", 10.0f); @@ -1971,13 +1971,13 @@ namespace Barotrauma if (c.Removed) { continue; } if (c.TeamID != Character.TeamID) { continue; } if (c.IsIncapacitated) { continue; } - other = c; if (c.IsPlayer) { if (c.SelectedConstruction == target.Item) { // If the other character is player, don't try to operate - return true; + other = c; + break; } } else if (c.AIController is HumanAIController operatingAI) @@ -1991,7 +1991,8 @@ namespace Barotrauma if (!isOrder && isTargetOrdered) { // If the other bot is ordered to operate the item, let him do it, unless we are ordered too - return true; + other = c; + break; } else { @@ -2012,18 +2013,20 @@ namespace Barotrauma // Steering is hard-coded -> cannot use the required skills collection defined in the xml if (Character.GetSkillLevel("helm") <= c.GetSkillLevel("helm")) { - return true; + other = c; + break; } } else if (target.DegreeOfSuccess(Character) <= target.DegreeOfSuccess(c)) { - return true; + other = c; + break; } } } } } - return false; + return other != null; bool IsOrderedToOperateThis(AIController ai) => ai is HumanAIController humanAI && humanAI.ObjectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder.Component.Item == target.Item; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs index cb8bdeff7..918287729 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs @@ -161,12 +161,13 @@ namespace Barotrauma private Vector2 CalculateSteeringSeek(Vector2 target, float weight, Func startNodeFilter = null, Func endNodeFilter = null, Func nodeFilter = null, bool checkVisibility = true) { Vector2 targetDiff = target - currentTarget; - if (currentPath != null && currentPath.Nodes.Any()) + if (currentPath != null && currentPath.Nodes.Any() && character.Submarine != null) { - //current path calculated relative to a different sub than where the character is now + //target in a different sub than where the character is now //take that into account when calculating if the target has moved - Submarine currentPathSub = currentPath?.Nodes.First().Submarine; - if (currentPathSub != character.Submarine && character.Submarine != null) + Submarine currentPathSub = currentPath?.CurrentNode?.Submarine; + if (currentPathSub == character.Submarine) { currentPathSub = currentPath?.Nodes.LastOrDefault()?.Submarine; } + if (currentPathSub != character.Submarine && targetDiff.LengthSquared() > 1 && currentPathSub != null) { Vector2 subDiff = character.Submarine.SimPosition - currentPathSub.SimPosition; targetDiff += subDiff; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index fa7112563..79842539a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -101,11 +101,11 @@ namespace Barotrauma public enum CombatMode { - Defensive, - Offensive, - Arrest, - Retreat, - None + Defensive, // Use weapons against the enemy, but try to retreat to a safe place + Offensive, // Engage the enemy and keep attacking it + Arrest, // Try to arrest the enemy without using lethal weapons (stunning + handcuffs) + Retreat, // Run to a safe place without attacking the target + None // Don't use } public CombatMode Mode { get; private set; } @@ -958,14 +958,15 @@ namespace Barotrauma } if (reloadTimer > 0) { return; } if (holdFireCondition != null && holdFireCondition()) { return; } - float sqrDist = Vector2.DistanceSquared(character.Position, Enemy.Position); + sqrDistance = Vector2.DistanceSquared(character.WorldPosition, Enemy.WorldPosition); + distanceTimer = distanceCheckInterval; if (WeaponComponent is MeleeWeapon meleeWeapon) { bool closeEnough = true; float sqrRange = meleeWeapon.Range * meleeWeapon.Range; if (character.AnimController.InWater) { - if (sqrDist > sqrRange) + if (sqrDistance > sqrRange) { closeEnough = false; } @@ -1003,7 +1004,7 @@ namespace Barotrauma { if (WeaponComponent is RepairTool repairTool) { - if (sqrDist > repairTool.Range * repairTool.Range) { return; } + if (sqrDistance > repairTool.Range * repairTool.Range) { return; } } float aimFactor = MathHelper.PiOver2 * (1 - AimAccuracy); if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.Position - Weapon.Position) < MathHelper.PiOver4 + aimFactor) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs index b6163032a..251c0362b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs @@ -53,9 +53,13 @@ namespace Barotrauma distanceFactor = 1; } float severity = AIObjectiveExtinguishFires.GetFireSeverity(targetHull); - if (severity > 0.5f && !isOrder) + if (severity > 0.75f && !isOrder && + targetHull.RoomName != null && + !targetHull.RoomName.Contains("reactor", StringComparison.OrdinalIgnoreCase) && + !targetHull.RoomName.Contains("engine", StringComparison.OrdinalIgnoreCase) && + !targetHull.RoomName.Contains("command", StringComparison.OrdinalIgnoreCase)) { - // Ignore severe fires unless ordered. (Let the fire drain all the oxygen instead). + // Ignore severe fires to prevent casualities unless ordered to extinguish. Priority = 0; Abandon = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs index 62787477d..97d9450c2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs @@ -25,7 +25,7 @@ namespace Barotrauma /// /// 0-1 based on the horizontal size of all of the fires in the hull. /// - public static float GetFireSeverity(Hull hull) => MathHelper.Lerp(0, 1, MathUtils.InverseLerp(0, Math.Min(hull.Rect.Width, 1000), hull.FireSources.Sum(fs => fs.Size.X))); + public static float GetFireSeverity(Hull hull) => MathHelper.Lerp(0, 1, MathUtils.InverseLerp(0, 500, hull.FireSources.Sum(fs => fs.Size.X))); protected override IEnumerable GetList() => Hull.hullList; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs index 14c01cb47..a28df6cf9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs @@ -63,6 +63,7 @@ namespace Barotrauma if (target.CurrentHull == null) { return false; } if (HumanAIController.IsFriendly(character, target)) { return false; } if (!character.Submarine.IsConnectedTo(target.Submarine)) { return false; } + if (target.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI)) { return false; } return true; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs index 76c51c0d9..012c3a858 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs @@ -57,7 +57,20 @@ namespace Barotrauma }; }, onAbandon: () => Abandon = true, - onCompleted: () => RemoveSubObjective(ref getDivingGear)); + onCompleted: () => + { + RemoveSubObjective(ref getDivingGear); + if (gearTag == HEAVY_DIVING_GEAR && HumanAIController.HasItem(character, LIGHT_DIVING_GEAR, out IEnumerable masks, requireEquipped: true)) + { + foreach (Item mask in masks) + { + if (mask != targetItem) + { + character.Inventory.TryPutItem(mask, character, CharacterInventory.anySlot); + } + } + } + }); } else { @@ -71,9 +84,13 @@ namespace Barotrauma { if (character.IsOnPlayerTeam) { - if (HumanAIController.HasItem(character, "oxygensource", out _, conditionPercentage: min)) + if (HumanAIController.HasItem(character, OXYGEN_SOURCE, out _, conditionPercentage: min)) { character.Speak(TextManager.Get("dialogswappingoxygentank"), null, 0, "swappingoxygentank", 30.0f); + if (character.Inventory.FindAllItems(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > min).Count == 1) + { + character.Speak(TextManager.Get("dialoglastoxygentank"), null, 0.0f, "dialoglastoxygentank", 30.0f); + } } else { @@ -105,7 +122,7 @@ namespace Barotrauma onAbandon: () => { Abandon = true; - if (remainingTanks > 0 && !HumanAIController.HasItem(character, "oxygensource", out _, conditionPercentage: 0.01f)) + if (remainingTanks > 0 && !HumanAIController.HasItem(character, OXYGEN_SOURCE, out _, conditionPercentage: 0.01f)) { character.Speak(TextManager.Get("dialogcantfindtoxygen"), null, 0, "cantfindoxygen", 30.0f); } @@ -121,7 +138,7 @@ namespace Barotrauma int ReportOxygenTankCount() { if (character.Submarine != Submarine.MainSub) { return 1; } - int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag("oxygensource") && i.Condition > 1); + int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > 1); if (remainingOxygenTanks == 0) { character.Speak(TextManager.Get("DialogOutOfOxygenTanks"), null, 0.0f, "outofoxygentanks", 30.0f); @@ -136,17 +153,6 @@ namespace Barotrauma } } - /// - /// Returns false only when no inventory can be found from the item. - /// - public static bool EjectEmptyTanks(Character actor, Item target, out IEnumerable containedItems) - { - containedItems = target.OwnInventory?.AllItems; - if (containedItems == null) { return false; } - AIController.UnequipEmptyItems(actor, target); - return true; - } - public override void Reset() { base.Reset(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs index 1b7b68af7..3be3dab7e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs @@ -242,9 +242,8 @@ namespace Barotrauma if (!searchingNewHull) { //find all available hulls first - FindTargetHulls(); searchingNewHull = true; - return; + FindTargetHulls(); } else if (targetHulls.Any()) { @@ -255,11 +254,10 @@ namespace Barotrauma var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsgStr: null, nodeFilter: node => { if (node.Waypoint.CurrentHull == null) { return false; } - // Check that there is no unsafe or forbidden hulls on the way to the target + // Check that there is no unsafe hulls on the way to the target if (node.Waypoint.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(node.Waypoint.CurrentHull)) { return false; } - if (isCurrentHullAllowed && IsForbidden(node.Waypoint.CurrentHull)) { return false; } return true; - }); + }, endNodeFilter: node => !isCurrentHullAllowed | !IsForbidden(node.Waypoint.CurrentHull)); if (path.Unreachable) { //can't go to this room, remove it from the list and try another room @@ -271,30 +269,19 @@ namespace Barotrauma SetTargetTimerLow(); return; } + character.AIController.SelectTarget(currentTarget.AiTarget); + PathSteering.SetPath(path); + SetTargetTimerNormal(); searchingNewHull = false; } else { - // Couldn't find a target for some reason -> reset + // Couldn't find a valid hull SetTargetTimerHigh(); searchingNewHull = false; } - - if (currentTarget != null) - { - character.AIController.SelectTarget(currentTarget.AiTarget); - string errorMsg = null; -#if DEBUG - bool isRoomNameFound = currentTarget.DisplayName != null; - errorMsg = "(Character " + character.Name + " idling, target " + (isRoomNameFound ? currentTarget.DisplayName : currentTarget.ToString()) + ")"; -#endif - var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsgStr: errorMsg, nodeFilter: node => node.Waypoint.CurrentHull != null); - PathSteering.SetPath(path); - } - SetTargetTimerNormal(); } newTargetTimer -= deltaTime; - if (!character.IsClimbing && IsSteeringFinished()) { Wander(deltaTime); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs index 09a73221f..57b024e16 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs @@ -141,6 +141,7 @@ namespace Barotrauma public Entity TargetEntity; public ItemComponent TargetItemComponent; public readonly bool UseController; + public readonly string[] ControllerTags; public Controller ConnectedController; public Character OrderGiver; @@ -309,6 +310,7 @@ namespace Barotrauma color = orderElement.GetAttributeColor("color"); FadeOutTime = orderElement.GetAttributeFloat("fadeouttime", 0.0f); UseController = orderElement.GetAttributeBool("usecontroller", false); + ControllerTags = orderElement.GetAttributeStringArray("controllertags", new string[0]); TargetAllCharacters = orderElement.GetAttributeBool("targetallcharacters", false); AppropriateJobs = orderElement.GetAttributeStringArray("appropriatejobs", new string[0]); Options = orderElement.GetAttributeStringArray("options", new string[0]); @@ -380,6 +382,7 @@ namespace Barotrauma SymbolSprite = prefab.SymbolSprite; Color = prefab.Color; UseController = prefab.UseController; + ControllerTags = prefab.ControllerTags; TargetAllCharacters = prefab.TargetAllCharacters; AppropriateJobs = prefab.AppropriateJobs; FadeOutTime = prefab.FadeOutTime; @@ -399,7 +402,7 @@ namespace Barotrauma { if (UseController) { - ConnectedController = targetItem.Item?.FindController(); + ConnectedController = targetItem.Item?.FindController(tags: ControllerTags); if (ConnectedController == null) { DebugConsole.AddWarning("AI: Tried to use a controller for operating an item, but couldn't find any."); @@ -450,19 +453,37 @@ namespace Barotrauma return false; } - public string GetChatMessage(string targetCharacterName, string targetRoomName, bool givingOrderToSelf, string orderOption = "") + public string GetChatMessage(string targetCharacterName, string targetRoomName, bool givingOrderToSelf, string orderOption = "", int? priority = null) { - orderOption ??= ""; - - string messageTag = (givingOrderToSelf && !TargetAllCharacters ? "OrderDialogSelf." : "OrderDialog.") + Identifier; - if (Identifier != "dismissed" && !string.IsNullOrEmpty(orderOption)) { messageTag += "." + orderOption; } - - if (targetCharacterName == null) { targetCharacterName = ""; } - if (targetRoomName == null) { targetRoomName = ""; } - string msg = TextManager.GetWithVariables(messageTag, new string[2] { "[name]", "[roomname]" }, new string[2] { targetCharacterName, targetRoomName }, new bool[2] { false, true }, true); - if (msg == null) { return ""; } - - return msg; + priority ??= CharacterInfo.HighestManualOrderPriority; + // If the order has a lesser priority, it means we are rearranging character orders + if (!TargetAllCharacters && priority != CharacterInfo.HighestManualOrderPriority && Identifier != "dismissed") + { + return TextManager.GetWithVariable("rearrangedorders", "[name]", targetCharacterName ?? string.Empty, returnNull: true) ?? string.Empty; + } + string messageTag = $"{(givingOrderToSelf && !TargetAllCharacters ? "OrderDialogSelf" : "OrderDialog")}"; + messageTag += $".{Identifier}"; + if (!string.IsNullOrEmpty(orderOption)) + { + if (Identifier != "dismissed") + { + messageTag += $".{orderOption}"; + } + else + { + string[] splitOption = orderOption.Split('.'); + if (splitOption.Length > 0) + { + messageTag += $".{splitOption[0]}"; + } + } + } + string msg = TextManager.GetWithVariables(messageTag, + new string[2] { "[name]", "[roomname]" }, + new string[2] { targetCharacterName ?? string.Empty, targetRoomName ?? string.Empty }, + formatCapitals: new bool[2] { false, true }, + returnNull: true); + return msg ?? string.Empty; } /// @@ -505,7 +526,7 @@ namespace Barotrauma if (item.NonInteractable) { continue; } if (ItemComponentType != null && item.Components.None(c => c.GetType() == ItemComponentType)) { continue; } Controller controller = null; - if (UseController && !item.TryFindController(out controller)) { continue; } + if (UseController && !item.TryFindController(out controller, tags: ControllerTags)) { continue; } if (interactableFor != null && (!item.IsInteractable(interactableFor) || (UseController && !controller.Item.IsInteractable(interactableFor)))) { continue; } matchingItems.Add(item); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs index 82b4ed49b..21d8cebe4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs @@ -97,64 +97,51 @@ namespace Barotrauma foreach (WayPoint wp in wayPoints) { - wp.linkedTo.CollectionChanged += WaypointLinksChanged; + wp.OnLinksChanged += WaypointLinksChanged; } IndoorsSteering = indoorsSteering; } - void WaypointLinksChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) + void WaypointLinksChanged(WayPoint wp) { if (Submarine.Unloading) { return; } - var waypoints = sender as IEnumerable; + var node = nodes.Find(n => n.Waypoint == wp); + if (node == null) { return; } - foreach (MapEntity me in waypoints) + for (int i = node.connections.Count - 1; i >= 0; i--) { - WayPoint wp = me as WayPoint; - if (me == null) { continue; } - - var node = nodes.Find(n => n.Waypoint == wp); - if (node == null) { return; } - - if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove) + //remove connection if the waypoint isn't connected anymore + if (wp.linkedTo.FirstOrDefault(l => l == node.connections[i].Waypoint) == null) { - for (int i = node.connections.Count - 1; i >= 0; i--) - { - //remove connection if the waypoint isn't connected anymore - if (wp.linkedTo.FirstOrDefault(l => l == node.connections[i].Waypoint) == null) - { - node.connections.RemoveAt(i); - node.distances.RemoveAt(i); - } - } + node.connections.RemoveAt(i); + node.distances.RemoveAt(i); } - else if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add) + } + + for (int i = 0; i < wp.linkedTo.Count; i++) + { + if (!(wp.linkedTo[i] is WayPoint connected)) { continue; } + + //already connected, continue + if (node.connections.Any(n => n.Waypoint == connected)) { continue; } + + var matchingNode = nodes.Find(n => n.Waypoint == connected); + if (matchingNode == null) { - for (int i = 0; i < wp.linkedTo.Count; i++) - { - if (!(wp.linkedTo[i] is WayPoint connected)) { continue; } - - //already connected, continue - if (node.connections.Any(n => n.Waypoint == connected)) { continue; } - - var matchingNode = nodes.Find(n => n.Waypoint == connected); - if (matchingNode == null) - { #if DEBUG - DebugConsole.ThrowError("Waypoint connections were changed, no matching path node found in PathFinder"); + DebugConsole.ThrowError("Waypoint connections were changed, no matching path node found in PathFinder"); #endif - return; - } - - node.connections.Add(matchingNode); - node.distances.Add(Vector2.Distance(node.Position, matchingNode.Position)); - } + return; } + + node.connections.Add(matchingNode); + node.distances.Add(Vector2.Distance(node.Position, matchingNode.Position)); } } - private static readonly List sortedNodes = new List(); + private readonly List sortedNodes = new List(); public SteeringPath FindPath(Vector2 start, Vector2 end, Submarine hostSub = null, string errorMsgStr = null, Func startNodeFilter = null, Func endNodeFilter = null, Func nodeFilter = null, bool checkVisibility = true) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index 0ca3c9c1c..475a16a08 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -219,6 +219,7 @@ namespace Barotrauma if (character.SelectedCharacter != null) { DragCharacter(character.SelectedCharacter, deltaTime); + return; } //don't flip when simply physics is enabled diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index 7e04b454f..1e910d566 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -938,19 +938,23 @@ namespace Barotrauma float rotation = MathHelper.WrapAngle(Collider.Rotation); rotation = MathHelper.ToDegrees(rotation); - if (rotation < 0.0f) rotation += 360; - + if (rotation < 0.0f) + { + rotation += 360; + } if (!character.IsRemotelyControlled && !aiming && Anim != Animation.UsingConstruction && !(character.SelectedConstruction?.GetComponent()?.ControlCharacterPose ?? false)) { if (rotation > 20 && rotation < 170) + { TargetDir = Direction.Left; + } else if (rotation > 190 && rotation < 340) + { TargetDir = Direction.Right; + } } - float targetSpeed = TargetMovement.Length(); - if (targetSpeed > 0.1f) { if (!aiming) @@ -965,9 +969,7 @@ namespace Barotrauma { Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition); Vector2 diff = (mousePos - torso.SimPosition) * Dir; - TargetMovement = new Vector2(0.0f, -0.1f); - float newRotation = MathUtils.VectorToAngle(diff); Collider.SmoothRotate(newRotation, 5.0f * character.SpeedMultiplier); } @@ -1622,7 +1624,10 @@ namespace Barotrauma { Vector2 pullLimbAnchor = targetLimb.SimPosition; pullLimb.PullJointMaxForce = 5000.0f; - targetMovement *= MathHelper.Clamp(Mass / target.Mass, 0.5f, 1.0f); + if (!character.HasAbilityFlag(AbilityFlags.MoveNormallyWhileDragging)) + { + targetMovement *= MathHelper.Clamp(Mass / target.Mass, 0.5f, 1.0f); + } Vector2 shoulderPos = rightShoulder.WorldAnchorA; Vector2 dragDir = inWater ? Vector2.Normalize(targetLimb.SimPosition - shoulderPos) : Vector2.UnitY; @@ -1679,7 +1684,7 @@ namespace Barotrauma } //limit movement if moving away from the target - if (Vector2.Dot(target.WorldPosition - WorldPosition, targetMovement) < 0) + if (!character.HasAbilityFlag(AbilityFlags.MoveNormallyWhileDragging) && Vector2.Dot(target.WorldPosition - WorldPosition, targetMovement) < 0) { targetMovement *= MathHelper.Clamp(1.5f - dist, 0.0f, 1.0f); } @@ -1750,8 +1755,9 @@ namespace Barotrauma Vector2 diff = holdable.Aimable ? (mousePos - AimSourceSimPos) * Dir : Vector2.UnitX; holdAngle = MathUtils.VectorToAngle(new Vector2(diff.X, diff.Y * Dir)) - torso.body.Rotation * Dir; + holdAngle += GetAimWobble(rightHand, leftHand, item); - itemAngle = (torso.body.Rotation + holdAngle * Dir); + itemAngle = torso.body.Rotation + holdAngle * Dir; if (holdable.ControlPose) { @@ -1869,6 +1875,26 @@ namespace Barotrauma } } + private float GetAimWobble(Limb rightHand, Limb leftHand, Item heldItem) + { + float wobbleStrength = 0.0f; + if (character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand) == heldItem) + { + wobbleStrength += Character.CharacterHealth.GetLimbDamage(rightHand, afflictionType: "damage"); + } + if (character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand) == heldItem) + { + wobbleStrength += Character.CharacterHealth.GetLimbDamage(leftHand, afflictionType: "damage"); + } + if (wobbleStrength <= 0.1f) { return 0.0f; } + wobbleStrength = (float)Math.Min(wobbleStrength, 1.0f); + + float lowFreqNoise = PerlinNoise.GetPerlin((float)Timing.TotalTime / 320.0f, (float)Timing.TotalTime / 240.0f) - 0.5f; + float highFreqNoise = PerlinNoise.GetPerlin((float)Timing.TotalTime / 40.0f, (float)Timing.TotalTime / 50.0f) - 0.5f; + + return (lowFreqNoise * 1.0f + highFreqNoise * 0.1f) * wobbleStrength; + } + private void HandIK(Limb hand, Vector2 pos, float force = 1.0f) { Vector2 shoulderPos; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index c89b02b76..dc2f44f07 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -74,6 +74,20 @@ namespace Barotrauma } } + class AttackData + { + public float DamageMultiplier { get; set; } = 1f; + public float AddedPenetration { get; set; } = 0f; + public List Afflictions { get; set; } + public Attack SourceAttack { get; } + + public AttackData(Attack sourceAttack) + { + SourceAttack = sourceAttack; + } + + } + partial class Attack : ISerializableEntity { [Serialize(AttackContext.Any, true, description: "The attack will be used only in this context."), Editable] @@ -271,6 +285,9 @@ namespace Barotrauma statusEffect.SetUser(user); } } + + // used for talents/ability conditions + public Item SourceItem { get; } public List GetMultipliedAfflictions(float multiplier) { @@ -320,6 +337,10 @@ namespace Barotrauma Penetration = Penetration; } + public Attack(XElement element, string parentDebugName, Item sourceItem) : this(element, parentDebugName) + { + SourceItem = sourceItem; + } public Attack(XElement element, string parentDebugName) { Deserialize(element); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 8c2506cc9..c97e76569 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -461,10 +461,7 @@ namespace Barotrauma } } - public bool AllowInput - { - get { return Stun <= 0.0f && !IsDead && !IsIncapacitated; } - } + public bool AllowInput => !Removed && !IsIncapacitated && Stun <= 0.0f; public bool CanMove { @@ -476,10 +473,10 @@ namespace Barotrauma } } - public bool CanInteract - { - get { return AllowInput && IsHumanoid && !LockHands && !Removed && !IsIncapacitated; } - } + public bool CanInteract => AllowInput && IsHumanoid && !LockHands; + + // Eating is not implemented for humanoids. If we implement that at some point, we could remove this restriction. + public bool CanEat => !IsHumanoid && Params.CanEat && AllowInput && AnimController.GetLimb(LimbType.Head) != null; public Vector2 CursorPosition { @@ -773,8 +770,6 @@ namespace Barotrauma } } - public bool IsObserving => AIController is EnemyAIController enemyAI && enemyAI.Enabled && enemyAI.State == AIState.Observe; - public bool EnableDespawn { get; set; } = true; public CauseOfDeath CauseOfDeath @@ -1378,6 +1373,12 @@ namespace Barotrauma if (Info?.Job == null) { return 0.0f; } float skillLevel = Info.Job.GetSkillLevel(skillIdentifier); + // apply multipliers first so that multipliers only affect base skill value + foreach (Affliction affliction in CharacterHealth.GetAllAfflictions()) + { + skillLevel *= affliction.GetSkillMultiplier(); + } + if (skillIdentifier != null) { for (int i = 0; i < Inventory.Capacity; i++) @@ -1392,10 +1393,8 @@ namespace Barotrauma } } - foreach (Affliction affliction in CharacterHealth.GetAllAfflictions()) - { - skillLevel *= affliction.GetSkillMultiplier(); - } + skillLevel += GetStatValue(GetSkillStatType(skillIdentifier)); + return skillLevel; } @@ -1432,9 +1431,9 @@ namespace Barotrauma // - dragging someone // - crouching // - moving backwards - public bool CanRun => (SelectedCharacter == null || !SelectedCharacter.CanBeDragged) && + public bool CanRun => (SelectedCharacter == null || !SelectedCharacter.CanBeDragged || HasAbilityFlag(AbilityFlags.MoveNormallyWhileDragging)) && (!(AnimController is HumanoidAnimController) || !((HumanoidAnimController)AnimController).Crouching) && - !AnimController.IsMovingBackwards; + !AnimController.IsMovingBackwards && !HasAbilityFlag(AbilityFlags.MustWalk); public Vector2 ApplyMovementLimits(Vector2 targetMovement, float currentSpeed) { @@ -1595,7 +1594,7 @@ namespace Barotrauma return Math.Clamp(reduction, 0, 1f); } - private float CalculateMovementPenalty(Limb limb, float sum, float max = 0.4f) + private float CalculateMovementPenalty(Limb limb, float sum, float max = 0.8f) { if (limb != null) { @@ -1628,7 +1627,7 @@ namespace Barotrauma float max; if (AnimController is HumanoidAnimController) { - max = AnimController.InWater ? 0.5f : 0.7f; + max = AnimController.InWater ? 0.5f : 0.8f; } else { @@ -2044,7 +2043,7 @@ namespace Barotrauma return false; } - public Item GetEquippedItem(string tagOrIdentifier, InvSlotType? slotType = null) + public Item GetEquippedItem(string tagOrIdentifier = null, InvSlotType? slotType = null) { if (Inventory == null) { return null; } for (int i = 0; i < Inventory.Capacity; i++) @@ -2059,7 +2058,7 @@ namespace Barotrauma } var item = Inventory.GetItemAt(i); if (item == null) { continue; } - if (item.Prefab.Identifier == tagOrIdentifier || item.HasTag(tagOrIdentifier)) { return item; } + if (tagOrIdentifier == null || item.Prefab.Identifier == tagOrIdentifier || item.HasTag(tagOrIdentifier)) { return item; } } return null; } @@ -2365,9 +2364,9 @@ namespace Barotrauma { if (!IsMouseOnUI && (ViewTarget == null || ViewTarget == this)) { - if (findFocusedTimer <= 0.0f || Screen.Selected == GameMain.SubEditorScreen) + if ((findFocusedTimer <= 0.0f || Screen.Selected == GameMain.SubEditorScreen) && (!PlayerInput.PrimaryMouseButtonHeld() || Barotrauma.Inventory.DraggingItemToWorld)) { - FocusedCharacter = CanInteract ? FindCharacterAtPosition(mouseSimPos) : null; + FocusedCharacter = CanInteract || CanEat ? FindCharacterAtPosition(mouseSimPos) : null; if (FocusedCharacter != null && !CanSeeCharacter(FocusedCharacter)) { FocusedCharacter = null; } float aimAssist = GameMain.Config.AimAssistAmount * (AnimController.InWater ? 1.5f : 1.0f); if (HeldItems.Any(it => it?.GetComponent()?.IsActive ?? false)) @@ -2445,7 +2444,7 @@ namespace Barotrauma { DeselectCharacter(); } - else if (FocusedCharacter != null && IsKeyHit(InputType.Grab) && FocusedCharacter.CanBeDragged && CanInteract) + else if (FocusedCharacter != null && IsKeyHit(InputType.Grab) && FocusedCharacter.CanBeDragged && (CanInteract || FocusedCharacter.IsDead && CanEat)) { SelectCharacter(FocusedCharacter); } @@ -2624,6 +2623,11 @@ namespace Barotrauma UpdateAttackers(deltaTime); + foreach (var characterTalent in characterTalents) + { + characterTalent.UpdateTalent(deltaTime); + } + if (IsDead) { return; } if (GameMain.NetworkMember != null) @@ -2712,6 +2716,9 @@ namespace Barotrauma //Do ragdoll shenanigans before Stun because it's still technically a stun, innit? Less network updates for us! bool allowRagdoll = GameMain.NetworkMember?.ServerSettings?.AllowRagdollButton ?? true; bool tooFastToUnragdoll = AnimController.Collider.LinearVelocity.LengthSquared() > 5.0f * 5.0f; + bool wasRagdolled = false; + bool selfRagdolled = false; + if (IsForceRagdolled) { IsRagdolled = IsForceRagdolled; @@ -2730,12 +2737,17 @@ namespace Barotrauma } else { - bool wasRagdolled = IsRagdolled; - IsRagdolled = IsKeyDown(InputType.Ragdoll); //Handle this here instead of Control because we can stop being ragdolled ourselves + wasRagdolled = IsRagdolled; + IsRagdolled = selfRagdolled = IsKeyDown(InputType.Ragdoll); //Handle this here instead of Control because we can stop being ragdolled ourselves if (wasRagdolled != IsRagdolled) { ragdollingLockTimer = 0.25f; } } } + if (!wasRagdolled && IsRagdolled && selfRagdolled) + { + CheckTalents(AbilityEffectType.OnSelfRagdoll); + } + lowPassMultiplier = MathHelper.Lerp(lowPassMultiplier, 1.0f, 0.1f); //ragdoll button @@ -3092,6 +3104,12 @@ namespace Barotrauma OrderInfo newOrderInfo = new OrderInfo(order, orderOption, priority); AddCurrentOrder(newOrderInfo); + + if (orderGiver != null) + { + orderGiver.CheckTalents(AbilityEffectType.OnGiveOrder, this); + } + if (AIController is HumanAIController humanAI) { humanAI.SetOrder(order, orderOption, priority, orderGiver, speak); @@ -3337,9 +3355,25 @@ namespace Barotrauma float attackImpulse = attack.TargetImpulse + attack.TargetForce * deltaTime; + AttackData attackData = new AttackData(attack); + attacker.CheckTalents(AbilityEffectType.OnAttack, attackData); + CheckTalents(AbilityEffectType.OnAttacked, attackData); + attackData.DamageMultiplier *= (1 + attacker.GetStatValue(StatTypes.AttackMultiplier)); + + IEnumerable attackAfflictions; + + if (attackData.Afflictions != null) + { + attackAfflictions = attackData.Afflictions.Union(attack.Afflictions.Keys); + } + else + { + attackAfflictions = attack.Afflictions.Keys; + } + var attackResult = targetLimb == null ? - AddDamage(worldPosition, attack.Afflictions.Keys, attack.Stun, playSound, attackImpulse, out limbHit, attacker, attack.DamageMultiplier) : - DamageLimb(worldPosition, targetLimb, attack.Afflictions.Keys, attack.Stun, playSound, attackImpulse, attacker, attack.DamageMultiplier, penetration: penetration); + AddDamage(worldPosition, attackAfflictions, attack.Stun, playSound, attackImpulse, out limbHit, attacker, attack.DamageMultiplier * attackData.DamageMultiplier) : + DamageLimb(worldPosition, targetLimb, attackAfflictions, attack.Stun, playSound, attackImpulse, attacker, attack.DamageMultiplier * attackData.DamageMultiplier, penetration: penetration + attackData.AddedPenetration); if (limbHit == null) { return new AttackResult(); } Vector2 forceWorld = attack.TargetImpulseWorld + attack.TargetForceWorld; @@ -3457,6 +3491,11 @@ namespace Barotrauma public void RecordKill(Character target) { + foreach (Character attackerCrewmember in GetFriendlyCrew(this)) + { + attackerCrewmember.CheckTalents(AbilityEffectType.OnCrewKillCharacter, target); + } + if (!IsOnPlayerTeam) { return; } if (GameMain.Config.KilledCreatures.Any(name => name.Equals(target.SpeciesName, StringComparison.OrdinalIgnoreCase))) { return; } GameMain.Config.KilledCreatures.Add(target.SpeciesName); @@ -3524,7 +3563,7 @@ namespace Barotrauma bool wasDead = IsDead; Vector2 simPos = hitLimb.SimPosition + ConvertUnits.ToSimUnits(dir); float prevVitality = CharacterHealth.Vitality; - AttackResult attackResult = hitLimb.AddDamage(simPos, afflictions, playSound, damageMultiplier: damageMultiplier, penetration: penetration); + AttackResult attackResult = hitLimb.AddDamage(simPos, afflictions, playSound, damageMultiplier: damageMultiplier, penetration: penetration, attacker: attacker); CharacterHealth.ApplyDamage(hitLimb, attackResult, allowStacking); if (attacker != this) { @@ -3551,6 +3590,9 @@ namespace Barotrauma ApplyStatusEffects(ActionType.OnDamaged, 1.0f); hitLimb.ApplyStatusEffects(ActionType.OnDamaged, 1.0f); } + + attacker?.CheckTalents(AbilityEffectType.OnAttackResult, attackResult); + return attackResult; } @@ -3775,6 +3817,8 @@ namespace Barotrauma causeOfDeathAffliction?.Source ?? LastAttacker, LastDamageSource); OnDeath?.Invoke(this, CauseOfDeath); + CheckTalents(AbilityEffectType.OnDieToCharacter, CauseOfDeath.Killer); + if (GameMain.GameSession != null && Screen.Selected == GameMain.GameScreen) { SteamAchievementManager.OnCharacterKilled(this, CauseOfDeath); @@ -3782,7 +3826,11 @@ namespace Barotrauma KillProjSpecific(causeOfDeath, causeOfDeathAffliction, log); - if (info != null) { info.CauseOfDeath = CauseOfDeath; } + if (info != null) + { + info.CauseOfDeath = CauseOfDeath; + info.ResetSavedStatValues(); + } AnimController.movement = Vector2.Zero; AnimController.TargetMovement = Vector2.Zero; @@ -3805,6 +3853,11 @@ namespace Barotrauma if (GameMain.GameSession != null) { + if (GameMain.GameSession.Campaign != null && TeamID == CharacterTeamType.Team1 && !IsAssistant) + { + GameMain.GameSession.Campaign.CrewHasDied = true; + } + GameMain.GameSession.KillCharacter(this); } } @@ -4203,8 +4256,249 @@ namespace Barotrauma public bool IsProtectedFromPressure() { - return PressureProtection >= (Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 1.0f); + return HasAbilityFlag(AbilityFlags.ImmuneToPressure) || PressureProtection >= (Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 1.0f); } + + // Talent logic begins here. Should be encapsulated to its own controller soon + + private readonly List characterTalents = new List(); + + public void LoadTalents() + { + List toBeRemoved = null; + foreach (string talent in info.UnlockedTalents) + { + if (!GiveTalent(talent, addingFirstTime: false)) + { + DebugConsole.AddWarning(Name + " had talent that did not exist! Removing talent from CharacterInfo."); + toBeRemoved ??= new List(); + toBeRemoved.Add(talent); + } + } + + if (toBeRemoved != null) + { + foreach (string removeTalent in toBeRemoved) + { + Info.UnlockedTalents.Remove(removeTalent); + } + } + } + + public bool GiveTalent(string talentIdentifier, bool addingFirstTime = true) + { + TalentPrefab talentPrefab = TalentPrefab.TalentPrefabs.Find(c => c.Identifier.Equals(talentIdentifier, StringComparison.OrdinalIgnoreCase)); + if (talentPrefab == null) + { + DebugConsole.AddWarning($"Tried to add talent by identifier {talentIdentifier} to character {Name}, but no such talent exists."); + return false; + } + return GiveTalent(talentPrefab, addingFirstTime); + } + + public bool GiveTalent(UInt32 talentIdentifier, bool addingFirstTime = true) + { + TalentPrefab talentPrefab = TalentPrefab.TalentPrefabs.Find(c => c.UIntIdentifier == talentIdentifier); + if (talentPrefab == null) + { + DebugConsole.AddWarning($"Tried to add talent by identifier {talentIdentifier} to character {Name}, but no such talent exists."); + return false; + } + return GiveTalent(talentPrefab, addingFirstTime); + } + + private bool GiveTalent(TalentPrefab talentPrefab, bool addingFirstTime = true) + { + if (addingFirstTime) + { + if (!info.UnlockedTalents.Add(talentPrefab.Identifier)) { return false; } + } + + DebugConsole.AddWarning("added " + talentPrefab.OriginalName); + CharacterTalent characterTalent = new CharacterTalent(talentPrefab, this); + characterTalent.ActivateTalent(addingFirstTime); + characterTalents.Add(characterTalent); + +#if SERVER + GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.UpdateTalents }); +#endif + + return true; + } + + public bool HasTalent(string identifier) + { + return info.UnlockedTalents.Contains(identifier); + } + + public static IEnumerable GetFriendlyCrew(Character character) + { + return CharacterList.Where(c => HumanAIController.IsFriendly(character, c, onlySameTeam: true) && !c.IsDead); + } + + public void CheckTalents(AbilityEffectType abilityEffectType, object abilityData) + { + foreach (var characterTalent in characterTalents) + { + characterTalent.CheckTalent(abilityEffectType, abilityData); + } + } + + public void CheckTalents(AbilityEffectType abilityEffectType) + { + foreach (var characterTalent in characterTalents) + { + characterTalent.CheckTalent(abilityEffectType, (object)null); + } + } + + public bool HasRecipeForItem(string recipeIdentifier) + { + return characterTalents.Any(t => t.UnlockedRecipes.Contains(recipeIdentifier)); + } + + /// + /// Shows visual notification of money gained by the specific player. Useful for mid-mission monetary gains. + /// + public void GiveMoney(int amount) + { + if (!(GameMain.GameSession?.Campaign is CampaignMode campaign)) { return; } + if (amount <= 0) { return; } + + int prevAmount = campaign.Money; + campaign.Money += amount; + OnMoneyChanged(prevAmount, campaign.Money); + } + + public void SetMoney(int amount) + { + if (!(GameMain.GameSession?.Campaign is CampaignMode campaign)) { return; } + if (amount == campaign.Money) { return; } + + int prevAmount = campaign.Money; + campaign.Money = amount; + OnMoneyChanged(prevAmount, campaign.Money); + } + + partial void OnMoneyChanged(int prevAmount, int newAmount); + + /// + /// This dictionary is used for stats that are required very frequently. Not very performant, but easier to develop with for now. + /// If necessary, the approach of using a dictionary could be replaced by an encapsulated class that contains the stats as attributes. + /// + private readonly Dictionary statValues = new Dictionary(); + + public float GetStatValue(StatTypes statType) + { + if (!IsHuman) { return 0f; } + + float statValue = 0f; + if (statValues.TryGetValue(statType, out float value)) + { + statValue += value; + } + if (CharacterHealth != null) + { + statValue += CharacterHealth.GetStatValue(statType); + } + if (Info != null) + { + // could be optimized by instead updating the Character.cs statvalues dictionary whenever the CharacterInfo.cs values change + statValue += Info.GetSavedStatValue(statType); + } + + //replace by updating the character wearable stat values when equipping or unequipping wearables + for (int i = 0; i < Inventory.Capacity; i++) + { + if (Inventory.SlotTypes[i] != InvSlotType.Any && Inventory.GetItemAt(i)?.GetComponent() is Wearable wearable) + { + if (wearable.WearableStatValues.TryGetValue(statType, out float wearableValue)) + { + statValue += wearableValue; + } + } + } + + return statValue; + } + + public void ChangeStat(StatTypes statType, float value) + { + if (statValues.ContainsKey(statType)) + { + statValues[statType] += value; + } + else + { + statValues.Add(statType, value); + } + } + + private StatTypes GetSkillStatType(string skillIdentifier) + { + // Using this method to translate between skill identifiers and stat types. Feel free to replace it if there's a better way + switch (skillIdentifier) + { + case "electrical": + return StatTypes.ElectricalSkillBonus; + case "helm": + return StatTypes.HelmSkillBonus; + case "mechanical": + return StatTypes.MechanicalSkillBonus; + case "medical": + return StatTypes.MedicalSkillBonus; + case "weapons": + return StatTypes.WeaponsSkillBonus; + default: + return StatTypes.None; + } + } + + private readonly List abilityFlags = new List(); + + public void AddAbilityFlag(AbilityFlags abilityFlag) + { + abilityFlags.Add(abilityFlag); + } + + public void RemoveAbilityFlag(AbilityFlags abilityFlag) + { + abilityFlags.Remove(abilityFlag); + } + + public bool HasAbilityFlag(AbilityFlags abilityFlag) + { + return abilityFlags.Contains(abilityFlag); + } + + private readonly Dictionary abilityResistances = new Dictionary(); + + public float GetAbilityResistance(string resistanceId) + { + return abilityResistances.TryGetValue(resistanceId, out float value) ? value : 1f; + } + + public void ChangeAbilityResistance(string resistanceId, float value) + { + if (abilityResistances.ContainsKey(resistanceId)) + { + abilityResistances[resistanceId] *= value; + } + else + { + abilityResistances.Add(resistanceId, value); + } + } + + /// + /// Compares just the species name and the group, ignores teams. There's a more complex version found in HumanAIController.cs + /// + public bool IsFriendly(Character other) => IsFriendly(this, other); + + /// + /// Compares just the species name and the group, ignores teams. There's a more complex version found in HumanAIController.cs + /// + public static bool IsFriendly(Character me, Character other) => other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group); } class ActiveTeamChange diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index 14e7f34c5..9526ce3a4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using Barotrauma.IO; using System.Linq; using System.Xml.Linq; +using Barotrauma.Abilities; namespace Barotrauma { @@ -215,6 +216,12 @@ namespace Barotrauma public int Salary; + public int ExperiencePoints { get; private set; } + + public HashSet UnlockedTalents { get; private set; } = new HashSet(); + + public int AdditionalTalentPoints { get; private set; } + private Sprite headSprite; public Sprite HeadSprite { @@ -529,6 +536,8 @@ namespace Barotrauma OriginalName = infoElement.GetAttributeString("originalname", null); string genderStr = infoElement.GetAttributeString("gender", "male").ToLowerInvariant(); Salary = infoElement.GetAttributeInt("salary", 1000); + ExperiencePoints = infoElement.GetAttributeInt("experiencepoints", 0); + UnlockedTalents = new HashSet(infoElement.GetAttributeStringArray("unlockedtalents", new string[0], convertToLowerInvariant: true)); Enum.TryParse(infoElement.GetAttributeString("race", "White"), true, out Race race); Enum.TryParse(infoElement.GetAttributeString("gender", "None"), true, out Gender gender); _speciesName = infoElement.GetAttributeString("speciesname", null); @@ -599,10 +608,38 @@ namespace Barotrauma } foreach (XElement subElement in infoElement.Elements()) { - if (subElement.Name.ToString().Equals("job", StringComparison.OrdinalIgnoreCase)) + bool jobCreated = false; + if (subElement.Name.ToString().Equals("job", StringComparison.OrdinalIgnoreCase) && !jobCreated) { Job = new Job(subElement); - break; + jobCreated = true; + // there used to be a break here, but it had to be removed to make room for statvalues + // using the jobCreated boolean to make sure that only the first job found is created + } + else if (subElement.Name.ToString().Equals("savedstatvalues", StringComparison.OrdinalIgnoreCase)) + { + foreach (XElement savedStat in subElement.Elements()) + { + string statTypeString = savedStat.GetAttributeString("stattype", "").ToLowerInvariant(); + if (!Enum.TryParse(statTypeString, true, out StatTypes statType)) + { + DebugConsole.ThrowError("Invalid stat type type \"" + statTypeString + "\" when loading character data in CharacterInfo!"); + continue; + } + + float value = savedStat.GetAttributeFloat("statvalue", 0f); + if (value == 0f) { continue; } + + string statIdentifier = savedStat.GetAttributeString("statidentifier", "").ToLowerInvariant(); + if (string.IsNullOrEmpty(statIdentifier)) + { + DebugConsole.ThrowError("Stat identifier not specified for Stat Value when loading character data in CharacterInfo!"); + return; + } + + bool removeOnDeath = savedStat.GetAttributeBool("removeondeath", true); + ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath); + } } } LoadHeadAttachments(); @@ -939,6 +976,15 @@ namespace Barotrauma Job.IncreaseSkillLevel(skillIdentifier, increase); float newLevel = Job.GetSkillLevel(skillIdentifier); + if ((int)newLevel > (int)prevLevel) + { + Character.CheckTalents(AbilityEffectType.OnGainSkillPoint, skillIdentifier); + + foreach (Character character in Character.GetFriendlyCrew(Character)) + { + character.CheckTalents(AbilityEffectType.OnAllyGainSkillPoint, (skillIdentifier, Character)); + } + } OnSkillChanged(skillIdentifier, prevLevel, newLevel, pos); } @@ -963,6 +1009,90 @@ namespace Barotrauma partial void OnSkillChanged(string skillIdentifier, float prevLevel, float newLevel, Vector2 textPopupPos); + public void GiveExperience(int amount, float popupOffset = 0f, bool isMissionExperience = false) + { + int prevAmount = ExperiencePoints; + + var experienceGainMultiplier = new AbilityValue(1f); + if (isMissionExperience) + { + Character.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplier); + } + experienceGainMultiplier.Value += Character.GetStatValue(StatTypes.ExperienceGainMultiplier); + + amount = (int)(amount * experienceGainMultiplier.Value); + + if (amount < 0) { return; } + + ExperiencePoints += amount; + OnExperienceChanged(prevAmount, ExperiencePoints, Character.Position + Vector2.UnitY * (150.0f + popupOffset)); + } + + public void SetExperience(int newExperience) + { + if (newExperience < 0) { return; } + + int prevAmount = ExperiencePoints; + ExperiencePoints = newExperience; + OnExperienceChanged(prevAmount, ExperiencePoints, Character.Position + Vector2.UnitY * 150.0f); + } + + const int BaseExperienceRequired = 150; + const int AddedExperienceRequiredPerLevel = 350; + + public int GetTotalTalentPoints() + { + return GetCurrentLevel() + AdditionalTalentPoints - 1; + } + + public int GetAvailableTalentPoints() + { + // hashset always has at least 1 + return Math.Max(GetTotalTalentPoints() - UnlockedTalents.Count, 0); + } + + public float GetProgressTowardsNextLevel() + { + float progress = (ExperiencePoints - GetExperienceRequiredForCurrentLevel()) / (GetExperienceRequiredToLevelUp() - GetExperienceRequiredForCurrentLevel()); + return progress; + } + + public float GetExperienceRequiredForCurrentLevel() + { + GetCurrentLevel(out int experienceRequired); + return experienceRequired; + } + + public float GetExperienceRequiredToLevelUp() + { + int level = GetCurrentLevel(out int experienceRequired); + return experienceRequired + ExperienceRequiredPerLevel(level); + } + + public int GetCurrentLevel() + { + return GetCurrentLevel(out _); + } + + private int GetCurrentLevel(out int experienceRequired) + { + int level = 1; + experienceRequired = 0; + while (experienceRequired + ExperienceRequiredPerLevel(level) <= ExperiencePoints) + { + experienceRequired += ExperienceRequiredPerLevel(level); + level++; + } + return level; + } + + private int ExperienceRequiredPerLevel(int level) + { + return BaseExperienceRequired + AddedExperienceRequiredPerLevel * level; + } + + partial void OnExperienceChanged(int prevAmount, int newAmount, Vector2 textPopupPos); + public void Rename(string newName) { if (string.IsNullOrEmpty(newName)) { return; } @@ -999,6 +1129,8 @@ namespace Barotrauma new XAttribute("gender", Head.gender == Gender.Male ? "male" : "female"), new XAttribute("race", Head.race.ToString()), new XAttribute("salary", Salary), + new XAttribute("experiencepoints", ExperiencePoints), + new XAttribute("unlockedtalents", string.Join(",", UnlockedTalents)), new XAttribute("headspriteid", HeadSpriteId), new XAttribute("hairindex", HairIndex), new XAttribute("beardindex", BeardIndex), @@ -1020,6 +1152,24 @@ namespace Barotrauma Job.Save(charElement); + XElement savedStatElement = new XElement("savedstatvalues"); + foreach (var statValuePair in savedStatValues) + { + foreach (var savedStat in statValuePair.Value) + { + if (savedStat.StatValue == 0f) { continue; } + + savedStatElement.Add(new XElement("savedstatvalue", + new XAttribute("stattype", statValuePair.Key.ToString()), + new XAttribute("statidentifier", savedStat.StatIdentifier), + new XAttribute("statvalue", savedStat.StatValue), + new XAttribute("removeondeath", savedStat.RemoveOnDeath) + )); + } + } + + charElement.Add(savedStatElement); + parentElement.Add(charElement); return charElement; } @@ -1332,5 +1482,68 @@ namespace Barotrauma Portrait = null; AttachmentSprites = null; } + + // This could maybe be a LookUp instead? + private readonly Dictionary> savedStatValues = new Dictionary>(); + + public void ResetSavedStatValues() + { + foreach (var savedStatValue in savedStatValues.SelectMany(s => s.Value)) + { + if (savedStatValue.RemoveOnDeath) + { + savedStatValue.StatValue = 0f; + } + } + } + + public void ResetSavedStatValue(string statIdentifier) + { + savedStatValues.SelectMany(s => s.Value).Where(s => s.StatIdentifier == statIdentifier).ForEach(v => v.StatValue = 0f); + } + + public float GetSavedStatValue(StatTypes statType) + { + if (savedStatValues.TryGetValue(statType, out var statValues)) + { + return statValues.Sum(v => v.StatValue); + } + else + { + return 0f; + } + } + + public void ChangeSavedStatValue(StatTypes statType, float value, string statIdentifier, bool removeOnDeath) + { + if (!savedStatValues.ContainsKey(statType)) + { + savedStatValues.Add(statType, new List()); + } + + if (savedStatValues[statType].FirstOrDefault(s => s.StatIdentifier == statIdentifier) is SavedStatValue savedStat) + { + savedStat.StatValue += value; + savedStat.RemoveOnDeath = removeOnDeath; + } + else + { + savedStatValues[statType].Add(new SavedStatValue(statIdentifier, value, removeOnDeath)); + } + } + } + + public class SavedStatValue + { + public string StatIdentifier { get; set; } + public float StatValue { get; set; } + public bool RemoveOnDeath { get; set; } + + public SavedStatValue(string statIdentifier, float value, bool removeOnDeath) + { + StatValue = value; + RemoveOnDeath = removeOnDeath; + StatIdentifier = statIdentifier; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs index 82f03ec56..96b550b47 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs @@ -237,6 +237,26 @@ namespace Barotrauma (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); } + public float GetStatValue(StatTypes statType) + { + if (!(GetViableEffect() is AfflictionPrefab.Effect currentEffect)) { return 0.0f; } + + if (currentEffect.AfflictionStatValues.TryGetValue(statType, out var value)) + { + return MathHelper.Lerp( + value.minValue, + value.maxValue, + (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + } + return 0.0f; + } + + private AfflictionPrefab.Effect GetViableEffect() + { + if (Strength < Prefab.ActivationThreshold) { return null; } + return GetActiveEffect(); + } + public virtual void Update(CharacterHealth characterHealth, Limb targetLimb, float deltaTime) { foreach (AfflictionPrefab.PeriodicEffect periodicEffect in Prefab.PeriodicEffects) @@ -264,7 +284,11 @@ namespace Barotrauma if (currentEffect.StrengthChange < 0) // Reduce diminishing of buffs if boosted { - _strength += currentEffect.StrengthChange * deltaTime * StrengthDiminishMultiplier; + float durationMultiplier = 1 / (1 + (Prefab.IsBuff ? characterHealth.Character.GetStatValue(StatTypes.BuffDurationMultiplier) + : characterHealth.Character.GetStatValue(StatTypes.DebuffDurationMultiplier))); + + _strength += currentEffect.StrengthChange * deltaTime * StrengthDiminishMultiplier * durationMultiplier; + } else // Reduce strengthening of afflictions if resistant { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs index 10907fe96..82f12e892 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs @@ -212,6 +212,25 @@ namespace Barotrauma husk.Info.TeamID = CharacterTeamType.None; } + if (Prefab is AfflictionPrefabHusk huskPrefab) + { + if (huskPrefab.ControlHusk) + { +#if SERVER + var client = GameMain.Server?.ConnectedClients.FirstOrDefault(c => c.CharacterInfo.Character == character); + if (client != null) + { + GameMain.Server.SetClientCharacter(client, husk); + } +#else + if (!character.IsRemotelyControlled && character == Character.Controlled) + { + Character.Controlled = husk; + } +#endif + } + } + foreach (Limb limb in husk.AnimController.Limbs) { if (limb.type == LimbType.None) @@ -229,15 +248,19 @@ namespace Barotrauma } } + if ((Prefab as AfflictionPrefabHusk)?.TransferBuffs ?? false) + { + foreach (Affliction affliction in character.CharacterHealth.Afflictions) + { + if (affliction.Prefab.IsBuff) + { + husk.CharacterHealth.ApplyAffliction(null, affliction.Prefab.Instantiate(affliction.Strength)); + } + } + } + if (character.Inventory != null && husk.Inventory != null) { - if (character.Inventory.Capacity != husk.Inventory.Capacity) - { - string errorMsg = "Failed to move items from the source character's inventory into a husk's inventory (inventory sizes don't match)"; - DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("AfflictionHusk.CreateAIHusk:InventoryMismatch", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); - yield return CoroutineStatus.Success; - } for (int i = 0; i < character.Inventory.Capacity && i < husk.Inventory.Capacity; i++) { character.Inventory.GetItemsAt(i).ForEachMod(item => husk.Inventory.TryPutItem(item, i, true, false, null)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index 0606a8368..619fb81f0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -5,6 +5,7 @@ using System.Reflection; using System.Xml.Linq; using System.Linq; using System.Security.Cryptography; +using Barotrauma.Abilities; namespace Barotrauma { @@ -91,9 +92,11 @@ namespace Barotrauma AttachLimbType = LimbType.None; } + TransferBuffs = element.GetAttributeBool("transferbuffs", true); SendMessages = element.GetAttributeBool("sendmessages", true); CauseSpeechImpediment = element.GetAttributeBool("causespeechimpediment", true); NeedsAir = element.GetAttributeBool("needsair", false); + ControlHusk = element.GetAttributeBool("controlhusk", false); } // Use any of these to define which limb the appendage is attached to. @@ -106,9 +109,11 @@ namespace Barotrauma public readonly string[] TargetSpecies; public const string Tag = "[speciesname]"; + public readonly bool TransferBuffs; public readonly bool SendMessages; public readonly bool CauseSpeechImpediment; public readonly bool NeedsAir; + public readonly bool ControlHusk; } class AfflictionPrefab : IPrefab, IDisposable, IHasUintIdentifier @@ -116,83 +121,93 @@ namespace Barotrauma public class Effect { //this effect is applied when the strength is within this range - public float MinStrength, MaxStrength; + [Serialize(0.0f, false)] + public float MinStrength { get; private set; } + + [Serialize(0.0f, false)] + public float MaxStrength { get; private set; } + + [Serialize(0.0f, false)] + public float MinVitalityDecrease { get; private set; } + + [Serialize(0.0f, false)] + public float MaxVitalityDecrease { get; private set; } - public readonly float MinVitalityDecrease = 0.0f; - public readonly float MaxVitalityDecrease = 0.0f; - //how much the strength of the affliction changes per second - public readonly float StrengthChange = 0.0f; + [Serialize(0.0f, false)] + public float StrengthChange { get; private set; } - public readonly bool MultiplyByMaxVitality; + [Serialize(false, false)] + public bool MultiplyByMaxVitality { get; private set; } - public float MinScreenBlurStrength, MaxScreenBlurStrength; - public float MinScreenDistortStrength, MaxScreenDistortStrength; - public float MinGrainStrength, MaxGrainStrength; - public float MinRadialDistortStrength, MaxRadialDistortStrength; - public float MinChromaticAberrationStrength, MaxChromaticAberrationStrength; - public float MinSpeedMultiplier, MaxSpeedMultiplier; - public float MinBuffMultiplier, MaxBuffMultiplier; + [Serialize(0.0f, false)] + public float MinScreenBlurStrength { get; private set; } - public float MinSkillMultiplier, MaxSkillMultiplier; + [Serialize(0.0f, false)] + public float MaxScreenBlurStrength { get; private set; } - public float MinResistance, MaxResistance; - public string ResistanceFor; - public string DialogFlag; + [Serialize(0.0f, false)] + public float MinScreenDistortStrength { get; private set; } + + [Serialize(0.0f, false)] + public float MaxScreenDistortStrength { get; private set; } + + [Serialize(0.0f, false)] + public float MinRadialDistortStrength { get; private set; } + + [Serialize(0.0f, false)] + public float MaxRadialDistortStrength { get; private set; } + + [Serialize(0.0f, false)] + public float MinChromaticAberrationStrength { get; private set; } + + [Serialize(0.0f, false)] + public float MaxChromaticAberrationStrength { get; private set; } + + [Serialize(0.0f, false)] + public float MinGrainStrength { get; private set; } + + [Serialize(0.0f, false)] + public float MaxGrainStrength { get; private set; } + + [Serialize(1.0f, false)] + public float MinBuffMultiplier { get; private set; } + + [Serialize(1.0f, false)] + public float MaxBuffMultiplier { get; private set; } + + [Serialize(1.0f, false)] + public float MinSpeedMultiplier { get; private set; } + + [Serialize(1.0f, false)] + public float MaxSpeedMultiplier { get; private set; } + + [Serialize(1.0f, false)] + public float MinSkillMultiplier { get; private set; } + + [Serialize(1.0f, false)] + public float MaxSkillMultiplier { get; private set; } + + [Serialize("", false)] + public string ResistanceFor { get; private set; } + + [Serialize(0.0f, false)] + public float MinResistance { get; private set; } + + [Serialize(0.0f, false)] + public float MaxResistance { get; private set; } + + [Serialize("", false)] + public string DialogFlag { get; private set; } + + public readonly Dictionary AfflictionStatValues = new Dictionary(); //statuseffects applied on the character when the affliction is active public readonly List StatusEffects = new List(); public Effect(XElement element, string parentDebugName) { - MinStrength = element.GetAttributeFloat("minstrength", 0); - MaxStrength = element.GetAttributeFloat("maxstrength", 0); - - MultiplyByMaxVitality = element.GetAttributeBool("multiplybymaxvitality", false); - - MinVitalityDecrease = element.GetAttributeFloat("minvitalitydecrease", 0.0f); - MaxVitalityDecrease = element.GetAttributeFloat("maxvitalitydecrease", 0.0f); - MaxVitalityDecrease = Math.Max(MinVitalityDecrease, MaxVitalityDecrease); - - MinScreenDistortStrength = element.GetAttributeFloat("minscreendistort", 0.0f); - MaxScreenDistortStrength = element.GetAttributeFloat("maxscreendistort", 0.0f); - MaxScreenDistortStrength = Math.Max(MinScreenDistortStrength, MaxScreenDistortStrength); - - MinRadialDistortStrength = element.GetAttributeFloat("minradialdistort", 0.0f); - MaxRadialDistortStrength = element.GetAttributeFloat("maxradialdistort", 0.0f); - MaxRadialDistortStrength = Math.Max(MinRadialDistortStrength, MaxRadialDistortStrength); - - MinChromaticAberrationStrength = element.GetAttributeFloat("minchromaticaberration", 0.0f); - MaxChromaticAberrationStrength = element.GetAttributeFloat("maxchromaticaberration", 0.0f); - MaxChromaticAberrationStrength = Math.Max(MinChromaticAberrationStrength, MaxChromaticAberrationStrength); - - MinGrainStrength = element.GetAttributeFloat(nameof(MinGrainStrength).ToLower(), 0.0f); - MaxGrainStrength = element.GetAttributeFloat(nameof(MaxGrainStrength).ToLower(), 0.0f); - MaxGrainStrength = Math.Max(MinGrainStrength, MaxGrainStrength); - - MinScreenBlurStrength = element.GetAttributeFloat("minscreenblur", 0.0f); - MaxScreenBlurStrength = element.GetAttributeFloat("maxscreenblur", 0.0f); - MaxScreenBlurStrength = Math.Max(MinScreenBlurStrength, MaxScreenBlurStrength); - - MinSkillMultiplier = element.GetAttributeFloat("minskillmultiplier", 1.0f); - MaxSkillMultiplier = element.GetAttributeFloat("maxskillmultiplier", 1.0f); - - ResistanceFor = element.GetAttributeString("resistancefor", ""); - MinResistance = element.GetAttributeFloat("minresistance", 0.0f); - MaxResistance = element.GetAttributeFloat("maxresistance", 0.0f); - MaxResistance = Math.Max(MinResistance, MaxResistance); - - MinSpeedMultiplier = element.GetAttributeFloat("minspeedmultiplier", 1.0f); - MaxSpeedMultiplier = element.GetAttributeFloat("maxspeedmultiplier", 1.0f); - MaxSpeedMultiplier = Math.Max(MinSpeedMultiplier, MaxSpeedMultiplier); - - MinBuffMultiplier = element.GetAttributeFloat("minbuffmultiplier", 1.0f); - MaxBuffMultiplier = element.GetAttributeFloat("maxbuffmultiplier", 1.0f); - MaxBuffMultiplier = Math.Max(MinBuffMultiplier, MaxBuffMultiplier); - - DialogFlag = element.GetAttributeString("dialogflag", ""); - - StrengthChange = element.GetAttributeFloat("strengthchange", 0.0f); + SerializableProperty.DeserializeProperties(this, element); foreach (XElement subElement in element.Elements()) { @@ -201,6 +216,15 @@ namespace Barotrauma case "statuseffect": StatusEffects.Add(StatusEffect.Load(subElement, parentDebugName)); break; + case "statvalue": + var statType = CharacterAbilityGroup.ParseStatType(subElement.GetAttributeString("stattype", ""), parentDebugName); + + float defaultValue = subElement.GetAttributeFloat("value", 0f); + float minValue = subElement.GetAttributeFloat("minvalue", defaultValue); + float maxValue = subElement.GetAttributeFloat("maxvalue", defaultValue); + + AfflictionStatValues.TryAdd(statType, (minValue, maxValue)); + break; } } } @@ -590,7 +614,7 @@ namespace Barotrauma ShowIconThreshold = element.GetAttributeFloat("showiconthreshold", Math.Max(ActivationThreshold, 0.05f)); ShowIconToOthersThreshold = element.GetAttributeFloat("showicontoothersthreshold", ShowIconThreshold); MaxStrength = element.GetAttributeFloat("maxstrength", 100.0f); - GrainBurst = element.GetAttributeFloat(nameof(GrainBurst).ToLower(), 0.0f); + GrainBurst = element.GetAttributeFloat(nameof(GrainBurst).ToLowerInvariant(), 0.0f); ShowInHealthScannerThreshold = element.GetAttributeFloat("showinhealthscannerthreshold", Math.Max(ActivationThreshold, 0.05f)); TreatmentThreshold = element.GetAttributeFloat("treatmentthreshold", Math.Max(ActivationThreshold, 5.0f)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index ad2d820c5..a4908115e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -116,15 +116,15 @@ namespace Barotrauma private set => Character.Params.Health.CrushDepth = value; } - private List limbHealths = new List(); + private readonly List limbHealths = new List(); //non-limb-specific afflictions - private List afflictions = new List(); + private readonly List afflictions = new List(); /// /// Note: returns only the non-limb-secific afflictions. Use GetAllAfflictions or some other method for getting also the limb-specific afflictions. /// public IEnumerable Afflictions => afflictions; - private HashSet irremovableAfflictions = new HashSet(); + private readonly HashSet irremovableAfflictions = new HashSet(); private Affliction bloodlossAffliction; private Affliction oxygenLowAffliction; private Affliction pressureAffliction; @@ -151,6 +151,7 @@ namespace Barotrauma max += Character.Info.Job.Prefab.VitalityModifier; } max *= Character.StaticHealthMultiplier; + max *= 1f + Character.GetStatValue(StatTypes.MaximumHealthMultiplier); return max * Character.HealthMultiplier; } } @@ -434,10 +435,21 @@ namespace Barotrauma float temp = afflictions[i].GetResistance(resistanceId); if (temp > resistance) resistance = temp; } + resistance = 1 - ((1 - resistance) * Character.GetAbilityResistance(resistanceId)); return resistance; } + public float GetStatValue(StatTypes statType) + { + float value = 0f; + for (int i = 0; i < afflictions.Count; i++) + { + value += afflictions[i].GetStatValue(statType); + } + return value; + } + private readonly List matchingAfflictions = new List(); public void ReduceAffliction(Limb targetLimb, string affliction, float amount) { @@ -468,6 +480,11 @@ namespace Barotrauma for (int i = matchingAfflictions.Count - 1; i >= 0; i--) { var matchingAffliction = matchingAfflictions[i]; + + // kind of bad to create a tuple every time, but I can't think of another way to easily do this + var afflictionReduction = (matchingAffliction, reduceAmount); + Character.CheckTalents(AbilityEffectType.OnReduceAffliction, afflictionReduction); + if (matchingAffliction.Strength < reduceAmount) { float surplus = reduceAmount - matchingAffliction.Strength; @@ -539,9 +556,9 @@ namespace Barotrauma else { // Instead of using the limbhealth count here, I think it's best to define the max vitality per limb roughly with a constant value. - // Therefore with e.g. 80 health, the max damage per limb would be 20. - // Having at least 20 damage on both legs would cause maximum limping. - float max = MaxVitality / 4; + // Therefore with e.g. 80 health, the max damage per limb would be 40. + // Having at least 40 damage on both legs would cause maximum limping. + float max = MaxVitality / 2; if (string.IsNullOrEmpty(afflictionType)) { float damage = GetAfflictionStrength("damage", limb, true); @@ -738,7 +755,15 @@ namespace Barotrauma affliction.DamagePerSecondTimer += deltaTime; Character.StackSpeedMultiplier(affliction.GetSpeedMultiplier()); } - + + Character.StackSpeedMultiplier(1f + Character.GetStatValue(StatTypes.MovementSpeed)); + + // maybe a bit of a hacky way to do this. should inquire if there is a better way. M61T + if (Character.InWater) + { + Character.StackSpeedMultiplier(1f + Character.GetStatValue(StatTypes.SwimmingSpeed)); + } + UpdateLimbAfflictionOverlays(); CalculateVitality(); @@ -825,8 +850,8 @@ namespace Barotrauma { if (Unkillable || Character.GodMode) { return; } - var causeOfDeath = GetCauseOfDeath(); - Character.Kill(causeOfDeath.First, causeOfDeath.Second); + var (type, affliction) = GetCauseOfDeath(); + Character.Kill(type, affliction); #if CLIENT DisplayVitalityDelay = 0.0f; DisplayedVitality = Vitality; @@ -859,7 +884,7 @@ namespace Barotrauma } } - public Pair GetCauseOfDeath() + public (CauseOfDeathType type, Affliction affliction) GetCauseOfDeath() { List currentAfflictions = GetAllAfflictions(true); @@ -880,7 +905,7 @@ namespace Barotrauma causeOfDeath = Character.AnimController.InWater ? CauseOfDeathType.Drowning : CauseOfDeathType.Suffocation; } - return new Pair(causeOfDeath, strongestAffliction); + return (causeOfDeath, strongestAffliction); } // TODO: this method is called a lot (every half second) -> optimize, don't create new class instances and lists every time! @@ -968,7 +993,7 @@ namespace Barotrauma } private readonly List activeAfflictions = new List(); - private readonly List> limbAfflictions = new List>(); + private readonly List<(LimbHealth limbHealth, Affliction affliction)> limbAfflictions = new List<(LimbHealth limbHealth, Affliction affliction)>(); public void ServerWrite(IWriteMessage msg) { activeAfflictions.Clear(); @@ -999,22 +1024,22 @@ namespace Barotrauma foreach (Affliction limbAffliction in limbHealth.Afflictions) { if (limbAffliction.Strength <= 0.0f || limbAffliction.Strength < limbAffliction.Prefab.ActivationThreshold) continue; - limbAfflictions.Add(new Pair(limbHealth, limbAffliction)); + limbAfflictions.Add((limbHealth, limbAffliction)); } } msg.Write((byte)limbAfflictions.Count); - foreach (var limbAffliction in limbAfflictions) + foreach (var (limbHealth, affliction) in limbAfflictions) { - msg.WriteRangedInteger(limbHealths.IndexOf(limbAffliction.First), 0, limbHealths.Count - 1); - msg.Write(limbAffliction.Second.Prefab.UIntIdentifier); + msg.WriteRangedInteger(limbHealths.IndexOf(limbHealth), 0, limbHealths.Count - 1); + msg.Write(affliction.Prefab.UIntIdentifier); msg.WriteRangedSingle( - MathHelper.Clamp(limbAffliction.Second.Strength, 0.0f, limbAffliction.Second.Prefab.MaxStrength), - 0.0f, limbAffliction.Second.Prefab.MaxStrength, 8); - msg.Write((byte)limbAffliction.Second.Prefab.PeriodicEffects.Count()); - foreach (AfflictionPrefab.PeriodicEffect periodicEffect in limbAffliction.Second.Prefab.PeriodicEffects) + MathHelper.Clamp(affliction.Strength, 0.0f, affliction.Prefab.MaxStrength), + 0.0f, affliction.Prefab.MaxStrength, 8); + msg.Write((byte)affliction.Prefab.PeriodicEffects.Count()); + foreach (AfflictionPrefab.PeriodicEffect periodicEffect in affliction.Prefab.PeriodicEffects) { - msg.WriteRangedSingle(limbAffliction.Second.PeriodicEffectTimers[periodicEffect], periodicEffect.MinInterval, periodicEffect.MaxInterval, 8); + msg.WriteRangedSingle(affliction.PeriodicEffectTimers[periodicEffect], periodicEffect.MinInterval, periodicEffect.MaxInterval, 8); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs index f87c976ed..e1dfcebc8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs @@ -217,10 +217,6 @@ namespace Barotrauma if (item.Prefab.Identifier == "idcard" || item.Prefab.Identifier == "idcardwreck") { item.AddTag("name:" + character.Name); - if (Level.Loaded != null) - { - item.ReplaceTag("wreck_id", Level.Loaded.GetWreckIDTag("wreck_id", submarine)); - } var job = character.Info?.Job; if (job != null) { @@ -229,6 +225,10 @@ namespace Barotrauma IdCard idCardComponent = item.GetComponent(); idCardComponent?.Initialize(character.Info); + if (submarine != null && (submarine.Info.IsWreck || submarine.Info.IsOutpost)) + { + idCardComponent.SubmarineSpecificID = submarine.SubmarineSpecificIDTag; + } var idCardTags = itemElement.GetAttributeStringArray("tags", new string[0]); foreach (string tag in idCardTags) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index 92087ad79..ed33edcd9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -219,7 +219,7 @@ namespace Barotrauma public bool inWater; - private readonly FixedMouseJoint pullJoint; + private FixedMouseJoint pullJoint; public readonly LimbType type; @@ -683,7 +683,7 @@ namespace Barotrauma private readonly List appliedDamageModifiers = new List(); private readonly List tempModifiers = new List(); private readonly List afflictionsCopy = new List(); - public AttackResult AddDamage(Vector2 simPosition, IEnumerable afflictions, bool playSound, float damageMultiplier = 1, float penetration = 0f) + public AttackResult AddDamage(Vector2 simPosition, IEnumerable afflictions, bool playSound, float damageMultiplier = 1, float penetration = 0f, Character attacker = null) { appliedDamageModifiers.Clear(); afflictionsCopy.Clear(); @@ -741,7 +741,7 @@ namespace Barotrauma { newAffliction.SetStrength(affliction.NonClampedStrength); } - + attacker?.CheckTalents(AbilityEffectType.OnAddDamageAffliction, newAffliction); if (applyAffliction) { afflictionsCopy.Add(newAffliction); @@ -1263,6 +1263,14 @@ namespace Barotrauma { body?.Remove(); body = null; + if (pullJoint != null) + { + if (GameMain.World.JointList.Contains(pullJoint)) + { + GameMain.World.Remove(pullJoint); + } + pullJoint = null; + } Release(); RemoveProjSpecific(); Removed = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index 40e20c196..59e92989f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -70,6 +70,9 @@ namespace Barotrauma [Serialize(1f, true), Editable] public float BleedParticleMultiplier { get; private set; } + [Serialize(true, true, description: "Can the creature eat bodies? Used by player controlled creatures to allow them to eat. Currently applicable only to non-humanoids. To allow an AI controller to eat, just add an ai target with the state \"eat\""), Editable] + public bool CanEat { get; set; } + [Serialize(10f, true, description: "How effectively/easily the character eats other characters. Affects the forces, the amount of particles, and the time required before the target is eaten away"), Editable(MinValueFloat = 1, MaxValueFloat = 1000, ValueStep = 1)] public float EatingSpeed { get; set; } @@ -88,6 +91,9 @@ namespace Barotrauma [Serialize(25000f, true, "If the character is farther than this (in pixels) from the sub and the players, it will be disabled. The halved value is used for triggering simple physics where the ragdoll is disabled and only the main collider is updated."), Editable(MinValueFloat = 10000f, MaxValueFloat = 100000f)] public float DisableDistance { get; set; } + [Serialize(10f, true, "How frequent the recurring idle and attack sounds are?"), Editable(MinValueFloat = 1f, MaxValueFloat = 100f)] + public float SoundInterval { get; set; } + public readonly string File; public XDocument VariantFile { get; private set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityCondition.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityCondition.cs new file mode 100644 index 000000000..80b4be072 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityCondition.cs @@ -0,0 +1,89 @@ +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + abstract class AbilityCondition + { + protected CharacterTalent characterTalent; + protected Character character; + protected bool invert; + + public virtual bool AllowClientSimulation => true; + + public AbilityCondition(CharacterTalent characterTalent, XElement conditionElement) + { + this.characterTalent = characterTalent; + character = characterTalent.Character; + invert = conditionElement.GetAttributeBool("invert", false); + } + public abstract bool MatchesCondition(object abilityData); + public abstract bool MatchesCondition(); + + + // tools + protected enum TargetType + { + Any = 0, + Enemy = 1, + Ally = 2, + NotSelf = 3, + Alive = 4, + Monster = 5, + }; + + protected List ParseTargetTypes(string[] targetTypeStrings) + { + List targetTypes = new List(); + foreach (string targetTypeString in targetTypeStrings) + { + TargetType targetType = TargetType.Any; + if (!Enum.TryParse(targetTypeString, true, out targetType)) + { + DebugConsole.ThrowError("Invalid target type type \"" + targetTypeString + "\" in CharacterTalent (" + characterTalent.DebugIdentifier + ")"); + } + targetTypes.Add(targetType); + } + return targetTypes; + } + + protected bool IsViableTarget(IEnumerable targetTypes, Character targetCharacter) + { + if (targetCharacter == null) { return false; } + + bool isViable = true; + foreach (TargetType targetType in targetTypes) + { + if (!IsViableTarget(targetType, targetCharacter)) + { + isViable = false; + break; + } + } + return isViable; + } + + private bool IsViableTarget(TargetType targetType, Character targetCharacter) + { + switch (targetType) + { + case TargetType.Enemy: + return !HumanAIController.IsFriendly(character, targetCharacter); + case TargetType.Ally: + return HumanAIController.IsFriendly(character, targetCharacter); + case TargetType.NotSelf: + return targetCharacter != character; + case TargetType.Alive: + return !targetCharacter.IsDead; + case TargetType.Monster: + return !targetCharacter.IsHuman; + default: + return true; + } + } + + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs new file mode 100644 index 000000000..2a80e9122 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs @@ -0,0 +1,79 @@ +using Barotrauma.Items.Components; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionAttackData : AbilityConditionData + { + private enum WeaponType + { + Any = 0, + Melee = 1, + Ranged = 2 + }; + + private readonly string itemIdentifier; + private readonly string[] tags; + private WeaponType weapontype; + public AbilityConditionAttackData(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + itemIdentifier = conditionElement.GetAttributeString("itemidentifier", ""); + tags = conditionElement.GetAttributeStringArray("tags", new string[0], convertToLowerInvariant: true); + switch (conditionElement.GetAttributeString("weapontype", "")) + { + case "melee": + weapontype = WeaponType.Melee; + break; + case "ranged": + weapontype = WeaponType.Ranged; + break; + } + } + + protected override bool MatchesConditionSpecific(object abilityData) + { + if (abilityData is AttackData attackData) + { + Item item = attackData?.SourceAttack?.SourceItem; + + if (item == null) + { + DebugConsole.AddWarning($"Source Item was not found in {this} for talent {characterTalent.DebugIdentifier}!"); + return false; + } + + if (!string.IsNullOrEmpty(itemIdentifier)) + { + if (item.prefab.Identifier != itemIdentifier) + { + return false; + } + } + + if (tags.Any()) + { + if (!tags.All(t => item.HasTag(t))) + { + return false; + } + } + + switch (weapontype) + { + case WeaponType.Melee: + return item.GetComponent() != null; + case WeaponType.Ranged: + return item.GetComponent() != null; + } + + return true; + } + else + { + LogAbilityConditionError(abilityData, typeof(AttackData)); + return false; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackResult.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackResult.cs new file mode 100644 index 000000000..909c694f2 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackResult.cs @@ -0,0 +1,38 @@ +using Barotrauma.Items.Components; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionAttackResult : AbilityConditionData + { + private readonly List targetTypes; + private readonly string[] afflictions; + public AbilityConditionAttackResult(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + targetTypes = ParseTargetTypes(conditionElement.GetAttributeStringArray("targettypes", new string[0], convertToLowerInvariant: true)); + afflictions = conditionElement.GetAttributeStringArray("afflictions", new string[0], convertToLowerInvariant: true); + } + + protected override bool MatchesConditionSpecific(object abilityData) + { + if (abilityData is AttackResult attackResult) + { + if (!IsViableTarget(targetTypes, attackResult.HitLimb?.character)) { return false; } + + if (afflictions.Any()) + { + if (!afflictions.Any(a => attackResult.Afflictions.Select(c => c.Identifier).Contains(a))) { return false; } + } + + return true; + } + else + { + LogAbilityConditionError(abilityData, typeof(AttackData)); + return false; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs new file mode 100644 index 000000000..1254f8058 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionCharacter : AbilityConditionData + { + private readonly List targetTypes; + + public AbilityConditionCharacter(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + targetTypes = ParseTargetTypes(conditionElement.GetAttributeStringArray("targettypes", new string[0], convertToLowerInvariant: true)); + } + + protected override bool MatchesConditionSpecific(object abilityData) + { + if (abilityData is Character character) + { + if (!IsViableTarget(targetTypes, character)) { return false; } + + return true; + } + else + { + LogAbilityConditionError(abilityData, typeof(Character)); + return false; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionData.cs new file mode 100644 index 000000000..d80a6257d --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionData.cs @@ -0,0 +1,39 @@ +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + abstract class AbilityConditionData : AbilityCondition + { + /// + /// Some conditions rely on specific ability data that is integrally connected to the AbilityEffectType. + /// This is done in order to avoid having to create duplicate ability behavior, such as if an ability needs to trigger + /// a common ability effect but in specific circumstances. These conditions could also be partially replaced by + /// more explicit AbilityEffectType enums, but this would introduce bloat and overhead to integral game logic + /// when instead said logic can be made to only run when required using these conditions. + /// + /// These conditions will return an error if used outside their limited intended use. + /// + public AbilityConditionData(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { } + + protected void LogAbilityConditionError(T abilityData, Type expectedData) + { + DebugConsole.ThrowError($"Used data-reliant ability condition when data is incompatible! Expected {expectedData}, but received {abilityData}"); + } + + protected abstract bool MatchesConditionSpecific(object abilityData); + public override bool MatchesCondition() + { + DebugConsole.ThrowError("Used data-reliant ability condition in a state-based ability! This is not allowed."); + return false; + } + public override bool MatchesCondition(object abilityData) + { + if (abilityData is null) { return invert; } + return invert ? !MatchesConditionSpecific(abilityData) : MatchesConditionSpecific(abilityData); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionEvasiveManeuvers.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionEvasiveManeuvers.cs new file mode 100644 index 000000000..44336e5b8 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionEvasiveManeuvers.cs @@ -0,0 +1,22 @@ +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionEvasiveManeuvers : AbilityConditionData + { + public AbilityConditionEvasiveManeuvers(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { } + + protected override bool MatchesConditionSpecific(object abilityData) + { + if (abilityData is Submarine submarine) + { + return submarine.TeamID == character.TeamID && character.Submarine == submarine; + } + else + { + LogAbilityConditionError(abilityData, typeof(Submarine)); + return false; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionHandsomeStranger.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionHandsomeStranger.cs new file mode 100644 index 000000000..55ee4fa06 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionHandsomeStranger.cs @@ -0,0 +1,27 @@ +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionHandsomeStranger : AbilityConditionData + { + string skillIdentifier; + + public AbilityConditionHandsomeStranger(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + skillIdentifier = conditionElement.GetAttributeString("skillidentifier", "").ToLowerInvariant(); + } + + protected override bool MatchesConditionSpecific(object abilityData) + { + if (abilityData is string skillIdentifier) + { + return this.skillIdentifier == skillIdentifier; + } + else + { + LogAbilityConditionError(abilityData, typeof(string)); + return false; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs new file mode 100644 index 000000000..cfa1db21c --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs @@ -0,0 +1,50 @@ +using System; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionItem : AbilityConditionData + { + private readonly string identifier; + private readonly string[] tags; + + public AbilityConditionItem(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + identifier = conditionElement.GetAttributeString("identifier", string.Empty).ToLowerInvariant(); + tags = conditionElement.GetAttributeStringArray("tags", Array.Empty(), convertToLowerInvariant: true); + } + + protected override bool MatchesConditionSpecific(object abilityData) + { + ItemPrefab item = null; + if (abilityData is Item tempItem) + { + item = tempItem.Prefab; + } + // this and other instances of this type of casting will be refactored + else if (abilityData is (ItemPrefab itemPrefab, object _)) + { + item = itemPrefab; + } + + if (item != null) + { + if (!string.IsNullOrEmpty(identifier)) + { + if (item.Identifier != identifier) + { + return false; + } + } + + return tags.Any(t => item.Tags.Any(p => t == p)); + } + else + { + LogAbilityConditionError(abilityData, typeof(Item)); + return false; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionReduceAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionReduceAffliction.cs new file mode 100644 index 000000000..24044dc0e --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionReduceAffliction.cs @@ -0,0 +1,33 @@ +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionReduceAffliction : AbilityConditionData + { + private readonly string[] allowedTypes; + private readonly string identifier; + + public AbilityConditionReduceAffliction(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + allowedTypes = conditionElement.GetAttributeStringArray("allowedtypes", new string[0], convertToLowerInvariant: true); + identifier = conditionElement.GetAttributeString("identifier", ""); + } + + protected override bool MatchesConditionSpecific(object abilityData) + { + if (abilityData is (Affliction affliction, float reduceAmount)) + { + if (allowedTypes.Find(c => c == affliction.Prefab.AfflictionType) == null) { return false; } + + if (!string.IsNullOrEmpty(identifier) && affliction.Prefab.Identifier != identifier) { return false; } + + return true; + } + else + { + LogAbilityConditionError(abilityData, typeof((Affliction, float))); + return false; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionScavenger.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionScavenger.cs new file mode 100644 index 000000000..2156ccae1 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionScavenger.cs @@ -0,0 +1,22 @@ +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionScavenger : AbilityConditionData + { + public AbilityConditionScavenger(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { } + + protected override bool MatchesConditionSpecific(object abilityData) + { + if (abilityData is Item item) + { + return item.Submarine != character.Submarine; + } + else + { + LogAbilityConditionError(abilityData, typeof(Item)); + return false; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAboveVitality.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAboveVitality.cs new file mode 100644 index 000000000..6543c7b32 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAboveVitality.cs @@ -0,0 +1,20 @@ +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionAboveVitality : AbilityConditionDataless + { + float vitalityPercentage; + + public AbilityConditionAboveVitality(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + vitalityPercentage = conditionElement.GetAttributeFloat("vitalitypercentage", 0f); + } + + protected override bool MatchesConditionSpecific() + { + return character.HealthPercentage / 100f > vitalityPercentage; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAlliesAboveVitality.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAlliesAboveVitality.cs new file mode 100644 index 000000000..29256ab7c --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAlliesAboveVitality.cs @@ -0,0 +1,19 @@ +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionAlliesAboveVitality : AbilityConditionDataless + { + float vitalityPercentage; + + public AbilityConditionAlliesAboveVitality(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + vitalityPercentage = conditionElement.GetAttributeFloat("vitalitypercentage", 0f); + } + protected override bool MatchesConditionSpecific() + { + return Character.GetFriendlyCrew(character).All(c => c.HealthPercentage / 100f >= vitalityPercentage); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCrouched.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCrouched.cs new file mode 100644 index 000000000..cd96edb58 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCrouched.cs @@ -0,0 +1,18 @@ +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionCrouched : AbilityConditionDataless + { + + public AbilityConditionCrouched(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + } + + protected override bool MatchesConditionSpecific() + { + return character.AnimController is HumanoidAnimController humanoidAnimController && humanoidAnimController.Crouching; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionDataless.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionDataless.cs new file mode 100644 index 000000000..023fe029f --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionDataless.cs @@ -0,0 +1,24 @@ +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + abstract class AbilityConditionDataless : AbilityCondition + { + public AbilityConditionDataless(CharacterTalent characterTalent, XElement conditionElement) : base (characterTalent, conditionElement) { } + + protected abstract bool MatchesConditionSpecific(); + public override bool MatchesCondition() + { + return invert ? !MatchesConditionSpecific() : MatchesConditionSpecific(); + } + + public override bool MatchesCondition(object abilityData) + { + return invert ? !MatchesConditionSpecific() : MatchesConditionSpecific(); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasAffliction.cs new file mode 100644 index 000000000..9f449e43c --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasAffliction.cs @@ -0,0 +1,31 @@ +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionHasAffliction : AbilityConditionDataless + { + private string afflictionIdentifier; + private float minimumPercentage; + + + public AbilityConditionHasAffliction(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + afflictionIdentifier = conditionElement.GetAttributeString("afflictionidentifier", ""); + minimumPercentage = conditionElement.GetAttributeFloat("minimumpercentage", 0f); + } + + protected override bool MatchesConditionSpecific() + { + if (!string.IsNullOrEmpty(afflictionIdentifier)) + { + var affliction = character.CharacterHealth.GetAffliction(afflictionIdentifier); + + if (affliction == null) { return false; } + + return minimumPercentage <= affliction.Strength / affliction.Prefab.MaxStrength; + } + return false; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasDifferentJobs.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasDifferentJobs.cs new file mode 100644 index 000000000..0f1707d3d --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasDifferentJobs.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionHasDifferentJobs : AbilityConditionDataless + { + private readonly int amount; + public AbilityConditionHasDifferentJobs(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + amount = conditionElement.GetAttributeInt("amount", 0); + } + + protected override bool MatchesConditionSpecific() + { + IEnumerable crewmembers = Character.GetFriendlyCrew(character); + int differentCrewAmount = crewmembers.Select(c => c.Info?.Job?.Prefab.Identifier).Distinct().Count(); + return differentCrewAmount >= amount; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs new file mode 100644 index 000000000..8f4fc7c35 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs @@ -0,0 +1,57 @@ +using Barotrauma.Items.Components; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionHasItem : AbilityConditionDataless + { + // not used for anything atm, will be used for clown subclass + private readonly string[] tags; + private InvSlotType? invSlotType; + bool requireAll; + + private List items = new List(); + + public AbilityConditionHasItem(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + tags = conditionElement.GetAttributeStringArray("tags", new string[0], convertToLowerInvariant: true); + requireAll = conditionElement.GetAttributeBool("requireall", false); + //this.invSlotType = invSlotType; + } + + protected override bool MatchesConditionSpecific() + { + items.Clear(); + if (tags.Any()) + { + foreach (string tag in tags) + { + // there is a better method, should use that instead + if (character.GetEquippedItem(tag, invSlotType) is Item foundItem) + { + items.Add(foundItem); + } + } + + } + else + { + if (character.GetEquippedItem(null, invSlotType) is Item foundItem) + { + items.Add(foundItem); + } + } + + if (requireAll) + { + return (items.Count >= tags.Count()); + } + else + { + return items.Any(); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInWater.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInWater.cs new file mode 100644 index 000000000..d93731514 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInWater.cs @@ -0,0 +1,15 @@ + +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionInWater : AbilityConditionDataless + { + public AbilityConditionInWater(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { } + + protected override bool MatchesConditionSpecific() + { + return character.InWater; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionMission.cs new file mode 100644 index 000000000..f27ecf4c1 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionMission.cs @@ -0,0 +1,38 @@ +using System; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionMission : AbilityConditionData + { + private readonly MissionType missionType; + public AbilityConditionMission(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + string missionTypeString = conditionElement.GetAttributeString("missiontype", "None"); + if (!Enum.TryParse(missionTypeString, out missionType)) + { + DebugConsole.ThrowError("Error in AbilityConditionMission \"" + characterTalent.DebugIdentifier + "\" - \"" + missionTypeString + "\" is not a valid mission type."); + return; + } + if (missionType == MissionType.None) + { + DebugConsole.ThrowError("Error in AbilityConditionMission \"" + characterTalent.DebugIdentifier + "\" - mission type cannot be none."); + return; + } + } + + protected override bool MatchesConditionSpecific(object abilityData) + { + if (abilityData is (Mission mission, AbilityValue missionAbilityValue)) + { + return mission.Prefab.Type == missionType; + } + else + { + LogAbilityConditionError(abilityData, typeof((Mission, AbilityValue))); + return false; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionNoCrewDied.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionNoCrewDied.cs new file mode 100644 index 000000000..bb4390106 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionNoCrewDied.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionNoCrewDied : AbilityConditionDataless + { + public AbilityConditionNoCrewDied(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + } + + protected override bool MatchesConditionSpecific() + { + if (GameMain.GameSession?.Campaign is CampaignMode campaign) + { + return !campaign.CrewHasDied; + } + return true; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionOnMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionOnMission.cs new file mode 100644 index 000000000..dac9a3f1a --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionOnMission.cs @@ -0,0 +1,17 @@ +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionOnMission : AbilityConditionDataless + { + public AbilityConditionOnMission(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + } + + protected override bool MatchesConditionSpecific() + { + return Level.Loaded?.Type != LevelData.LevelType.Outpost; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionRagdolled.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionRagdolled.cs new file mode 100644 index 000000000..192ea6f4f --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionRagdolled.cs @@ -0,0 +1,18 @@ +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionRagdolled : AbilityConditionDataless + { + + public AbilityConditionRagdolled(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + } + + protected override bool MatchesConditionSpecific() + { + return character.IsRagdolled || character.Stun > 0f || character.IsIncapacitated; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionRunning.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionRunning.cs new file mode 100644 index 000000000..3186b852f --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionRunning.cs @@ -0,0 +1,15 @@ + +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionRunning : AbilityConditionDataless + { + public AbilityConditionRunning(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { } + + protected override bool MatchesConditionSpecific() + { + return character.AnimController is HumanoidAnimController animController && animController.IsMovingFast; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionServerRandom.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionServerRandom.cs new file mode 100644 index 000000000..3cc8ae4f5 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionServerRandom.cs @@ -0,0 +1,24 @@ +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionServerRandom : AbilityConditionDataless + { + private float randomChance = 0f; + public override bool AllowClientSimulation => false; + + public AbilityConditionServerRandom(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + randomChance = conditionElement.GetAttributeFloat("randomchance", 1f); + } + + protected override bool MatchesConditionSpecific() + { + return randomChance >= Rand.Range(0f, 1f, Rand.RandSync.Unsynced); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionShipFlooded.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionShipFlooded.cs new file mode 100644 index 000000000..ba5f10ccc --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionShipFlooded.cs @@ -0,0 +1,21 @@ +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionShipFlooded : AbilityConditionDataless + { + private readonly float floodPercentage; + public AbilityConditionShipFlooded(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + floodPercentage = conditionElement.GetAttributeFloat("floodpercentage", 0f); + } + + protected override bool MatchesConditionSpecific() + { + if (character.Submarine == null || character.Submarine.TeamID != character.TeamID) { return false; } + float currentFloodPercentage = character.Submarine.GetHulls(false).Average(h => h.WaterPercentage); + return currentFloodPercentage / 100 > floodPercentage; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs new file mode 100644 index 000000000..739e7ced1 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + abstract class CharacterAbility + { + public CharacterAbilityGroup CharacterAbilityGroup { get; } + public CharacterTalent CharacterTalent { get; } + public Character Character { get; } + + public virtual bool RequiresAlive => true; + public virtual bool AllowClientSimulation => false; + public virtual bool AppliesEffectOnIntervalUpdate => false; + + private const float DefaultEffectTime = 1.0f; + + /// + /// Used primarily for StatusEffects. Default to constant outside interval abilities. + /// + protected float EffectDeltaTime => CharacterAbilityGroup is CharacterAbilityGroupInterval abilityGroupInterval ? abilityGroupInterval.TimeSinceLastUpdate : DefaultEffectTime; + + public CharacterAbility(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) + { + CharacterAbilityGroup = characterAbilityGroup; + CharacterTalent = characterAbilityGroup.CharacterTalent; + Character = CharacterTalent.Character; + } + + public bool IsViable() + { + if (!AllowClientSimulation && GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; } + if (RequiresAlive && Character.IsDead) { return false; } + return true; + } + + public virtual void InitializeAbility(bool addingFirstTime) { } + + public virtual void UpdateCharacterAbility(bool conditionsMatched, float timeSinceLastUpdate) + { + // may need a separate Update for changing state on non-interval-based abilities + if (AppliesEffectOnIntervalUpdate) + { + if (conditionsMatched) + { + ApplyEffect(); + } + } + else + { + VerifyState(conditionsMatched, timeSinceLastUpdate); + } + } + + protected virtual void VerifyState(bool conditionsMatched, float timeSinceLastUpdate) + { + DebugConsole.ThrowError($"Ability {this} does not have an implementation for VerifyState! This ability does not work in interval ability groups."); + } + + public void ApplyAbilityEffect(object abilityData) + { + if (abilityData is null) + { + ApplyEffect(); + } + else + { + ApplyEffect(abilityData); + } + } + + protected virtual void ApplyEffect() + { + DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not have a definition for ApplyEffect"); + } + + protected virtual void ApplyEffect(object abilityData) + { + DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not take a parameter for ApplyEffect"); + } + + protected void LogAbilityDataMismatch() + { + DebugConsole.ThrowError($"Incompatible ability! Ability {this} is incompatitible with this type of ability effect type."); + } + + // XML + public static CharacterAbility Load(XElement abilityElement, CharacterAbilityGroup characterAbilityGroup, bool errorMessages = true) + { + Type abilityType; + string type = abilityElement.Name.ToString().ToLowerInvariant(); + try + { + abilityType = Type.GetType("Barotrauma.Abilities." + type + "", false, true); + if (abilityType == null) + { + if (errorMessages) DebugConsole.ThrowError("Could not find the CharacterAbility \"" + type + "\" (" + characterAbilityGroup.CharacterTalent.DebugIdentifier + ")"); + return null; + } + } + catch (Exception e) + { + if (errorMessages) DebugConsole.ThrowError("Could not find the CharacterAbility \"" + type + "\" (" + characterAbilityGroup.CharacterTalent.DebugIdentifier + ")", e); + return null; + } + + object[] args = { characterAbilityGroup, abilityElement }; + CharacterAbility characterAbility; + + try + { + characterAbility = (CharacterAbility)Activator.CreateInstance(abilityType, args); + } + catch (TargetInvocationException e) + { + DebugConsole.ThrowError("Error while creating an instance of a CharacterAbility of the type " + abilityType + ".", e.InnerException); + return null; + } + + DebugConsole.AddWarning("Instantiated " + characterAbility + " for talent " + characterAbilityGroup.CharacterTalent.DebugIdentifier); + return characterAbility; + } + public static AbilityFlags ParseFlagType(string flagTypeString, string debugIdentifier) + { + AbilityFlags flagType = AbilityFlags.None; + if (!Enum.TryParse(flagTypeString, true, out flagType)) + { + DebugConsole.ThrowError("Invalid flag type type \"" + flagTypeString + "\" in CharacterTalent (" + debugIdentifier + ")"); + } + return flagType; + } + + public static float DistanceToSquaredDistance(float distance) + { + return distance * distance; + } + + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyForce.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyForce.cs new file mode 100644 index 000000000..3b35a274a --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyForce.cs @@ -0,0 +1,34 @@ +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityApplyForce : CharacterAbility + { + private readonly float impulseStrength; + private readonly float maxVelocity; + + private readonly string afflictionIdentifier; + public override bool AppliesEffectOnIntervalUpdate => true; + public CharacterAbilityApplyForce(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + impulseStrength = abilityElement.GetAttributeFloat("impulsestrength", 0f); + maxVelocity = abilityElement.GetAttributeFloat("maxvelocity", 10f); + + afflictionIdentifier = abilityElement.GetAttributeString("afflictionidentifier", ""); + } + + protected override void ApplyEffect() + { + Affliction affliction = Character.CharacterHealth.GetAffliction(afflictionIdentifier); + + if (affliction == null) { return; } + + foreach (Limb limb in Character.AnimController.Limbs) + { + limb.body.ApplyForce(Vector2.Normalize(limb.Mass * Character.AnimController.TargetMovement) * impulseStrength * (affliction.Strength / affliction.Prefab.MaxStrength), maxVelocity); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs new file mode 100644 index 000000000..e370e94b3 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityApplyStatusEffects : CharacterAbility + { + public override bool AppliesEffectOnIntervalUpdate => true; + public override bool AllowClientSimulation => true; + + protected readonly List statusEffects; + + public CharacterAbilityApplyStatusEffects(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + statusEffects = CharacterAbilityGroup.ParseStatusEffects(CharacterTalent, abilityElement.GetChildElement("statuseffects")); + } + + protected void ApplyEffectSpecific(Character targetCharacter) + { + foreach (var statusEffect in statusEffects) + { + statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, targetCharacter); + } + } + + protected override void ApplyEffect() + { + ApplyEffectSpecific(Character); + } + + protected override void ApplyEffect(object abilityData) + { + if (abilityData is Character targetCharacter) + { + ApplyEffectSpecific(targetCharacter); + } + else + { + ApplyEffect(); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToNearestAlly.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToNearestAlly.cs new file mode 100644 index 000000000..a12622816 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToNearestAlly.cs @@ -0,0 +1,36 @@ +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityApplyStatusEffectsToNearestAlly : CharacterAbilityApplyStatusEffects + { + protected float squaredMaxDistance; + public CharacterAbilityApplyStatusEffectsToNearestAlly(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + squaredMaxDistance = DistanceToSquaredDistance(abilityElement.GetAttributeFloat("maxdistance", float.MaxValue)); + } + + protected override void ApplyEffect() + { + Character closestCharacter = null; + float closestDistance = float.MaxValue; + + foreach (Character crewCharacter in Character.GetFriendlyCrew(Character)) + { + if (crewCharacter != Character && Vector2.DistanceSquared(Character.SimPosition, Character.GetRelativeSimPosition(crewCharacter)) is float tempDistance && tempDistance < closestDistance) + { + closestCharacter = crewCharacter; + closestDistance = tempDistance; + } + } + + if (closestDistance < squaredMaxDistance) + { + ApplyEffectSpecific(closestCharacter); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToRandomAlly.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToRandomAlly.cs new file mode 100644 index 000000000..8fe1ea29d --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToRandomAlly.cs @@ -0,0 +1,40 @@ +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityApplyStatusEffectsToRandomAlly : CharacterAbilityApplyStatusEffects + { + private readonly float squaredMaxDistance; + private readonly bool allowDifferentSub; + private readonly bool allowSelf; + + public override bool AllowClientSimulation => false; + + public CharacterAbilityApplyStatusEffectsToRandomAlly(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + squaredMaxDistance = DistanceToSquaredDistance(abilityElement.GetAttributeFloat("maxdistance", float.MaxValue)); + allowDifferentSub = abilityElement.GetAttributeBool("mustbeonsamesub", true); + allowSelf = abilityElement.GetAttributeBool("allowself", true); + } + + protected override void ApplyEffect() + { + Character chosenCharacter = null; + + chosenCharacter = Character.GetFriendlyCrew(Character).Where(c => + (allowSelf ||c != Character) && + (allowDifferentSub || c.Submarine == Character.Submarine) && + Vector2.DistanceSquared(Character.SimPosition, Character.GetRelativeSimPosition(c)) is float tempDistance && + tempDistance < squaredMaxDistance).GetRandom(); + + if (chosenCharacter == null) { return; } + + ApplyEffectSpecific(chosenCharacter); + } + + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveFlag.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveFlag.cs new file mode 100644 index 000000000..921807085 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveFlag.cs @@ -0,0 +1,20 @@ +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityGiveFlag : CharacterAbility + { + private AbilityFlags abilityFlag; + + // this and resistance giving should probably be moved directly to charactertalent attributes, as they don't need to interact with either ability group types + public CharacterAbilityGiveFlag(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + abilityFlag = ParseFlagType(abilityElement.GetAttributeString("flagtype", ""), CharacterTalent.DebugIdentifier); + } + + public override void InitializeAbility(bool addingFirstTime) + { + Character.AddAbilityFlag(abilityFlag); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMissionCount.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMissionCount.cs new file mode 100644 index 000000000..629202f93 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMissionCount.cs @@ -0,0 +1,21 @@ +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityGiveMissionCount : CharacterAbility + { + private readonly int amount; + + public CharacterAbilityGiveMissionCount(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + amount = abilityElement.GetAttributeInt("amount", 0); + } + + public override void InitializeAbility(bool addingFirstTime) + { + if (!addingFirstTime) { return; } + if (!(GameMain.GameSession?.Campaign is CampaignMode campaign)) { return; } + campaign.Settings.AddedMissionCount += amount; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs new file mode 100644 index 000000000..86e1cb093 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs @@ -0,0 +1,22 @@ +using Microsoft.Xna.Framework; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityGiveMoney : CharacterAbility + { + public override bool AppliesEffectOnIntervalUpdate => true; + + private int amount; + + public CharacterAbilityGiveMoney(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + amount = abilityElement.GetAttributeInt("amount", 0); + } + + protected override void ApplyEffect() + { + Character.GiveMoney(amount); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs new file mode 100644 index 000000000..2a8797b4e --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs @@ -0,0 +1,49 @@ +using Barotrauma.Extensions; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityGivePermanentStat : CharacterAbility + { + private readonly string statIdentifier; + private readonly StatTypes statType; + private readonly float value; + private readonly bool targetAllies; + private readonly bool removeOnDeath; + //private readonly float maximumValue; + + public override bool AppliesEffectOnIntervalUpdate => true; + + public CharacterAbilityGivePermanentStat(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + statIdentifier = abilityElement.GetAttributeString("statidentifier", "").ToLowerInvariant(); + statType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("stattype", ""), CharacterTalent.DebugIdentifier); + value = abilityElement.GetAttributeFloat("value", 0f); + targetAllies = abilityElement.GetAttributeBool("targetallies", false); + removeOnDeath = abilityElement.GetAttributeBool("removeondeath", true); + //maximumValue = abilityElement.GetAttributeFloat("maximumvalue", float.MaxValue); + } + + protected override void ApplyEffect(object abilityData) + { + ApplyEffectSpecific(); + } + + protected override void ApplyEffect() + { + ApplyEffectSpecific(); + } + + private void ApplyEffectSpecific() + { + if (targetAllies) + { + Character.GetFriendlyCrew(Character).ForEach(c => c?.Info.ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath)); + } + else + { + Character?.Info.ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveResistance.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveResistance.cs new file mode 100644 index 000000000..5d9cd7fee --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveResistance.cs @@ -0,0 +1,21 @@ +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityGiveResistance : CharacterAbility + { + private string resistanceId; + private float resistance; + + public CharacterAbilityGiveResistance(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + resistanceId = abilityElement.GetAttributeString("resistanceid", ""); + resistance = abilityElement.GetAttributeFloat("resistance", 1f); + } + + public override void InitializeAbility(bool addingFirstTime) + { + Character.ChangeAbilityResistance(resistanceId, resistance); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveStat.cs new file mode 100644 index 000000000..3b55618d7 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveStat.cs @@ -0,0 +1,22 @@ +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityGiveStat : CharacterAbility + { + private StatTypes statType; + private float value; + + // this and resistance giving should probably be moved directly to charactertalent attributes, as they don't need to interact with either ability group types + public CharacterAbilityGiveStat(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + statType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("stattype", ""), CharacterTalent.DebugIdentifier); + value = abilityElement.GetAttributeFloat("value", 0f); + } + + public override void InitializeAbility(bool addingFirstTime) + { + Character.ChangeStat(statType, value); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityIncreaseSkill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityIncreaseSkill.cs new file mode 100644 index 000000000..44caf675a --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityIncreaseSkill.cs @@ -0,0 +1,41 @@ +using Microsoft.Xna.Framework; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityIncreaseSkill : CharacterAbility + { + public override bool AppliesEffectOnIntervalUpdate => true; + + private string skillIdentifier; + private float skillIncrease; + + public CharacterAbilityIncreaseSkill(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + skillIdentifier = abilityElement.GetAttributeString("skillidentifier", "").ToLowerInvariant(); + skillIncrease = abilityElement.GetAttributeFloat("skillincrease", 0f); + } + + protected override void ApplyEffect() + { + ApplyEffectSpecific(Character); + } + + protected override void ApplyEffect(object abilityData) + { + if (abilityData is Character character) + { + ApplyEffectSpecific(character); + } + else + { + ApplyEffectSpecific(Character); + } + } + + private void ApplyEffectSpecific(Character character) + { + character.Info?.IncreaseSkillLevel(skillIdentifier, skillIncrease, character.Position + Vector2.UnitY * 175.0f); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAffliction.cs new file mode 100644 index 000000000..84aa32f90 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAffliction.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityModifyAffliction : CharacterAbility + { + private readonly string[] afflictionIdentifiers; + + private readonly float addedMultiplier; + + public CharacterAbilityModifyAffliction(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + afflictionIdentifiers = abilityElement.GetAttributeStringArray("afflictionidentifiers", new string[0], convertToLowerInvariant: true); + addedMultiplier = abilityElement.GetAttributeFloat("addedmultiplier", 0f); + } + + protected override void ApplyEffect(object abilityData) + { + if (abilityData is Affliction affliction) + { + foreach (string afflictionIdentifier in afflictionIdentifiers) + { + if (affliction.Identifier == afflictionIdentifier) + { + affliction.Strength *= 1 + addedMultiplier; + } + } + } + else + { + LogAbilityDataMismatch(); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAttackData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAttackData.cs new file mode 100644 index 000000000..6ee9dc1dc --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAttackData.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityModifyAttackData : CharacterAbility + { + private readonly List afflictions; + + float addedDamageMultiplier; + float addedPenetration; + + public CharacterAbilityModifyAttackData(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + if (abilityElement.GetChildElement("afflictions") is XElement afflictionElements) + { + afflictions = CharacterAbilityGroup.ParseAfflictions(CharacterTalent, afflictionElements); + } + addedDamageMultiplier = abilityElement.GetAttributeFloat("addeddamagemultiplier", 0f); + addedPenetration = abilityElement.GetAttributeFloat("addedpenetration", 0f); + } + + protected override void ApplyEffect(object abilityData) + { + if (abilityData is AttackData attackData) + { + if (attackData.Afflictions == null) + { + attackData.Afflictions = afflictions; + } + else + { + attackData.Afflictions.AddRange(afflictions); + } + attackData.DamageMultiplier += addedDamageMultiplier; + attackData.AddedPenetration += addedPenetration; + } + else + { + LogAbilityDataMismatch(); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyFlag.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyFlag.cs new file mode 100644 index 000000000..4ab462ccf --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyFlag.cs @@ -0,0 +1,34 @@ +using System; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityModifyFlag : CharacterAbility + { + private AbilityFlags abilityFlag; + + private bool lastState; + + public CharacterAbilityModifyFlag(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + abilityFlag = ParseFlagType(abilityElement.GetAttributeString("flagtype", ""), CharacterTalent.DebugIdentifier); + } + + protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate) + { + if (conditionsMatched != lastState) + { + if (conditionsMatched) + { + Character.AddAbilityFlag(abilityFlag); + } + else + { + Character.RemoveAbilityFlag(abilityFlag); + } + + lastState = conditionsMatched; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyReduceAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyReduceAffliction.cs new file mode 100644 index 000000000..828714266 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyReduceAffliction.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityModifyReduceAffliction : CharacterAbility + { + float addedAmountMultiplier; + + public CharacterAbilityModifyReduceAffliction(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + addedAmountMultiplier = abilityElement.GetAttributeFloat("addedamountmultiplier", 0f); + } + + protected override void ApplyEffect(object abilityData) + { + if (abilityData is (Affliction affliction, float reduceAmount)) + { + affliction.Strength -= addedAmountMultiplier * reduceAmount; + } + else + { + LogAbilityDataMismatch(); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyResistance.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyResistance.cs new file mode 100644 index 000000000..4e44403d7 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyResistance.cs @@ -0,0 +1,27 @@ +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityModifyResistance : CharacterAbility + { + private string resistanceId; + private float resistance; + bool lastState; + + // should probably be split to different classes + public CharacterAbilityModifyResistance(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + resistanceId = abilityElement.GetAttributeString("resistanceid", ""); + resistance = abilityElement.GetAttributeFloat("resistance", 1f); + } + + public override void UpdateCharacterAbility(bool conditionsMatched, float timeSinceLastUpdate) + { + if (conditionsMatched != lastState) + { + Character.ChangeAbilityResistance(resistanceId, conditionsMatched ? resistance : 1 / resistance); + lastState = conditionsMatched; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStat.cs new file mode 100644 index 000000000..c61a5a646 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStat.cs @@ -0,0 +1,26 @@ +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityModifyStat : CharacterAbility + { + private readonly StatTypes statType; + private readonly float value; + bool lastState; + + public CharacterAbilityModifyStat(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + statType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("stattype", ""), CharacterTalent.DebugIdentifier); + value = abilityElement.GetAttributeFloat("value", 0f); + } + + protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate) + { + if (conditionsMatched != lastState) + { + Character.ChangeStat(statType, conditionsMatched ? value : -value); + lastState = conditionsMatched; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyValue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyValue.cs new file mode 100644 index 000000000..7f27c334f --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyValue.cs @@ -0,0 +1,46 @@ +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityModifyValue : CharacterAbility + { + private float addedValue; + private float multiplierValue; + + public CharacterAbilityModifyValue(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + addedValue = abilityElement.GetAttributeFloat("addedvalue", 0f); + multiplierValue = abilityElement.GetAttributeFloat("multipliervalue", 1f); + } + + protected override void ApplyEffect(object abilityData) + { + if (abilityData is AbilityValue abilityValue) + { + ApplyEffectSpecific(abilityValue); + } + else if (abilityData is (object _, AbilityValue tupleAbilityValue)) + { + ApplyEffectSpecific(tupleAbilityValue); + } + } + + private void ApplyEffectSpecific(AbilityValue abilityValue) + { + abilityValue.Value += addedValue; + abilityValue.Value *= multiplierValue; + } + + } + + // this seems like a real silly way to have to pass values by reference into these same interfaces + // if more of these are required, maybe there should be an additional set of interfaces to easily pass values by reference instead + class AbilityValue + { + public float Value { get; set; } + public AbilityValue(float value) + { + Value = value; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityPutItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityPutItem.cs new file mode 100644 index 000000000..e80bf63db --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityPutItem.cs @@ -0,0 +1,46 @@ +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityPutItem : CharacterAbility + { + private readonly string itemIdentifier; + private readonly int amount; + public override bool AppliesEffectOnIntervalUpdate => true; + public CharacterAbilityPutItem(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + itemIdentifier = abilityElement.GetAttributeString("itemidentifier", ""); + amount = abilityElement.GetAttributeInt("amount", 1); + } + + protected override void ApplyEffect() + { + if (string.IsNullOrEmpty(itemIdentifier)) + { + DebugConsole.ThrowError("Cannot put item in inventory - itemIdentifier not defined."); + return; + } + + ItemPrefab itemPrefab = ItemPrefab.Find(null, itemIdentifier); + if (itemPrefab == null) + { + DebugConsole.ThrowError("Cannot put item in inventory - item prefab " + itemIdentifier + " not found."); + return; + } + for (int i = 0; i < amount; i++) + { + if (GameMain.GameSession?.RoundEnding ?? true) + { + Item item = new Item(itemPrefab, Character.WorldPosition, Character.Submarine); + Character.Inventory.TryPutItem(item, Character, new List() { InvSlotType.Any }); + } + else + { + Entity.Spawner.AddToSpawnQueue(itemPrefab, Character.Inventory); + } + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityResetPermanentStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityResetPermanentStat.cs new file mode 100644 index 000000000..eb57809e5 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityResetPermanentStat.cs @@ -0,0 +1,29 @@ +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityResetPermanentStat : CharacterAbility + { + private readonly string statIdentifier; + public override bool RequiresAlive => false; + + public CharacterAbilityResetPermanentStat(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + statIdentifier = abilityElement.GetAttributeString("statidentifier", "").ToLowerInvariant(); + } + protected override void ApplyEffect(object abilityData) + { + ApplyEffectSpecific(); + } + + protected override void ApplyEffect() + { + ApplyEffectSpecific(); + } + + private void ApplyEffectSpecific() + { + Character?.Info.ResetSavedStatValue(statIdentifier); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityApprenticeship.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityApprenticeship.cs new file mode 100644 index 000000000..808ca4b0b --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityApprenticeship.cs @@ -0,0 +1,21 @@ +using Microsoft.Xna.Framework; +using System; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityApprenticeship : CharacterAbility + { + public CharacterAbilityApprenticeship(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + } + + protected override void ApplyEffect(object abilityData) + { + if (abilityData is (string skillIdentifier, Character character) && character != Character) + { + character.Info?.IncreaseSkillLevel(skillIdentifier, 1.0f, character.Position + Vector2.UnitY * 175.0f); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityBountyHunter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityBountyHunter.cs new file mode 100644 index 000000000..fde6e081e --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityBountyHunter.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityBountyHunter : CharacterAbility + { + private float vitalityPercentage; + + public CharacterAbilityBountyHunter(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + vitalityPercentage = abilityElement.GetAttributeFloat("vitalitypercentage", 0f); + } + + protected override void ApplyEffect(object abilityData) + { + if (abilityData is Character character) + { + Character.GiveMoney((int)(vitalityPercentage * character.MaxVitality)); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityIndustrialRevolution.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityIndustrialRevolution.cs new file mode 100644 index 000000000..324bf919d --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityIndustrialRevolution.cs @@ -0,0 +1,30 @@ +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityIndustrialRevolution : CharacterAbility + { + float addedFabricationSpeed; + + public CharacterAbilityIndustrialRevolution(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + addedFabricationSpeed = abilityElement.GetAttributeFloat("addedfabricationspeed", 0f); + } + + public override void UpdateCharacterAbility(bool conditionsMatched, float timeSinceLastUpdate) + { + if (conditionsMatched) + { + // not necessarily the cleanest or performant way, but at least this shouldn't break anything. + // must be done every frame in order to work. + if (Character.SelectedConstruction?.GetComponent() is Fabricator fabricator && fabricator.IsActive) + { + fabricator.FabricationSpeedMultiplier += addedFabricationSpeed; + } + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityInsurancePolicy.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityInsurancePolicy.cs new file mode 100644 index 000000000..9241769e4 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityInsurancePolicy.cs @@ -0,0 +1,54 @@ +using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityInsurancePolicy : CharacterAbility + { + public override bool AppliesEffectOnIntervalUpdate => true; + public override bool RequiresAlive => false; + + private readonly int moneyPerLevel; + private bool hasOccurred = false; + + private static List clientsAlreadyUsed = new List(); + + public CharacterAbilityInsurancePolicy(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + moneyPerLevel = abilityElement.GetAttributeInt("moneyperlevel", 0); + } + + protected override void ApplyEffect() + { + if (Character?.Info is CharacterInfo info && !hasOccurred) + { + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) + { + foreach (Client client in GameMain.NetworkMember.ConnectedClients) + { + if (client.Character == Character && clientsAlreadyUsed.Contains(client)) { return; } + } + } + + Character.GiveMoney(moneyPerLevel * info.GetCurrentLevel()); + hasOccurred = true; + + // this is an ugly way to do this, but this effect should not occur more than once per round for a client + // this seemed like the simplest way to do it since characters are instantiated from scratch each time + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) + { + foreach (Client client in GameMain.NetworkMember.ConnectedClients) + { + if (client.Character == Character) + { + clientsAlreadyUsed.Add(client); + } + } + } + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityMultitasker.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityMultitasker.cs new file mode 100644 index 000000000..0017b6d98 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityMultitasker.cs @@ -0,0 +1,26 @@ +using Microsoft.Xna.Framework; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityMultitasker : CharacterAbility + { + private string lastSkillIdentifier; + + public CharacterAbilityMultitasker(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + } + + protected override void ApplyEffect(object abilityData) + { + if (abilityData is string skillIdentifier) + { + if (skillIdentifier != lastSkillIdentifier) + { + lastSkillIdentifier = skillIdentifier; + Character.Info?.IncreaseSkillLevel(skillIdentifier, 1.0f, Character.Position + Vector2.UnitY * 175.0f); + } + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityPsychoClown.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityPsychoClown.cs new file mode 100644 index 000000000..4e7ac7225 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityPsychoClown.cs @@ -0,0 +1,44 @@ +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityPsychoClown : CharacterAbility + { + private StatTypes statType; + private float value; + private string afflictionIdentifier; + private float lastValue = 0f; + + public CharacterAbilityPsychoClown(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + statType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("stattype", ""), CharacterTalent.DebugIdentifier); + value = abilityElement.GetAttributeFloat("value", 0f); + afflictionIdentifier = abilityElement.GetAttributeString("afflictionidentifier", ""); + } + + protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate) + { + // managing state this way seems liable to cause bugs, maybe instead create abstraction to reset these values more safely + // talents cannot be removed while in active play because of the lack of this, for example + Character.ChangeStat(statType, -lastValue); + + if (conditionsMatched) + { + var affliction = Character.CharacterHealth.GetAffliction(afflictionIdentifier); + + float afflictionStrength = 0f; + if (affliction != null) + { + afflictionStrength = affliction.Strength / affliction.Prefab.MaxStrength; + } + + lastValue = afflictionStrength * value; + Character.ChangeStat(statType, lastValue); + } + else + { + lastValue = 0f; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityRegenerateLoot.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityRegenerateLoot.cs new file mode 100644 index 000000000..5a2ff174e --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityRegenerateLoot.cs @@ -0,0 +1,30 @@ +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityRegenerateLoot : CharacterAbility + { + List openedContainers = new List(); + + public CharacterAbilityRegenerateLoot(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + } + + protected override void ApplyEffect(object abilityData) + { + if (abilityData is Item item && !openedContainers.Contains(item)) + { + openedContainers.Add(item); + + if (item.GetComponent() is ItemContainer itemContainer) + { + AutoItemPlacer.RegenerateLoot(item.Submarine, itemContainer); + } + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityStonewall.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityStonewall.cs new file mode 100644 index 000000000..6597c6973 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityStonewall.cs @@ -0,0 +1,42 @@ +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityStonewall : CharacterAbility + { + private readonly List statusEffects; + private readonly List statusEffectsReset; + private int maxEnemyCount; + private float squaredDistance; + + public CharacterAbilityStonewall(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + statusEffects = CharacterAbilityGroup.ParseStatusEffects(CharacterTalent, abilityElement.GetChildElement("statuseffects")); + statusEffectsReset = CharacterAbilityGroup.ParseStatusEffects(CharacterTalent, abilityElement.GetChildElement("statuseffectsreset")); + maxEnemyCount = abilityElement.GetAttributeInt("maxenemycount", 0); + squaredDistance = DistanceToSquaredDistance(abilityElement.GetAttributeFloat("distance", 0)); + } + + protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate) + { + int numberOfEnemiesInRange = Character.CharacterList.Where(c => !HumanAIController.IsFriendly(Character, c) && !c.IsDead && Vector2.DistanceSquared(Character.SimPosition, Character.GetRelativeSimPosition(c)) < squaredDistance).Count(); + + foreach (var statusEffect in statusEffectsReset) + { + statusEffect.Apply(ActionType.OnAbility, 1f, Character, Character); + } + + if (conditionsMatched && numberOfEnemiesInRange > 0) + { + foreach (var statusEffect in statusEffects) + { + statusEffect.Apply(ActionType.OnAbility, Math.Min(numberOfEnemiesInRange, maxEnemyCount), Character, Character); + } + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs new file mode 100644 index 000000000..284ec4ae6 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs @@ -0,0 +1,42 @@ +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityTandemFire : CharacterAbilityApplyStatusEffectsToNearestAlly + { + private string tag; + public CharacterAbilityTandemFire(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + tag = abilityElement.GetAttributeString("tag", ""); + } + + protected override void ApplyEffect() + { + if (Character.SelectedConstruction == null || !Character.SelectedConstruction.HasTag(tag)) { return; } + + Character closestCharacter = null; + float closestDistance = float.MaxValue; + + foreach (Character crewCharacter in Character.GetFriendlyCrew(Character)) + { + if (crewCharacter != Character && Vector2.DistanceSquared(Character.SimPosition, Character.GetRelativeSimPosition(crewCharacter)) is float tempDistance && tempDistance < closestDistance) + { + closestCharacter = crewCharacter; + closestDistance = tempDistance; + } + } + + if (closestCharacter.SelectedConstruction == null || !Character.SelectedConstruction.HasTag(tag)) { return; } + + if (closestDistance < squaredMaxDistance) + { + ApplyEffectSpecific(Character); + ApplyEffectSpecific(closestCharacter); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTaskmaster.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTaskmaster.cs new file mode 100644 index 000000000..d54950e74 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTaskmaster.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityTaskmaster : CharacterAbility + { + private readonly List statusEffects; + private readonly List statusEffectsRemove; + + private Character lastCharacter; + + public CharacterAbilityTaskmaster(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + statusEffects = CharacterAbilityGroup.ParseStatusEffects(CharacterTalent, abilityElement.GetChildElement("statuseffects")); + statusEffectsRemove = CharacterAbilityGroup.ParseStatusEffects(CharacterTalent, abilityElement.GetChildElement("statuseffectsremove")); + } + + protected override void ApplyEffect(object abilityData) + { + if (abilityData is Character targetCharacter) + { + if (targetCharacter == Character) { return; } + + foreach (var statusEffect in statusEffectsRemove) + { + statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, lastCharacter); + } + + foreach (var statusEffect in statusEffects) + { + statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, targetCharacter); + } + + lastCharacter = targetCharacter; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs new file mode 100644 index 000000000..c49fa439c --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs @@ -0,0 +1,206 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + abstract class CharacterAbilityGroup + { + public CharacterTalent CharacterTalent { get; } + public Character Character { get; } + + // currently only used to turn off simulation if random conditions are in use + public bool IsActive { get; private set; } = true; + + // add support for OR conditions? + protected readonly List abilityConditions = new List(); + + // separate dictionaries for each type of characterability? + protected readonly List characterAbilities = new List(); + + public CharacterAbilityGroup(CharacterTalent characterTalent, XElement abilityElementGroup) + { + CharacterTalent = characterTalent; + Character = CharacterTalent.Character; + + foreach (XElement subElement in abilityElementGroup.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "abilities": + LoadAbilities(subElement); + break; + case "conditions": + LoadConditions(subElement); + break; + } + } + } + + public void ActivateAbilityGroup(bool addingFirstTime) + { + foreach (var characterAbility in characterAbilities) + { + characterAbility.InitializeAbility(addingFirstTime); + } + } + + public void LoadConditions(XElement conditionElements) + { + foreach (XElement conditionElement in conditionElements.Elements()) + { + AbilityCondition newCondition = ConstructCondition(CharacterTalent, conditionElement); + + if (newCondition == null) + { + DebugConsole.ThrowError($"AbilityCondition was not found in talent {CharacterTalent.DebugIdentifier}!"); + return; + } + + if (!newCondition.AllowClientSimulation && GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) + { + IsActive = false; + } + + abilityConditions.Add(newCondition); + } + } + + public void AddAbility(CharacterAbility characterAbility) + { + if (characterAbility == null) + { + DebugConsole.ThrowError($"Trying to add null ability for talent {CharacterTalent.DebugIdentifier}!"); + return; + } + + characterAbilities.Add(characterAbility); + } + + // XML + private AbilityCondition ConstructCondition(CharacterTalent characterTalent, XElement conditionElement, bool errorMessages = true) + { + AbilityCondition newCondition = null; + + Type conditionType; + string type = conditionElement.Name.ToString().ToLowerInvariant(); + try + { + conditionType = Type.GetType("Barotrauma.Abilities." + type + "", false, true); + if (conditionType == null) + { + if (errorMessages) DebugConsole.ThrowError("Could not find the component \"" + type + "\" (" + characterTalent.DebugIdentifier + ")"); + return null; + } + } + catch (Exception e) + { + if (errorMessages) DebugConsole.ThrowError("Could not find the component \"" + type + "\" (" + characterTalent.DebugIdentifier + ")", e); + return null; + } + + object[] args = { characterTalent, conditionElement }; + + try + { + newCondition = (AbilityCondition)Activator.CreateInstance(conditionType, args); + } + catch (TargetInvocationException e) + { + DebugConsole.ThrowError("Error while creating an instance of an ability condition of the type " + conditionType + ".", e.InnerException); + return null; + } + + if (newCondition == null) + { + DebugConsole.ThrowError("Error while creating an instance of an ability condition of the type " + conditionType + ", instance was null"); + return null; + } + + return newCondition; + } + + private void LoadAbilities(XElement abilityElements) + { + foreach (XElement abilityElementGroup in abilityElements.Elements()) + { + AddAbility(ConstructAbility(abilityElementGroup, CharacterTalent)); + } + } + + private CharacterAbility ConstructAbility(XElement abilityElement, CharacterTalent characterTalent) + { + CharacterAbility newAbility = CharacterAbility.Load(abilityElement, this); + + if (newAbility == null) + { + DebugConsole.ThrowError($"Unable to create an ability for {characterTalent.DebugIdentifier}!"); + return null; + } + + return newAbility; + } + + public static List ParseStatusEffects(CharacterTalent characterTalent, XElement statusEffectElements) + { + if (statusEffectElements == null) + { + DebugConsole.ThrowError("StatusEffect list was not found in talent " + characterTalent.DebugIdentifier); + return null; + } + + List statusEffects = new List(); + + foreach (XElement statusEffectElement in statusEffectElements.Elements()) + { + var statusEffect = StatusEffect.Load(statusEffectElement, characterTalent.DebugIdentifier); + statusEffects.Add(statusEffect); + } + + return statusEffects; + } + + public static StatTypes ParseStatType(string statTypeString, string debugIdentifier) + { + StatTypes statType; + if (!Enum.TryParse(statTypeString, true, out statType)) + { + DebugConsole.ThrowError("Invalid stat type type \"" + statTypeString + "\" in CharacterTalent (" + debugIdentifier + ")"); + } + return statType; + } + + public static List ParseAfflictions(CharacterTalent characterTalent, XElement afflictionElements) + { + if (afflictionElements == null) + { + DebugConsole.ThrowError("Affliction list was not found in talent " + characterTalent.DebugIdentifier); + return null; + } + + List afflictions = new List(); + + // similar logic to affliction creation in statuseffects + // might be worth unifying + + foreach (XElement afflictionElement in afflictionElements.Elements()) + { + string afflictionIdentifier = afflictionElement.GetAttributeString("identifier", "").ToLowerInvariant(); + AfflictionPrefab afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.ToLowerInvariant() == afflictionIdentifier); + if (afflictionPrefab == null) + { + DebugConsole.ThrowError("Error in CharacterTalent (" + characterTalent.DebugIdentifier + ") - Affliction prefab with the identifier \"" + afflictionIdentifier + "\" not found."); + continue; + } + + Affliction afflictionInstance = afflictionPrefab.Instantiate(afflictionElement.GetAttributeFloat(1.0f, "amount", "strength")); + afflictionInstance.Probability = afflictionElement.GetAttributeFloat(1.0f, "probability"); + afflictions.Add(afflictionInstance); + } + + return afflictions; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs new file mode 100644 index 000000000..f49851390 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityGroupEffect : CharacterAbilityGroup + { + public CharacterAbilityGroupEffect(CharacterTalent characterTalent, XElement abilityElementGroup) : base(characterTalent, abilityElementGroup) { } + + public void CheckAbilityGroup(object abilityData) + { + if (!IsActive) { return; } + if (IsApplicable(abilityData)) + { + foreach (var characterAbility in characterAbilities) + { + if (characterAbility.IsViable()) + { + characterAbility.ApplyAbilityEffect(abilityData); + } + } + } + } + + private bool IsApplicable(object abilityData) + { + return abilityConditions.All(c => c.MatchesCondition(abilityData)); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs new file mode 100644 index 000000000..6597bbcd6 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityGroupInterval : CharacterAbilityGroup + { + private float interval { get; set; } + public float TimeSinceLastUpdate { get; private set; } + + private float effectDelay; + private float effectDelayTimer; + + public CharacterAbilityGroupInterval(CharacterTalent characterTalent, XElement abilityElementGroup) : base(characterTalent, abilityElementGroup) + { + // too many overlapping intervals could cause hitching? maybe randomize a little + interval = abilityElementGroup.GetAttributeFloat("interval", 0f); + effectDelay = abilityElementGroup.GetAttributeFloat("effectdelay", 0f); + } + public void UpdateAbilityGroup(float deltaTime) + { + if (!IsActive) { return; } + TimeSinceLastUpdate += deltaTime; + if (TimeSinceLastUpdate >= interval) + { + bool conditionsMatched = IsApplicable(); + effectDelayTimer = conditionsMatched ? effectDelayTimer + TimeSinceLastUpdate : 0f; + conditionsMatched &= effectDelayTimer >= effectDelay; + + foreach (var characterAbility in characterAbilities) + { + if (characterAbility.IsViable()) + { + characterAbility.UpdateCharacterAbility(conditionsMatched, TimeSinceLastUpdate); + } + } + TimeSinceLastUpdate = 0; + } + } + private bool IsApplicable() + { + return abilityConditions.All(c => c.MatchesCondition()); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs new file mode 100644 index 000000000..9c64f85ea --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using Barotrauma.Abilities; + +namespace Barotrauma +{ + class CharacterTalent + { + public Character Character { get; } + public string DebugIdentifier { get; } + + public readonly TalentPrefab Prefab; + + private readonly Dictionary> characterAbilityGroupEffectDictionary = new Dictionary>(); + + private readonly List characterAbilityGroupIntervals = new List(); + + // works functionally but a missing recipe is not represented on GUI side. this might be better placed in the character class itself, though it might be fine here as well + public List UnlockedRecipes { get; } = new List(); + + public CharacterTalent(TalentPrefab talentPrefab, Character character) + { + Character = character; + + Prefab = talentPrefab; + XElement element = talentPrefab.ConfigElement; + DebugIdentifier = talentPrefab.OriginalName; + + foreach (XElement subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "abilitygroupeffect": + LoadAbilityGroupEffect(subElement); + break; + case "abilitygroupinterval": + LoadAbilityGroupInterval(subElement); + break; + case "addedrecipe": + if (subElement.GetAttributeString("itemidentifier", string.Empty) is string recipeIdentifier && recipeIdentifier != string.Empty) + { + UnlockedRecipes.Add(recipeIdentifier); + } + else + { + DebugConsole.ThrowError("No recipe identifier defined for talent " + DebugIdentifier); + } + break; + } + } + } + + public virtual void UpdateTalent(float deltaTime) + { + foreach (var characterAbilityGroupInterval in characterAbilityGroupIntervals) + { + characterAbilityGroupInterval.UpdateAbilityGroup(deltaTime); + } + } + + public void CheckTalent(AbilityEffectType abilityEffectType, object abilityData) + { + if (characterAbilityGroupEffectDictionary.TryGetValue(abilityEffectType, out var characterAbilityGroups)) + { + foreach (var characterAbilityGroup in characterAbilityGroups) + { + characterAbilityGroup.CheckAbilityGroup(abilityData); + } + } + } + + public void ActivateTalent(bool addingFirstTime) + { + foreach (var characterAbilityGroups in characterAbilityGroupEffectDictionary.Values) + { + foreach (var characterAbilityGroup in characterAbilityGroups) + { + characterAbilityGroup.ActivateAbilityGroup(addingFirstTime); + } + } + } + + // XML logic + private void LoadAbilityGroupInterval(XElement abilityGroup) + { + string name = abilityGroup.Name.ToString().ToLowerInvariant(); + characterAbilityGroupIntervals.Add(new CharacterAbilityGroupInterval(this, abilityGroup)); + } + + private void LoadAbilityGroupEffect(XElement abilityGroup) + { + AbilityEffectType abilityEffectType = ParseAbilityEffectType(this, abilityGroup.GetAttributeString("abilityeffecttype", "none")); + AddAbilityGroupEffect(new CharacterAbilityGroupEffect(this, abilityGroup), abilityEffectType); + } + + public void AddAbilityGroupEffect(CharacterAbilityGroupEffect characterAbilityGroup, AbilityEffectType abilityEffectType = AbilityEffectType.None) + { + if (characterAbilityGroupEffectDictionary.TryGetValue(abilityEffectType, out var characterAbilityList)) + { + characterAbilityList.Add(characterAbilityGroup); + } + else + { + List characterAbilityGroups = new List(); + characterAbilityGroups.Add(characterAbilityGroup); + characterAbilityGroupEffectDictionary.Add(abilityEffectType, characterAbilityGroups); + } + } + + public static AbilityEffectType ParseAbilityEffectType(CharacterTalent characterTalent, string abilityEffectTypeString) + { + AbilityEffectType abilityEffectType = AbilityEffectType.Undefined; + if (!Enum.TryParse(abilityEffectTypeString, true, out abilityEffectType)) + { + DebugConsole.ThrowError("Invalid ability effect type \"" + abilityEffectTypeString + "\" in CharacterTalent (" + characterTalent.DebugIdentifier + ")"); + } + if (abilityEffectType == AbilityEffectType.Undefined) + { + DebugConsole.ThrowError("Ability effect type not defined in CharacterTalent (" + characterTalent.DebugIdentifier + ")"); + } + + return abilityEffectType; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs new file mode 100644 index 000000000..5c42eb8e7 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs @@ -0,0 +1,102 @@ +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + class TalentPrefab : IPrefab, IDisposable, IHasUintIdentifier + { + public string Identifier { get; private set; } + public string OriginalName => Identifier; + public ContentPackage ContentPackage { get; private set; } + public string FilePath { get; private set; } + + public static readonly PrefabCollection TalentPrefabs = new PrefabCollection(); + + public XElement ConfigElement + { + get; + private set; + } + + public TalentPrefab(XElement element, string filePath) + { + FilePath = filePath; + ConfigElement = element; + Identifier = element.GetAttributeString("identifier", "noidentifier"); + this.CalculatePrefabUIntIdentifier(TalentPrefabs); + } + + private bool disposed = false; + public void Dispose() + { + if (disposed) { return; } + disposed = true; + TalentPrefabs.Remove(this); + } + + /// + /// Unique identifier that's generated by hashing the prefab's string identifier. + /// Used to reduce the amount of bytes needed to write talent data into network messages in multiplayer. + /// + public uint UIntIdentifier { get; set; } + + public static void RemoveByFile(string filePath) => TalentPrefabs.RemoveByFile(filePath); + + public static void LoadFromFile(ContentFile file) + { + DebugConsole.Log("Loading talent prefab: " + file.Path); + RemoveByFile(file.Path); + + XDocument doc = XMLExtensions.TryLoadXml(file.Path); + if (doc == null) { return; } + + var rootElement = doc.Root; + switch (rootElement.Name.ToString().ToLowerInvariant()) + { + case "talent": + TalentPrefabs.Add(new TalentPrefab(rootElement, file.Path), false); + break; + case "talents": + foreach (var element in rootElement.Elements()) + { + if (element.IsOverride()) + { + var itemElement = element.GetChildElement("talent"); + if (itemElement != null) + { + TalentPrefabs.Add(new TalentPrefab(rootElement, file.Path), true); + } + else + { + DebugConsole.ThrowError($"Cannot find a talent element from the children of the override element defined in {file.Path}"); + } + } + else + { + TalentPrefabs.Add(new TalentPrefab(element, file.Path), false); + } + } + break; + default: + DebugConsole.ThrowError($"Invalid XML root element: '{rootElement.Name.ToString()}' in {file.Path}"); + break; + } + } + + public static void LoadAll(IEnumerable files) + { + DebugConsole.Log("Loading talent prefabs: "); + + foreach (ContentFile file in files) + { + LoadFromFile(file); + } + + } + + + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs new file mode 100644 index 000000000..2a2f67bdf --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs @@ -0,0 +1,219 @@ +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + class TalentTree + { + public static readonly Dictionary JobTalentTrees = new Dictionary(); + + public readonly List TalentSubTrees = new List(); + + private static HashSet subtreeTalents = new HashSet(); + + public XElement ConfigElement + { + get; + private set; + } + + public TalentTree(XElement element, string filePath) + { + ConfigElement = element; + + string jobIdentifier = element.GetAttributeString("jobidentifier", ""); + + if (string.IsNullOrEmpty(jobIdentifier)) + { + DebugConsole.ThrowError("No job defined for talent tree!"); + return; + } + + foreach (XElement subTreeElement in element.GetChildElements("subtree")) + { + TalentSubTrees.Add(new TalentSubTree(subTreeElement)); + } + + // talents found and unlocked using the identifier wihin the talent tree, so no duplicates may occur + HashSet duplicateSet = new HashSet(); + foreach (string talent in TalentSubTrees.SelectMany(s => s.TalentOptionStages.SelectMany(o => o.Talents.Select(t => t.Identifier)))) + { + TalentPrefab talentPrefab = TalentPrefab.TalentPrefabs.Find(c => c.Identifier.Equals(talent, StringComparison.OrdinalIgnoreCase)); + if (talentPrefab == null) + { + DebugConsole.AddWarning($"Talent tree for job {jobIdentifier} contains non-existent talent {talent}! Talent tree not added."); + return; + } + if (!duplicateSet.Add(talent)) + { + DebugConsole.ThrowError($"Talent tree for job {jobIdentifier} contains duplicate talent {talent}! Talent tree not added."); + return; + } + } + + if (!JobTalentTrees.TryAdd(jobIdentifier, this)) + { + DebugConsole.ThrowError($"Could not add talent tree for job {jobIdentifier}! A talent tree for this job is already likely defined"); + } + } + + public static void LoadFromFile(ContentFile file) + { + DebugConsole.Log("Loading talent tree: " + file.Path); + + XDocument doc = XMLExtensions.TryLoadXml(file.Path); + if (doc == null) { return; } + + var rootElement = doc.Root; + switch (rootElement.Name.ToString().ToLowerInvariant()) + { + case "talenttree": + new TalentTree(rootElement, file.Path); + break; + case "talenttrees": + foreach (var element in rootElement.Elements()) + { + if (element.IsOverride()) + { + var treeElement = element.GetChildElement("talenttree"); + if (treeElement != null) + { + new TalentTree(rootElement, file.Path); + } + else + { + DebugConsole.ThrowError($"Cannot find a talent tree element from the children of the override element defined in {file.Path}"); + } + } + else + { + new TalentTree(element, file.Path); + } + } + break; + default: + DebugConsole.ThrowError($"Invalid XML root element: '{rootElement.Name.ToString()}' in {file.Path}"); + break; + } + } + + public static void LoadAll(IEnumerable files) + { + DebugConsole.Log("Loading talent tree: "); + + foreach (ContentFile file in files) + { + LoadFromFile(file); + } + } + + public static bool IsViableTalentForCharacter(Character character, string talentIdentifier) + { + return IsViableTalentForCharacter(character, talentIdentifier, character?.Info?.UnlockedTalents ?? Enumerable.Empty()); + } + + + public static bool IsViableTalentForCharacter(Character character, string talentIdentifier, IEnumerable selectedTalents) + { + if (character?.Info?.Job.Prefab == null) { return false; } + if (character.Info.GetTotalTalentPoints() - selectedTalents.Count() <= 0) { return false; } + + if (!JobTalentTrees.TryGetValue(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return false; } + + foreach (var subTree in talentTree.TalentSubTrees) + { + foreach (var talentOptionStage in subTree.TalentOptionStages) + { + bool hasTalentInThisTier = talentOptionStage.Talents.Any(t => selectedTalents.Contains(t.Identifier)); + if (!hasTalentInThisTier) + { + if (talentOptionStage.Talents.Any(t => t.Identifier == talentIdentifier)) + { + return true; + } + else + { + break; + } + } + } + } + + return false; + } + + public static List CheckTalentSelection(Character controlledCharacter, IEnumerable selectedTalents) + { + List viableTalents = new List(); + bool canStillUnlock = true; + // keep trying to unlock talents until none of the talents are unlockable + while (canStillUnlock && selectedTalents.Any()) + { + canStillUnlock = false; + foreach (string talent in selectedTalents) + { + if (IsViableTalentForCharacter(controlledCharacter, talent, viableTalents)) + { + viableTalents.Add(talent); + canStillUnlock = true; + } + } + } + return viableTalents; + } + } + + class TalentSubTree + { + public string Identifier { get; } + + public readonly List TalentOptionStages = new List(); + + public TalentSubTree(XElement subTreeElement) + { + Identifier = subTreeElement.GetAttributeString("identifier", ""); + + foreach (XElement talentOptionsElement in subTreeElement.GetChildElements("talentoptions")) + { + TalentOptionStages.Add(new TalentOption(talentOptionsElement)); + } + } + + } + + class TalentOption + { + public readonly List Talents = new List(); + + public TalentOption(XElement talentOptionsElement) + { + foreach (XElement talentOptionElement in talentOptionsElement.GetChildElements("talentoption")) + { + Talents.Add(new Talent(talentOptionElement)); + } + } + } + + class Talent + { + public readonly string Identifier; + public readonly Sprite Icon; + public Talent(XElement talentOptionElement) + { + Identifier = talentOptionElement.GetAttributeString("identifier", ""); + foreach (XElement subElement in talentOptionElement.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "icon": + Icon = new Sprite(subElement); + break; + } + } + } + } + +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs index dbfb4cd19..ed67c56be 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs @@ -51,7 +51,9 @@ namespace Barotrauma WreckAIConfig, UpgradeModules, MapCreature, - EnemySubmarine + EnemySubmarine, + Talents, + TalentTrees, } public class ContentPackage @@ -103,7 +105,8 @@ namespace Barotrauma ContentType.Corpses, ContentType.UpgradeModules, ContentType.MapCreature, - ContentType.EnemySubmarine + ContentType.EnemySubmarine, + ContentType.Talents, }; //at least one file of each these types is required in core content packages @@ -135,7 +138,8 @@ namespace Barotrauma ContentType.Orders, ContentType.Corpses, ContentType.UpgradeModules, - ContentType.EnemySubmarine + ContentType.EnemySubmarine, + ContentType.Talents, }; public static IEnumerable CorePackageRequiredFiles diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index acaa5db14..e768d963d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -191,11 +191,6 @@ namespace Barotrauma GameMain.NetworkMember.ShowNetStats = !GameMain.NetworkMember.ShowNetStats; })); - commands.Add(new Command("createfilelist", "", (string[] args) => - { - UpdaterUtil.SaveFileList("filelist.xml"); - })); - commands.Add(new Command("spawn|spawncharacter", "spawn [creaturename/jobname] [near/inside/outside/cursor] [team (0-3)]: Spawn a creature at a random spawnpoint (use the second parameter to only select spawnpoints near/inside/outside the submarine). You can also enter the name of a job (e.g. \"Mechanic\") to spawn a character with a specific job and the appropriate equipment.", null, () => { @@ -580,7 +575,7 @@ namespace Barotrauma } })); - commands.Add(new Command("dumptofile", "", (string[] args) => + commands.Add(new Command("dumptofile", "findentityids [filename]: Outputs the contents of the debug console into a text file in the game folder. If the filename argument is omitted, \"consoleOutput.txt\" is used as the filename.", (string[] args) => { string filename = "consoleOutput.txt"; if (args.Length > 0) { filename = string.Join(" ", args); } @@ -607,7 +602,7 @@ namespace Barotrauma } })); - commands.Add(new Command("giveaffliction", "giveaffliction [affliction name] [affliction strength] [character name]: Add an affliction to a character. If the name parameter is omitted, the affliction is added to the controlled character.", (string[] args) => + commands.Add(new Command("giveaffliction", "giveaffliction [affliction name] [affliction strength] [character name] [limb type]: Add an affliction to a character. If the name parameter is omitted, the affliction is added to the controlled character.", (string[] args) => { if (args.Length < 2) { return; } @@ -632,14 +627,21 @@ namespace Barotrauma bool.TryParse(args[2], out relativeStrength); } - Character targetCharacter = (relativeStrength || args.Length <= 2) ? Character.Controlled : FindMatchingCharacter(args.Skip(2).ToArray()); + Character targetCharacter = (relativeStrength || args.Length <= 2) ? Character.Controlled : FindMatchingCharacter(new string[] { args[2] }); + + if (targetCharacter != null) { + Limb targetLimb = targetCharacter.AnimController.MainLimb; + if (args.Length > 3) + { + targetLimb = targetCharacter.AnimController.Limbs.FirstOrDefault(l => l.type.ToString().Equals(args[3], StringComparison.OrdinalIgnoreCase)); + } if (relativeStrength) { afflictionStrength *= targetCharacter.MaxVitality / afflictionPrefab.MaxStrength; } - targetCharacter.CharacterHealth.ApplyAffliction(targetCharacter.AnimController.MainLimb, afflictionPrefab.Instantiate(afflictionStrength)); + targetCharacter.CharacterHealth.ApplyAffliction(targetLimb ?? targetCharacter.AnimController.MainLimb, afflictionPrefab.Instantiate(afflictionStrength)); } }, () => @@ -648,7 +650,8 @@ namespace Barotrauma { AfflictionPrefab.List.Select(a => a.Name).ToArray(), new string[] { "1" }, - Character.CharacterList.Select(c => c.Name).ToArray() + Character.CharacterList.Select(c => c.Name).ToArray(), + Enum.GetNames(typeof(LimbType)).ToArray() }; }, isCheat: true)); @@ -833,9 +836,68 @@ namespace Barotrauma commands.Add(new Command("water|editwater", "water/editwater: Toggle water editing. Allows adding water into rooms by holding the left mouse button and removing it by holding the right mouse button.", (string[] args) => { Hull.EditWater = !Hull.EditWater; - NewMessage(Hull.EditWater ? "Water editing on" : "Water editing off", Color.White); + NewMessage(Hull.EditWater ? "Water editing on" : "Water editing off", Color.White); }, isCheat: true)); + commands.Add(new Command("givetalent", "give [player] testing [talent]", (string[] args) => + { + if (args.Length < 2) return; + var character = FindMatchingCharacter(args.Skip(1).ToArray()) ?? Character.Controlled; + if (character != null) + { + character.GiveTalent(args[0]); + } + }, + () => + { + List talentNames = new List(); + foreach (TalentPrefab itemPrefab in TalentPrefab.TalentPrefabs) + { + talentNames.Add(itemPrefab.Identifier); + } + + return new string[][] + { + talentNames.ToArray(), + Character.CharacterList.Select(c => c.Name).Distinct().ToArray() + }; + }, isCheat: true)); + + commands.Add(new Command("giveexperience", "giveexperience [amount] [character]: Give experience to character.", (string[] args) => + { + if (args.Length < 1) + { + NewMessage($"Missing arguments. Expected at least 1 but got {args.Length} (experience, name)"); + return; + } + + string experienceString = args[0]; + var character = FindMatchingCharacter(args.Skip(1).ToArray()) ?? Character.Controlled; + + if (character?.Info == null) + { + NewMessage("Character is not valid."); + return; + } + + if (int.TryParse(experienceString, NumberStyles.Number, CultureInfo.InvariantCulture, out int experience)) + { + character.Info.GiveExperience(experience); + NewMessage($"Gave {character.Name} {experience} experience"); + } + else + { + NewMessage($"{experienceString} is not a valid value. Expected number."); + } + }, isCheat: true, getValidArgs: () => + { + return new[] + { + new string[] { "100" }, + Character.CharacterList.Select(c => c.Name).Distinct().ToArray(), + }; + })); + commands.Add(new Command("fire|editfire", "fire/editfire: Allows putting up fires by left clicking.", (string[] args) => { Hull.EditFire = !Hull.EditFire; @@ -947,7 +1009,7 @@ namespace Barotrauma string subName = GameMain.Config.QuickStartSubmarineName; if (!string.IsNullOrEmpty(subName)) { - selectedSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name.ToLower() == subName.ToLower()); + selectedSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name.Equals(subName, StringComparison.OrdinalIgnoreCase)); } int count = 0; @@ -1013,7 +1075,7 @@ namespace Barotrauma if (args.Length == 0) { return; } if (float.TryParse(args[0], NumberStyles.Any, CultureInfo.InvariantCulture, out float reputation)) { - campaign.Map.CurrentLocation.Reputation.Value = reputation; + campaign.Map.CurrentLocation.Reputation.SetReputation(reputation); } else { @@ -1040,7 +1102,7 @@ namespace Barotrauma { if (float.TryParse(args[1], NumberStyles.Any, CultureInfo.InvariantCulture, out float reputation)) { - faction.Reputation.Value = reputation; + faction.Reputation.SetReputation(reputation); } else { @@ -1368,7 +1430,7 @@ namespace Barotrauma NewMessage((GameMain.GameSession.Map.AllowDebugTeleport ? "Enabled" : "Disabled") + " teleportation on the campaign map.", Color.White); }, isCheat: true)); - commands.Add(new Command("money", "", args => + commands.Add(new Command("money", "money [amount]: Gives the specified amount of money to the crew when a campaign is active.", args => { if (args.Length == 0) { return; } if (GameMain.GameSession?.GameMode is CampaignMode campaign) @@ -1488,8 +1550,8 @@ namespace Barotrauma { if (args.Length > 0) { - string packageName = string.Join(" ", args).ToLower(); - var package = GameMain.Config.AllEnabledPackages.FirstOrDefault(p => p.Name.ToLower() == packageName); + string packageName = string.Join(" ", args); + var package = GameMain.Config.AllEnabledPackages.FirstOrDefault(p => p.Name.Equals(packageName, StringComparison.OrdinalIgnoreCase)); if (package == null) { ThrowError("Content package \"" + packageName + "\" not found."); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs index d0a91b108..c127555da 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs @@ -23,5 +23,78 @@ OnProduceSpawned, OnOpen, OnClose, OnDeath = OnBroken, + OnSuccess, + OnAbility, } + + public enum AbilityEffectType + { + Undefined, + None, + OnAttack, + OnAttackResult, + OnAttacked, + OnGainSkillPoint, + OnAllyGainSkillPoint, + OnRepairComplete, + OnItemFabricationSkillGain, + OnItemFabricatedAmount, + OnAllyItemFabricatedAmount, + OnOpenItemContainer, + OnUseRangedWeapon, + OnReduceAffliction, + OnAddDamageAffliction, + OnSelfRagdoll, + OnAnyMissionCompleted, + OnAllMissionsCompleted, + OnGiveOrder, + OnCrewKillCharacter, + OnDieToCharacter, + OnAllyGainMissionExperience, + OnGainMissionExperience, + OnGainMissionMoney, + AfterSubmarineAttacked, + } + + public enum StatTypes + { + None, + // Skills + ElectricalSkillBonus, + HelmSkillBonus, + MechanicalSkillBonus, + MedicalSkillBonus, + WeaponsSkillBonus, + // Character attributes + MaximumHealthMultiplier, + MovementSpeed, + SwimmingSpeed, + BuffDurationMultiplier, + DebuffDurationMultiplier, + // Combat + AttackMultiplier, + RangedAttackSpeed, + TurretAttackSpeed, + MeleeAttackSpeed, + SpreadMultiplier, + // Utility + RepairSpeed, + // Misc + ReputationGainMultiplier, + MissionMoneyGainMultiplier, + ExperienceGainMultiplier, + MissionExperienceGainMultiplier, + + } + + public enum AbilityFlags + { + None, + MustWalk, + ImmuneToPressure, + IgnoredByEnemyAI, + MoveNormallyWhileDragging, + CanTinker, + } + } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs index 2ed053c81..d4f86a1f0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs @@ -50,7 +50,7 @@ namespace Barotrauma Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier.Equals(Identifier, StringComparison.OrdinalIgnoreCase)); if (faction != null) { - faction.Reputation.Value += Increase; + faction.Reputation.AddReputation(Increase); } else { @@ -64,14 +64,14 @@ namespace Barotrauma Location location = campaign.Map.CurrentLocation; if (location != null) { - location.Reputation.Value += Increase; + location.Reputation.AddReputation(Increase); IEnumerable locations = location.Connections.SelectMany(c => c.Locations).Distinct().Where(l => l != null && l != location); foreach (Location connectedLocation in locations) { Debug.Assert(connectedLocation.Reputation != null, "connectedLocation.Reputation != null"); if (connectedLocation.Reputation != null) { - connectedLocation.Reputation.Value += (Increase / 4); + connectedLocation.Reputation.AddReputation(Increase / 4); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index 3f8a0a93a..c9b03e99c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -380,9 +380,13 @@ namespace Barotrauma { pendingEventSets.Clear(); selectedEvents.Clear(); + activeEvents.Clear(); + QueuedEvents.Clear(); preloadedSprites.ForEach(s => s.Remove()); preloadedSprites.Clear(); + + pathFinder = null; } private float CalculateCommonness(EventPrefab eventPrefab, float baseCommonness) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index 77747d13a..47b9637e2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -1,4 +1,6 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Abilities; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Globalization; @@ -343,19 +345,40 @@ namespace Barotrauma public void GiveReward() { if (!(GameMain.GameSession.GameMode is CampaignMode campaign)) { return; } - campaign.Money += GetReward(Submarine.MainSub); + int reward = GetReward(Submarine.MainSub); + + float baseExperienceGain = reward * 0.15f; + + IEnumerable crewCharacters = GameSession.GetSessionCrewCharacters(); + + // use multipliers here so that we can easily add them together without introducing multiplicative XP stacking + var experienceGainMultiplier = new AbilityValue(1f); + crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnAllyGainMissionExperience, experienceGainMultiplier)); + crewCharacters.ForEach(c => experienceGainMultiplier.Value += c.GetStatValue(StatTypes.MissionExperienceGainMultiplier)); + + foreach (Character character in crewCharacters) + { + character.Info.GiveExperience((int)(baseExperienceGain * experienceGainMultiplier.Value), isMissionExperience: true); + } + + // apply money gains afterwards to prevent them from affecting XP gains + var moneyGainMultiplier = new AbilityValue(1f); + crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnGainMissionMoney, (this, moneyGainMultiplier))); + crewCharacters.ForEach(c => moneyGainMultiplier.Value += c.GetStatValue(StatTypes.MissionMoneyGainMultiplier)); + + campaign.Money += (int)(reward * moneyGainMultiplier.Value); foreach (KeyValuePair reputationReward in ReputationRewards) { if (reputationReward.Key.Equals("location", StringComparison.OrdinalIgnoreCase)) { - Locations[0].Reputation.Value += reputationReward.Value; - Locations[1].Reputation.Value += reputationReward.Value; + Locations[0].Reputation.AddReputation(reputationReward.Value); + Locations[1].Reputation.AddReputation(reputationReward.Value); } else { Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier.Equals(reputationReward.Key, StringComparison.OrdinalIgnoreCase)); - if (faction != null) { faction.Reputation.Value += reputationReward.Value; } + if (faction != null) { faction.Reputation.AddReputation(reputationReward.Value); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs index c93b28077..127e56d71 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs @@ -8,8 +8,6 @@ namespace Barotrauma { static class AutoItemPlacer { - private static readonly List spawnedItems = new List(); - public static bool OutputDebugInfo = false; public static void PlaceIfNeeded() @@ -41,7 +39,12 @@ namespace Barotrauma } } - private static void Place(IEnumerable subs) + public static void RegenerateLoot(Submarine sub, ItemContainer regeneratedContainer) + { + Place(sub.ToEnumerable(), regeneratedContainer); + } + + private static void Place(IEnumerable subs, ItemContainer regeneratedContainer = null) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { @@ -49,19 +52,29 @@ namespace Barotrauma return; } + List spawnedItems = new List(100); + int itemCountApprox = MapEntityPrefab.List.Count() / 3; var containers = new List(70 + 30 * subs.Count()); var prefabsWithContainer = new List(itemCountApprox / 3); var prefabsWithoutContainer = new List(itemCountApprox); var removals = new List(); - foreach (Item item in Item.ItemList) + // generate loot only for a specific container if defined + if (regeneratedContainer != null) { - if (!subs.Contains(item.Submarine)) { continue; } - if (item.GetRootInventoryOwner() is Character) { continue; } - containers.AddRange(item.GetComponents()); + containers.Add(regeneratedContainer); + } + else + { + foreach (Item item in Item.ItemList) + { + if (!subs.Contains(item.Submarine)) { continue; } + if (item.GetRootInventoryOwner() is Character) { continue; } + containers.AddRange(item.GetComponents()); + } + containers.Shuffle(Rand.RandSync.Server); } - containers.Shuffle(Rand.RandSync.Server); foreach (MapEntityPrefab prefab in MapEntityPrefab.List) { @@ -77,7 +90,6 @@ namespace Barotrauma } } - spawnedItems.Clear(); var validContainers = new Dictionary(); prefabsWithContainer.Shuffle(Rand.RandSync.Server); // Spawn items that have an ItemContainer component first so we can fill them up with items if needed (oxygen tanks inside the spawned diving masks, etc) @@ -152,8 +164,10 @@ namespace Barotrauma } foreach (var validContainer in validContainers) { - if (SpawnItem(itemPrefab, containers, validContainer)) + var newItems = SpawnItem(itemPrefab, containers, validContainer); + if (newItems.Any()) { + spawnedItems.AddRange(newItems); success = true; } } @@ -184,14 +198,15 @@ namespace Barotrauma return validContainers; } - private static bool SpawnItem(ItemPrefab itemPrefab, List containers, KeyValuePair validContainer) + private static List SpawnItem(ItemPrefab itemPrefab, List containers, KeyValuePair validContainer) { + List spawnedItems = new List(); bool success = false; - if (Rand.Value(Rand.RandSync.Server) > validContainer.Value.SpawnProbability) { return false; } + if (Rand.Value(Rand.RandSync.Server) > validContainer.Value.SpawnProbability) { return spawnedItems; } // Don't add dangerously reactive materials in thalamus wrecks if (validContainer.Key.Item.Submarine.WreckAI != null && itemPrefab.Tags.Contains("explodesinwater")) { - return false; + return spawnedItems; } int amount = Rand.Range(validContainer.Value.MinAmount, validContainer.Value.MaxAmount + 1, Rand.RandSync.Server); for (int i = 0; i < amount; i++) @@ -219,7 +234,7 @@ namespace Barotrauma containers.AddRange(item.GetComponents()); success = true; } - return success; + return spawnedItems; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs index 8eed7fcfa..49e0aa705 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs @@ -237,6 +237,9 @@ namespace Barotrauma { CharacterInfo.ApplyHealthData(character, character.Info.HealthData); } + + character.LoadTalents(); + character.GiveIdCardTags(spawnWaypoints[i]); character.Info.StartItemsGiven = true; if (character.Info.OrderData != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs index 9944fd2bc..49f175753 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs @@ -1,5 +1,6 @@ using Microsoft.Xna.Framework; using System; +using System.Linq; namespace Barotrauma { @@ -31,7 +32,7 @@ namespace Barotrauma public float Value { get => Math.Min(MaxReputation, Metadata.GetFloat(metaDataIdentifier, InitialReputation)); - set + private set { if (MathUtils.NearlyEqual(Value, value)) { return; } Metadata.SetValue(metaDataIdentifier, Math.Clamp(value, MinReputation, MaxReputation)); @@ -40,6 +41,25 @@ namespace Barotrauma } } + public void SetReputation(float newReputation) + { + Value = newReputation; + } + + public void AddReputation(float reputationChange) + { + if (reputationChange > 0f) + { + float reputationGainMultiplier = 1f; + foreach (Character character in Character.CharacterList.Where(c => c.TeamID == CharacterTeamType.Team1)) + { + reputationGainMultiplier += character.GetStatValue(StatTypes.ReputationGainMultiplier); + } + reputationChange *= reputationGainMultiplier; + } + Value += reputationChange; + } + public Action OnReputationValueChanged; public static Action OnAnyReputationValueChanged; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index 9c7e037e4..fdf7fa68a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -18,6 +18,10 @@ namespace Barotrauma public static CampaignSettings Unsure = Empty; public bool RadiationEnabled { get; set; } public int MaxMissionCount { get; set; } + public int AddedMissionCount { get; set; } + + public int TotalMaxMissionCount => MaxMissionCount + AddedMissionCount; + public const int DefaultMaxMissionCount = 2; public const int MaxMissionCountLimit = 10; @@ -27,23 +31,26 @@ namespace Barotrauma { RadiationEnabled = inc.ReadBoolean(); MaxMissionCount = inc.ReadInt32(); + AddedMissionCount = inc.ReadInt32(); } - + public CampaignSettings(XElement element) { - RadiationEnabled = element.GetAttributeBool(nameof(RadiationEnabled).ToLower(), true); - MaxMissionCount = element.GetAttributeInt(nameof(MaxMissionCount).ToLower(), DefaultMaxMissionCount); + RadiationEnabled = element.GetAttributeBool(nameof(RadiationEnabled).ToLowerInvariant(), true); + MaxMissionCount = element.GetAttributeInt(nameof(MaxMissionCount).ToLowerInvariant(), DefaultMaxMissionCount); + AddedMissionCount = element.GetAttributeInt(nameof(AddedMissionCount).ToLowerInvariant(), 0); } public void Serialize(IWriteMessage msg) { msg.Write(RadiationEnabled); msg.Write(MaxMissionCount); + msg.Write(AddedMissionCount); } public XElement Save() { - return new XElement(nameof(CampaignSettings), new XAttribute(nameof(RadiationEnabled).ToLower(), RadiationEnabled), new XAttribute(nameof(MaxMissionCount).ToLower().ToLower(), MaxMissionCount)); + return new XElement(nameof(CampaignSettings), new XAttribute(nameof(RadiationEnabled).ToLowerInvariant(), RadiationEnabled), new XAttribute(nameof(MaxMissionCount).ToLowerInvariant(), MaxMissionCount), new XAttribute(nameof(AddedMissionCount).ToLowerInvariant(), AddedMissionCount)); } } @@ -226,6 +233,8 @@ namespace Barotrauma PurchasedLostShuttles = false; var connectedSubs = Submarine.MainSub.GetConnectedSubs(); wasDocked = Level.Loaded.StartOutpost != null && connectedSubs.Contains(Level.Loaded.StartOutpost); + + ResetTalentData(); } public void InitCampaignData() @@ -846,7 +855,7 @@ namespace Barotrauma Location location = Map?.CurrentLocation; if (location != null) { - location.Reputation.Value -= attackResult.Damage * Reputation.ReputationLossPerNPCDamage; + location.Reputation.AddReputation(-attackResult.Damage * Reputation.ReputationLossPerNPCDamage); } } @@ -898,15 +907,24 @@ namespace Barotrauma { foreach (Location location in currentLocation.Connections.Select(c => c.OtherLocation(currentLocation))) { - if (NumberOfMissionsAtLocation(location) > Settings.MaxMissionCount) + if (NumberOfMissionsAtLocation(location) > Settings.TotalMaxMissionCount) { DebugConsole.AddWarning($"Client {sender.Name} had too many missions selected for location {location.Name}! Count was {NumberOfMissionsAtLocation(location)}. Deselecting extra missions."); - foreach (Mission mission in currentLocation.SelectedMissions.Where(m => m.Locations[1] == location).Skip(Settings.MaxMissionCount).ToList()) + foreach (Mission mission in currentLocation.SelectedMissions.Where(m => m.Locations[1] == location).Skip(Settings.TotalMaxMissionCount).ToList()) { currentLocation.DeselectMission(mission); } } } } + + // Talent relevant data, only stored for the duration of the mission + private void ResetTalentData() + { + CrewHasDied = false; + } + + public bool CrewHasDied { get; set; } + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index b17a7ec74..0a2e0bdd3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -29,6 +29,8 @@ namespace Barotrauma public bool IsRunning { get; private set; } + public bool RoundEnding { get; private set; } + public Level Level { get; private set; } public LevelData LevelData { get; private set; } @@ -654,43 +656,83 @@ namespace Barotrauma partial void UpdateProjSpecific(float deltaTime); + public static IEnumerable GetSessionCrewCharacters() + { +#if SERVER + return GameMain.Server.ConnectedClients.Select(c => c.Character).Where(c => c.Info != null); +#else + return GameMain.GameSession.CrewManager.CharacterInfos.Select(i => i.Character).Where(c => c != null); +#endif + } + public void EndRound(string endMessage, List traitorResults = null, CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None) { - foreach (Mission mission in missions) - { - mission.End(); - } -#if CLIENT - if (GUI.PauseMenuOpen) - { - GUI.TogglePauseMenu(); - } - GUI.PreventPauseMenuToggle = true; + RoundEnding = true; - if (!(GameMode is TestGameMode) && Screen.Selected == GameMain.GameScreen && RoundSummary != null) + try { - GUI.ClearMessages(); - GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData is RoundSummary); - GUIFrame summaryFrame = RoundSummary.CreateSummaryFrame(this, endMessage, traitorResults, transitionType); - GUIMessageBox.MessageBoxes.Add(summaryFrame); - RoundSummary.ContinueButton.OnClicked = (_, __) => { GUIMessageBox.MessageBoxes.Remove(summaryFrame); return true; }; - } + IEnumerable crewCharacters = GameSession.GetSessionCrewCharacters(); - if (GameMain.NetLobbyScreen != null) GameMain.NetLobbyScreen.OnRoundEnded(); - TabMenu.OnRoundEnded(); - GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData as string == "ConversationAction" || ReadyCheck.IsReadyCheck(mb)); -#endif - SteamAchievementManager.OnRoundEnded(this); + foreach (Mission mission in missions) + { + mission.End(); + } - GameMode?.End(transitionType); - EventManager?.EndRound(); - StatusEffect.StopAll(); - missions.Clear(); - IsRunning = false; + if (missions.Any()) + { + if (missions.Any(m => m.Completed)) + { + foreach (CharacterInfo characterInfo in GameMain.GameSession.CrewManager.CharacterInfos) + { + characterInfo.Character?.CheckTalents(AbilityEffectType.OnAnyMissionCompleted); + } + } + + if (missions.All(m => m.Completed)) + { + foreach (CharacterInfo characterInfo in GameMain.GameSession.CrewManager.CharacterInfos) + { + characterInfo.Character?.CheckTalents(AbilityEffectType.OnAllMissionsCompleted); + } + } + } #if CLIENT - HintManager.OnRoundEnded(); + if (GUI.PauseMenuOpen) + { + GUI.TogglePauseMenu(); + } + GUI.PreventPauseMenuToggle = true; + + if (!(GameMode is TestGameMode) && Screen.Selected == GameMain.GameScreen && RoundSummary != null) + { + GUI.ClearMessages(); + GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData is RoundSummary); + GUIFrame summaryFrame = RoundSummary.CreateSummaryFrame(this, endMessage, traitorResults, transitionType); + GUIMessageBox.MessageBoxes.Add(summaryFrame); + RoundSummary.ContinueButton.OnClicked = (_, __) => { GUIMessageBox.MessageBoxes.Remove(summaryFrame); return true; }; + } + + if (GameMain.NetLobbyScreen != null) { GameMain.NetLobbyScreen.OnRoundEnded(); } + TabMenu.OnRoundEnded(); + GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData as string == "ConversationAction" || ReadyCheck.IsReadyCheck(mb)); #endif + SteamAchievementManager.OnRoundEnded(this); + + GameMode?.End(transitionType); + EventManager?.EndRound(); + StatusEffect.StopAll(); + missions.Clear(); + IsRunning = false; + +#if CLIENT + HintManager.OnRoundEnded(); +#endif + } + finally + { + RoundEnding = false; + } } public void KillCharacter(Character character) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs index d5cc99aea..2183d6866 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs @@ -307,6 +307,7 @@ namespace Barotrauma public bool AutomaticQuickStartEnabled { get; set; } public bool AutomaticCampaignLoadEnabled { get; set; } public bool TextManagerDebugModeEnabled { get; set; } + public bool TestScreenEnabled { get; set; } public bool ModBreakerMode { get; set; } #endif @@ -548,6 +549,12 @@ namespace Barotrauma case ContentType.Text: TextManager.LoadTextPack(file.Path); break; + case ContentType.Talents: + TalentPrefab.LoadFromFile(file); + break; + case ContentType.TalentTrees: + TalentTree.LoadFromFile(file); + break; #if CLIENT case ContentType.Particles: GameMain.ParticleManager?.LoadPrefabsFromFile(file); @@ -594,6 +601,12 @@ namespace Barotrauma case ContentType.Text: TextManager.RemoveTextPack(file.Path); break; + case ContentType.Talents: + TalentPrefab.LoadFromFile(file); + break; + case ContentType.TalentTrees: + TalentTree.LoadFromFile(file); + break; #if CLIENT case ContentType.Particles: GameMain.ParticleManager?.RemovePrefabsByFile(file.Path); @@ -703,7 +716,6 @@ namespace Barotrauma public string MasterServerUrl { get; set; } public string RemoteContentUrl { get; set; } public bool AutoCheckUpdates { get; set; } - public bool WasGameUpdated { get; set; } private string playerName; public string PlayerName @@ -796,13 +808,6 @@ namespace Barotrauma LoadDefaultConfig(); - if (WasGameUpdated) - { - UpdaterUtil.CleanOldFiles(); - WasGameUpdated = false; - SaveNewDefaultConfig(); - } - LoadPlayerConfig(); } @@ -827,7 +832,6 @@ namespace Barotrauma MasterServerUrl = doc.Root.GetAttributeString("masterserverurl", MasterServerUrl); RemoteContentUrl = doc.Root.GetAttributeString("remotecontenturl", RemoteContentUrl); - WasGameUpdated = doc.Root.GetAttributeBool("wasgameupdated", WasGameUpdated); VerboseLogging = doc.Root.GetAttributeBool("verboselogging", VerboseLogging); SaveDebugConsoleLogs = doc.Root.GetAttributeBool("savedebugconsolelogs", SaveDebugConsoleLogs); AutoUpdateWorkshopItems = doc.Root.GetAttributeBool("autoupdateworkshopitems", AutoUpdateWorkshopItems); @@ -889,11 +893,6 @@ namespace Barotrauma doc.Root.Add(new XAttribute("senduserstatistics", sendUserStatistics)); } - if (WasGameUpdated) - { - doc.Root.Add(new XAttribute("wasgameupdated", true)); - } - XElement gMode = doc.Root.Element("graphicsmode"); if (gMode == null) { @@ -1147,6 +1146,7 @@ namespace Barotrauma new XAttribute("disableingamehints", DisableInGameHints) #if DEBUG , new XAttribute("automaticquickstartenabled", AutomaticQuickStartEnabled) + , new XAttribute(nameof(TestScreenEnabled).ToLower(), TestScreenEnabled) , new XAttribute("automaticcampaignloadenabled", AutomaticCampaignLoadEnabled) , new XAttribute("textmanagerdebugmodeenabled", TextManagerDebugModeEnabled) , new XAttribute("modbreakermode", ModBreakerMode) @@ -1402,6 +1402,7 @@ namespace Barotrauma DisableInGameHints = doc.Root.GetAttributeBool("disableingamehints", DisableInGameHints); #if DEBUG AutomaticQuickStartEnabled = doc.Root.GetAttributeBool("automaticquickstartenabled", AutomaticQuickStartEnabled); + TestScreenEnabled = doc.Root.GetAttributeBool(nameof(TestScreenEnabled).ToLower(), TestScreenEnabled); AutomaticCampaignLoadEnabled = doc.Root.GetAttributeBool("automaticcampaignloadenabled", AutomaticCampaignLoadEnabled); TextManagerDebugModeEnabled = doc.Root.GetAttributeBool("textmanagerdebugmodeenabled", TextManagerDebugModeEnabled); ModBreakerMode = doc.Root.GetAttributeBool("modbreakermode", ModBreakerMode); @@ -1686,7 +1687,6 @@ namespace Barotrauma Language = "English"; } MasterServerUrl = "http://www.undertowgames.com/baromaster"; - WasGameUpdated = false; VerboseLogging = false; SaveDebugConsoleLogs = false; AutoUpdateWorkshopItems = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs index a5e681c9a..e2d433d8c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs @@ -204,8 +204,8 @@ namespace Barotrauma.Items.Components target.InitializeLinks(); - if (!item.linkedTo.Contains(target.item)) item.linkedTo.Add(target.item); - if (!target.item.linkedTo.Contains(item)) target.item.linkedTo.Add(item); + if (!item.linkedTo.Contains(target.item)) { item.linkedTo.Add(target.item); } + if (!target.item.linkedTo.Contains(item)) { target.item.linkedTo.Add(item); } if (!target.item.Submarine.DockedTo.Contains(item.Submarine)) target.item.Submarine.ConnectedDockingPorts.Add(item.Submarine, target); if (!item.Submarine.DockedTo.Contains(target.item.Submarine)) item.Submarine.ConnectedDockingPorts.Add(target.item.Submarine, this); @@ -291,7 +291,7 @@ namespace Barotrauma.Items.Components List removedEntities = item.linkedTo.Where(e => e.Removed).ToList(); - foreach (MapEntity removed in removedEntities) item.linkedTo.Remove(removed); + foreach (MapEntity removed in removedEntities) { item.linkedTo.Remove(removed); } if (!item.linkedTo.Any(e => e is Hull) && !DockingTarget.item.linkedTo.Any(e => e is Hull)) { @@ -306,9 +306,8 @@ namespace Barotrauma.Items.Components if (myWayPoint != null && targetWayPoint != null) { myWayPoint.FindHull(); - myWayPoint.linkedTo.Add(targetWayPoint); targetWayPoint.FindHull(); - targetWayPoint.linkedTo.Add(myWayPoint); + myWayPoint.ConnectTo(targetWayPoint); } } } @@ -597,8 +596,9 @@ namespace Barotrauma.Items.Components { hullRects[i].X -= expand; hullRects[i].Width += expand * 2; - hullRects[i].Location -= MathUtils.ToPoint((subs[i].WorldPosition - subs[i].HiddenSubPosition)); + hullRects[i].Location -= MathUtils.ToPoint(subs[i].WorldPosition - subs[i].HiddenSubPosition); hulls[i] = new Hull(MapEntityPrefab.Find(null, "hull"), hullRects[i], subs[i]); + hulls[i].RoomName = IsHorizontal ? "entityname.dockingport" : "entityname.dockinghatch"; hulls[i].AddToGrid(subs[i]); hulls[i].FreeID(); @@ -716,8 +716,9 @@ namespace Barotrauma.Items.Components { hullRects[i].Y += expand; hullRects[i].Height += expand * 2; - hullRects[i].Location -= MathUtils.ToPoint((subs[i].WorldPosition - subs[i].HiddenSubPosition)); + hullRects[i].Location -= MathUtils.ToPoint(subs[i].WorldPosition - subs[i].HiddenSubPosition); hulls[i] = new Hull(MapEntityPrefab.Find(null, "hull"), hullRects[i], subs[i]); + hulls[i].RoomName = IsHorizontal ? "entityname.dockingport" : "entityname.dockinghatch"; hulls[i].AddToGrid(subs[i]); hulls[i].FreeID(); @@ -873,8 +874,10 @@ namespace Barotrauma.Items.Components { myWayPoint.FindHull(); myWayPoint.linkedTo.Remove(targetWayPoint); + myWayPoint.OnLinksChanged?.Invoke(myWayPoint); targetWayPoint.FindHull(); targetWayPoint.linkedTo.Remove(myWayPoint); + targetWayPoint.OnLinksChanged?.Invoke(targetWayPoint); } } @@ -1058,7 +1061,7 @@ namespace Barotrauma.Items.Components } } - if (!item.linkedTo.Any()) return; + if (!item.linkedTo.Any()) { return; } List linked = new List(item.linkedTo); foreach (MapEntity entity in linked) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs index a86e3f923..2d58be590 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs @@ -475,6 +475,21 @@ namespace Barotrauma.Items.Components } } + public override void ReceiveSignal(Signal signal, Connection connection) + { + switch (connection.Name) + { + case "activate": + case "use": + case "trigger_in": + if (signal.value != "0") + { + item.Use(1.0f, null); + } + break; + } + } + protected override void RemoveComponentSpecific() { base.RemoveComponentSpecific(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index cbfc92b1a..8c48733a9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -33,7 +33,7 @@ namespace Barotrauma.Items.Components private bool attachable, attached, attachedByDefault; private Voronoi2.VoronoiCell attachTargetCell; - private readonly PhysicsBody body; + private PhysicsBody body; public PhysicsBody Pusher { get; @@ -780,7 +780,19 @@ namespace Barotrauma.Items.Components DeattachFromWall(); } } - + + protected override void RemoveComponentSpecific() + { + base.RemoveComponentSpecific(); + attachTargetCell = null; + if (Pusher != null) + { + GameMain.World.Remove(Pusher.FarseerBody); + Pusher = null; + } + body = null; + } + public override XElement Save(XElement parentElement) { if (!attachable) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs index d426b1a70..d362beb6f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs @@ -1,11 +1,29 @@ -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using System.Collections.Generic; +using System.Linq; using System.Xml.Linq; namespace Barotrauma.Items.Components { partial class IdCard : Pickable { + [Serialize(CharacterTeamType.None, true, alwaysUseInstanceValues: true)] + public CharacterTeamType TeamID + { + get; + set; + } + + [Serialize(0, true, alwaysUseInstanceValues: true)] + public int SubmarineSpecificID + { + get; + set; + } + + private JobPrefab cachedJobPrefab; + private string cachedName; + public IdCard(Item item, XElement element) : base(item, element) { @@ -20,6 +38,8 @@ namespace Barotrauma.Items.Components item.AddTag("jobid:" + info.Job.Prefab.Identifier); } + TeamID = info.TeamID; + var head = info.Head; if (info != null && head != null) @@ -50,5 +70,48 @@ namespace Barotrauma.Items.Components base.Unequip(character); character.Info?.CheckDisguiseStatus(true, this); } + + public JobPrefab GetJob() + { + if (cachedJobPrefab != null) + { + return cachedJobPrefab; + } + + foreach (string tag in item.GetTags()) + { + if (tag.StartsWith("jobid:")) + { + string jobIdentifier = tag.Split(':').Last(); + if (JobPrefab.Get(jobIdentifier) is { } jobPrefab) + { + cachedJobPrefab = jobPrefab; + return jobPrefab; + } + } + } + + return null; + } + + public string GetName() + { + if (cachedName != null) + { + return cachedName; + } + + foreach (string tag in item.GetTags()) + { + if (tag.StartsWith("name:")) + { + string ownerName = tag.Split(':').Last(); + cachedName = ownerName; + return ownerName; + } + } + + return null; + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index f59597c61..b7174abdf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -61,7 +61,7 @@ namespace Barotrauma.Items.Components foreach (XElement subElement in element.Elements()) { if (!subElement.Name.ToString().Equals("attack", StringComparison.OrdinalIgnoreCase)) { continue; } - Attack = new Attack(subElement, item.Name + ", MeleeWeapon"); + Attack = new Attack(subElement, item.Name + ", MeleeWeapon", item); Attack.DamageRange = item.body == null ? 10.0f : ConvertUnits.ToDisplayUnits(item.body.GetMaxExtent()); } item.IsShootable = true; @@ -95,7 +95,7 @@ namespace Barotrauma.Items.Components if (hitPos < MathHelper.PiOver4) { return false; } ActivateNearbySleepingCharacters(); - reloadTimer = reload; + reloadTimer = reload / (1 + character.GetStatValue(StatTypes.MeleeAttackSpeed)); item.body.FarseerBody.CollisionCategories = Physics.CollisionProjectile; item.body.FarseerBody.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs index 8a73e00a7..d628c5d40 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs @@ -5,6 +5,7 @@ using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Xml.Linq; @@ -52,6 +53,21 @@ namespace Barotrauma.Items.Components set; } + [Serialize(0f, true, description: "The time required for a charge-type turret to charge up before able to fire.")] + public float MaxChargeTime + { + get; + private set; + } + + private enum ChargingState + { + Inactive, + WindingUp, + WindingDown, + } + private ChargingState currentChargingState; + public Vector2 TransformedBarrelPos { get @@ -62,7 +78,10 @@ namespace Barotrauma.Items.Components return Vector2.Transform(flippedPos, bodyTransform); } } - + + private float currentChargeTime; + private bool tryingToCharge; + public RangedWeapon(Item item, XElement element) : base(item, element) { @@ -88,10 +107,41 @@ namespace Barotrauma.Items.Components if (ReloadTimer < 0.0f) { ReloadTimer = 0.0f; - IsActive = false; + // was this an optimization or related to something else? currently disabled for charge-type weapons + //IsActive = false; + if (MaxChargeTime == 0.0f) + { + IsActive = false; + return; + } } + + float previousChargeTime = currentChargeTime; + + float chargeDeltaTime = tryingToCharge ? deltaTime : -deltaTime; + currentChargeTime = Math.Clamp(currentChargeTime + chargeDeltaTime, 0f, MaxChargeTime); + + tryingToCharge = false; + + if (currentChargeTime == 0f) + { + currentChargingState = ChargingState.Inactive; + } + else if (currentChargeTime < previousChargeTime) + { + currentChargingState = ChargingState.WindingDown; + } + else + { + // if we are charging up or at maxed charge, remain winding up + currentChargingState = ChargingState.WindingUp; + } + + UpdateProjSpecific(deltaTime); } + partial void UpdateProjSpecific(float deltaTime); + private float GetSpread(Character user) { float degreeOfFailure = 1.0f - DegreeOfSuccess(user); @@ -102,11 +152,16 @@ namespace Barotrauma.Items.Components private readonly List limbBodies = new List(); public override bool Use(float deltaTime, Character character = null) { + tryingToCharge = true; if (character == null || character.Removed) { return false; } if ((item.RequireAimToUse && !character.IsKeyDown(InputType.Aim)) || ReloadTimer > 0.0f) { return false; } + if (currentChargeTime < MaxChargeTime) { return false; } IsActive = true; - ReloadTimer = reload; + ReloadTimer = reload / (1 + character.GetStatValue(StatTypes.RangedAttackSpeed)); + currentChargeTime = 0f; + + character.CheckTalents(AbilityEffectType.OnUseRangedWeapon, item); if (item.AiTarget != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index 22f334166..90fd92739 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -711,7 +711,27 @@ namespace Barotrauma.Items.Components bool CheckItems(RelatedItem relatedItem, IEnumerable itemList) { - bool Predicate(Item it) => it != null && it.Condition > 0.0f && relatedItem.MatchesItem(it); + bool Predicate(Item it) + { + if (it == null || it.Condition <= 0.0f || !relatedItem.MatchesItem(it)) { return false; } + if (item.Submarine != null) + { + var idCard = it.GetComponent(); + if (idCard != null) + { + //id cards don't work in enemy subs (except on items that only require the default "idcard" tag) + if (idCard.TeamID != CharacterTeamType.None && idCard.TeamID != item.Submarine.TeamID && relatedItem.Identifiers.Any(id => id != "idcard")) + { + return false; + } + else if (idCard.SubmarineSpecificID != 0 && item.Submarine.SubmarineSpecificIDTag != idCard.SubmarineSpecificID) + { + return false; + } + } + } + return true; + }; bool shouldBreak = false; bool inEditor = false; #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index 5502221ae..336a5b183 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -140,6 +140,20 @@ namespace Barotrauma.Items.Components set; } + [Serialize(false, false, description: "Should the items be injected into the user.")] + public bool AutoInject + { + get; + set; + } + + [Serialize(0.5f, false, description: "The rotation in which the contained sprites are drawn (in degrees).")] + public float AutoInjectThreshold + { + get; + set; + } + [Serialize(false, false)] public bool RemoveContainedItemsOnDeconstruct { get; set; } @@ -237,9 +251,23 @@ namespace Barotrauma.Items.Components SpawnAlwaysContainedItems(); } - if (item.ParentInventory is CharacterInventory) + if (item.ParentInventory is CharacterInventory ownerInventory) { item.SetContainedItemPositions(); + + if (AutoInject) + { + if (ownerInventory?.Owner is Character ownerCharacter && + ownerCharacter.HealthPercentage / 100f <= AutoInjectThreshold && + ownerCharacter.HasEquippedItem(item)) + { + foreach (Item item in Inventory.AllItemsMod) + { + item.ApplyStatusEffects(ActionType.OnUse, 1.0f, ownerCharacter); + } + } + } + } else if (item.body != null && item.body.Enabled && @@ -256,6 +284,7 @@ namespace Barotrauma.Items.Components foreach (var activeContainedItem in activeContainedItems) { Item contained = activeContainedItem.Item; + if (activeContainedItem.ExcludeBroken && contained.Condition <= 0.0f) { continue; } StatusEffect effect = activeContainedItem.StatusEffect; @@ -299,6 +328,8 @@ namespace Barotrauma.Items.Components } } } + character.CheckTalents(AbilityEffectType.OnOpenItemContainer, item); + return base.Select(character); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs index 0e9457394..d766f09fa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs @@ -122,6 +122,12 @@ namespace Barotrauma.Items.Components float voltageFactor = MinVoltage <= 0.0f ? 1.0f : Math.Min(Voltage, 1.0f); Vector2 currForce = new Vector2(force * maxForce * forceMultiplier * voltageFactor, 0.0f); + + if (item.GetComponent()?.IsTinkering ?? false) + { + currForce *= 2.5f; + } + //less effective when in a bad condition currForce *= MathHelper.Lerp(0.5f, 2.0f, item.Condition / item.MaxCondition); if (item.Submarine.FlippedX) { currForce *= -1; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index ba0fd4715..8d84d9d26 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -4,8 +4,8 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Security.Cryptography; using System.Xml.Linq; +using Barotrauma.Abilities; namespace Barotrauma.Items.Components { @@ -24,6 +24,12 @@ namespace Barotrauma.Items.Components private Character user; + public float FabricationSpeedMultiplier + { + get; + set; + } + private ItemContainer inputContainer, outputContainer; [Serialize(1.0f, true)] @@ -240,8 +246,10 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { - if (fabricatedItem == null || !CanBeFabricated(fabricatedItem)) + var availableIngredients = GetAvailableIngredients(); + if (fabricatedItem == null || !CanBeFabricated(fabricatedItem, availableIngredients, user)) { + FabricationSpeedMultiplier = 1f; CancelFabricating(); return; } @@ -278,38 +286,56 @@ namespace Barotrauma.Items.Components if (powerConsumption <= 0) { Voltage = 1.0f; } - timeUntilReady -= deltaTime * Math.Min(Voltage, 1.0f); + timeUntilReady -= deltaTime * Math.Min(Voltage, 1.0f) * FabricationSpeedMultiplier; + FabricationSpeedMultiplier = 1f; + UpdateRequiredTimeProjSpecific(); if (timeUntilReady > 0.0f) { return; } if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) { - var availableIngredients = GetAvailableIngredients(); - foreach (FabricationRecipe.RequiredItem ingredient in fabricatedItem.RequiredItems) - { - for (int i = 0; i < ingredient.Amount; i++) + fabricatedItem.RequiredItems.ForEach(requiredItem => { + for (int usedPrefabsAmount = 0; usedPrefabsAmount < requiredItem.Amount; usedPrefabsAmount++) { - var availableItem = availableIngredients.FirstOrDefault(it => - it != null && ingredient.ItemPrefabs.Contains(it.Prefab) && - it.ConditionPercentage >= ingredient.MinCondition * 100.0f && - it.ConditionPercentage <= ingredient.MaxCondition * 100.0f); - if (availableItem == null) { continue; } - - if (ingredient.UseCondition && availableItem.ConditionPercentage - ingredient.MinCondition * 100 > 0.0f) //Leave it behind with reduced condition if it has enough to stay above 0 + foreach (ItemPrefab requiredPrefab in requiredItem.ItemPrefabs) { - availableItem.Condition -= availableItem.Prefab.Health * ingredient.MinCondition; - continue; + if (!availableIngredients.ContainsKey(requiredPrefab.Identifier)) { continue; } + + var availablePrefabs = availableIngredients[requiredPrefab.Identifier]; + var availablePrefab = availablePrefabs.FirstOrDefault(potentialPrefab => + { + return potentialPrefab.ConditionPercentage >= requiredItem.MinCondition * 100.0f && + potentialPrefab.ConditionPercentage <= requiredItem.MaxCondition * 100.0f; + }); + + if (availablePrefab == null) { continue; } + + if (requiredItem.UseCondition && availablePrefab.ConditionPercentage - requiredItem.MinCondition * 100 > 0.0f) //Leave it behind with reduced condition if it has enough to stay above 0 + { + availablePrefab.Condition -= availablePrefab.Prefab.Health * requiredItem.MinCondition; + continue; + } + + availablePrefabs.Remove(availablePrefab); + Entity.Spawner.AddToRemoveQueue(availablePrefab); + inputContainer.Inventory.RemoveItem(availablePrefab); } - availableIngredients.Remove(availableItem); - Entity.Spawner.AddToRemoveQueue(availableItem); - inputContainer.Inventory.RemoveItem(availableItem); } - } + }); Character tempUser = user; + int amountFittingContainer = outputContainer.Inventory.HowManyCanBePut(fabricatedItem.TargetItem, fabricatedItem.OutCondition * fabricatedItem.TargetItem.Health); - for (int i = 0; i < fabricatedItem.Amount; i++) + var itemsCreated = new AbilityValue(fabricatedItem.Amount); + foreach (Character character in Character.CharacterList.Where(c => c.TeamID == user.TeamID)) + { + character.CheckTalents(AbilityEffectType.OnAllyItemFabricatedAmount, (fabricatedItem.TargetItem, itemsCreated)); + } + + tempUser.CheckTalents(AbilityEffectType.OnItemFabricatedAmount, (fabricatedItem.TargetItem, itemsCreated)); + + for (int i = 0; i < (int)itemsCreated.Value; i++) { if (i < amountFittingContainer) { @@ -339,9 +365,13 @@ namespace Barotrauma.Items.Components foreach (Skill skill in fabricatedItem.RequiredSkills) { float userSkill = user.GetSkillLevel(skill.Identifier); + float addedSkill = skill.Level * SkillSettings.Current.SkillIncreasePerFabricatorRequiredSkill / Math.Max(userSkill, 1.0f); + var addedSkillValue = new AbilityValue(0f); + user.CheckTalents(AbilityEffectType.OnItemFabricationSkillGain, addedSkillValue); + user.Info.IncreaseSkillLevel( skill.Identifier, - skill.Level * SkillSettings.Current.SkillIncreasePerFabricatorRequiredSkill / Math.Max(userSkill, 1.0f), + addedSkill + addedSkillValue.Value, user.Position + Vector2.UnitY * 150.0f); } } @@ -365,24 +395,36 @@ namespace Barotrauma.Items.Components partial void UpdateRequiredTimeProjSpecific(); - private bool CanBeFabricated(FabricationRecipe fabricableItem) + private bool CanBeFabricated(FabricationRecipe fabricableItem, Dictionary> availableIngredients, Character character) { if (fabricableItem == null) { return false; } - List availableIngredients = GetAvailableIngredients(); - return CanBeFabricated(fabricableItem, availableIngredients); - } + if (fabricableItem.RequiresRecipe && (character == null || !character.HasRecipeForItem(fabricableItem.TargetItem.Identifier))) { return false; } - private bool CanBeFabricated(FabricationRecipe fabricableItem, IEnumerable availableIngredients) - { - if (fabricableItem == null) { return false; } - foreach (FabricationRecipe.RequiredItem requiredItem in fabricableItem.RequiredItems) + return fabricableItem.RequiredItems.All(requiredItem => { - if (availableIngredients.Count(it => IsItemValidIngredient(it, requiredItem)) < requiredItem.Amount) + int availablePrefabsAmount = 0; + foreach (ItemPrefab requiredPrefab in requiredItem.ItemPrefabs) { - return false; + if (!availableIngredients.ContainsKey(requiredPrefab.Identifier)) { continue; } + + var availablePrefabs = availableIngredients[requiredPrefab.Identifier]; + foreach (Item availablePrefab in availablePrefabs) + { + if (availablePrefab.Condition / availablePrefab.Prefab.Health >= requiredItem.MinCondition && + availablePrefab.Condition / availablePrefab.Prefab.Health <= requiredItem.MaxCondition) + { + availablePrefabsAmount++; + } + + if (availablePrefabsAmount >= requiredItem.Amount) + { + return true; + } + } } - } - return true; + + return false; + }); } private float GetRequiredTime(FabricationRecipe fabricableItem, Character user) @@ -416,7 +458,7 @@ namespace Barotrauma.Items.Components /// Get a list of all items available in the input container and linked containers /// /// - private List GetAvailableIngredients() + private Dictionary> GetAvailableIngredients() { List availableIngredients = new List(); availableIngredients.AddRange(inputContainer.Inventory.AllItems); @@ -448,7 +490,19 @@ namespace Barotrauma.Items.Components } #endif - return availableIngredients; + Dictionary> ingredientsDictionary = new Dictionary>(); + for (int i = 0; i < availableIngredients.Count; i++) + { + var itemIdentifier = availableIngredients[i].prefab.Identifier; + if (!ingredientsDictionary.ContainsKey(itemIdentifier)) + { + ingredientsDictionary[itemIdentifier] = new List(availableIngredients.Count); + } + + ingredientsDictionary[itemIdentifier].Add(availableIngredients[i]); + } + + return ingredientsDictionary; } /// @@ -463,40 +517,41 @@ namespace Barotrauma.Items.Components bool isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient; var availableIngredients = GetAvailableIngredients(); - foreach (var requiredItem in targetItem.RequiredItems) - { + targetItem.RequiredItems.ForEach(requiredItem => { for (int i = 0; i < requiredItem.Amount; i++) { - var matchingItem = availableIngredients.Find(it => !usedItems.Contains(it) && IsItemValidIngredient(it, requiredItem)); - if (matchingItem == null) { continue; } + foreach (ItemPrefab requiredPrefab in requiredItem.ItemPrefabs) + { + if (!availableIngredients.ContainsKey(requiredPrefab.Identifier)) { continue; } - availableIngredients.Remove(matchingItem); - - if (matchingItem.ParentInventory == inputContainer.Inventory) - { - //already in input container, all good - usedItems.Add(matchingItem); - } - else //in another inventory, we need to move the item - { - if (!inputContainer.Inventory.CanBePut(matchingItem)) + var availablePrefabs = availableIngredients[requiredPrefab.Identifier]; + var availablePrefab = availablePrefabs.FirstOrDefault(potentialPrefab => { - var unneededItem = inputContainer.Inventory.AllItems.FirstOrDefault(it => !usedItems.Contains(it)); - unneededItem?.Drop(null, createNetworkEvent: !isClient); - } - inputContainer.Inventory.TryPutItem(matchingItem, user: null, createNetworkEvent: !isClient); - } - } - } - } + return !usedItems.Contains(potentialPrefab) && + potentialPrefab.ConditionPercentage >= requiredItem.MinCondition * 100.0f && + potentialPrefab.ConditionPercentage <= requiredItem.MaxCondition * 100.0f; + }); + if (availablePrefab == null) { continue; } - private bool IsItemValidIngredient(Item item, FabricationRecipe.RequiredItem requiredItem) - { - return - item != null && - requiredItem.ItemPrefabs.Contains(item.prefab) && - item.Condition / item.Prefab.Health >= requiredItem.MinCondition && - item.Condition / item.Prefab.Health <= requiredItem.MaxCondition; + availablePrefabs.Remove(availablePrefab); + + if (availablePrefab.ParentInventory == inputContainer.Inventory) + { + //already in input container, all good + usedItems.Add(availablePrefab); + } + else //in another inventory, we need to move the item + { + if (!inputContainer.Inventory.CanBePut(availablePrefab)) + { + var unneededItem = inputContainer.Inventory.AllItems.FirstOrDefault(it => !usedItems.Contains(it)); + unneededItem?.Drop(null, createNetworkEvent: !isClient); + } + inputContainer.Inventory.TryPutItem(availablePrefab, user: null, createNetworkEvent: !isClient); + } + } + } + }); } public override XElement Save(XElement parentElement) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs index 8ffa96100..38d3c40fa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs @@ -7,10 +7,15 @@ namespace Barotrauma.Items.Components { partial class MiniMap : Powered { - class HullData + internal class HullData { - public float? Oxygen; - public float? Water; + public float? HullOxygenAmount, + HullWaterAmount; + + public float? ReceivedOxygenAmount, + ReceivedWaterAmount; + + public readonly HashSet Cards = new HashSet(); public bool Distort; public float DistortionTimer; @@ -45,17 +50,45 @@ namespace Barotrauma.Items.Components set; } + [Editable, Serialize(true, true, description: "Enable hull status mode.")] + public bool EnableHullStatus + { + get; + set; + } + + [Editable, Serialize(true, true, description: "Enable electrical view mode.")] + public bool EnableElectricalView + { + get; + set; + } + + [Editable, Serialize(true, true, description: "Enable hull condition mode.")] + public bool EnableHullCondition + { + get; + set; + } + + [Editable, Serialize(true, true, description: "Enable item finder mode.")] + public bool EnableItemFinder + { + get; + set; + } + public MiniMap(Item item, XElement element) : base(item, element) { IsActive = true; hullDatas = new Dictionary(); - InitProjSpecific(element); + InitProjSpecific(); } - partial void InitProjSpecific(XElement element); + partial void InitProjSpecific(); - public override void Update(float deltaTime, Camera cam) + public override void Update(float deltaTime, Camera cam) { //periodically reset all hull data //(so that outdated hull info won't be shown if detectors stop sending signals) @@ -65,13 +98,29 @@ namespace Barotrauma.Items.Components { if (!hullData.Distort) { - hullData.Oxygen = null; - hullData.Water = null; + hullData.ReceivedOxygenAmount = null; + hullData.ReceivedWaterAmount = null; } } resetDataTime = DateTime.Now + new TimeSpan(0, 0, 1); } +#if CLIENT + if (cardRefreshTimer > cardRefreshDelay) + { + if (item.Submarine is { } sub) + { + UpdateIDCards(sub); + } + + cardRefreshTimer = 0; + } + else + { + cardRefreshTimer += deltaTime; + } +#endif + currPowerConsumption = powerConsumption; currPowerConsumption *= MathHelper.Lerp(1.5f, 1.0f, item.Condition / item.MaxCondition); @@ -81,7 +130,7 @@ namespace Barotrauma.Items.Components ApplyStatusEffects(ActionType.OnActive, deltaTime, null); } } - + public override bool Pick(Character picker) { return picker != null; @@ -107,11 +156,11 @@ namespace Barotrauma.Items.Components //cheating a bit because water detectors don't actually send the water level if (source.GetComponent() == null) { - hullData.Water = Rand.Range(0.0f, 1.0f); + hullData.ReceivedWaterAmount = Rand.Range(0.0f, 1.0f); } else { - hullData.Water = Math.Min(sourceHull.WaterVolume / sourceHull.Volume, 1.0f); + hullData.ReceivedWaterAmount = Math.Min(sourceHull.WaterVolume / sourceHull.Volume, 1.0f); } break; case "oxygen_data_in": @@ -122,7 +171,7 @@ namespace Barotrauma.Items.Components oxy = Rand.Range(0.0f, 100.0f); } - hullData.Oxygen = oxy; + hullData.ReceivedOxygenAmount = oxy; break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs index 073ae51cf..4231489a5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs @@ -105,6 +105,12 @@ namespace Barotrauma.Items.Components float powerFactor = Math.Min(currPowerConsumption <= 0.0f || MinVoltage <= 0.0f ? 1.0f : Voltage, 1.0f); currFlow = flowPercentage / 100.0f * maxFlow * powerFactor; + + if (item.GetComponent()?.IsTinkering ?? false) + { + currFlow *= 2.5f; + } + //less effective when in a bad condition currFlow *= MathHelper.Lerp(0.5f, 1.0f, item.Condition / item.MaxCondition); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs index 1d0ad14bb..543e9bc1b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs @@ -1,4 +1,4 @@ -using Barotrauma.Networking; +using Barotrauma.Networking; using FarseerPhysics; using Microsoft.Xna.Framework; using System; @@ -364,6 +364,19 @@ namespace Barotrauma.Items.Components float velY = MathHelper.Lerp((neutralBallastLevel * 100 - 50) * 2, -100 * Math.Sign(targetVelocity.Y), Math.Abs(targetVelocity.Y) / 100.0f); item.SendSignal(new Signal(velY.ToString(CultureInfo.InvariantCulture), sender: user), "velocity_y_out"); + // converts the controlled sub's velocity to km/h and sends it. + // TODO: add current_velocity_x and current_velocity_y pins on the navigation terminals and shuttle terminals + // TODO: increase the size of the connection panels of both navigation terminals + + if (controlledSub is { } sub) + { + item.SendSignal(new Signal((ConvertUnits.ToDisplayUnits(sub.Velocity.X * Physics.DisplayToRealWorldRatio) * 3.6f).ToString("0.0000", CultureInfo.InvariantCulture), sender: user), "current_velocity_x"); + item.SendSignal(new Signal((ConvertUnits.ToDisplayUnits(sub.Velocity.Y * Physics.DisplayToRealWorldRatio) * -3.6f).ToString("0.0000", CultureInfo.InvariantCulture), sender: user), "current_velocity_y"); + + item.SendSignal(new Signal(sub.WorldPosition.X.ToString("0.0000", CultureInfo.InvariantCulture), sender: user), "current_position_x"); + item.SendSignal(new Signal(sub.RealWorldDepth.ToString("0.0000", CultureInfo.InvariantCulture), sender: user), "current_depth"); + } + // if our tactical AI pilot has left, revert back to maintaining position if (navigateTactically && (user == null || user.SelectedConstruction != item)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs index 1828f7cee..a587d47ff 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs @@ -370,5 +370,12 @@ namespace Barotrauma.Items.Components } } } + + protected override void RemoveComponentSpecific() + { + base.RemoveComponentSpecific(); + connectedRecipients?.Clear(); + connectionDirty?.Clear(); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index 1134861ec..6af8ff629 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -201,7 +201,7 @@ namespace Barotrauma.Items.Components foreach (XElement subElement in element.Elements()) { if (!subElement.Name.ToString().Equals("attack", StringComparison.OrdinalIgnoreCase)) { continue; } - Attack = new Attack(subElement, item.Name + ", Projectile"); + Attack = new Attack(subElement, item.Name + ", Projectile", item); } InitProjSpecific(element); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index 26a50ebf7..2b474213b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -103,18 +103,22 @@ namespace Barotrauma.Items.Components } } + public bool IsTinkering { get; private set; } = false; + public float RepairIconThreshold { get { return RepairThreshold / 2; } } public Character CurrentFixer { get; private set; } + private Item currentRepairItem; public enum FixActions : int { None = 0, Repair = 1, - Sabotage = 2 + Sabotage = 2, + Tinker = 3, } private FixActions currentFixerAction = FixActions.None; @@ -161,12 +165,14 @@ namespace Barotrauma.Items.Components /// /// Check if the character manages to succesfully repair the item /// - public bool CheckCharacterSuccess(Character character) + public bool CheckCharacterSuccess(Character character, Item bestRepairItem) { if (character == null) { return false; } if (statusEffectLists == null || statusEffectLists.None(s => s.Key == ActionType.OnFailure)) { return true; } + if (bestRepairItem != null && bestRepairItem.Prefab.CannotRepairFail) { return true; } + // unpowered (electrical) items can be repaired without a risk of electrical shock if (requiredSkills.Any(s => s != null && s.Identifier.Equals("electrical", StringComparison.OrdinalIgnoreCase)) && item.GetComponent() is Powered powered && powered.Voltage < 0.1f) { return true; } @@ -201,10 +207,11 @@ namespace Barotrauma.Items.Components } else { + Item bestRepairItem = GetBestRepairItem(character); #if SERVER if (CurrentFixer != character || currentFixerAction != action) { - if (!CheckCharacterSuccess(character)) + if (!CheckCharacterSuccess(character, bestRepairItem)) { GameServer.Log($"{GameServer.CharacterLogName(character)} failed to {(action == FixActions.Sabotage ? "sabotage" : "repair")} {item.Name}", ServerLog.MessageType.ItemInteraction); GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFailure, this, character.ID }); @@ -215,11 +222,18 @@ namespace Barotrauma.Items.Components item.CreateServerEvent(this); } #else - if (GameMain.Client == null && (CurrentFixer != character || currentFixerAction != action) && !CheckCharacterSuccess(character)) { return false; } + if (GameMain.Client == null && (CurrentFixer != character || currentFixerAction != action) && !CheckCharacterSuccess(character, bestRepairItem)) { return false; } #endif CurrentFixer = character; + currentRepairItem = bestRepairItem; CurrentFixerAction = action; return true; + + Item GetBestRepairItem(Character character) + { + return character.HeldItems.OrderByDescending(i => i.Prefab.AddedRepairSpeedMultiplier).FirstOrDefault(); + } + } } @@ -233,8 +247,16 @@ namespace Barotrauma.Items.Components item.CreateServerEvent(this); } #endif + if (currentRepairItem != null) + { + foreach (var ic in currentRepairItem.GetComponents()) + { + ic.ApplyStatusEffects(ActionType.OnSuccess, 1.0f, character); + } + } CurrentFixer.AnimController.Anim = AnimController.Animation.None; CurrentFixer = null; + currentRepairItem = null; currentFixerAction = FixActions.None; #if CLIENT repairSoundChannel?.FadeOutAndDispose(); @@ -266,7 +288,8 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { UpdateProjSpecific(deltaTime); - + IsTinkering = false; + if (CurrentFixer == null) { if (deteriorateAlwaysResetTimer > 0.0f) @@ -314,6 +337,20 @@ namespace Barotrauma.Items.Components return; } + if (currentFixerAction == FixActions.Tinker) + { + // this is a bit code rotty to interject it here, should be less reliant on returning + if (!CanTinker(CurrentFixer)) + { + StopRepairing(CurrentFixer); + } + else + { + IsTinkering = true; + } + return; + } + float successFactor = requiredSkills.Count == 0 ? 1.0f : RepairDegreeOfSuccess(CurrentFixer, requiredSkills); //item must have been below the repair threshold for the player to get an achievement or XP for repairing it @@ -327,6 +364,8 @@ namespace Barotrauma.Items.Components } float fixDuration = MathHelper.Lerp(FixDurationLowSkill, FixDurationHighSkill, successFactor); + fixDuration /= 1 + CurrentFixer.GetStatValue(StatTypes.RepairSpeed) + currentRepairItem?.Prefab.AddedRepairSpeedMultiplier ?? 0f; + if (currentFixerAction == FixActions.Repair) { if (fixDuration <= 0.0f) @@ -354,6 +393,7 @@ namespace Barotrauma.Items.Components CurrentFixer.Position + Vector2.UnitY * 100.0f); } SteamAchievementManager.OnItemRepaired(item, CurrentFixer); + CurrentFixer.CheckTalents(AbilityEffectType.OnRepairComplete); } deteriorationTimer = Rand.Range(MinDeteriorationDelay, MaxDeteriorationDelay); wasBroken = false; @@ -399,6 +439,12 @@ namespace Barotrauma.Items.Components } } + private bool CanTinker(Character character) + { + if (!character.HasAbilityFlag(AbilityFlags.CanTinker)) { return false; } + return true; + } + partial void UpdateProjSpecific(float deltaTime); public void AdjustPowerConsumption(ref float powerConsumption) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs index 38c56e427..e7648c9ac 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs @@ -350,6 +350,7 @@ namespace Barotrauma.Items.Components } } } + Connections.Clear(); #if CLIENT rewireSoundChannel?.FadeOutAndDispose(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OscillatorComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OscillatorComponent.cs index 309bfee73..6d92474fe 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OscillatorComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OscillatorComponent.cs @@ -12,8 +12,10 @@ namespace Barotrauma.Items.Components public enum WaveType { Pulse, + Sawtooth, Sine, Square, + Triangle, } private float frequency; @@ -22,8 +24,11 @@ namespace Barotrauma.Items.Components [InGameEditable, Serialize(WaveType.Pulse, true, description: "What kind of a signal the item outputs." + " Pulse: periodically sends out a signal of 1." + + " Sawtooth: sends out a periodic wave that increases linearly from 0 to 1." + " Sine: sends out a sine wave oscillating between -1 and 1." + - " Square: sends out a signal that alternates between 0 and 1.", alwaysUseInstanceValues: true)] + " Square: sends out a signal that alternates between 0 and 1." + + " Triangle: sends out a wave that alternates between increasing linearly from -1 to 1 and decreasing from 1 to -1.", + alwaysUseInstanceValues: true)] public WaveType OutputType { get; @@ -63,6 +68,10 @@ namespace Barotrauma.Items.Components phase -= pulseInterval; } break; + case WaveType.Sawtooth: + phase = (phase + deltaTime * frequency) % 1.0f; + item.SendSignal(phase.ToString(CultureInfo.InvariantCulture), "signal_out"); + break; case WaveType.Square: phase = (phase + deltaTime * frequency) % 1.0f; item.SendSignal(phase < 0.5f ? "0" : "1", "signal_out"); @@ -71,6 +80,11 @@ namespace Barotrauma.Items.Components phase = (phase + deltaTime * frequency) % 1.0f; item.SendSignal(Math.Sin(phase * MathHelper.TwoPi).ToString(CultureInfo.InvariantCulture), "signal_out"); break; + case WaveType.Triangle: + phase = (phase + deltaTime * frequency) % 1.0f; + float output = 4.0f * MathF.Abs(MathUtils.PositiveModulo(phase - 0.25f, 1.0f) - 0.5f) - 1.0f; + item.SendSignal(output.ToString(CultureInfo.InvariantCulture), "signal_out"); + break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs index b2f512ca2..ed67a0e46 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs @@ -85,14 +85,15 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { - if (string.IsNullOrWhiteSpace(expression) || regex == null) return; + if (string.IsNullOrWhiteSpace(expression) || regex == null) { return; } + if (!ContinuousOutput && nonContinuousOutputSent) { return; } if (receivedSignal != previousReceivedSignal && receivedSignal != null) { try { Match match = regex.Match(receivedSignal); - previousResult = match.Success; + previousResult = match.Success; previousGroups = UseCaptureGroup && previousResult ? match.Groups : null; previousReceivedSignal = receivedSignal; @@ -133,7 +134,7 @@ namespace Barotrauma.Items.Components { if (!string.IsNullOrEmpty(signalOut)) { item.SendSignal(signalOut, "signal_out"); } } - else if (!nonContinuousOutputSent) + else { if (!string.IsNullOrEmpty(signalOut)) { item.SendSignal(signalOut, "signal_out"); } nonContinuousOutputSent = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs index fb93543ab..540284013 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs @@ -45,6 +45,9 @@ namespace Barotrauma.Items.Components } } + [Editable, Serialize(false, true, description: "The terminal will use a monospace font if this box is ticked.", alwaysUseInstanceValues: true)] + public bool UseMonospaceFont { get; set; } + private string OutputValue { get; set; } public Terminal(Item item, XElement element) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs index 7c6c61021..763800065 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs @@ -100,9 +100,11 @@ namespace Barotrauma.Items.Components if (item.CurrentHull != null) { - int waterPercentage = MathHelper.Clamp((int)Math.Round(item.CurrentHull.WaterPercentage), 0, 100); + int waterPercentage = MathHelper.Clamp((int)Math.Ceiling(item.CurrentHull.WaterPercentage), 0, 100); item.SendSignal(waterPercentage.ToString(), "water_%"); } + string highPressureOut = (item.CurrentHull == null || item.CurrentHull.LethalPressure > 5.0f) ? "1" : "0"; + item.SendSignal(highPressureOut, "high_pressure"); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index 3ea88ae11..0bed7d299 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -544,6 +544,7 @@ namespace Barotrauma.Items.Components Projectile launchedProjectile = null; bool loaderBroken = false; + bool isTinkering = false; for (int i = 0; i < ProjectileCount; i++) { var projectiles = GetLoadedProjectiles(); @@ -575,6 +576,7 @@ namespace Barotrauma.Items.Components projectiles = GetLoadedProjectiles(); if (projectiles.Any()) { break; } } + } } if (projectiles.Count == 0 && !LaunchWithoutProjectile) @@ -601,10 +603,25 @@ namespace Barotrauma.Items.Components return false; } failedLaunchAttempts = 0; + + foreach (MapEntity e in item.linkedTo) + { + if (!(e is Item linkedItem)) { continue; } + if (!item.prefab.IsLinkAllowed(e.prefab)) { continue; } + if (linkedItem.GetComponent() is Repairable repairable && linkedItem.HasTag("turretammosource")) + { + isTinkering = repairable.IsTinkering; + } + } + if (!ignorePower) { var batteries = item.GetConnectedComponents(); float neededPower = powerConsumption; + if (isTinkering) + { + neededPower /= 1.25f; + } while (neededPower > 0.0001f && batteries.Count > 0) { batteries.RemoveAll(b => b.Charge <= 0.0001f || b.MaxOutPut <= 0.0001f); @@ -622,7 +639,8 @@ namespace Barotrauma.Items.Components } launchedProjectile = projectiles.FirstOrDefault(); - if (launchedProjectile?.Item.Container != null) + Item container = launchedProjectile?.Item.Container; + if (container != null) { var repairable = launchedProjectile?.Item.Container.GetComponent(); if (repairable != null) @@ -637,18 +655,22 @@ namespace Barotrauma.Items.Components { foreach (Projectile projectile in projectiles) { - Launch(projectile.Item, character); + Launch(projectile.Item, character, isTinkering: isTinkering); } } else { - Launch(null, character); + Launch(null, character, isTinkering: isTinkering); } if (item.AiTarget != null) { item.AiTarget.SoundRange = item.AiTarget.MaxSoundRange; // Turrets also have a light component, which handles the sight range. } + if (container != null) + { + ShiftItemsInProjectileContainer(container.GetComponent()); + } } } @@ -672,9 +694,18 @@ namespace Barotrauma.Items.Components return true; } - private void Launch(Item projectile, Character user = null, float? launchRotation = null) + private void Launch(Item projectile, Character user = null, float? launchRotation = null, bool isTinkering = false) { reload = reloadTime; + if (isTinkering) + { + reload /= 1.25f; + } + + if (user != null) + { + reload /= 1 + user.GetStatValue(StatTypes.TurretAttackSpeed); + } if (projectile != null) { @@ -698,6 +729,10 @@ namespace Barotrauma.Items.Components if (projectileComponent != null) { projectileComponent.Attacker = user; + if (isTinkering) + { + projectileComponent.Attack.DamageMultiplier *= 1.25f; + } projectileComponent.Use(); projectile.GetComponent()?.Attach(item, projectile); projectileComponent.User = user; @@ -725,6 +760,26 @@ namespace Barotrauma.Items.Components partial void LaunchProjSpecific(); + private void ShiftItemsInProjectileContainer(ItemContainer container) + { + if (container == null) { return; } + bool moved; + do + { + moved = false; + for (int i = 1; i < container.Capacity; i++) + { + if (container.Inventory.GetItemAt(i) is Item item1 && container.Inventory.CanBePutInSlot(item1, i - 1)) + { + if (container.Inventory.TryPutItem(item1, i - 1, allowSwapping: false, allowCombine: false, user: null, createNetworkEvent: true)) + { + moved = true; + } + } + } + } while (moved); + } + private float waitTimer; private float disorderTimer; @@ -864,57 +919,26 @@ namespace Barotrauma.Items.Components float turretAngle = -rotation; if (Math.Abs(MathUtils.GetShortestAngle(enemyAngle, turretAngle)) > 0.15f) { return; } } - Vector2 start = ConvertUnits.ToSimUnits(item.WorldPosition); Vector2 end = ConvertUnits.ToSimUnits(target.WorldPosition); + // Check that there's not other entities that shouldn't be targeted (like a friendly sub) between us and the target. + Body worldTarget = CheckLineOfSight(start, end); + bool shoot; if (target.Submarine != null) { start -= target.Submarine.SimPosition; end -= target.Submarine.SimPosition; - } - var collisionCategories = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel; - var pickedBody = Submarine.PickBody(start, end, null, collisionCategories, allowInsideFixture: true, - customPredicate: (Fixture f) => - { - if (f.UserData is Item i && i.GetComponent() != null) { return false; } - return !item.StaticFixtures.Contains(f); - }); - if (pickedBody == null) { return; } - Character targetCharacter = null; - if (pickedBody.UserData is Character c) - { - targetCharacter = c; - } - else if (pickedBody.UserData is Limb limb) - { - targetCharacter = limb.character; - } - if (targetCharacter != null) - { - if (targetCharacter.Params.Group.Equals(ai.Config.Entity, StringComparison.OrdinalIgnoreCase)) - { - // Don't shoot friendly characters - return; - } + Body transformedTarget = CheckLineOfSight(start, end); + shoot = CanShoot(transformedTarget, user: null, ai, targetSubmarines) && (worldTarget == null || CanShoot(worldTarget, user: null, ai, targetSubmarines)); } else { - if (pickedBody.UserData is ISpatialEntity e) - { - Submarine sub = e.Submarine; - if (sub == null) { return; } - if (!targetSubmarines) { return; } - if (sub == Item.Submarine) { return; } - // Don't shoot non-player submarines, i.e. wrecks or outposts. - if (!sub.Info.IsPlayer) { return; } - } - else - { - // Hit something else, probably a level wall - return; - } + shoot = CanShoot(worldTarget, user: null, ai, targetSubmarines); + } + if (shoot) + { + TryLaunch(deltaTime, ignorePower: true); } - TryLaunch(deltaTime, ignorePower: true); } public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) @@ -1067,7 +1091,8 @@ namespace Barotrauma.Items.Components { // Ignore dead, friendly, and those that are inside the same sub if (enemy.IsDead || !enemy.Enabled || enemy.Submarine == character.Submarine) { continue; } - // Don't aim monsters that are inside a submarine. + if (enemy.Submarine != null && enemy.Submarine.TeamID == character.Submarine.TeamID) { continue; } + // Don't aim monsters that are inside any submarine. if (!enemy.IsHuman && enemy.CurrentHull != null) { continue; } if (HumanAIController.IsFriendly(character, enemy)) { continue; } float dist = Vector2.DistanceSquared(enemy.WorldPosition, item.WorldPosition); @@ -1091,26 +1116,34 @@ namespace Barotrauma.Items.Components if (closestEnemy != null) { - // Target the closest limb. Doesn't make much difference with smaller creatures, but enables the bots to shoot longer abyss creatures like the endworm. Otherwise they just target the main body = head. targetPos = closestEnemy.WorldPosition; - float closestDist = closestDistance; - foreach (Limb limb in closestEnemy.AnimController.Limbs) + //if the enemy is inside another sub, aim at the room they're in to make it less obvious that the enemy "knows" exactly where the target is + if (closestEnemy.Submarine != null && closestEnemy.CurrentHull != null && closestEnemy.Submarine != item.Submarine) { - if (limb.IsSevered) { continue; } - if (limb.Hidden) { continue; } - if (!CheckTurretAngle(limb.WorldPosition)) { continue; } - float dist = Vector2.DistanceSquared(limb.WorldPosition, item.WorldPosition); - if (dist < closestDist) - { - closestDist = dist; - targetPos = limb.WorldPosition; - } + targetPos = closestEnemy.CurrentHull.WorldPosition; } - if (closestDist > shootDistance * shootDistance) + else { - // Not close enough to shoot - closestEnemy = null; - targetPos = null; + // Target the closest limb. Doesn't make much difference with smaller creatures, but enables the bots to shoot longer abyss creatures like the endworm. Otherwise they just target the main body = head. + float closestDist = closestDistance; + foreach (Limb limb in closestEnemy.AnimController.Limbs) + { + if (limb.IsSevered) { continue; } + if (limb.Hidden) { continue; } + if (!CheckTurretAngle(limb.WorldPosition)) { continue; } + float dist = Vector2.DistanceSquared(limb.WorldPosition, item.WorldPosition); + if (dist < closestDist) + { + closestDist = dist; + targetPos = limb.WorldPosition; + } + } + if (closestDist > shootDistance * shootDistance) + { + // Not close enough to shoot + closestEnemy = null; + targetPos = null; + } } } else if (item.Submarine != null && Level.Loaded != null) @@ -1158,7 +1191,7 @@ namespace Barotrauma.Items.Components continue; } // Allow targeting farther when heading towards the spire (up to 1000 px) - dist -= MathHelper.Lerp(0, 1000, MathUtils.InverseLerp(minAngle, 1, dot)); ; + dist -= MathHelper.Lerp(0, 1000, MathUtils.InverseLerp(minAngle, 1, dot)); if (dist > closestDistance) { continue; } targetPos = closestPoint; closestDistance = dist; @@ -1222,58 +1255,25 @@ namespace Barotrauma.Items.Components if (Math.Abs(MathUtils.GetShortestAngle(enemyAngle, turretAngle)) > maxAngleError) { return false; } - Vector2 start = ConvertUnits.ToSimUnits(item.WorldPosition); - Vector2 end = ConvertUnits.ToSimUnits(targetPos.Value); - if (closestEnemy != null && closestEnemy.Submarine != null) - { - start -= closestEnemy.Submarine.SimPosition; - end -= closestEnemy.Submarine.SimPosition; - } - var collisionCategories = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel; - var pickedBody = Submarine.PickBody(start, end, null, collisionCategories, allowInsideFixture: true, - customPredicate: (Fixture f) => - { - if (f.UserData is Item i && i.GetComponent() != null) { return false; } - return !item.StaticFixtures.Contains(f); - }); - if (pickedBody == null) { return false; } - Character targetCharacter = null; - if (pickedBody.UserData is Character c) - { - targetCharacter = c; - } - else if (pickedBody.UserData is Limb limb) - { - targetCharacter = limb.character; - } - if (targetCharacter != null) - { - if (HumanAIController.IsFriendly(character, targetCharacter)) - { - // Don't shoot friendly characters - return false; - } - } - else - { - if (pickedBody.UserData is ISpatialEntity e) - { - Submarine sub = e.Submarine; - if (sub == null) { return false; } - if (sub == Item.Submarine) { return false; } - // Don't shoot non-player submarines, i.e. wrecks or outposts. - if (!sub.Info.IsPlayer) { return false; } - // Don't shoot friendly submarines. - if (sub.TeamID == Item.Submarine.TeamID) { return false; } - } - else if (!(pickedBody.UserData is Voronoi2.VoronoiCell cell && cell.IsDestructible)) - { - // Hit something else, probably a level wall - return false; - } - } if (canShoot) { + Vector2 start = ConvertUnits.ToSimUnits(item.WorldPosition); + Vector2 end = ConvertUnits.ToSimUnits(targetPos.Value); + // Check that there's not other entities that shouldn't be targeted (like a friendly sub) between us and the target. + Body worldTarget = CheckLineOfSight(start, end); + bool shoot; + if (closestEnemy != null && closestEnemy.Submarine != null) + { + start -= closestEnemy.Submarine.SimPosition; + end -= closestEnemy.Submarine.SimPosition; + Body transformedTarget = CheckLineOfSight(start, end); + shoot = CanShoot(transformedTarget, character) && (worldTarget == null || CanShoot(worldTarget, character)); + } + else + { + shoot = CanShoot(worldTarget, character); + } + if (!shoot) { return false; } if (character.IsOnPlayerTeam) { character.Speak(TextManager.Get("DialogFireTurret"), null, 0.0f, "fireturret", 10.0f); @@ -1284,6 +1284,67 @@ namespace Barotrauma.Items.Components return false; } + private bool CanShoot(Body targetBody, Character user = null, WreckAI ai = null, bool targetSubmarines = true) + { + if (targetBody == null) { return false; } + Character targetCharacter = null; + if (targetBody.UserData is Character c) + { + targetCharacter = c; + } + else if (targetBody.UserData is Limb limb) + { + targetCharacter = limb.character; + } + if (targetCharacter != null) + { + if (user != null) + { + if (HumanAIController.IsFriendly(user, targetCharacter)) + { + return false; + } + } + if (ai != null) + { + if (targetCharacter.Params.Group.Equals(ai.Config.Entity, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + } + } + else + { + if (targetBody.UserData is ISpatialEntity e) + { + Submarine sub = e.Submarine ?? e as Submarine; + if (!targetSubmarines && e is Submarine) { return false; } + if (sub == null) { return false; } + if (sub == Item.Submarine) { return false; } + if (sub.Info.IsOutpost || sub.Info.IsWreck || sub.Info.IsBeacon) { return false; } + if (sub.TeamID == Item.Submarine.TeamID) { return false; } + } + else if (!(targetBody.UserData is Voronoi2.VoronoiCell cell && cell.IsDestructible)) + { + // Hit something else, probably a level wall + return false; + } + } + return true; + } + + private Body CheckLineOfSight(Vector2 start, Vector2 end) + { + var collisionCategories = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel; + Body pickedBody = Submarine.PickBody(start, end, null, collisionCategories, allowInsideFixture: true, + customPredicate: (Fixture f) => + { + if (f.UserData is Item i && i.GetComponent() != null) { return false; } + return !item.StaticFixtures.Contains(f); + }); + return pickedBody; + } + private Vector2 GetRelativeFiringPosition(bool useOffset = true) { Vector2 transformedFiringOffset = Vector2.Zero; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index b541c4812..baec2c0b0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -7,6 +7,7 @@ using System.Xml.Linq; using Barotrauma.Items.Components; using Barotrauma.Extensions; using Barotrauma.Networking; +using Barotrauma.Abilities; namespace Barotrauma { @@ -210,7 +211,9 @@ namespace Barotrauma.Items.Components private readonly Limb[] limb; private readonly List damageModifiers; - public readonly Dictionary SkillModifiers; + public readonly Dictionary SkillModifiers = new Dictionary(); + + public readonly Dictionary WearableStatValues = new Dictionary(); public IEnumerable DamageModifiers { @@ -266,7 +269,6 @@ namespace Barotrauma.Items.Components this.item = item; damageModifiers = new List(); - SkillModifiers = new Dictionary(); int spriteCount = element.Elements().Count(x => x.Name.ToString() == "sprite"); Variants = element.GetAttributeInt("variants", 0); @@ -322,6 +324,18 @@ namespace Barotrauma.Items.Components SkillModifiers.TryAdd(skillIdentifier, skillValue); } break; + case "statvalue": + StatTypes statType = CharacterAbilityGroup.ParseStatType(subElement.GetAttributeString("stattype", ""), Name); + float statValue = subElement.GetAttributeFloat("value", 0f); + if (WearableStatValues.ContainsKey(statType)) + { + WearableStatValues[statType] += statValue; + } + else + { + WearableStatValues.TryAdd(statType, statValue); + } + break; } } } @@ -334,6 +348,7 @@ namespace Barotrauma.Items.Components } picker = character; + for (int i = 0; i < wearableSprites.Length; i++ ) { var wearableSprite = wearableSprites[i]; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 0636069b9..db66ce9f0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -1903,17 +1903,17 @@ namespace Barotrauma return connectedComponents; } - - public static readonly Pair[] connectionPairs = new Pair[] + + public static readonly (string input, string output)[] connectionPairs = new (string input, string output)[] { - new Pair("power_in", "power_out"), - new Pair("signal_in1", "signal_out1"), - new Pair("signal_in2", "signal_out2"), - new Pair("signal_in3", "signal_out3"), - new Pair("signal_in4", "signal_out4"), - new Pair("signal_in", "signal_out"), - new Pair("signal_in1", "signal_out"), - new Pair("signal_in2", "signal_out") + ("power_in", "power_out"), + ("signal_in1", "signal_out1"), + ("signal_in2", "signal_out2"), + ("signal_in3", "signal_out3"), + ("signal_in4", "signal_out4"), + ("signal_in", "signal_out"), + ("signal_in1", "signal_out"), + ("signal_in2", "signal_out") }; private void GetConnectedComponentsRecursive(Connection c, HashSet alreadySearched, List connectedComponents) where T : ItemComponent @@ -1949,20 +1949,20 @@ namespace Barotrauma recipient.Item.GetConnectedComponentsRecursive(recipient, alreadySearched, connectedComponents); } - foreach (Pair connectionPair in connectionPairs) + foreach ((string input, string output) in connectionPairs) { - if (connectionPair.First == c.Name) + if (input == c.Name) { - var pairedConnection = c.Item.Connections.FirstOrDefault(c2 => c2.Name == connectionPair.Second); + var pairedConnection = c.Item.Connections.FirstOrDefault(c2 => c2.Name == output); if (pairedConnection != null) { if (alreadySearched.Contains(pairedConnection)) { continue; } GetConnectedComponentsRecursive(pairedConnection, alreadySearched, connectedComponents); } } - else if (connectionPair.Second == c.Name) + else if (output == c.Name) { - var pairedConnection = c.Item.Connections.FirstOrDefault(c2 => c2.Name == connectionPair.First); + var pairedConnection = c.Item.Connections.FirstOrDefault(c2 => c2.Name == input); if (pairedConnection != null) { if (alreadySearched.Contains(pairedConnection)) { continue; } @@ -1972,18 +1972,27 @@ namespace Barotrauma } } - public Controller FindController() + public Controller FindController(string[] tags = null) { //try finding the controller with the simpler non-recursive method first var controllers = GetConnectedComponents(); - if (controllers.None()) { controllers = GetConnectedComponents(recursive: true); } - return controllers.Count < 2 ? controllers.FirstOrDefault() : - (controllers.FirstOrDefault(c => c.GetFocusTarget() == this) ?? controllers.FirstOrDefault()); + bool needsTag = tags != null && tags.Length > 0; + if (controllers.None() || (needsTag && controllers.None(c => c.Item.HasTag(tags)))) + { + controllers = GetConnectedComponents(recursive: true); + } + if (needsTag) + { + controllers.RemoveAll(c => !c.Item.HasTag(tags)); + } + return controllers.Count < 2 ? + controllers.FirstOrDefault() : + controllers.FirstOrDefault(c => c.GetFocusTarget() == this) ?? controllers.FirstOrDefault(); } - public bool TryFindController(out Controller controller) + public bool TryFindController(out Controller controller, string[] tags = null) { - controller = FindController(); + controller = FindController(tags: tags); return controller != null; } @@ -3029,6 +3038,8 @@ namespace Barotrauma } } + connections?.Clear(); + if (parentInventory != null) { if (parentInventory is CharacterInventory characterInventory) @@ -3054,6 +3065,8 @@ namespace Barotrauma body = null; } + CurrentHull = null; + if (StaticFixtures != null) { foreach (Fixture fixture in StaticFixtures) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index c7256157a..e2c81690d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -4,6 +4,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Globalization; using System.Linq; using System.Xml.Linq; @@ -22,7 +23,7 @@ namespace Barotrauma public readonly bool CopyCondition; public float Commonness { get; } - public DeconstructItem(XElement element) + public DeconstructItem(XElement element, string parentDebugName) { ItemIdentifier = element.GetAttributeString("identifier", "notfound"); MinCondition = element.GetAttributeFloat("mincondition", -0.1f); @@ -30,6 +31,11 @@ namespace Barotrauma OutCondition = element.GetAttributeFloat("outcondition", 1.0f); CopyCondition = element.GetAttributeBool("copycondition", false); Commonness = element.GetAttributeFloat("commonness", 1.0f); + + if (element.Attribute("copycondition") != null && element.Attribute("outcondition") != null) + { + DebugConsole.AddWarning($"Invalid deconstruction output in \"{parentDebugName}\": the output item \"{ItemIdentifier}\" has the out condition set, but is also set to copy the condition of the deconstructed item. Ignoring the out condition."); + } } } @@ -67,8 +73,10 @@ namespace Barotrauma public readonly List RequiredItems; public readonly string[] SuitableFabricatorIdentifiers; public readonly float RequiredTime; + public readonly bool RequiresRecipe; public readonly float OutCondition; //Percentage-based from 0 to 1 public readonly List RequiredSkills; + public int Amount { get; } public FabricationRecipe(XElement element, ItemPrefab itemPrefab) @@ -83,6 +91,7 @@ namespace Barotrauma RequiredTime = element.GetAttributeFloat("requiredtime", 1.0f); OutCondition = element.GetAttributeFloat("outcondition", 1.0f); RequiredItems = new List(); + RequiresRecipe = element.GetAttributeBool("requiresrecipe", false); Amount = element.GetAttributeInt("amount", 1); foreach (XElement subElement in element.Elements()) @@ -281,7 +290,7 @@ namespace Barotrauma /// public List Triggers; - private List fabricationRecipeElements = new List(); + private readonly List fabricationRecipeElements = new List(); private readonly Dictionary treatmentSuitability = new Dictionary(); @@ -513,6 +522,20 @@ namespace Barotrauma private set; } + [Serialize(0.0f, false)] + public float AddedRepairSpeedMultiplier + { + get; + private set; + } + + [Serialize(false, false)] + public bool CannotRepairFail + { + get; + private set; + } + [Serialize(null, false)] public string EquipConfirmationText { get; set; } @@ -732,6 +755,20 @@ namespace Barotrauma name = originalName; identifier = element.GetAttributeString("identifier", ""); + string variantOf = element.GetAttributeString("variantof", ""); + if (!string.IsNullOrEmpty(variantOf)) + { + ItemPrefab basePrefab = Find(null, variantOf); + if (basePrefab == null) + { + DebugConsole.ThrowError($"Failed to load the item variant \"{identifier}\" - could not find the base prefab \"{variantOf}\""); + } + else + { + ConfigElement = element = CreateVariantXML(element, basePrefab); + } + } + string categoryStr = element.GetAttributeString("category", "Misc"); if (!Enum.TryParse(categoryStr, true, out MapEntityCategory category)) { @@ -1031,7 +1068,7 @@ namespace Barotrauma DebugConsole.ThrowError("Error in item config \"" + Name + "\" - use item identifiers instead of names to configure the deconstruct items."); continue; } - DeconstructItems.Add(new DeconstructItem(deconstructItem)); + DeconstructItems.Add(new DeconstructItem(deconstructItem, identifier)); } RandomDeconstructionOutputAmount = Math.Min(RandomDeconstructionOutputAmount, DeconstructItems.Count); break; @@ -1044,7 +1081,7 @@ namespace Barotrauma var preferredContainer = new PreferredContainer(subElement); if (preferredContainer.Primary.Count == 0 && preferredContainer.Secondary.Count == 0) { - DebugConsole.ThrowError($"Error in item prefab {Name}: preferred container has no preferences defined ({subElement.ToString()})."); + DebugConsole.ThrowError($"Error in item prefab {Name}: preferred container has no preferences defined ({subElement})."); } else { @@ -1313,5 +1350,99 @@ namespace Barotrauma public static bool IsContainerPreferred(IEnumerable preferences, ItemContainer c) => preferences.Any(id => c.Item.Prefab.Identifier == id || c.Item.HasTag(id)); public static bool IsContainerPreferred(IEnumerable preferences, IEnumerable ids) => ids.Any(id => preferences.Contains(id)); + + private XElement CreateVariantXML(XElement variantElement, ItemPrefab basePrefab) + { + XElement newElement = new XElement(variantElement.Name); + newElement.Add(basePrefab.ConfigElement.Attributes()); + newElement.Add(basePrefab.ConfigElement.Elements()); + + ReplaceElement(newElement, variantElement); + + void ReplaceElement(XElement element, XElement replacement) + { + List elementsToRemove = new List(); + foreach (XAttribute attribute in replacement.Attributes()) + { + ReplaceAttribute(element, attribute); + } + foreach (XElement replacementSubElement in replacement.Elements()) + { + int index = replacement.Elements().ToList().FindAll(e => e.Name.ToString().Equals(replacementSubElement.Name.ToString(), StringComparison.OrdinalIgnoreCase)).IndexOf(replacementSubElement); + System.Diagnostics.Debug.Assert(index > -1); + + int i = 0; + bool matchingElementFound = false; + foreach (XElement subElement in element.Elements()) + { + if (!subElement.Name.ToString().Equals(replacementSubElement.Name.ToString(), StringComparison.OrdinalIgnoreCase)) { continue; } + if (i == index) + { + if (!replacementSubElement.HasAttributes && !replacementSubElement.HasElements) + { + //if the replacement is empty (no attributes or child elements) + //remove the element from the variant + elementsToRemove.Add(subElement); + } + else + { + ReplaceElement(subElement, replacementSubElement); + } + matchingElementFound = true; + break; + } + i++; + } + if (!matchingElementFound) + { + element.Add(replacementSubElement); + } + } + elementsToRemove.ForEach(e => e.Remove()); + } + + void ReplaceAttribute(XElement element, XAttribute newAttribute) + { + XAttribute existingAttribute = element.Attributes().FirstOrDefault(a => a.Name.ToString().Equals(newAttribute.Name.ToString(), StringComparison.OrdinalIgnoreCase)); + if (existingAttribute == null) + { + element.Add(newAttribute); + return; + } + float.TryParse(existingAttribute.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out float value); + if (newAttribute.Value.StartsWith('*')) + { + string multiplierStr = newAttribute.Value.Substring(1, newAttribute.Value.Length - 1); + float.TryParse(multiplierStr, NumberStyles.Any, CultureInfo.InvariantCulture, out float multiplier); + if (multiplierStr.Contains('.') || existingAttribute.Value.Contains('.')) + { + existingAttribute.Value = (value * multiplier).ToString("G", CultureInfo.InvariantCulture); + } + else + { + existingAttribute.Value = ((int)(value * multiplier)).ToString(); + } + } + else if (newAttribute.Value.StartsWith('+')) + { + string additionStr = newAttribute.Value.Substring(1, newAttribute.Value.Length - 1); + float.TryParse(additionStr, NumberStyles.Any, CultureInfo.InvariantCulture, out float addition); + if (additionStr.Contains('.') || existingAttribute.Value.Contains('.')) + { + existingAttribute.Value = (value + addition).ToString("G", CultureInfo.InvariantCulture); + } + else + { + existingAttribute.Value = ((int)(value + addition)).ToString(); + } + } + else + { + existingAttribute.Value = newAttribute.Value; + } + } + + return newElement; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index 4e6759d60..a1958db8e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -13,10 +13,8 @@ namespace Barotrauma { partial class Explosion { - private static readonly List> prevExplosions = new List>(); - public readonly Attack Attack; - + private readonly float force; private readonly float cameraShake, cameraShakeRange; @@ -36,6 +34,8 @@ namespace Barotrauma private readonly string decal; private readonly float decalSize; + private readonly float itemRepairStrength; + public float EmpStrength { get; set; } public float BallastFloraDamage { get; set; } @@ -63,22 +63,24 @@ namespace Barotrauma force = element.GetAttributeFloat("force", 0.0f); - sparks = element.GetAttributeBool("sparks", true); - shockwave = element.GetAttributeBool("shockwave", true); - flames = element.GetAttributeBool("flames", true); - underwaterBubble = element.GetAttributeBool("underwaterbubble", true); - smoke = element.GetAttributeBool("smoke", true); + bool showEffects = element.GetAttributeBool("showeffects", true); - playTinnitus = element.GetAttributeBool("playtinnitus", true); + sparks = element.GetAttributeBool("sparks", showEffects); + shockwave = element.GetAttributeBool("shockwave", showEffects); + flames = element.GetAttributeBool("flames", showEffects); + underwaterBubble = element.GetAttributeBool("underwaterbubble", showEffects); + smoke = element.GetAttributeBool("smoke", showEffects); - applyFireEffects = element.GetAttributeBool("applyfireeffects", flames); + playTinnitus = element.GetAttributeBool("playtinnitus", showEffects); + + applyFireEffects = element.GetAttributeBool("applyfireeffects", flames && showEffects); ignoreFireEffectsForTags = element.GetAttributeStringArray("ignorefireeffectsfortags", new string[0], convertToLowerInvariant: true); ignoreCover = element.GetAttributeBool("ignorecover", false); onlyInside = element.GetAttributeBool("onlyinside", false); onlyOutside = element.GetAttributeBool("onlyoutside", false); - flash = element.GetAttributeBool("flash", true); + flash = element.GetAttributeBool("flash", showEffects); flashDuration = element.GetAttributeFloat("flashduration", 0.05f); if (element.Attribute("flashrange") != null) { flashRange = element.GetAttributeFloat("flashrange", 100.0f); } flashColor = element.GetAttributeColor("flashcolor", Color.LightYellow); @@ -86,15 +88,18 @@ namespace Barotrauma EmpStrength = element.GetAttributeFloat("empstrength", 0.0f); BallastFloraDamage = element.GetAttributeFloat("ballastfloradamage", 0.0f); - decal = element.GetAttributeString("decal", ""); + itemRepairStrength = element.GetAttributeFloat("itemrepairstrength", 0.0f); + + decal = element.GetAttributeString("decal", ""); decalSize = element.GetAttributeFloat(1.0f, "decalSize", "decalsize"); - cameraShake = element.GetAttributeFloat("camerashake", Attack.Range * 0.1f); - cameraShakeRange = element.GetAttributeFloat("camerashakerange", Attack.Range); + cameraShake = element.GetAttributeFloat("camerashake", showEffects ? Attack.Range * 0.1f : 0f); + cameraShakeRange = element.GetAttributeFloat("camerashakerange", showEffects ? Attack.Range : 0f); - screenColorRange = element.GetAttributeFloat("screencolorrange", Attack.Range * 0.1f); + screenColorRange = element.GetAttributeFloat("screencolorrange", showEffects ? Attack.Range * 0.1f : 0f); screenColor = element.GetAttributeColor("screencolor", Color.Transparent); screenColorDuration = element.GetAttributeFloat("screencolorduration", 0.1f); + } public void DisableParticles() @@ -107,19 +112,8 @@ namespace Barotrauma underwaterBubble = false; } - public List> GetRecentExplosions(float maxSecondsAgo) - { - return prevExplosions.FindAll(e => e.Third >= Timing.TotalTime - maxSecondsAgo); - } - public void Explode(Vector2 worldPosition, Entity damageSource, Character attacker = null) { - prevExplosions.Add(new Triplet(this, worldPosition, (float)Timing.TotalTime)); - if (prevExplosions.Count > 100) - { - prevExplosions.RemoveAt(0); - } - Hull hull = Hull.FindHull(worldPosition); ExplodeProjSpecific(worldPosition, hull); @@ -180,6 +174,23 @@ namespace Barotrauma } } + if (itemRepairStrength > 0.0f) + { + float displayRangeSqr = displayRange * displayRange; + foreach (Item item in Item.ItemList) + { + float distSqr = Vector2.DistanceSquared(item.WorldPosition, worldPosition); + if (distSqr > displayRangeSqr) continue; + + float distFactor = 1.0f - (float)Math.Sqrt(distSqr) / displayRange; + //repair repairable items + if (item.Repairables.Any()) + { + item.Condition += itemRepairStrength * distFactor; + } + } + } + if (MathUtils.NearlyEqual(force, 0.0f) && MathUtils.NearlyEqual(Attack.Stun, 0.0f) && MathUtils.NearlyEqual(Attack.GetTotalDamage(false), 0.0f)) { return; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index ee40e20a9..d99fa611d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -14,8 +14,8 @@ namespace Barotrauma partial class BackgroundSection { public Rectangle Rect; - public int Index; - public int RowIndex; + public ushort Index; + public ushort RowIndex; private Vector4 colorVector4; private Color color; @@ -39,7 +39,7 @@ namespace Barotrauma } } - public BackgroundSection(Rectangle rect, int index, int rowIndex) + public BackgroundSection(Rectangle rect, ushort index, ushort rowIndex) { Rect = rect; Index = index; @@ -53,7 +53,7 @@ namespace Barotrauma Color = DirtColor = Color.Lerp(new Color(10, 10, 10, 100), new Color(54, 57, 28, 200), Noise.X); } - public BackgroundSection(Rectangle rect, int index, float colorStrength, Color color, int rowIndex) + public BackgroundSection(Rectangle rect, ushort index, float colorStrength, Color color, ushort rowIndex) { System.Diagnostics.Debug.Assert(rect.Width > 0 && rect.Height > 0); @@ -674,6 +674,9 @@ namespace Barotrauma Gap.UpdateHulls(); } + BackgroundSections?.Clear(); + submergedSections?.Clear(); + List fireSourcesToRemove = new List(FireSources); foreach (FireSource fireSource in fireSourcesToRemove) { @@ -1260,9 +1263,9 @@ namespace Barotrauma { for (int x = 0; x < xBackgroundMax; x++) { - int index = BackgroundSections.Count; + ushort index = (ushort)BackgroundSections.Count; int sector = (int)Math.Floor(index / (float)sectorWidth - xSectors * y) + y / sectorHeight * (int)Math.Ceiling(xSectors); - BackgroundSections.Add(new BackgroundSection(new Rectangle(x * sectionWidth, y * -sectionHeight, sectionWidth, sectionHeight), index, y)); + BackgroundSections.Add(new BackgroundSection(new Rectangle(x * sectionWidth, y * -sectionHeight, sectionWidth, sectionHeight), index, (ushort)y)); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 5a0c3bd8c..afb5f6086 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -325,7 +325,6 @@ namespace Barotrauma get { return LevelData.Seed; } } - public static float? ForcedDifficulty; public float Difficulty { @@ -3020,13 +3019,6 @@ namespace Barotrauma } } - public string GetWreckIDTag(string originalTag, Submarine wreck) - { - string shortSeed = ToolBox.StringToInt(LevelData.Seed + wreck?.Info.Name).ToString(); - if (shortSeed.Length > 6) { shortSeed = shortSeed.Substring(0, 6); } - return originalTag + "_" + shortSeed; - } - public bool IsCloseToStart(Vector2 position, float minDist) => IsCloseToStart(position.ToPoint(), minDist); public bool IsCloseToEnd(Vector2 position, float minDist) => IsCloseToEnd(position.ToPoint(), minDist); @@ -3891,12 +3883,39 @@ namespace Barotrauma LevelObjectManager = null; } + AbyssIslands?.Clear(); + AbyssResources?.Clear(); + Caves?.Clear(); + Tunnels?.Clear(); + PathPoints?.Clear(); + PositionsOfInterest?.Clear(); + + wreckPositions?.Clear(); + Wrecks?.Clear(); + + BeaconStation = null; + beaconSonar = null; + StartOutpost = null; + EndOutpost = null; + + blockedRects?.Clear(); + + EntitiesBeforeGenerate?.Clear(); + EqualityCheckValues?.Clear(); + if (Ruins != null) { Ruins.Clear(); Ruins = null; } + bottomPositions?.Clear(); + BottomBarrier = null; + TopBarrier = null; + SeaFloor = null; + + distanceField = null; + if (ExtraWalls != null) { foreach (LevelWall w in ExtraWalls) { w.Dispose(); } @@ -3908,7 +3927,9 @@ namespace Barotrauma UnsyncedExtraWalls = null; } + tempCells?.Clear(); cells = null; + cellGrid = null; if (bodies != null) { @@ -3916,6 +3937,9 @@ namespace Barotrauma bodies = null; } + StartLocation = null; + EndLocation = null; + Loaded = null; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs index 6259d240d..55850a421 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs @@ -264,7 +264,7 @@ namespace Barotrauma var tagsArray = element.GetAttributeStringArray("tags", new string[0]); foreach (string tag in tagsArray) { - tags.Add(tag.ToLower()); + tags.Add(tag.ToLowerInvariant()); } if (triggeredBy.HasFlag(TriggererType.OtherTrigger)) @@ -272,7 +272,7 @@ namespace Barotrauma var otherTagsArray = element.GetAttributeStringArray("allowedothertriggertags", new string[0]); foreach (string tag in otherTagsArray) { - allowedOtherTriggerTags.Add(tag.ToLower()); + allowedOtherTriggerTags.Add(tag.ToLowerInvariant()); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Radiation.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Radiation.cs index fbe5d775f..766ef959e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Radiation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Radiation.cs @@ -149,7 +149,7 @@ namespace Barotrauma public XElement Save() { XElement element = new XElement(nameof(Radiation)); - SerializableProperty.SerializeProperties(this, element); + SerializableProperty.SerializeProperties(this, element, saveIfDefault: true); return element; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index 61e14591b..ad5279bc9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -48,8 +48,7 @@ namespace Barotrauma } } - //observable collection because some entities may need to be notified when the collection is modified - public readonly ObservableCollection linkedTo = new ObservableCollection(); + public readonly List linkedTo = new List(); protected bool flippedX, flippedY; public bool FlippedX { get { return flippedX; } } @@ -515,7 +514,11 @@ namespace Barotrauma } #endif - if (aiTarget != null) aiTarget.Remove(); + if (aiTarget != null) + { + aiTarget.Remove(); + aiTarget = null; + } if (linkedTo != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index 23644df10..80ba583ee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -47,7 +47,7 @@ namespace Barotrauma const float LeakThreshold = 0.1f; #if CLIENT - private SpriteEffects SpriteEffects = SpriteEffects.None; + public SpriteEffects SpriteEffects = SpriteEffects.None; #endif //dimensions of the wall sections' physics bodies (only used for debug rendering) @@ -955,6 +955,15 @@ namespace Barotrauma SoundPlayer.PlayDamageSound(attack.StructureSoundType, damageAmount, worldPosition, tags: Tags); } #endif + + if (Submarine != null && damageAmount > 0) + { + foreach (Character character in Character.CharacterList) + { + character.CheckTalents(AbilityEffectType.AfterSubmarineAttacked, Submarine); + } + } + return new AttackResult(damageAmount, null); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index 2270e5d69..3e92a9aea 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -249,6 +249,17 @@ namespace Barotrauma get { return subBody?.HullVertices; } } + private int? submarineSpecificIDTag; + public int SubmarineSpecificIDTag + { + get + { + submarineSpecificIDTag ??= ToolBox.StringToInt((Level.Loaded?.Seed ?? "") + Info.Name); + return submarineSpecificIDTag.Value; + } + } + + public bool AtDamageDepth { get @@ -329,48 +340,6 @@ namespace Barotrauma DockedTo.ForEach(s => s.ShowSonarMarker = false); PhysicsBody.FarseerBody.BodyType = BodyType.Static; TeamID = CharacterTeamType.None; - - string defaultTag = Level.Loaded.GetWreckIDTag("wreck_id", this); - ReplaceIDCardTagRequirements("wreck_id", defaultTag); - - foreach (Item item in Item.ItemList) - { - if (item.Submarine != this) { continue; } - if (item.prefab.Identifier == "idcardwreck" || item.prefab.Identifier == "idcard") - { - foreach (string tag in item.GetTags().ToList()) - { - if (tag == "smallitem") { continue; } - string newTag = Level.Loaded.GetWreckIDTag(tag, this); - item.ReplaceTag(tag, newTag); - ReplaceIDCardTagRequirements(tag, newTag); - } - } - } - - void ReplaceIDCardTagRequirements(string oldTag, string newTag) - { - foreach (Item item in Item.ItemList) - { - if (item.Submarine != this) { continue; } - foreach (ItemComponent ic in item.Components) - { - ReplaceIDCardTagRequirement(ic, RelatedItem.RelationType.Picked, oldTag, newTag); - ReplaceIDCardTagRequirement(ic, RelatedItem.RelationType.Equipped, oldTag, newTag); - } - } - } - - static void ReplaceIDCardTagRequirement(ItemComponent ic, RelatedItem.RelationType relationType, string oldTag, string newTag) - { - if (!ic.requiredItems.ContainsKey(relationType)) { return; } - foreach (RelatedItem requiredItem in ic.requiredItems[relationType]) - { - int index = Array.IndexOf(requiredItem.Identifiers, oldTag); - if (index == -1) { continue; } - requiredItem.Identifiers[index] = newTag; - } - } } public WreckAI WreckAI { get; private set; } @@ -1718,7 +1687,10 @@ namespace Barotrauma PhysicsBody.RemoveAll(); - GameMain.World.Clear(); + GameMain.World?.Clear(); + GameMain.World = null; + + GC.Collect(); Unloading = false; } @@ -1730,6 +1702,9 @@ namespace Barotrauma subBody?.Remove(); subBody = null; + outdoorNodes?.Clear(); + outdoorNodes = null; + if (GameMain.GameSession?.Campaign?.UpgradeManager != null) { GameMain.GameSession.Campaign.UpgradeManager.OnUpgradesChanged -= ResetCrushDepth; @@ -1743,8 +1718,8 @@ namespace Barotrauma visibleEntities = null; - if (MainSub == this) MainSub = null; - if (MainSubs[1] == this) MainSubs[1] = null; + if (MainSub == this) { MainSub = null; } + if (MainSubs[1] == this) { MainSubs[1] = null; } ConnectedDockingPorts?.Clear(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs index 5fe93ad3b..c24088ba5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -569,8 +569,7 @@ namespace Barotrauma } var gaps = newHull?.ConnectedGaps ?? Gap.GapList.Where(g => g.Submarine == submarine); - targetPos = character.WorldPosition; - Gap adjacentGap = Gap.FindAdjacent(gaps, targetPos, 500.0f); + Gap adjacentGap = Gap.FindAdjacent(gaps, ConvertUnits.ToDisplayUnits(points[0]), 200.0f); if (adjacentGap == null) { return true; } if (newHull != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs index 2f7152ffa..e6942d2bb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs @@ -54,6 +54,8 @@ namespace Barotrauma set { spawnType = value; } } + public Action OnLinksChanged { get; set; } + public override string Name { get @@ -761,9 +763,16 @@ namespace Barotrauma public void ConnectTo(WayPoint wayPoint2) { System.Diagnostics.Debug.Assert(this != wayPoint2); - - if (!linkedTo.Contains(wayPoint2)) { linkedTo.Add(wayPoint2); } - if (!wayPoint2.linkedTo.Contains(this)) { wayPoint2.linkedTo.Add(this); } + if (!linkedTo.Contains(wayPoint2)) + { + OnLinksChanged?.Invoke(this); + linkedTo.Add(wayPoint2); + } + if (!wayPoint2.linkedTo.Contains(this)) + { + wayPoint2.OnLinksChanged?.Invoke(wayPoint2); + wayPoint2.linkedTo.Add(this); + } } public static WayPoint GetRandom(SpawnType spawnType = SpawnType.Human, JobPrefab assignedJob = null, Submarine sub = null, Ruin ruin = null, bool useSyncedRand = false) @@ -986,14 +995,18 @@ namespace Barotrauma public override void ShallowRemove() { base.ShallowRemove(); - WayPointList.Remove(this); } public override void Remove() { base.Remove(); - + CurrentHull = null; + ConnectedGap = null; + Tunnel = null; + Stairs = null; + Ladders = null; + OnLinksChanged = null; WayPointList.Remove(this); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs index 7402c316d..d43ef274d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs @@ -89,7 +89,7 @@ namespace Barotrauma.Networking private static int ReadIncomingMsgs() { Task readTask = readStream?.ReadAsync(tempBytes, 0, tempBytes.Length, readCancellationToken.Token); - TimeSpan ts = TimeSpan.FromMilliseconds(100); + TimeSpan timeOut = TimeSpan.FromMilliseconds(100); for (int i = 0; i < 150; i++) { if (shutDown) @@ -99,7 +99,7 @@ namespace Barotrauma.Networking return -1; } - if ((readTask?.IsCompleted ?? true) || (readTask?.Wait(ts) ?? true)) + if ((readTask?.IsCompleted ?? true) || (readTask?.Wait(timeOut) ?? true)) { break; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEvent.cs index f0b800508..fa35ae9aa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEvent.cs @@ -23,6 +23,9 @@ namespace Barotrauma.Networking TeamChange, ObjectiveManagerState, AddToCrew, + UpdateExperience, + UpdateTalents, + UpdateMoney, } public readonly Entity Entity; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs index b6eb1674f..dc79afcd9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs @@ -23,9 +23,12 @@ namespace Barotrauma.Networking /// public int? WallSectionIndex { get; set; } + /// + /// Same as calling , but the text parameter is set using + /// public OrderChatMessage(Order order, string orderOption, int priority, ISpatialEntity targetEntity, Character targetCharacter, Character sender) : this(order, orderOption, priority, - order?.GetChatMessage(targetCharacter?.Name, sender?.CurrentHull?.DisplayName, givingOrderToSelf: targetCharacter == sender, orderOption: orderOption), + order?.GetChatMessage(targetCharacter?.Name, sender?.CurrentHull?.DisplayName, givingOrderToSelf: targetCharacter == sender, orderOption: orderOption, priority: priority), targetEntity, targetCharacter, sender) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs index c0074d353..e4d53fc9c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs @@ -26,6 +26,9 @@ namespace Barotrauma.Networking //any respawn items left in the shuttle are removed when the shuttle despawns private readonly List respawnItems = new List(); + //characters who spawned during the last respawn + private readonly List respawnedCharacters = new List(); + public bool UsingShuttle { get { return RespawnShuttle != null; } @@ -277,11 +280,17 @@ namespace Barotrauma.Networking hull.BallastFlora?.Kill(); } + Dictionary characterPositions = new Dictionary(); foreach (Character c in Character.CharacterList) { if (c.Submarine != RespawnShuttle) { continue; } + if (!respawnedCharacters.Contains(c)) + { + characterPositions.Add(c, c.WorldPosition); + continue; + } #if CLIENT - if (Character.Controlled == c) Character.Controlled = null; + if (Character.Controlled == c) { Character.Controlled = null; } #endif c.Kill(CauseOfDeathType.Unknown, null, true); c.Enabled = false; @@ -298,6 +307,11 @@ namespace Barotrauma.Networking RespawnShuttle.SetPosition(new Vector2(Level.Loaded.StartPosition.X, Level.Loaded.Size.Y + RespawnShuttle.Borders.Height)); RespawnShuttle.Velocity = Vector2.Zero; + + foreach (var characterPosition in characterPositions) + { + characterPosition.Key.TeleportTo(characterPosition.Value); + } } partial void RespawnCharactersProjSpecific(Vector2? shuttlePos); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index 8b72e0075..c68215f53 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -507,7 +507,7 @@ namespace Barotrauma.Networking } [Serialize(800, true)] - private int LinesPerLogFile + public int LinesPerLogFile { get { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index fb24f1a63..00c4f0517 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -12,7 +12,7 @@ namespace Barotrauma { public static class XMLExtensions { - public static string ParseContentPathFromUri(this XObject element) => ToolBox.ConvertAbsoluteToRelativePath(element.BaseUri); + public static string ParseContentPathFromUri(this XObject element) => Path.GetRelativePath(Environment.CurrentDirectory, element.BaseUri); public static XDocument TryLoadXml(string filePath) { @@ -52,7 +52,7 @@ namespace Barotrauma return null; } - if (doc.Root == null) return null; + if (doc.Root == null) { return null; } } return doc; @@ -60,20 +60,18 @@ namespace Barotrauma public static object GetAttributeObject(XAttribute attribute) { - if (attribute == null) return null; + if (attribute == null) { return null; } return ParseToObject(attribute.Value.ToString()); } public static object ParseToObject(string value) { - float floatVal; - int intVal; - if (value.Contains(".") && Single.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out floatVal)) + if (value.Contains(".") && Single.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out float floatVal)) { return floatVal; } - if (Int32.TryParse(value, out intVal)) + if (Int32.TryParse(value, out int intVal)) { return intVal; } @@ -94,7 +92,7 @@ namespace Barotrauma public static string GetAttributeString(this XElement element, string name, string defaultValue) { - if (element?.Attribute(name) == null) return defaultValue; + if (element?.Attribute(name) == null) { return defaultValue; } return GetAttributeString(element.Attribute(name), defaultValue); } @@ -106,10 +104,10 @@ namespace Barotrauma public static string[] GetAttributeStringArray(this XElement element, string name, string[] defaultValue, bool trim = true, bool convertToLowerInvariant = false) { - if (element?.Attribute(name) == null) return defaultValue; + if (element?.Attribute(name) == null) { return defaultValue; } string stringValue = element.Attribute(name).Value; - if (string.IsNullOrEmpty(stringValue)) return defaultValue; + if (string.IsNullOrEmpty(stringValue)) { return defaultValue; } string[] splitValue = stringValue.Split(',', ','); @@ -133,11 +131,11 @@ namespace Barotrauma public static float GetAttributeFloat(this XElement element, float defaultValue, params string[] matchingAttributeName) { - if (element == null) return defaultValue; + if (element == null) { return defaultValue; } foreach (string name in matchingAttributeName) { - if (element.Attribute(name) == null) continue; + if (element.Attribute(name) == null) { continue; } float val; try @@ -162,7 +160,7 @@ namespace Barotrauma public static float GetAttributeFloat(this XElement element, string name, float defaultValue) { - if (element?.Attribute(name) == null) return defaultValue; + if (element?.Attribute(name) == null) { return defaultValue; } float val = defaultValue; try @@ -184,7 +182,7 @@ namespace Barotrauma public static float GetAttributeFloat(this XAttribute attribute, float defaultValue) { - if (attribute == null) return defaultValue; + if (attribute == null) { return defaultValue; } float val = defaultValue; @@ -207,10 +205,10 @@ namespace Barotrauma public static float[] GetAttributeFloatArray(this XElement element, string name, float[] defaultValue) { - if (element?.Attribute(name) == null) return defaultValue; + if (element?.Attribute(name) == null) { return defaultValue; } string stringValue = element.Attribute(name).Value; - if (string.IsNullOrEmpty(stringValue)) return defaultValue; + if (string.IsNullOrEmpty(stringValue)) { return defaultValue; } string[] splitValue = stringValue.Split(','); float[] floatValue = new float[splitValue.Length]; @@ -236,13 +234,16 @@ namespace Barotrauma public static int GetAttributeInt(this XElement element, string name, int defaultValue) { - if (element?.Attribute(name) == null) return defaultValue; + if (element?.Attribute(name) == null) { return defaultValue; } int val = defaultValue; try { - val = Int32.Parse(element.Attribute(name).Value, CultureInfo.InvariantCulture); + if (!Int32.TryParse(element.Attribute(name).Value, NumberStyles.Any, CultureInfo.InvariantCulture, out val)) + { + val = (int)float.Parse(element.Attribute(name).Value, CultureInfo.InvariantCulture); + } } catch (Exception e) { @@ -254,7 +255,7 @@ namespace Barotrauma public static uint GetAttributeUInt(this XElement element, string name, uint defaultValue) { - if (element?.Attribute(name) == null) return defaultValue; + if (element?.Attribute(name) == null) { return defaultValue; } uint val = defaultValue; @@ -272,7 +273,7 @@ namespace Barotrauma public static UInt64 GetAttributeUInt64(this XElement element, string name, UInt64 defaultValue) { - if (element?.Attribute(name) == null) return defaultValue; + if (element?.Attribute(name) == null) { return defaultValue; } UInt64 val = defaultValue; @@ -290,7 +291,7 @@ namespace Barotrauma public static UInt64 GetAttributeSteamID(this XElement element, string name, UInt64 defaultValue) { - if (element?.Attribute(name) == null) return defaultValue; + if (element?.Attribute(name) == null) { return defaultValue; } UInt64 val = defaultValue; @@ -308,10 +309,10 @@ namespace Barotrauma public static int[] GetAttributeIntArray(this XElement element, string name, int[] defaultValue) { - if (element?.Attribute(name) == null) return defaultValue; + if (element?.Attribute(name) == null) { return defaultValue; } string stringValue = element.Attribute(name).Value; - if (string.IsNullOrEmpty(stringValue)) return defaultValue; + if (string.IsNullOrEmpty(stringValue)) { return defaultValue; } string[] splitValue = stringValue.Split(','); int[] intValue = new int[splitValue.Length]; @@ -332,10 +333,10 @@ namespace Barotrauma } public static ushort[] GetAttributeUshortArray(this XElement element, string name, ushort[] defaultValue) { - if (element?.Attribute(name) == null) return defaultValue; + if (element?.Attribute(name) == null) { return defaultValue; } string stringValue = element.Attribute(name).Value; - if (string.IsNullOrEmpty(stringValue)) return defaultValue; + if (string.IsNullOrEmpty(stringValue)) { return defaultValue; } string[] splitValue = stringValue.Split(','); ushort[] ushortValue = new ushort[splitValue.Length]; @@ -357,13 +358,13 @@ namespace Barotrauma public static bool GetAttributeBool(this XElement element, string name, bool defaultValue) { - if (element?.Attribute(name) == null) return defaultValue; + if (element?.Attribute(name) == null) { return defaultValue; } return element.Attribute(name).GetAttributeBool(defaultValue); } public static bool GetAttributeBool(this XAttribute attribute, bool defaultValue) { - if (attribute == null) return defaultValue; + if (attribute == null) { return defaultValue; } string val = attribute.Value.ToLowerInvariant().Trim(); if (val == "true") @@ -381,31 +382,31 @@ namespace Barotrauma public static Point GetAttributePoint(this XElement element, string name, Point defaultValue) { - if (element?.Attribute(name) == null) return defaultValue; + if (element?.Attribute(name) == null) { return defaultValue; } return ParsePoint(element.Attribute(name).Value); } public static Vector2 GetAttributeVector2(this XElement element, string name, Vector2 defaultValue) { - if (element?.Attribute(name) == null) return defaultValue; + if (element?.Attribute(name) == null) { return defaultValue; } return ParseVector2(element.Attribute(name).Value); } public static Vector3 GetAttributeVector3(this XElement element, string name, Vector3 defaultValue) { - if (element == null || element.Attribute(name) == null) return defaultValue; + if (element == null || element.Attribute(name) == null) { return defaultValue; } return ParseVector3(element.Attribute(name).Value); } public static Vector4 GetAttributeVector4(this XElement element, string name, Vector4 defaultValue) { - if (element == null || element.Attribute(name) == null) return defaultValue; + if (element == null || element.Attribute(name) == null) { return defaultValue; } return ParseVector4(element.Attribute(name).Value); } public static Color GetAttributeColor(this XElement element, string name, Color defaultValue) { - if (element == null || element.Attribute(name) == null) return defaultValue; + if (element == null || element.Attribute(name) == null) { return defaultValue; } return ParseColor(element.Attribute(name).Value); } @@ -417,32 +418,32 @@ namespace Barotrauma public static Color[] GetAttributeColorArray(this XElement element, string name, Color[] defaultValue) { - if (element?.Attribute(name) == null) return defaultValue; + if (element?.Attribute(name) == null) { return defaultValue; } - string stringValue = element.Attribute(name).Value; - if (string.IsNullOrEmpty(stringValue)) return defaultValue; + string stringValue = element.Attribute(name).Value; + if (string.IsNullOrEmpty(stringValue)) { return defaultValue; } - string[] splitValue = stringValue.Split(';'); - Color[] colorValue = new Color[splitValue.Length]; - for (int i = 0; i < splitValue.Length; i++) + string[] splitValue = stringValue.Split(';'); + Color[] colorValue = new Color[splitValue.Length]; + for (int i = 0; i < splitValue.Length; i++) + { + try { - try - { - Color val = ParseColor(splitValue[i], true); - colorValue[i] = val; - } - catch (Exception e) - { - DebugConsole.ThrowError("Error in " + element + "! ", e); - } + Color val = ParseColor(splitValue[i], true); + colorValue[i] = val; } + catch (Exception e) + { + DebugConsole.ThrowError("Error in " + element + "! ", e); + } + } - return colorValue; + return colorValue; } public static Rectangle GetAttributeRect(this XElement element, string name, Rectangle defaultValue) { - if (element == null || element.Attribute(name) == null) return defaultValue; + if (element == null || element.Attribute(name) == null) { return defaultValue; } return ParseRect(element.Attribute(name).Value, false); } @@ -498,7 +499,7 @@ namespace Barotrauma if (components.Length != 2) { - if (!errorMessages) return point; + if (!errorMessages) { return point; } DebugConsole.ThrowError("Failed to parse the string \"" + stringPoint + "\" to Vector2"); return point; } @@ -516,7 +517,7 @@ namespace Barotrauma if (components.Length != 2) { - if (!errorMessages) return vector; + if (!errorMessages) { return vector; } DebugConsole.ThrowError("Failed to parse the string \"" + stringVector2 + "\" to Vector2"); return vector; } @@ -535,7 +536,7 @@ namespace Barotrauma if (components.Length != 3) { - if (!errorMessages) return vector; + if (!errorMessages) { return vector; } DebugConsole.ThrowError("Failed to parse the string \"" + stringVector3 + "\" to Vector3"); return vector; } @@ -555,7 +556,7 @@ namespace Barotrauma if (components.Length < 3) { - if (errorMessages) DebugConsole.ThrowError("Failed to parse the string \"" + stringVector4 + "\" to Vector4"); + if (errorMessages) { DebugConsole.ThrowError("Failed to parse the string \"" + stringVector4 + "\" to Vector4"); } return vector; } @@ -563,7 +564,9 @@ namespace Barotrauma Single.TryParse(components[1], NumberStyles.Float, CultureInfo.InvariantCulture, out vector.Y); Single.TryParse(components[2], NumberStyles.Float, CultureInfo.InvariantCulture, out vector.Z); if (components.Length > 3) + { Single.TryParse(components[3], NumberStyles.Float, CultureInfo.InvariantCulture, out vector.W); + } return vector; } @@ -603,8 +606,7 @@ namespace Barotrauma { stringColor = stringColor.Substring(1); - int colorInt = 0; - if (int.TryParse(stringColor, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out colorInt)) + if (int.TryParse(stringColor, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int colorInt)) { if (stringColor.Length == 6) { @@ -621,7 +623,7 @@ namespace Barotrauma if (hexFailed) { - if (errorMessages) DebugConsole.ThrowError("Failed to parse the string \"" + stringColor + "\" to Color"); + if (errorMessages) { DebugConsole.ThrowError("Failed to parse the string \"" + stringColor + "\" to Color"); } return Color.White; } } @@ -651,7 +653,7 @@ namespace Barotrauma string[] strComponents = stringRect.Split(','); if ((strComponents.Length < 3 && requireSize) || strComponents.Length < 2) { - if (errorMessages) DebugConsole.ThrowError("Failed to parse the string \"" + stringRect + "\" to Rectangle"); + if (errorMessages) { DebugConsole.ThrowError("Failed to parse the string \"" + stringRect + "\" to Rectangle"); } return new Rectangle(0, 0, 0, 0); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 41c47a0b7..faa57d7b1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -309,6 +309,9 @@ namespace Barotrauma public readonly List> ReduceAffliction; + private readonly List giveExperiences; + private readonly List<(string identifier, float amount)> giveSkills; + public float Duration => duration; //only applicable if targeting NearbyCharacters or NearbyItems @@ -357,6 +360,9 @@ namespace Barotrauma Explosions = new List(); triggeredEvents = new List(); ReduceAffliction = new List>(); + giveExperiences = new List(); + giveSkills = new List<(string, float)>(); + tags = new HashSet(element.GetAttributeString("tags", "").Split(',')); OnlyInside = element.GetAttributeBool("onlyinside", false); OnlyOutside = element.GetAttributeBool("onlyoutside", false); @@ -486,13 +492,22 @@ namespace Barotrauma } } + if (duration > 0.0f && !setValue) + { + //a workaround to "tags" possibly meaning either an item's tags or this status effect's tags: + //if the status effect has a duration, assume tags mean this status effect's tags and leave item tags untouched. + propertyAttributes.RemoveAll(a => a.Name.ToString().Equals("tags", StringComparison.OrdinalIgnoreCase)); + } + int count = propertyAttributes.Count; + propertyNames = new string[count]; propertyEffects = new object[count]; int n = 0; foreach (XAttribute attribute in propertyAttributes) { + propertyNames[n] = attribute.Name.ToString().ToLowerInvariant(); propertyEffects[n] = XMLExtensions.GetAttributeObject(attribute); n++; @@ -626,6 +641,12 @@ namespace Barotrauma case "aitrigger": aiTriggers.Add(new AITrigger(subElement)); break; + case "giveexperience": + giveExperiences.Add(subElement.GetAttributeInt("amount", 0)); + break; + case "giveskill": + giveSkills.Add((subElement.GetAttributeString("skillidentifier", ""), subElement.GetAttributeFloat("amount", 0))); + break; } } InitProjSpecific(element, parentDebugName); @@ -1181,6 +1202,47 @@ namespace Barotrauma } } } + + int i = 0; + foreach (int giveExperience in giveExperiences) + { + Character targetCharacter = CharacterFromTarget(target); + if (targetCharacter != null && !targetCharacter.Removed) + { + targetCharacter?.Info?.GiveExperience(giveExperience, popupOffset: i * 25f); + i++; + } + } + + if (giveSkills.Any()) + { + foreach ((string skillIdentifier, float amount) in giveSkills) + { + Character targetCharacter = CharacterFromTarget(target); + if (targetCharacter != null && !targetCharacter.Removed) + { + if (skillIdentifier?.ToLowerInvariant() == "randomskill") + { + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) + { + // don't let clients simulate random skill gain + continue; + } + targetCharacter.Info?.IncreaseSkillLevel(GetRandomSkill(), amount, targetCharacter.Position + Vector2.UnitY * (150.0f + i * 25f)); + + string GetRandomSkill() + { + return targetCharacter.Info?.Job?.Skills.Select(s => s.Identifier).GetRandom(); + } + } + else + { + targetCharacter.Info?.IncreaseSkillLevel(skillIdentifier?.ToLowerInvariant(), amount, targetCharacter.Position + Vector2.UnitY * (150.0f + i * 25f)); + } + i++; + } + } + } } if (FireSize > 0.0f && entity != null) @@ -1346,6 +1408,19 @@ namespace Barotrauma } ApplyProjSpecific(deltaTime, entity, targets, hull, position, playSound: true); + + Character CharacterFromTarget(ISerializableEntity target) + { + Character targetCharacter = target as Character; + if (targetCharacter == null) + { + if (target is Limb targetLimb && !targetLimb.Removed) + { + targetCharacter = targetLimb.character; + } + } + return targetCharacter; + } } partial void ApplyProjSpecific(float deltaTime, Entity entity, IEnumerable targets, Hull currentHull, Vector2 worldPosition, bool playSound); @@ -1353,38 +1428,31 @@ namespace Barotrauma private void ApplyToProperty(ISerializableEntity target, SerializableProperty property, object value, float deltaTime) { if (disableDeltaTime || setValue) { deltaTime = 1.0f; } - Type type = value.GetType(); - if (type == typeof(float) || (type == typeof(int) && property.GetValue(target) is float)) + if (value is int || value is float) { - float floatValue = Convert.ToSingle(value) * deltaTime; - if (!setValue) + var propertyValue = property.GetValue(target); + if (propertyValue is float propertyValueF) { - floatValue += (float)property.GetValue(target); + float floatValue = Convert.ToSingle(value) * deltaTime; + if (!setValue) + { + floatValue += propertyValueF; + } + property.TrySetValue(target, floatValue); + return; } - property.TrySetValue(target, floatValue); - } - else if (type == typeof(int) && value is int) - { - int intValue = (int)((int)value * deltaTime); - if (!setValue) + else if (propertyValue is int integer) { - intValue += (int)property.GetValue(target); + int intValue = (int)(Convert.ToInt32(value) * deltaTime); + if (!setValue) + { + intValue += integer; + } + property.TrySetValue(target, intValue); + return; } - property.TrySetValue(target, intValue); - } - else if (type == typeof(bool) && value is bool) - { - property.TrySetValue(target, (bool)value); - } - else if (type == typeof(string)) - { - property.TrySetValue(target, (string)value); - } - else - { - DebugConsole.ThrowError("Couldn't apply value " + value.ToString() + " (" + type + ") to property \"" + property.Name + "\" (" + property.GetValue(target).GetType() + ")! " - + "Make sure the type of the value set in the config files matches the type of the property."); } + property.TrySetValue(target, value); } public static void UpdateAll(float deltaTime) diff --git a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs index 24c3db01d..dd975c774 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs @@ -455,6 +455,8 @@ namespace Barotrauma UnlockAchievement(character, character.Info.Job.Prefab.Identifier + "round"); } } + + pathFinder = null; } private static void UnlockAchievement(Character recipient, string identifier) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs index 06f4c29fc..88835bfc0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs @@ -249,7 +249,7 @@ namespace Barotrauma public bool IsDisallowed(Item item) { - return item.disallowedUpgrades.Contains(Identifier); + return item.disallowedUpgrades.Contains(Identifier) || UpgradeCategories.Any(c => item.disallowedUpgrades.Contains(c.Identifier)); } public static UpgradePrefab? Find(string identifier) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/IdRemap.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/IdRemap.cs index af7097579..6e086afb8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/IdRemap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/IdRemap.cs @@ -12,7 +12,7 @@ namespace Barotrauma private int maxId; - private readonly List srcRanges; + private readonly List> srcRanges; private readonly int destOffset; public IdRemap(XElement parentElement, int offset) @@ -20,13 +20,13 @@ namespace Barotrauma destOffset = offset; if (parentElement != null && parentElement.HasElements) { - srcRanges = new List(); + srcRanges = new List>(); foreach (XElement subElement in parentElement.Elements()) { int id = subElement.GetAttributeInt("ID", -1); if (id > 0) { InsertId(id); } } - maxId = GetOffsetId(srcRanges.Last().Y) + 1; + maxId = GetOffsetId(srcRanges.Last().End) + 1; } else { @@ -44,38 +44,38 @@ namespace Barotrauma { for (int i = 0; i < srcRanges.Count; i++) { - if (srcRanges[i].X > id) + if (srcRanges[i].Start > id) { - if (srcRanges[i].X == (id + 1)) + if (srcRanges[i].Start == (id + 1)) { - srcRanges[i] = new Point(id, srcRanges[i].Y); - if (i > 0 && srcRanges[i].X == srcRanges[i - 1].Y) + srcRanges[i] = new Range(id, srcRanges[i].End); + if (i > 0 && srcRanges[i].Start == srcRanges[i - 1].End) { - srcRanges[i - 1] = new Point(srcRanges[i - 1].X, srcRanges[i].Y); + srcRanges[i - 1] = new Range(srcRanges[i - 1].Start, srcRanges[i].End); srcRanges.RemoveAt(i); } } else { - srcRanges.Insert(i, new Point(id, id)); + srcRanges.Insert(i, new Range(id, id)); } return; } - else if (srcRanges[i].Y < id) + else if (srcRanges[i].End < id) { - if (srcRanges[i].Y == (id - 1)) + if (srcRanges[i].End == (id - 1)) { - srcRanges[i] = new Point(srcRanges[i].X, id); - if (i < (srcRanges.Count - 1) && srcRanges[i].Y == srcRanges[i + 1].X) + srcRanges[i] = new Range(srcRanges[i].Start, id); + if (i < (srcRanges.Count - 1) && srcRanges[i].End == srcRanges[i + 1].Start) { - srcRanges[i] = new Point(srcRanges[i].X, srcRanges[i + 1].Y); + srcRanges[i] = new Range(srcRanges[i].Start, srcRanges[i + 1].End); srcRanges.RemoveAt(i + 1); } return; } } } - srcRanges.Add(new Point(id, id)); + srcRanges.Add(new Range(id, id)); } public ushort GetOffsetId(XElement element) @@ -92,11 +92,11 @@ namespace Barotrauma int currOffset = destOffset; for (int i = 0; i < srcRanges.Count; i++) { - if (id >= srcRanges[i].X && id <= srcRanges[i].Y) + if (id >= srcRanges[i].Start && id <= srcRanges[i].End) { - return (ushort)(id - srcRanges[i].X + 1 + currOffset); + return (ushort)(id - srcRanges[i].Start + 1 + currOffset); } - currOffset += srcRanges[i].Y - srcRanges[i].X + 1; + currOffset += srcRanges[i].End - srcRanges[i].Start + 1; } return 0; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs index 299e6d6dd..77ddd1d2f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs @@ -29,6 +29,11 @@ namespace Barotrauma return (i % n + n) % n; } + public static float PositiveModulo(float i, float n) + { + return (i % n + n) % n; + } + public static double Distance(double x1, double y1, double x2, double y2) { double dX = x1 - x2; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Range.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Range.cs new file mode 100644 index 000000000..5d380e221 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Range.cs @@ -0,0 +1,44 @@ +using System; + +namespace Barotrauma +{ + public struct Range where T : IComparable + { + private T start; private T end; + public T Start + { + get { return start; } + set + { + start = value; + VerifyStartLessThanEnd(); + } + } + + public T End + { + get { return end; } + set + { + end = value; + VerifyEndGreaterThanStart(); + } + } + + private void VerifyStartLessThanEnd() + { + if (start.CompareTo(end) > 0) { throw new InvalidOperationException($"Range<{typeof(T).Name}>.Start set to a value greater than End ({start} > {end})"); } + } + + private void VerifyEndGreaterThanStart() + { + if (end.CompareTo(start) < 0) { throw new InvalidOperationException($"Range<{typeof(T).Name}>.End set to a value less than Start ({end} < {start})"); } + } + + public Range(T start, T end) + { + this.start = start; this.end = end; + VerifyEndGreaterThanStart(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs index 201c9d7e9..ac830c575 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs @@ -138,6 +138,11 @@ namespace Barotrauma.IO return System.IO.Path.GetPathRoot(path); } + public static string GetRelativePath(string relativeTo, string path) + { + return System.IO.Path.GetRelativePath(relativeTo, path); + } + public static string GetDirectoryName(string path) { return System.IO.Path.GetDirectoryName(path); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs index 105395eb2..7f881cad9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs @@ -296,20 +296,10 @@ namespace Barotrauma public static void CompressStringToFile(string fileName, string value) { // A. - // Write string to temporary file. - string temp = Path.GetTempFileName(); - File.WriteAllText(temp, value); + // Convert the string to its byte representation. + byte[] b = Encoding.UTF8.GetBytes(value); // B. - // Read file into byte array buffer. - byte[] b; - using (FileStream f = File.Open(temp, System.IO.FileMode.Open)) - { - b = new byte[f.Length]; - f.Read(b, 0, (int)f.Length); - } - - // C. // Use GZipStream to write compressed bytes to target file. using (FileStream f2 = File.Open(fileName, System.IO.FileMode.Create)) using (GZipStream gz = new GZipStream(f2, CompressionMode.Compress, false)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs index fe65006e8..8d727bee0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs @@ -12,6 +12,7 @@ using System.Text; namespace Barotrauma { + [Obsolete("Use named tuples instead.")] public class Pair { public T1 First { get; set; } @@ -24,20 +25,6 @@ namespace Barotrauma } } - public class Triplet - { - public T1 First { get; set; } - public T2 Second { get; set; } - public T3 Third { get; set; } - - public Triplet(T1 first, T2 second, T3 third) - { - First = first; - Second = second; - Third = third; - } - } - public static partial class ToolBox { static internal class Epoch @@ -555,15 +542,6 @@ namespace Barotrauma return hex.ToString(); } - public static string ConvertAbsoluteToRelativePath(string path) - { - string[] splitted = path.Split(new char[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }); - string currentFolder = Environment.CurrentDirectory.Split(new char[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }).Last(); - // Filter out the current folder -> result is "Content/blaahblaah" or "Mods/blaahblaah" etc. - IEnumerable filtered = splitted.SkipWhile(part => part != currentFolder).Skip(1); - return string.Join("/", filtered); - } - public static string EscapeCharacters(string str) { return str.Replace("\\", "\\\\").Replace("\"", "\\\""); @@ -640,6 +618,17 @@ namespace Barotrauma Process.Start(startInfo); } + /// + /// Cleans up a path by replacing backslashes with forward slashes, and + /// optionally corrects the casing of the path. Recommended when serializing + /// paths to a human-readable file to force case correction on all platforms. + /// Also useful when working with paths to files that currently don't exist, + /// i.e. case cannot be corrected. + /// + /// Path to clean up + /// Should the case be corrected to match the filesystem? + /// Directories that the path should be found in, not returned. + /// Path with corrected slashes, and corrected case if requested. public static string CleanUpPathCrossPlatform(this string path, bool correctFilenameCase = true, string directory = "") { if (string.IsNullOrEmpty(path)) { return ""; } @@ -659,21 +648,24 @@ namespace Barotrauma return path; } + /// + /// Cleans up a path by replacing backslashes with forward slashes, and + /// corrects the casing of the path on non-Windows platforms. Recommended + /// when loading a path from a file, to make sure that it is found on all + /// platforms when attempting to open it. + /// + /// Path to clean up + /// Path with corrected slashes, and corrected case if required by the platform. public static string CleanUpPath(this string path) { - if (string.IsNullOrEmpty(path)) { return ""; } - - path = path.Replace('\\', '/'); - while (path.IndexOf("//") >= 0) - { - path = path.Replace("//", "/"); - } -#if LINUX || OSX - //required on *nix platforms to load in mods made on Windows - string correctedPath = CorrectFilenameCase(path, out _); - if (!string.IsNullOrEmpty(correctedPath)) { path = correctedPath; } + return path.CleanUpPathCrossPlatform( + correctFilenameCase: +#if WINDOWS + false +#else + true #endif - return path; + ); } public static float GetEasing(TransitionMode easing, float t) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/UpdaterUtil.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/UpdaterUtil.cs deleted file mode 100644 index dcd806d14..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/UpdaterUtil.cs +++ /dev/null @@ -1,223 +0,0 @@ -using System; -using System.Collections.Generic; -using Barotrauma.IO; -using System.Linq; -using System.Security.Cryptography; -using System.Xml.Linq; - -namespace Barotrauma -{ - public static class UpdaterUtil - { - public const string Version = "1.1"; - - public static void SaveFileList(string filePath) - { - XDocument doc = new XDocument(CreateFileList()); - - doc.SaveSafe(filePath); - } - - public static XElement CreateFileList() - { - XElement root = new XElement("filelist"); - string currentDir = Directory.GetCurrentDirectory(); - - IEnumerable files = Directory.GetFiles(currentDir, "*", System.IO.SearchOption.AllDirectories); - - foreach (string file in files) - { - XElement fileElement = new XElement("file"); - fileElement.Add(new XAttribute("path", GetRelativePath(file, currentDir))); - fileElement.Add(new XAttribute("md5", GetFileMd5Hash(file))); - - root.Add(fileElement); - } - - return root; - } - - public static List GetFileList(XDocument fileListDoc) - { - List fileList = new List(); - - XElement fileListElement = fileListDoc.Root; - - if (fileListElement == null) - { - throw new Exception("Received list of new files was corrupted"); - } - - foreach (XElement file in fileListElement.Elements()) - { - string filePath = file.GetAttributeString("path", ""); - - fileList.Add(filePath); - } - - return fileList; - } - - public static List GetRequiredFiles(XDocument fileListDoc) - { - List requiredFiles = new List(); - - XElement fileList = fileListDoc.Root; - - if (fileList==null) - { - throw new Exception("Received list of new files was corrupted"); - } - - foreach (XElement file in fileList.Elements()) - { - string filePath = file.GetAttributeString("path", ""); - - if (!File.Exists(filePath)) - { - requiredFiles.Add(filePath); - continue; - } - - string md5 = file.GetAttributeString("md5", ""); - - if (GetFileMd5Hash(filePath) != md5) - { - requiredFiles.Add(filePath); - } - } - - return requiredFiles; - } - - private static string GetFileMd5Hash(string filePath) - { - Md5Hash md5Hash = null; - var md5 = MD5.Create(); - using (var stream = File.OpenRead(filePath)) - { - md5Hash = new Md5Hash(md5.ComputeHash(stream)); - } - - return md5Hash.Hash; - } - - public static string GetRelativePath(string filespec, string folder) - { - Uri pathUri = new Uri(filespec); - // Folders must end in a slash - if (!folder.EndsWith(Path.DirectorySeparatorChar.ToString())) - { - folder += Path.DirectorySeparatorChar; - } - Uri folderUri = new Uri(folder); - return Uri.UnescapeDataString(folderUri.MakeRelativeUri(pathUri).ToString().Replace('/', Path.DirectorySeparatorChar)); - } - - /// - /// moves the files in the updatefolder to the install folder - /// if there's an existing file with the same name in the install folder and it can't be removed, - /// it will be renamed as "OLD_[filename]" - /// - /// - public static void InstallUpdatedFiles(string updateFileFolder) - { - IEnumerable files = Directory.GetFiles(updateFileFolder, "*", System.IO.SearchOption.AllDirectories); - - string currentDir = Directory.GetCurrentDirectory(); - - foreach (string file in files) - { - string fileRelPath = GetRelativePath(file, updateFileFolder); - - if (File.Exists(fileRelPath)) - { - try - { - File.Delete(fileRelPath); - } - - //couldn't delete file, probably because it's already in use - catch - { - string oldFileName = Path.Combine(currentDir, Path.GetDirectoryName(fileRelPath), "OLD_"+Path.GetFileName(fileRelPath)); - - if (File.Exists(oldFileName)) File.Delete(oldFileName); - - File.Move(fileRelPath, oldFileName); - } - } - - string directoryName = Path.GetDirectoryName(fileRelPath); - if (!string.IsNullOrWhiteSpace(directoryName)) - { - Directory.CreateDirectory(directoryName); - } - - - System.Diagnostics.Debug.WriteLine("moving: "+file+" -> "+fileRelPath); - File.Move(file, fileRelPath); - } - - Directory.Delete(updateFileFolder, true); - } - - public static void CleanUnnecessaryFiles(List filesToKeep) - { - string currentDir = Directory.GetCurrentDirectory(); - - IEnumerable files = Directory.GetFiles(currentDir, "*", System.IO.SearchOption.AllDirectories); - - foreach (string file in files) - { - string relativePath = GetRelativePath(file, currentDir); - - string dirRoot = relativePath.Split(Path.DirectorySeparatorChar).First(); - if (dirRoot != "Content") continue; - - if (filesToKeep.Contains(relativePath)) continue; - - if (Path.GetFileName(file).Split('_').First() == "OLD") continue; - - System.Diagnostics.Debug.WriteLine("deleting file "+file); - - try - { - File.Delete(file); - } - - catch (Exception e) - { - System.Diagnostics.Debug.WriteLine("Could not delete file \"" + file + "\" (" + e.Message + ")"); - continue; - } - } - } - - - public static void CleanOldFiles() - { - string currentDir = Directory.GetCurrentDirectory(); - - IEnumerable files = Directory.GetFiles(currentDir, "*", System.IO.SearchOption.AllDirectories); - - foreach (string file in files) - { - if (Path.GetFileName(file).Split('_').First() != "OLD") continue; - - System.Diagnostics.Debug.WriteLine("deleting file " + file); - - try - { - File.Delete(file); - } - - catch (Exception e) - { - System.Diagnostics.Debug.WriteLine("Could not delete file \"" + file + "\" (" + e.Message + ")"); - continue; - } - } - } - } -} diff --git a/Barotrauma/BarotraumaShared/Submarines/Humpback.sub b/Barotrauma/BarotraumaShared/Submarines/Humpback.sub index 879c0d4f2..79b880af6 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Humpback.sub and b/Barotrauma/BarotraumaShared/Submarines/Humpback.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Remora.sub b/Barotrauma/BarotraumaShared/Submarines/Remora.sub index ccce067ae..a79c97e86 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Remora.sub and b/Barotrauma/BarotraumaShared/Submarines/Remora.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub b/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub index 4751d3daf..4d2f9b30f 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub and b/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub differ diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 02351378a..f5866ba31 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,56 @@ +--------------------------------------------------------------------------------------------------------- +v0.1500.0.0 +--------------------------------------------------------------------------------------------------------- + +Additions and changes: +- Groundwork for the upcoming talent system: completing missions gives the characters experience points which can be used to unlock special abilities or buffs. Currently only the captain has talents implemented. +- Improved bot chatter when orders are being given, rearranged, or dismissed. +- Damage to arms reduces aiming accuracy. +- Crippled legs slow the player down more. +- Improvements to the blood particle effects when a character is bleeding. +- Added "targetlimb" argument to the giveaffliction command (allows applying the affliction to a specific limb). +- Players who wander inside a respawn shuttle don't get automatically killed when the shuttle despawns if they weren't part of the respawning crew. +- Bots no longer ignore severe fire in reactor, engine, or command rooms. The intention for them ignoring the severe fires was to prevent unwanted casualities when the fire can be left untreated and wait for it to fade out when not ordered to extinguish fires. +- Buffs are transferred to AI-controlled husks when a character transforms. +- Projectiles shift to the left in multi-slot loaders when firing. +- Option to make terminals use a monospaced font. +- Player-controlled monsters can now grab and eat bodies. +- Added triangle and sawtooth wave types to oscillator component. +- Added "high_pressure" output to water detector. +- Water detectors round the water percentage output up, so any amount of water will be at least 1%. +- Focus on the password field automatically in the server password prompt and allow submitting it with enter. +- Made pirates a little less accurate when they're operating turrets: they can no longer magically aim exactly at characters inside another sub. +- Biome noise loop volume is tied to sound volume instead of music volume. +- Endworms no longer always bleed to death when their tail is cut. +- Lever state is visualized on its sprite. +- Enabled NVidia Optimus on Windows. + +Overhauled status monitor: +- Improved visuals. +- Indicates the locations of the crew's ID cards. +- Indicates the locations of alerts. +- Electrical view, indicating locations and health of junction boxes, reactor and batteries. +- Allows searching for items and indicating the hulls in which they're located. + +Fixes: +- Fixed "linesperlogfile" server setting doing nothing. +- Fixed discharge coils not working when triggered by via a wired button. +- Fixed hatch waypoint and platforms on Remora Drone. +- Memory usage optimizations. +- Fixed bots shooting enemies even when there's a friendly sub between them and the target. +- Bots take their masks off when if they have successfully equipped a suit. +- Fixed a pathfinding issue in Remora caused by too sparse waypoint distribution. +- Fixed disguises not changing the color of a character's name when hovering the cursor over the character. +- Fixed monsters' attack sounds never playing in multiplayer. + +Modding: +- Implemented an item variant system that works similar to the character variants: you can create new items that inherit the properties of another item and only modify specific aspects of it, reducing the amount of duplicate XML code. See "Depleted Fuel Rod" in engineer_talent_items.xml for an usage example. +- Removed error message when trying to transfer items to a husk monster and inventory sizes don't match +- Submarine upgrades can be disallowed by category instead of having to do it separately for each upgrade in the sub editor. +- Fixed a modding related crash when trying to apply a property value of a wrong type using status effects. +- Option to create custom husk infections where player control carries over to the transformed creature. +- Display a console warning when an item's deconstruct output defines an out condition and is also set to copy the condition of the deconstructed item. + --------------------------------------------------------------------------------------------------------- v0.14.9.0 --------------------------------------------------------------------------------------------------------- diff --git a/Barotrauma/BarotraumaShared/serversettings.xml b/Barotrauma/BarotraumaShared/serversettings.xml index 9b96f6b02..38cae3c14 100644 --- a/Barotrauma/BarotraumaShared/serversettings.xml +++ b/Barotrauma/BarotraumaShared/serversettings.xml @@ -24,6 +24,7 @@ startwhenclientsreadyratio="0.8" allowspectating="True" saveserverlogs="True" + linesperlogfile="800" allowragdollbutton="True" allowfiletransfers="True" voicechatenabled="True" diff --git a/Libraries/Farseer Physics Engine 3.5/Dynamics/World.cs b/Libraries/Farseer Physics Engine 3.5/Dynamics/World.cs index e950db8d9..53ce244d8 100644 --- a/Libraries/Farseer Physics Engine 3.5/Dynamics/World.cs +++ b/Libraries/Farseer Physics Engine 3.5/Dynamics/World.cs @@ -1034,6 +1034,7 @@ namespace FarseerPhysics.Dynamics body.DestroyProxies(); for (int i = 0; i < body.FixtureList.Count; i++) { + body.FixtureList[i].UserData = null; if (FixtureRemoved != null) FixtureRemoved(this, body, body.FixtureList[i]); } @@ -1041,6 +1042,8 @@ namespace FarseerPhysics.Dynamics body._world = null; BodyList.Remove(body); + body.UserData = null; + if (BodyRemoved != null) BodyRemoved(this, body); diff --git a/Libraries/XNATypes/RectangleF.cs b/Libraries/XNATypes/RectangleF.cs new file mode 100644 index 000000000..1459e1e90 --- /dev/null +++ b/Libraries/XNATypes/RectangleF.cs @@ -0,0 +1,551 @@ +// MIT License - Copyright (C) The Mono.Xna Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; + +namespace Microsoft.Xna.Framework +{ + public struct RectangleF : IEquatable + { + #region Private Fields + + private static RectangleF emptyRectangle = new RectangleF(); + + #endregion + + #region Public Fields + + /// + /// The x coordinate of the top-left corner of this . + /// + + public float X; + + /// + /// The y coordinate of the top-left corner of this . + /// + + public float Y; + + /// + /// The width of this . + /// + + public float Width; + + /// + /// The height of this . + /// + + public float Height; + + #endregion + + #region Public Properties + + /// + /// Returns a with X=0, Y=0, Width=0, Height=0. + /// + public static RectangleF Empty + { + get { return emptyRectangle; } + } + + /// + /// Returns the x coordinate of the left edge of this . + /// + public float Left + { + get { return this.X; } + } + + /// + /// Returns the x coordinate of the right edge of this . + /// + public float Right + { + get { return (this.X + this.Width); } + } + + /// + /// Returns the y coordinate of the top edge of this . + /// + public float Top + { + get { return this.Y; } + } + + /// + /// Returns the y coordinate of the bottom edge of this . + /// + public float Bottom + { + get { return (this.Y + this.Height); } + } + + /// + /// Whether or not this has a and + /// of 0, and a of (0, 0). + /// + public bool IsEmpty + { + get + { + return ((((this.Width == 0) && (this.Height == 0)) && (this.X == 0)) && (this.Y == 0)); + } + } + + /// + /// The top-left coordinates of this . + /// + public Vector2 Location + { + get + { + return new Vector2(this.X, this.Y); + } + set + { + X = value.X; + Y = value.Y; + } + } + + /// + /// The width-height coordinates of this . + /// + public Vector2 Size + { + get + { + return new Vector2(this.Width, this.Height); + } + set + { + Width = value.X; + Height = value.Y; + } + } + + /// + /// A located in the center of this . + /// + /// + /// If or is an odd number, + /// the center point will be rounded down. + /// + public Vector2 Center + { + get + { + return new Vector2(this.X + (this.Width / 2f), this.Y + (this.Height / 2f)); + } + } + + #endregion + + #region Internal Properties + + internal string DebugDisplayString + { + get + { + return string.Concat( + this.X, " ", + this.Y, " ", + this.Width, " ", + this.Height + ); + } + } + + #endregion + + #region Constructors + + /// + /// Creates a new instance of struct, with the specified + /// position, width, and height. + /// + /// The x coordinate of the top-left corner of the created . + /// The y coordinate of the top-left corner of the created . + /// The width of the created . + /// The height of the created . + public RectangleF(float x, float y, float width, float height) + { + this.X = x; + this.Y = y; + this.Width = width; + this.Height = height; + } + + /// + /// Creates a new instance of struct, with the specified + /// location and size. + /// + /// The x and y coordinates of the top-left corner of the created . + /// The width and height of the created . + public RectangleF(Vector2 location, Vector2 size) + { + this.X = location.X; + this.Y = location.Y; + this.Width = size.X; + this.Height = size.Y; + } + + #endregion + + #region Operators + + /// + /// Compares whether two instances are equal. + /// + /// instance on the left of the equal sign. + /// instance on the right of the equal sign. + /// true if the instances are equal; false otherwise. + public static bool operator ==(RectangleF a, RectangleF b) + { + return ((a.X == b.X) && (a.Y == b.Y) && (a.Width == b.Width) && (a.Height == b.Height)); + } + + /// + /// Compares whether two instances are not equal. + /// + /// instance on the left of the not equal sign. + /// instance on the right of the not equal sign. + /// true if the instances are not equal; false otherwise. + public static bool operator !=(RectangleF a, RectangleF b) + { + return !(a == b); + } + + public static implicit operator RectangleF(Rectangle r) => new RectangleF(r.X, r.Y, r.Width, r.Height); + + #endregion + + #region Public Methods + + /// + /// Gets whether or not the provided coordinates lie within the bounds of this . + /// + /// The x coordinate of the point to check for containment. + /// The y coordinate of the point to check for containment. + /// true if the provided coordinates lie inside this ; false otherwise. + public bool Contains(int x, int y) + { + return ((((this.X <= x) && (x < (this.X + this.Width))) && (this.Y <= y)) && (y < (this.Y + this.Height))); + } + + /// + /// Gets whether or not the provided coordinates lie within the bounds of this . + /// + /// The x coordinate of the point to check for containment. + /// The y coordinate of the point to check for containment. + /// true if the provided coordinates lie inside this ; false otherwise. + public bool Contains(float x, float y) + { + return ((((this.X <= x) && (x < (this.X + this.Width))) && (this.Y <= y)) && (y < (this.Y + this.Height))); + } + + /// + /// Gets whether or not the provided lies within the bounds of this . + /// + /// The coordinates to check for inclusion in this . + /// true if the provided lies inside this ; false otherwise. + public bool Contains(Point value) + { + return ((((this.X <= value.X) && (value.X < (this.X + this.Width))) && (this.Y <= value.Y)) && (value.Y < (this.Y + this.Height))); + } + + /// + /// Gets whether or not the provided lies within the bounds of this . + /// + /// The coordinates to check for inclusion in this . + /// true if the provided lies inside this ; false otherwise. As an output parameter. + public void Contains(ref Point value, out bool result) + { + result = ((((this.X <= value.X) && (value.X < (this.X + this.Width))) && (this.Y <= value.Y)) && (value.Y < (this.Y + this.Height))); + } + + /// + /// Gets whether or not the provided lies within the bounds of this . + /// + /// The coordinates to check for inclusion in this . + /// true if the provided lies inside this ; false otherwise. + public bool Contains(Vector2 value) + { + return ((((this.X <= value.X) && (value.X < (this.X + this.Width))) && (this.Y <= value.Y)) && (value.Y < (this.Y + this.Height))); + } + + /// + /// Gets whether or not the provided lies within the bounds of this . + /// + /// The coordinates to check for inclusion in this . + /// true if the provided lies inside this ; false otherwise. As an output parameter. + public void Contains(ref Vector2 value, out bool result) + { + result = ((((this.X <= value.X) && (value.X < (this.X + this.Width))) && (this.Y <= value.Y)) && (value.Y < (this.Y + this.Height))); + } + + /// + /// Gets whether or not the provided lies within the bounds of this . + /// + /// The to check for inclusion in this . + /// true if the provided 's bounds lie entirely inside this ; false otherwise. + public bool Contains(RectangleF value) + { + return ((((this.X <= value.X) && ((value.X + value.Width) <= (this.X + this.Width))) && (this.Y <= value.Y)) && ((value.Y + value.Height) <= (this.Y + this.Height))); + } + + /// + /// Gets whether or not the provided lies within the bounds of this . + /// + /// The to check for inclusion in this . + /// true if the provided 's bounds lie entirely inside this ; false otherwise. As an output parameter. + public void Contains(ref RectangleF value, out bool result) + { + result = ((((this.X <= value.X) && ((value.X + value.Width) <= (this.X + this.Width))) && (this.Y <= value.Y)) && ((value.Y + value.Height) <= (this.Y + this.Height))); + } + + /// + /// Compares whether current instance is equal to specified . + /// + /// The to compare. + /// true if the instances are equal; false otherwise. + public override bool Equals(object obj) + { + return (obj is RectangleF) && this == ((RectangleF)obj); + } + + /// + /// Compares whether current instance is equal to specified . + /// + /// The to compare. + /// true if the instances are equal; false otherwise. + public bool Equals(RectangleF other) + { + return this == other; + } + + /// + /// Gets the hash code of this . + /// + /// Hash code of this . + public override int GetHashCode() + { + unchecked + { + var hash = 17; + hash = hash * 23 + X.GetHashCode(); + hash = hash * 23 + Y.GetHashCode(); + hash = hash * 23 + Width.GetHashCode(); + hash = hash * 23 + Height.GetHashCode(); + return hash; + } + } + + /// + /// Adjusts the edges of this by specified horizontal and vertical amounts. + /// + /// Value to adjust the left and right edges. + /// Value to adjust the top and bottom edges. + public void Inflate(int horizontalAmount, int verticalAmount) + { + X -= horizontalAmount; + Y -= verticalAmount; + Width += horizontalAmount * 2; + Height += verticalAmount * 2; + } + + /// + /// Adjusts the edges of this by specified horizontal and vertical amounts. + /// + /// Value to adjust the left and right edges. + /// Value to adjust the top and bottom edges. + public void Inflate(float horizontalAmount, float verticalAmount) + { + X -= (float)horizontalAmount; + Y -= (float)verticalAmount; + Width += (float)horizontalAmount * 2; + Height += (float)verticalAmount * 2; + } + + /// + /// Adjusts the edges of this by specified horizontal and vertical amounts. + /// + /// Value to adjust the edges. + public void Inflate(Vector2 amount) + { + Inflate(amount.X, amount.Y); + } + + /// + /// Gets whether or not the other intersects with this rectangle. + /// + /// The other rectangle for testing. + /// true if other intersects with this rectangle; false otherwise. + public bool Intersects(RectangleF value) + { + return value.Left < Right && + Left < value.Right && + value.Top < Bottom && + Top < value.Bottom; + } + + + /// + /// Gets whether or not the other intersects with this rectangle. + /// + /// The other rectangle for testing. + /// true if other intersects with this rectangle; false otherwise. As an output parameter. + public void Intersects(ref RectangleF value, out bool result) + { + result = value.Left < Right && + Left < value.Right && + value.Top < Bottom && + Top < value.Bottom; + } + + /// + /// Creates a new that contains overlapping region of two other rectangles. + /// + /// The first . + /// The second . + /// Overlapping region of the two rectangles. + public static RectangleF Intersect(RectangleF value1, RectangleF value2) + { + RectangleF rectangle; + Intersect(ref value1, ref value2, out rectangle); + return rectangle; + } + + /// + /// Creates a new that contains overlapping region of two other rectangles. + /// + /// The first . + /// The second . + /// Overlapping region of the two rectangles as an output parameter. + public static void Intersect(ref RectangleF value1, ref RectangleF value2, out RectangleF result) + { + if (value1.Intersects(value2)) + { + float right_side = MathF.Min(value1.X + value1.Width, value2.X + value2.Width); + float left_side = MathF.Max(value1.X, value2.X); + float top_side = MathF.Max(value1.Y, value2.Y); + float bottom_side = MathF.Min(value1.Y + value1.Height, value2.Y + value2.Height); + result = new RectangleF(left_side, top_side, right_side - left_side, bottom_side - top_side); + } + else + { + result = new RectangleF(0, 0, 0, 0); + } + } + + /// + /// Changes the of this . + /// + /// The x coordinate to add to this . + /// The y coordinate to add to this . + public void Offset(int offsetX, int offsetY) + { + X += offsetX; + Y += offsetY; + } + + /// + /// Changes the of this . + /// + /// The x coordinate to add to this . + /// The y coordinate to add to this . + public void Offset(float offsetX, float offsetY) + { + X += (float)offsetX; + Y += (float)offsetY; + } + + /// + /// Changes the of this . + /// + /// The x and y components to add to this . + public void Offset(Point amount) + { + X += amount.X; + Y += amount.Y; + } + + /// + /// Changes the of this . + /// + /// The x and y components to add to this . + public void Offset(Vector2 amount) + { + X += (float)amount.X; + Y += (float)amount.Y; + } + + /// + /// Returns a representation of this in the format: + /// {X:[] Y:[] Width:[] Height:[]} + /// + /// representation of this . + public override string ToString() + { + return "{X:" + X + " Y:" + Y + " Width:" + Width + " Height:" + Height + "}"; + } + + /// + /// Creates a new that completely contains two other rectangles. + /// + /// The first . + /// The second . + /// The union of the two rectangles. + public static RectangleF Union(RectangleF value1, RectangleF value2) + { + float x = MathF.Min(value1.X, value2.X); + float y = MathF.Min(value1.Y, value2.Y); + return new RectangleF(x, y, + Math.Max(value1.Right, value2.Right) - x, + Math.Max(value1.Bottom, value2.Bottom) - y); + } + + /// + /// Creates a new that completely contains two other rectangles. + /// + /// The first . + /// The second . + /// The union of the two rectangles as an output parameter. + public static void Union(ref RectangleF value1, ref RectangleF value2, out RectangleF result) + { + result.X = Math.Min(value1.X, value2.X); + result.Y = Math.Min(value1.Y, value2.Y); + result.Width = Math.Max(value1.Right, value2.Right) - result.X; + result.Height = Math.Max(value1.Bottom, value2.Bottom) - result.Y; + } + + public void AddPoint(Point point) + { + if (point.X < X) + { + Width += X - point.X; + X = point.X; + } + else if (point.X > Right) + { + Width += point.X - Right; + } + + if (point.Y < Y) + { + Height += Y - point.Y; + Y = point.Y; + } + else if (point.Y > Bottom) + { + Height += point.Y - Bottom; + } + } + + #endregion + } +}