Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// SPDX-FileCopyrightText: 2024 Piras314 <p1r4s@proton.me>
// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 Misandry <mary@thughunt.ing>
// SPDX-FileCopyrightText: 2025 gus <august.eymann@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later

namespace Content.Common.Data._Shitcode.Weapons.Misc;

/// <summary>
/// Marks an entity as unable to be tether or force gunned
/// </summary>
[RegisterComponent]
public sealed partial class PhysicsGunBlacklistComponent : Component
{
}
42 changes: 42 additions & 0 deletions Content.Module.Client/_Goobstation/Fishing/FishingSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// SPDX-FileCopyrightText: 2025 Aidenkrz <aiden@djkraz.com>
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Roudenn <romabond091@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later

using Content.Goobstation.Client.Fishing.Overlays;
using Content.Goobstation.Shared.Fishing.Components;
using Content.Goobstation.Shared.Fishing.Systems;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;

namespace Content.Goobstation.Client.Fishing;

public sealed class FishingSystem : SharedFishingSystem
{
[Dependency] private readonly IOverlayManager _overlay = default!;
[Dependency] private readonly IPlayerManager _player = default!;

public override void Initialize()
{
base.Initialize();
_overlay.AddOverlay(new FishingOverlay(EntityManager, _player));
}

public override void Shutdown()
{
base.Shutdown();
_overlay.RemoveOverlay<FishingOverlay>();
}

// Does nothing on client, because can't spawn entities in prediction
protected override void SetupFishingFloat(Entity<FishingRodComponent> fishingRod, EntityUid player, EntityCoordinates target) {}

// Does nothing on client, because can't delete entities in prediction
protected override void ThrowFishReward(EntProtoId fishId, EntityUid fishSpot, EntityUid target) {}

// Does nothing on client, because NUKE ALL PREDICTION!!!! (UseInHands event sometimes gets declined on Server side, and it desyncs, so we can't predict that sadly.
protected override void CalculateFightingTimings(Entity<ActiveFisherComponent> fisher, ActiveFishingSpotComponent activeSpotComp) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// SPDX-FileCopyrightText: 2025 Aidenkrz <aiden@djkraz.com>
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Roudenn <romabond091@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later

using System.Numerics;
using Content.Client.UserInterface.Systems;
using Content.Goobstation.Shared.Fishing.Components;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
using Robust.Client.Player;
using Robust.Shared.Utility;

namespace Content.Goobstation.Client.Fishing.Overlays;

public sealed class FishingOverlay : Overlay
{
private readonly IEntityManager _entManager;
private readonly IPlayerManager _player;
private readonly SharedTransformSystem _transform;
private readonly ProgressColorSystem _progressColor;

private readonly Texture _barTexture;

// Fractional positions for progress bar fill (relative to texture height/width)
private const float StartYFraction = 0.09375f; // 3/32
private const float EndYFraction = 0.90625f; // 29/32
private const float BarWidthFraction = 0.2f; // 2/10

// Apply a custom scale factor to reduce the size of the progress bar
// We dont want to do this because muh pixel consistency, but i'll keep it here as an option
private const float BarScale = 1f;

public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV;

public FishingOverlay(IEntityManager entManager, IPlayerManager player)
{
_entManager = entManager;
_player = player;
_transform = _entManager.EntitySysManager.GetEntitySystem<SharedTransformSystem>();
_progressColor = _entManager.System<ProgressColorSystem>();

// Load the progress bar texture
var sprite = new SpriteSpecifier.Rsi(new("/Textures/_Goobstation/Interface/Misc/fish_bar.rsi"), "icon");
_barTexture = _entManager.EntitySysManager.GetEntitySystem<SpriteSystem>().Frame0(sprite);
}

protected override void Draw(in OverlayDrawArgs args)
{
var handle = args.WorldHandle;
var rotation = args.Viewport.Eye?.Rotation ?? Angle.Zero;
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();

const float scale = 1f;
var scaleMatrix = Matrix3Helpers.CreateScale(new Vector2(scale, scale));
var rotationMatrix = Matrix3Helpers.CreateRotation(-rotation);

// Define bounds for culling entities outside the viewport
var bounds = args.WorldAABB.Enlarged(5f);
var localEnt = _player.LocalSession?.AttachedEntity;

// Calculate the size of the texture in world units
var textureSize = new Vector2(_barTexture.Width, _barTexture.Height) / EyeManager.PixelsPerMeter;

var scaledTextureSize = textureSize * BarScale;

// Define the progress bar's width as a fraction of the texture width
var barWidth = scaledTextureSize.X * BarWidthFraction;

// Iterate through all entities with ActiveFisherComponent
var enumerator = _entManager.AllEntityQueryEnumerator<ActiveFisherComponent, SpriteComponent, TransformComponent>();
while (enumerator.MoveNext(out var uid, out var comp, out var sprite, out var xform))
{
// Skip if the entity is not on the current map, has invalid progress, or is not the local player
if (xform.MapID != args.MapId ||
comp.TotalProgress == null ||
comp.TotalProgress < 0 ||
uid != localEnt)
continue;

// Get the world position of the entity
var worldPosition = _transform.GetWorldPosition(xform, xformQuery);
if (!bounds.Contains(worldPosition))
continue;

// Set up the transformation matrix for rendering
var worldMatrix = Matrix3Helpers.CreateTranslation(worldPosition);
var scaledWorld = Matrix3x2.Multiply(scaleMatrix, worldMatrix);
var matty = Matrix3x2.Multiply(rotationMatrix, scaledWorld);
handle.SetTransform(matty);

// Calculate the position of the progress bar relative to the entity
var position = new Vector2(
sprite.Bounds.Width / 2f,
-scaledTextureSize.Y / 2f // Center vertically
);

// Draw the background texture at the scaled size
handle.DrawTextureRect(_barTexture, new Box2(position, position + scaledTextureSize));

// Calculate progress and clamp it to [0, 1]
var progress = Math.Clamp(comp.TotalProgress.Value, 0f, 1f);

// Calculate the fill height based on progress
var startYPixel = scaledTextureSize.Y * StartYFraction;
var endYPixel = scaledTextureSize.Y * EndYFraction;
var yProgress = (endYPixel - startYPixel) * progress + startYPixel;

// Define the fill box with the correct width and height
var box = new Box2(
new Vector2((scaledTextureSize.X - barWidth) / 2f, startYPixel),
new Vector2((scaledTextureSize.X + barWidth) / 2f, yProgress)
);

// Translate the box to the correct position
box = box.Translated(position);

// Draw the progress fill
var color = GetProgressColor(progress);
handle.DrawRect(box, color);
}

// Reset the shader and transform
handle.UseShader(null);
handle.SetTransform(Matrix3x2.Identity);
}

/// <summary>
/// Gets the color for the progress bar based on the progress value.
/// </summary>
public Color GetProgressColor(float progress, float alpha = 1f)
{
return _progressColor.GetProgressColor(progress).WithAlpha(alpha);
}
}
167 changes: 167 additions & 0 deletions Content.Module.Server/_Goobstation/Fishing/FishingSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// SPDX-FileCopyrightText: 2025 Aidenkrz <aiden@djkraz.com>
// SPDX-FileCopyrightText: 2025 Aviu00 <93730715+Aviu00@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 Aviu00 <aviu00@protonmail.com>
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Rouden <149893554+Roudenn@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 Roudenn <romabond091@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later

using System.Linq;
using System.Numerics;
using Content.Goobstation.Shared.Fishing.Components;
using Content.Goobstation.Shared.Fishing.Systems;
using Content.Shared.EntityTable;
using Content.Shared.Interaction.Events;
using Content.Shared.Item;
using Content.Shared.Movement.Pulling.Components;
using Content.Shared.Physics;
using Robust.Server.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Events;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;

namespace Content.Goobstation.Server.Fishing;

public sealed class FishingSystem : SharedFishingSystem
{
// Here we calculate the start of fishing, because apparently StartCollideEvent
// works janky on clientside so we can't predict when fishing starts.
[Dependency] private readonly IComponentFactory _compFactory = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly PhysicsSystem _physics = default!;

public override void Initialize()
{
base.Initialize();

SubscribeLocalEvent<FishingLureComponent, StartCollideEvent>(OnFloatCollide);
SubscribeLocalEvent<FishingRodComponent, UseInHandEvent>(OnFishingInteract);
}

#region Event handling

private void OnFloatCollide(Entity<FishingLureComponent> ent, ref StartCollideEvent args)
{
// TODO: make it so this can collide with any unacnchored objects (items, mobs, etc) but not the player casting it (get parent of rod?)
// Fishing spot logic
var attachedEnt = args.OtherEntity;

if (HasComp<ActiveFishingSpotComponent>(attachedEnt))
return;

if (!FishSpotQuery.TryComp(attachedEnt, out var spotComp))
{
if (args.OtherBody.BodyType == BodyType.Static)
return;

Anchor(ent, attachedEnt);
return;
}

// Anchor fishing float on an entity
Anchor(ent, attachedEnt);

// Currently we don't support multiple loots from this
var fish = spotComp.FishList.GetSpawns(_random.GetRandom(), EntityManager, _proto, new EntityTableContext()).First();

// Get fish difficulty
_proto.Index(fish).TryGetComponent(out FishComponent? fishComp, _compFactory);

// Assign things that depend on the fish
var activeFishSpot = EnsureComp<ActiveFishingSpotComponent>(attachedEnt);
activeFishSpot.Fish = fish;
activeFishSpot.FishDifficulty = fishComp?.FishDifficulty ?? FishComponent.DefaultDifficulty;

// Assign things that depend on the spot
var time = spotComp.FishDefaultTimer + _random.NextFloat(-spotComp.FishTimerVariety, spotComp.FishTimerVariety);
activeFishSpot.FishingStartTime = Timing.CurTime + TimeSpan.FromSeconds(time);
activeFishSpot.AttachedFishingLure = ent;

// Declares war on prediction
Dirty(attachedEnt, activeFishSpot);
Dirty(ent);
}

private void OnFishingInteract(EntityUid uid, FishingRodComponent component, UseInHandEvent args)
{
if (!FisherQuery.TryComp(args.User, out var fisherComp) || fisherComp.TotalProgress == null || args.Handled || !Timing.IsFirstTimePredicted)
return;

fisherComp.TotalProgress += fisherComp.ProgressPerUse * component.Efficiency;
Dirty(args.User, fisherComp); // That's a bit evil, but we want to keep numbers real.

args.Handled = true;
}

private void Anchor(Entity<FishingLureComponent> ent, EntityUid attachedEnt)
{
var spotPosition = Xform.GetWorldPosition(attachedEnt);
Xform.SetWorldPosition(ent, spotPosition);
Xform.SetParent(ent, attachedEnt);
_physics.SetLinearVelocity(ent, Vector2.Zero);
_physics.SetAngularVelocity(ent, 0f);
ent.Comp.AttachedEntity = attachedEnt;
RemComp<ItemComponent>(ent);
RemComp<PullableComponent>(ent);
}

#endregion

protected override void SetupFishingFloat(Entity<FishingRodComponent> fishingRod, EntityUid player, EntityCoordinates target)
{
var (uid, component) = fishingRod;
var targetCoords = Xform.ToMapCoordinates(target);
var playerCoords = Xform.GetMapCoordinates(Transform(player));

var fishFloat = Spawn(component.FloatPrototype, playerCoords);
component.FishingLure = fishFloat;
Dirty(uid, component);

// Calculate throw direction
var direction = targetCoords.Position - playerCoords.Position;
if (direction == Vector2.Zero)
direction = Vector2.UnitX; // If the user somehow manages to click directly in the center of themself, just toss it to the right i guess.

// Yeet
Throwing.TryThrow(fishFloat, direction, 15f, player, 2f, null, true);

// Set up lure component
var fishLureComp = EnsureComp<FishingLureComponent>(fishFloat);
fishLureComp.FishingRod = uid;
Dirty(fishFloat, fishLureComp);

// Rope visuals
var visuals = EnsureComp<JointVisualsComponent>(fishFloat);
visuals.Sprite = component.RopeSprite;
visuals.OffsetA = component.RopeLureOffset;
visuals.OffsetB = component.RopeUserOffset;
visuals.Target = uid;
}

protected override void ThrowFishReward(EntProtoId fishId, EntityUid fishSpot, EntityUid target)
{
var position = Transform(fishSpot).Coordinates;
var fish = Spawn(fishId, position);
// Throw da fish back to the player because it looks funny
var direction = Xform.GetWorldPosition(target) - Xform.GetWorldPosition(fish);
var length = direction.Length();
var distance = Math.Clamp(length, 0.5f, 15f);
direction *= distance / length;

Throwing.TryThrow(fish, direction, 7f);
}

protected override void CalculateFightingTimings(Entity<ActiveFisherComponent> fisher, ActiveFishingSpotComponent activeSpotComp)
{
if (Timing.CurTime < fisher.Comp.NextStruggle)
return;

fisher.Comp.NextStruggle = Timing.CurTime + TimeSpan.FromSeconds(_random.NextFloat(0.06f, 0.18f));
fisher.Comp.TotalProgress -= activeSpotComp.FishDifficulty;
Dirty(fisher);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// SPDX-FileCopyrightText: 2025 Aidenkrz <aiden@djkraz.com>
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Roudenn <romabond091@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later

using Robust.Shared.GameStates;

namespace Content.Goobstation.Shared.Fishing.Components;

/// <summary>
/// Applied to players that are pulling fish out from water
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class ActiveFisherComponent : Component
{
[DataField, AutoNetworkedField]
public TimeSpan? NextStruggle;

[DataField, AutoNetworkedField]
public float? TotalProgress;

[DataField, AutoNetworkedField]
public float ProgressPerUse = 0.05f;

[DataField, AutoNetworkedField]
public EntityUid FishingRod;
}
Loading
Loading