using Barotrauma.Networking; using Barotrauma.Particles; using FarseerPhysics; using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; namespace Barotrauma.Items.Components { partial class Sprayer : RangedWeapon, IDrawableComponent { #if DEBUG private Vector2 debugRayStartPos, debugRayEndPos; #endif public Vector2 DrawSize { get { return Vector2.Zero; } } private readonly List particleEmitters = new List(); private Hull targetHull; private Vector2 rayStartWorldPosition; private Color color; partial void InitProjSpecific(ContentXElement element) { currentCrossHairPointerScale = element.GetAttributeFloat("crosshairscale", 0.1f); foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "particleemitter": particleEmitters.Add(new ParticleEmitter(subElement)); break; } } } private readonly List targetSections = new List(); // 0 = 1x1, 1 = 2x2, 2 = 3x3 private int spraySetting = 0; private readonly Point[] sprayArray = new Point[8]; public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) { if (character == null || !character.IsKeyDown(InputType.Aim)) return; if (PlayerInput.KeyHit(InputType.PreviousFireMode)) { if (spraySetting > 0) { spraySetting--; } else { spraySetting = 2; } targetSections.Clear(); } if (PlayerInput.KeyHit(InputType.NextFireMode)) { if (spraySetting < 2) { spraySetting++; } else { spraySetting = 0; } targetSections.Clear(); } crosshairPointerPos = PlayerInput.MousePosition; Vector2 rayStart; Vector2 sourcePos = character?.AnimController == null ? item.SimPosition : character.AnimController.AimSourceSimPos; Vector2 barrelPos = item.SimPosition + TransformedBarrelPos; //make sure there's no obstacles between the base of the item (or the shoulder of the character) and the end of the barrel if (Submarine.PickBody(sourcePos, barrelPos, collisionCategory: Physics.CollisionItem | Physics.CollisionItemBlocking | Physics.CollisionWall) == null) { //no obstacles -> we start the raycast at the end of the barrel rayStart = ConvertUnits.ToSimUnits(item.WorldPosition) + TransformedBarrelPos; } else { targetHull = null; targetSections.Clear(); return; } Vector2 pos = character.CursorWorldPosition; Vector2 rayEnd = ConvertUnits.ToSimUnits(pos); rayStartWorldPosition = ConvertUnits.ToDisplayUnits(rayStart); if (Vector2.Distance(rayStartWorldPosition, pos) > Range) { targetHull = null; targetSections.Clear(); return; } #if DEBUG debugRayStartPos = ConvertUnits.ToDisplayUnits(rayStart); debugRayEndPos = ConvertUnits.ToDisplayUnits(rayEnd); #endif Submarine parentSub = character.Submarine ?? item.Submarine; if (parentSub != null) { rayStart -= parentSub.SimPosition; rayEnd -= parentSub.SimPosition; } var obstacles = Submarine.PickBodies(rayStart, rayEnd, collisionCategory: Physics.CollisionItem | Physics.CollisionItemBlocking | Physics.CollisionWall); foreach (var body in obstacles) { if (body.UserData is Item item) { var door = item.GetComponent(); if (door != null && (door.IsOpen || door.IsBroken)) { continue; } } targetHull = null; targetSections.Clear(); return; } targetHull = Hull.GetCleanTarget(pos); if (targetHull == null) { targetSections.Clear(); return; } BackgroundSection mousedOverSection = targetHull.GetBackgroundSection(pos); if (mousedOverSection == null) { targetSections.Clear(); return; } // No need to refresh if (targetSections.Count > 0 && mousedOverSection == targetSections[0]) { return; } targetSections.Clear(); targetSections.Add(mousedOverSection); int mousedOverIndex = mousedOverSection.Index; // Start with 2x2 if (spraySetting > 0) { sprayArray[0].X = mousedOverIndex + 1; sprayArray[0].Y = mousedOverSection.RowIndex; sprayArray[1].X = mousedOverIndex + targetHull.xBackgroundMax; sprayArray[1].Y = mousedOverSection.RowIndex + 1; sprayArray[2].X = sprayArray[1].X + 1; sprayArray[2].Y = sprayArray[1].Y; for (int i = 0; i < 3; i++) { if (targetHull.DoesSectionMatch(sprayArray[i].X, sprayArray[i].Y)) { targetSections.Add(targetHull.BackgroundSections[sprayArray[i].X]); } } // Add more if it's 3x3 if (spraySetting == 2) { sprayArray[3].X = mousedOverIndex - 1; sprayArray[3].Y = mousedOverSection.RowIndex; sprayArray[4].X = sprayArray[1].X - 1; sprayArray[4].Y = sprayArray[1].Y; sprayArray[5].X = sprayArray[3].X - targetHull.xBackgroundMax; sprayArray[5].Y = sprayArray[3].Y - 1; sprayArray[6].X = sprayArray[5].X + 1; sprayArray[6].Y = sprayArray[5].Y; sprayArray[7].X = sprayArray[6].X + 1; sprayArray[7].Y = sprayArray[6].Y; for (int i = 3; i < sprayArray.Length; i++) { if (targetHull.DoesSectionMatch(sprayArray[i].X, sprayArray[i].Y)) { targetSections.Add(targetHull.BackgroundSections[sprayArray[i].X]); } } } } } public override void DrawHUD(SpriteBatch spriteBatch, Character character) { if (character == null || !character.IsKeyDown(InputType.Aim)) { return; } GUI.HideCursor = targetSections.Count > 0; } public override bool Use(float deltaTime, Character character = null) { if (character == null) { return false; } if (character == Character.Controlled) { Spray(character, deltaTime, applyColors: targetSections.Count > 0); return true; } else { //allow remote players to use the sprayer, but don't actually color the walls (we'll receive the data from the server) Spray(character, deltaTime, applyColors: false); return true; } } public void Spray(Character user, float deltaTime, bool applyColors) { Item liquidItem = liquidContainer?.Inventory.FirstOrDefault(); if (liquidItem == null) { return; } bool isCleaning = false; liquidColors.TryGetValue(liquidItem.Prefab.Identifier, out color); if (applyColors && targetSections.Any()) { // Ethanol or other cleaning solvent if (color.A == 0) { isCleaning = true; } float sizeAdjustedSprayStrength = SprayStrength / targetSections.Count; if (!isCleaning) { for (int i = 0; i < targetSections.Count; i++) { targetHull.IncreaseSectionColorOrStrength(targetSections[i], color, sizeAdjustedSprayStrength * deltaTime, true, false); } if (GameMain.GameSession != null) { GameMain.GameSession.TimeSpentCleaning += deltaTime; } } else { for (int i = 0; i < targetSections.Count; i++) { targetHull.CleanSection(targetSections[i], -sizeAdjustedSprayStrength * deltaTime, true); } if (GameMain.GameSession != null) { GameMain.GameSession.TimeSpentPainting += deltaTime; } } } Vector2 particleStartPos = item.WorldPosition + ConvertUnits.ToDisplayUnits(TransformedBarrelPos); Vector2 particleEndPos = user.CursorWorldPosition; //the cursor position is not exact for remote players, we only know the direction they're aiming at but not the distance // -> use 50% range, looks good enough float dist = Math.Min(Vector2.Distance(particleStartPos, particleEndPos), Range * 0.5f); foreach (ParticleEmitter particleEmitter in particleEmitters) { float particleAngle = item.body.Rotation + ((item.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi); float particleRange = particleEmitter.Prefab.Properties.VelocityMax * particleEmitter.Prefab.ParticlePrefab.LifeTime; particleEmitter.Emit( deltaTime, particleStartPos, item.CurrentHull, particleAngle, particleEmitter.Prefab.Properties.CopyEntityAngle ? -particleAngle : 0, velocityMultiplier: dist / particleRange * 1.5f, colorMultiplier: new Color(color.R, color.G, color.B, (byte)255)); } } public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null) { #if DEBUG if (GameMain.DebugDraw && Character.Controlled != null && Character.Controlled.IsKeyDown(InputType.Aim)) { GUI.DrawLine(spriteBatch, new Vector2(debugRayStartPos.X, -debugRayStartPos.Y), new Vector2(debugRayEndPos.X, -debugRayEndPos.Y), Color.Yellow); } #endif if (Character.Controlled == null || !Character.Controlled.HasEquippedItem(item) || !Character.Controlled.IsKeyDown(InputType.Aim) || targetHull == null || targetSections.Count == 0) return; Vector2 drawOffset = targetHull.Submarine == null ? Vector2.Zero : targetHull.Submarine.DrawPosition; Point sectionSize = targetSections[0].Rect.Size; Rectangle drawPositionRect = new Rectangle((int)(drawOffset.X + targetHull.Rect.X), (int)(drawOffset.Y + targetHull.Rect.Y), sectionSize.X, sectionSize.Y); if (crosshairSprite == null && crosshairPointerSprite == null) { for (int i = 0; i < targetSections.Count; i++) { GUI.DrawRectangle(spriteBatch, new Vector2(drawPositionRect.X + targetSections[i].Rect.X, -(drawPositionRect.Y + targetSections[i].Rect.Y)), new Vector2(sectionSize.X, sectionSize.Y), Color.White, false, 0.0f, 1); } } else if (targetSections.Count > 0) { Vector2 drawPos = Vector2.Zero; for (int i = 0; i < targetSections.Count; i++) { drawPos += new Vector2(drawPositionRect.X + targetSections[i].Rect.X + sectionSize.X / 2, -(drawPositionRect.Y + targetSections[i].Rect.Y - sectionSize.Y / 2)); } drawPos /= targetSections.Count; crosshairSprite?.Draw(spriteBatch, drawPos, scale: sectionSize.X * 3 / crosshairSprite.size.X); crosshairPointerSprite?.Draw(spriteBatch, drawPos, scale: sectionSize.X * (spraySetting + 1) / crosshairPointerSprite.size.X); } } } }