Port Fishing from Goob (#4615)

* FISHING REAL

* Update fishingspots.yml

* Delete fishingspots.yml

* Create fishingspots.yml

Removed the component that was causing errors

* Update Content.Client/_Goobstation/Fishing/Overlays/FishingOverlay.cs

Co-authored-by: Tobias Berger <toby@tobot.dev>
Signed-off-by: Apachito <ivanlopezlegos2004@gmail.com>

* Update Resources/Prototypes/Entities/Structures/Furniture/toilet.yml

Co-authored-by: Tobias Berger <toby@tobot.dev>
Signed-off-by: Apachito <ivanlopezlegos2004@gmail.com>

* Update Resources/Prototypes/Entities/Tiles/lava.yml

Co-authored-by: Tobias Berger <toby@tobot.dev>
Signed-off-by: Apachito <ivanlopezlegos2004@gmail.com>

* Update Resources/Prototypes/Entities/Tiles/liquid_plasma.yml

Co-authored-by: Tobias Berger <toby@tobot.dev>
Signed-off-by: Apachito <ivanlopezlegos2004@gmail.com>

* Update Resources/Prototypes/Entities/Tiles/liquid_plasma.yml

Co-authored-by: Tobias Berger <toby@tobot.dev>
Signed-off-by: Apachito <ivanlopezlegos2004@gmail.com>

* Update Resources/Prototypes/Entities/Tiles/water.yml

Co-authored-by: Tobias Berger <toby@tobot.dev>
Signed-off-by: Apachito <ivanlopezlegos2004@gmail.com>

* Update Resources/Prototypes/_Goobstation/Recipes/Construction/Graphs/misc/makeshift_rod.yml

Co-authored-by: Tobias Berger <toby@tobot.dev>
Signed-off-by: Apachito <ivanlopezlegos2004@gmail.com>

* Update Resources/Prototypes/Entities/Tiles/water.yml

Co-authored-by: Tobias Berger <toby@tobot.dev>
Signed-off-by: Apachito <ivanlopezlegos2004@gmail.com>

* Update Resources/Prototypes/_Goobstation/Actions/fishing.yml

Co-authored-by: Tobias Berger <toby@tobot.dev>
Signed-off-by: Apachito <ivanlopezlegos2004@gmail.com>

* Update Resources/Prototypes/_Goobstation/Catalog/Fills/Crates/service.yml

Co-authored-by: Tobias Berger <toby@tobot.dev>
Signed-off-by: Apachito <ivanlopezlegos2004@gmail.com>

* Changed the name spaces

* Update SharedFishingSystem.cs

* Revert "Update SharedFishingSystem.cs"

This reverts commit 78451cd5c2.

* Update FishingOverlay.cs

* Update FishingOverlay.cs

* Revert "Changed the name spaces"

This reverts commit 50bac6c4be.

* Revert "Changed the name spaces"

* Fixed name spaces and the sprite system

* Update makeshift_rod.yml

* Update makeshift_rod.yml

* Update Content.Client/_Goobstation/Fishing/Overlays/FishingOverlay.cs

Signed-off-by: Tobias Berger <toby@tobot.dev>

* Update Content.Client/_Goobstation/Fishing/Overlays/FishingOverlay.cs

Signed-off-by: Tobias Berger <toby@tobot.dev>

* Fixed Name Spaced

Changed the names spaces and did the ones I forgot to do initially.

* Changed name spaces again

* Fixed name spaces real

* Update SharedFishingSystem.cs

* Update SharedFishingSystem.cs

* Update SharedFishingSystem.cs

* Update FishingSystem.cs

* Update FishingSystem.cs

---------

Signed-off-by: Apachito <ivanlopezlegos2004@gmail.com>
Signed-off-by: Tobias Berger <toby@tobot.dev>
Co-authored-by: Tobias Berger <toby@tobot.dev>
This commit is contained in:
Apachito 2025-11-18 15:06:59 -06:00 committed by GitHub
parent 2e4e7e1ca1
commit ff01033eaa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
86 changed files with 2030 additions and 0 deletions

View File

@ -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.Client._Goobstation.Fishing.Overlays;
using Content.Shared._Goobstation.Fishing.Components;
using Content.Shared._Goobstation.Fishing.Systems;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
namespace Content.Client._Goobstation.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) {}
}

View File

@ -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.Shared._Goobstation.Fishing.Components;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
using Robust.Client.Player;
using Robust.Shared.Utility;
namespace Content.Client._Goobstation.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 SpriteSystem _sprite;
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>();
_sprite = _entManager.System<SpriteSystem>();
// 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.GetLocalBounds((uid, sprite)).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);
}
}

View File

@ -0,0 +1,168 @@
// 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.Shared._Goobstation.Fishing.Components;
using Content.Shared._Goobstation.Fishing.Events;
using Content.Shared._Goobstation.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.Server._Goobstation.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 = GetNetEntity(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);
}
}

View File

@ -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.Shared._Goobstation.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;
}

View File

@ -0,0 +1,37 @@
// 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;
using Robust.Shared.Prototypes;
namespace Content.Shared._Goobstation.Fishing.Components;
/// <summary>
/// Dynamic component, that is assigned to active fishing spots that are currently waiting for da fish.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class ActiveFishingSpotComponent : Component
{
[ViewVariables, AutoNetworkedField]
public EntityUid? AttachedFishingLure;
[DataField, AutoNetworkedField]
public TimeSpan? FishingStartTime;
/// <summary>
/// If true, someone is pulling fish out of this spot.
/// </summary>
[DataField, AutoNetworkedField]
public bool IsActive;
[DataField, AutoNetworkedField]
public float FishDifficulty;
/// <summary>
/// Fish that we're currently trying to catch
/// </summary>
[DataField]
public EntProtoId? Fish; // not networked because useless for client
}

View File

@ -0,0 +1,18 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Roudenn <romabond091@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace Content.Shared._Goobstation.Fishing.Components;
/// <summary>
/// The fish itself!
/// </summary>
[RegisterComponent]
public sealed partial class FishComponent : Component
{
public const float DefaultDifficulty = 0.021f;
[DataField("difficulty")]
public float FishDifficulty = DefaultDifficulty;
}

View File

@ -0,0 +1,25 @@
// 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.Shared._Goobstation.Fishing.Components;
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class FishingLureComponent : Component
{
[DataField, AutoNetworkedField]
public EntityUid FishingRod;
[DataField, AutoNetworkedField]
public EntityUid? AttachedEntity;
[ViewVariables]
public TimeSpan NextUpdate;
[DataField]
public float UpdateInterval = 1f;
}

View File

@ -0,0 +1,71 @@
// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 Aidenkrz <aiden@djkraz.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 Robust.Shared.Prototypes;
using Robust.Shared.GameStates;
using Robust.Shared.Utility;
using System.Numerics;
namespace Content.Shared._Goobstation.Fishing.Components;
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class FishingRodComponent : Component
{
/// <summary>
/// Higher value will make every interact more productive.
/// </summary>
[DataField]
public float Efficiency = 1f;
/// <summary>
/// At what progress fishing starts.
/// </summary>
[DataField]
public float StartingProgress = 0.33f;
/// <summary>
/// How many seconds we wait until fish starts to fight with us
/// </summary>
[DataField]
public float StartingStruggleTime = 0.3f;
/// <summary>
/// If lure moves bigger than this distance away from the rod,
/// it will force it to reel instantly.
/// </summary>
[DataField]
public float BreakOnDistance = 8f;
[DataField]
public EntProtoId FloatPrototype = "FishingLure";
[DataField]
public SpriteSpecifier RopeSprite =
new SpriteSpecifier.Rsi(new ResPath("_Goobstation/Objects/Specific/Fishing/fishing_lure.rsi"), "rope");
[DataField, ViewVariables]
public Vector2 RopeUserOffset = new (0f, 0f);
[DataField, ViewVariables]
public Vector2 RopeLureOffset = new (0f, 0f);
[DataField, AutoNetworkedField]
public EntityUid? FishingLure;
[DataField]
public EntProtoId ThrowLureActionId = "ActionStartFishing";
[DataField, AutoNetworkedField]
public EntityUid? ThrowLureActionEntity;
[DataField]
public EntProtoId PullLureActionId = "ActionStopFishing";
[DataField, AutoNetworkedField]
public EntityUid? PullLureActionEntity;
}

View File

@ -0,0 +1,30 @@
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Roudenn <romabond091@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared.EntityTable.EntitySelectors;
namespace Content.Shared._Goobstation.Fishing.Components;
[RegisterComponent]
public sealed partial class FishingSpotComponent : Component
{
/// <summary>
/// All possible fishes to catch here
/// </summary>
[DataField(required: true)]
public EntityTableSelector FishList;
/// <summary>
/// Default time for fish to occur
/// </summary>
[DataField]
public float FishDefaultTimer;
/// <summary>
/// Variety number that FishDefaultTimer can go up or down to randomly
/// </summary>
[DataField]
public float FishTimerVariety;
}

View File

@ -0,0 +1,31 @@
// 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.Shared.Actions;
using Robust.Shared.Serialization;
namespace Content.Shared._Goobstation.Fishing.Events;
public sealed partial class ThrowFishingLureActionEvent : WorldTargetActionEvent;
public sealed partial class PullFishingLureActionEvent : InstantActionEvent;
[Serializable, NetSerializable]
public sealed class ActiveFishingSpotComponentState : ComponentState
{
public readonly float FishDifficulty;
public bool IsActive;
public TimeSpan? FishingStartTime;
public NetEntity? AttachedFishingLure;
public ActiveFishingSpotComponentState(float fishDifficulty, bool isActive, TimeSpan? fishingStartTime, NetEntity? attachedFishingLure)
{
FishDifficulty = fishDifficulty;
IsActive = isActive;
FishingStartTime = fishingStartTime;
AttachedFishingLure = attachedFishingLure;
}
}

View File

@ -0,0 +1,390 @@
// SPDX-FileCopyrightText: 2025 Aidenkrz <aiden@djkraz.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 Content.Shared._Goobstation.Fishing.Components;
using Content.Shared._Goobstation.Fishing.Events;
using Content.Shared.Actions;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Popups;
using Content.Shared.Throwing;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Shared._Goobstation.Fishing.Systems;
/// <summary>
/// This handles... da fish
/// </summary>
public abstract class SharedFishingSystem : EntitySystem
{
[Dependency] protected readonly IGameTiming Timing = default!;
[Dependency] protected readonly INetManager Net = default!;
[Dependency] protected readonly ThrowingSystem Throwing = default!;
[Dependency] protected readonly SharedTransformSystem Xform = default!;
[Dependency] private readonly SharedActionsSystem _actions = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
protected EntityQuery<ActiveFisherComponent> FisherQuery;
protected EntityQuery<ActiveFishingSpotComponent> ActiveFishSpotQuery;
protected EntityQuery<FishingSpotComponent> FishSpotQuery;
protected EntityQuery<FishingRodComponent> FishRodQuery;
protected EntityQuery<FishingLureComponent> FishLureQuery;
public override void Initialize()
{
base.Initialize();
FisherQuery = GetEntityQuery<ActiveFisherComponent>();
ActiveFishSpotQuery = GetEntityQuery<ActiveFishingSpotComponent>();
FishSpotQuery = GetEntityQuery<FishingSpotComponent>();
FishRodQuery = GetEntityQuery<FishingRodComponent>();
FishLureQuery = GetEntityQuery<FishingLureComponent>();
SubscribeLocalEvent<FishingRodComponent, MapInitEvent>(OnFishingRodInit);
SubscribeLocalEvent<FishingRodComponent, GetItemActionsEvent>(OnGetActions);
SubscribeLocalEvent<FishingRodComponent, ThrowFishingLureActionEvent>(OnThrowFloat);
SubscribeLocalEvent<FishingRodComponent, PullFishingLureActionEvent>(OnPullFloat);
SubscribeLocalEvent<FishingRodComponent, EntParentChangedMessage>(OnRodParentChanged);
SubscribeLocalEvent<FishingRodComponent, EntityTerminatingEvent>(OnRodTerminating);
SubscribeLocalEvent<FishingLureComponent, EntityTerminatingEvent>(OnLureTerminating);
SubscribeLocalEvent<ActiveFishingSpotComponent, EntityTerminatingEvent>(OnSpotTerminating);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
UpdateFishing();
}
private void UpdateFishing()
{
if (!Timing.IsFirstTimePredicted)
return;
var currentTime = Timing.CurTime;
var activeFishers = EntityQueryEnumerator<ActiveFisherComponent>();
while (activeFishers.MoveNext(out var fisher, out var fisherComp))
{
// Get fishing rod, then float, then spot... ReCurse.
if (TerminatingOrDeleted(fisherComp.FishingRod) ||
!FishRodQuery.TryComp(fisherComp.FishingRod, out var fishingRodComp) ||
TerminatingOrDeleted(fishingRodComp.FishingLure) ||
!FishLureQuery.TryComp(fishingRodComp.FishingLure, out var fishingFloatComp) ||
TerminatingOrDeleted(fishingFloatComp.AttachedEntity) ||
!ActiveFishSpotQuery.TryComp(fishingFloatComp.AttachedEntity, out var activeSpotComp))
continue;
var fishRod = fisherComp.FishingRod;
var fishSpot = fishingFloatComp.AttachedEntity.Value;
fisherComp.TotalProgress ??= fishingRodComp.StartingProgress;
fisherComp.NextStruggle ??= Timing.CurTime + TimeSpan.FromSeconds(fishingRodComp.StartingStruggleTime);
// Fish fighting logic
CalculateFightingTimings((fisher, fisherComp), activeSpotComp);
switch (fisherComp.TotalProgress)
{
case < 0f:
// It's over
_popup.PopupEntity(Loc.GetString("fishing-progress-fail"), fisher, fisher);
StopFishing((fishRod, fishingRodComp), fisher);
continue;
case >= 1f:
if (activeSpotComp.Fish != null)
{
ThrowFishReward(activeSpotComp.Fish.Value, fishSpot, fisher);
_popup.PopupEntity(Loc.GetString("fishing-progress-success"), fisher, fisher);
}
StopFishing((fishRod, fishingRodComp), fisher);
break;
}
}
var fishingSpots = EntityQueryEnumerator<ActiveFishingSpotComponent>();
while (fishingSpots.MoveNext(out var activeSpotComp))
{
if (currentTime < activeSpotComp.FishingStartTime || activeSpotComp.IsActive || activeSpotComp.FishingStartTime == null)
continue;
// Trigger start of the fishing process
if (TerminatingOrDeleted(activeSpotComp.AttachedFishingLure))
continue;
// Get fishing lure, then rod, then player... ReCurse.
if (!FishLureQuery.TryComp(activeSpotComp.AttachedFishingLure, out var fishingFloatComp) ||
TerminatingOrDeleted(fishingFloatComp.FishingRod) ||
!FishRodQuery.TryComp(fishingFloatComp.FishingRod, out var fishRodComp))
continue;
var fishRod = fishingFloatComp.FishingRod;
if (TerminatingOrDeleted(fishingFloatComp.FishingRod))
continue;
var fisher = Transform(fishingFloatComp.FishingRod).ParentUid;
if (!Exists(fisher) || TerminatingOrDeleted(fisher))
continue;
var activeFisher = EnsureComp<ActiveFisherComponent>(fisher);
activeFisher.FishingRod = fishRod;
activeFisher.ProgressPerUse *= fishRodComp.Efficiency;
activeFisher.TotalProgress = fishRodComp.StartingProgress;
activeFisher.NextStruggle = Timing.CurTime + TimeSpan.FromSeconds(fishRodComp.StartingStruggleTime); // Compensate ping for 0.3 seconds
// Predicted because it works like 99.9% of the time anyway.
_popup.PopupPredicted(Loc.GetString("fishing-progress-start"), fisher, fisher);
activeSpotComp.IsActive = true;
}
var fishingLures = EntityQueryEnumerator<FishingLureComponent, TransformComponent>();
while (fishingLures.MoveNext(out var fishingLure, out var lureComp, out var xform))
{
if (lureComp.NextUpdate > Timing.CurTime)
continue;
lureComp.NextUpdate = Timing.CurTime + TimeSpan.FromSeconds(lureComp.UpdateInterval);
if (TerminatingOrDeleted(lureComp.FishingRod) ||
!FishRodQuery.TryComp(lureComp.FishingRod, out var fishingRodComp))
continue;
var lurePos = Xform.GetMapCoordinates(fishingLure, xform);
var rodPos = Xform.GetMapCoordinates(lureComp.FishingRod);
var distance = lurePos.Position - rodPos.Position;
var fisher = Transform(lureComp.FishingRod).ParentUid;
if (!Exists(fisher) || TerminatingOrDeleted(fisher) ||
distance.Length() > fishingRodComp.BreakOnDistance ||
lurePos.MapId != rodPos.MapId ||
!_hands.IsHolding(fisher, lureComp.FishingRod) ||
!HasComp<ActorComponent>(fisher))
{
var rod = (lureComp.FishingRod, fishingRodComp);
StopFishing(rod, fisher);
ToggleFishingActions(rod, fisher, false);
}
}
}
/// <summary>
/// if AddPulling is true, we ADD Pulling action and REMOVE Throwing action.
/// Basically true if we start, and false if we end.
/// </summary>
private void ToggleFishingActions(Entity<FishingRodComponent> ent, EntityUid fisher, bool addPulling)
{
if (TerminatingOrDeleted(ent) || !Exists(fisher) || TerminatingOrDeleted(fisher))
return;
if (addPulling)
{
_actions.RemoveAction(ent.Comp.ThrowLureActionEntity);
_actions.AddAction(fisher, ref ent.Comp.PullLureActionEntity, ent.Comp.PullLureActionId, ent);
}
else
{
_actions.RemoveAction(ent.Comp.PullLureActionEntity);
_actions.AddAction(fisher, ref ent.Comp.ThrowLureActionEntity, ent.Comp.ThrowLureActionId, ent);
}
}
protected abstract void CalculateFightingTimings(Entity<ActiveFisherComponent> fisher, ActiveFishingSpotComponent activeSpotComp);
/// <summary>
/// Server-side only, sets up fishing float and throws it
/// </summary>
protected abstract void SetupFishingFloat(Entity<FishingRodComponent> fishingRod, EntityUid player, EntityCoordinates target);
/// <summary>
/// Server-side only, spawns a fish and throws it to our player!
/// </summary>
protected abstract void ThrowFishReward(EntProtoId fishId, EntityUid fishSpot, EntityUid target);
/// <summary>
/// Reels the fishing rod back and stops fishing progress if arguments are passed to it.
/// </summary>
private void StopFishing(
Entity<FishingRodComponent> fishingRod,
EntityUid? fisher)
{
var nullOrDeleted =
fishingRod.Comp.FishingLure == null || TerminatingOrDeleted(fishingRod.Comp.FishingLure.Value);
if (!nullOrDeleted && FishLureQuery.TryComp(fishingRod.Comp.FishingLure, out var lureComp) &&
!TerminatingOrDeleted(lureComp.AttachedEntity) &&
ActiveFishSpotQuery.TryComp(lureComp.AttachedEntity, out var activeSpotComp))
RemCompDeferred(lureComp.AttachedEntity.Value, activeSpotComp);
if (!nullOrDeleted && Net.IsServer)
QueueDel(fishingRod.Comp.FishingLure);
if (Exists(fisher) && !TerminatingOrDeleted(fisher) && FisherQuery.TryComp(fisher, out var fisherComp))
{
RemCompDeferred(fisher.Value, fisherComp);
ToggleFishingActions(fishingRod, fisher.Value, false);
}
fishingRod.Comp.FishingLure = null;
}
#region Terminating Events
private void OnRodTerminating(Entity<FishingRodComponent> ent, ref EntityTerminatingEvent args)
{
TryStopFishing(ent);
}
private void OnLureTerminating(Entity<FishingLureComponent> ent, ref EntityTerminatingEvent args)
{
TryStopFishing(ent);
}
private void OnSpotTerminating(Entity<ActiveFishingSpotComponent> ent, ref EntityTerminatingEvent args)
{
TryStopFishing(ent);
}
#endregion
#region Deletion Helpers
/// <summary>
/// Stops fishing by taking only the Fishing rod as an argument.
/// </summary>
private void TryStopFishing(Entity<FishingRodComponent> rod)
{
var player = Transform(rod).ParentUid;
StopFishing(rod, player);
}
/// <summary>
/// Stops fishing by taking only the Fishing lure as an argument.
/// </summary>
private void TryStopFishing(Entity<FishingLureComponent> lure)
{
if (!FishRodQuery.TryComp(lure.Comp.FishingRod, out var rodComp))
return;
TryStopFishing((lure.Comp.FishingRod, rodComp));
}
/// <summary>
/// Stops fishing by taking only the Active spot as an argument.
/// </summary>
private void TryStopFishing(Entity<ActiveFishingSpotComponent> spot)
{
if (!FishLureQuery.TryComp(spot.Comp.AttachedFishingLure, out var lureComp))
return;
if (!FishRodQuery.TryComp(lureComp.FishingRod, out var rodComp))
return;
TryStopFishing((lureComp.FishingRod, rodComp));
}
#endregion
#region Event Handling
private void OnThrowFloat(Entity<FishingRodComponent> ent, ref ThrowFishingLureActionEvent args)
{
if (args.Handled || !Timing.IsFirstTimePredicted)
return;
var player = args.Performer;
if (ent.Comp.FishingLure != null || !Xform.IsValid(args.Target))
{
args.Handled = true;
return;
}
SetupFishingFloat(ent, player, args.Target);
ToggleFishingActions(ent, player, true);
args.Handled = true;
}
private void OnPullFloat(Entity<FishingRodComponent> ent, ref PullFishingLureActionEvent args)
{
if (args.Handled || !Timing.IsFirstTimePredicted)
return;
var player = args.Performer;
var (uid, component) = ent;
if (component.FishingLure == null)
{
ToggleFishingActions(ent, player, true);
args.Handled = true;
return;
}
_popup.PopupPredicted(Loc.GetString("fishing-rod-remove-lure", ("ent", Name(uid))), uid, uid);
if (!FishLureQuery.TryComp(component.FishingLure, out var lureComp))
return;
if (lureComp.AttachedEntity != null && Exists(lureComp.AttachedEntity))
{
// TODO: so this kinda just lets you pull anything right up to you, it should instead just apply an impulse in your direction modfiied by the weight of the player vs the object
// Also we need to autoreel/snap the line if the player gets too far away
// Also we should probably PVS override the lure if the rod is in PVS, and vice versa to stop the joint visuals from popping in/out
var attachedEnt = lureComp.AttachedEntity.Value;
var targetCoords = Xform.GetMapCoordinates(Transform(attachedEnt));
var playerCoords = Xform.GetMapCoordinates(Transform(player));
var rand = new System.Random((int) Timing.CurTick.Value); // evil random prediction hack
// Calculate throw direction
var direction = (playerCoords.Position - targetCoords.Position) * rand.NextFloat(0.2f, 0.85f);
// Yeet
Throwing.TryThrow(attachedEnt, direction, 4f, player);
}
StopFishing(ent, player);
ToggleFishingActions(ent, player, false);
args.Handled = true;
}
private void OnFishingRodInit(Entity<FishingRodComponent> ent, ref MapInitEvent args)
{
_actions.AddAction(ent, ref ent.Comp.ThrowLureActionEntity, ent.Comp.ThrowLureActionId);
}
private void OnRodParentChanged(Entity<FishingRodComponent> ent, ref EntParentChangedMessage args)
{
if (TerminatingOrDeleted(ent) || !Exists(args.Transform.ParentUid))
return;
// Anything that is an active fisher should be fine.
if (!FisherQuery.HasComp(args.Transform.ParentUid))
{
StopFishing(ent, args.OldParent);
}
}
private void OnGetActions(Entity<FishingRodComponent> ent, ref GetItemActionsEvent args)
{
if (ent.Comp.FishingLure == null)
args.AddAction(ref ent.Comp.ThrowLureActionEntity, ent.Comp.ThrowLureActionId);
else
args.AddAction(ref ent.Comp.PullLureActionEntity, ent.Comp.PullLureActionId);
}
#endregion
}

View File

@ -0,0 +1,14 @@
# SPDX-FileCopyrightText: 2025 Aidenkrz <aiden@djkraz.com>
# SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
- files: ["fishing_rod_cast.ogg"]
license: "CC-BY-NC-SA-4.0"
copyright: "Taken from Goon at commit 0c11a46275f493f295d6d05d1aa5fad1d935efb4"
source: "https://github.com/goonstation/goonstation/blob/master/sound/items/fishing_rod_cast.ogg"
- files: ["fishing_rod_reel.ogg"]
license: "CC-BY-NC-SA-4.0"
copyright: "Taken from Goon at commit 0c11a46275f493f295d6d05d1aa5fad1d935efb4"
source: "https://github.com/goonstation/goonstation/blob/master/sound/items/fishing_rod_cast.ogg"

View File

@ -0,0 +1,5 @@
fishing-rod-remove-lure = { $ent } is reeling
fishing-progress-success = You pull something up from the spot!
fishing-progress-fail = You missed it...
fishing-progress-lost-rod = You lost control over the { $ent }!
fishing-progress-start = You feel something clinging to the fishing lure!

View File

@ -63,6 +63,18 @@
- MachineMask
layer:
- None
# Begin Goobstation - fishing
fishing:
shape:
!type:PhysShapeCircle
radius: 0.4
layer:
- ItemMask
mask:
- HighImpassable
density: 1000
hard: false
# End Goobstation - fishing
- type: PlungerUse
- type: Appearance
- type: SecretStash
@ -92,6 +104,13 @@
reagents:
- ReagentId: Water
Quantity: 1
# Goobstation - fishing in toilet is fun
- type: FishingSpot
fishList: !type:NestedSelector
tableId: ToiletFishingLootTable
fishDefaultTimer: 45.0
fishTimerVariety: 15.0
# Goobstation - fishing
- type: DrainableSolution
solution: tank
- type: ReagentTank

View File

@ -21,6 +21,13 @@
multiplier: 3.75
multiplierOnExisting: 0.75
- !type:Ignite
# Begin Goobstation - fishing
- type: FishingSpot
fishList: !type:NestedSelector
tableId: LavaFishingLootTable
fishDefaultTimer: 25.0
fishTimerVariety: 15.0
# End Goobstation - fishing
- type: Transform
anchored: true
- type: SyncSprite

View File

@ -21,6 +21,13 @@
multiplier: 3.75
multiplierOnExisting: 0.75
- !type:Ignite
# Begin Goobstation - fishing
- type: FishingSpot
fishList: !type:NestedSelector
tableId: PlasmaFishingLootTable
fishDefaultTimer: 25.0
fishTimerVariety: 15.0
# End Goobstation - fishing
- type: Transform
anchored: true
- type: SyncSprite
@ -51,6 +58,18 @@
- ItemMask
density: 1000
hard: false
# Begin Goobstation - fishing
fishing:
shape:
!type:PhysShapeCircle
radius: 0.4
layer:
- ItemMask
mask:
- HighImpassable
density: 1000
hard: false
# End Goobstation - fishing
- type: Tag
tags:
- HideContextMenu

View File

@ -49,6 +49,18 @@
- ItemMask
density: 1000
hard: false
# Begin Goobstation - fishing
fishing:
shape:
!type:PhysShapeCircle
radius: 0.4
layer:
- ItemMask
mask:
- HighImpassable
density: 1000
hard: false
# End Goobstation - fishing
- type: FootstepModifier
footstepSoundCollection:
collection: FootstepWater
@ -63,5 +75,12 @@
- type: TileEntityEffect
effects:
- !type:ExtinguishReaction
# Begin Goobstation - fishing
- type: FishingSpot
fishList: !type:NestedSelector
tableId: WaterFishingLootTable
fishDefaultTimer: 25.0
fishTimerVariety: 15.0
# End Goobstation - fishing
- type: CosmicCorruptible # DeltaV - Cosmic Cult
convertTo: FloorCosmicDecay

View File

@ -0,0 +1,41 @@
# 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
- type: entity
parent: BaseAction
id: ActionStartFishing
name: Throw fishing lure
description: Throw lure from the fishing rod to catch something!
components:
- type: Action
icon:
sprite: _Goobstation/Objects/Specific/Fishing/fishing_lure.rsi
state: icon
sound:
path: /Audio/_Goobstation/Items/Fishing/fishing_rod_cast.ogg
itemIconStyle: BigAction
useDelay: 2.5
- type: TargetAction
range: 15
checkCanAccess: false
- type: WorldTargetAction
event: !type:ThrowFishingLureActionEvent
- type: entity
parent: BaseAction
id: ActionStopFishing
name: Reel fishing rod
description: Reel your fishing rod to pull an object that it attached to, or stop fishing.
components:
- type: Action
icon:
sprite: _Goobstation/Objects/Specific/Fishing/goon_rod.rsi
state: icon
sound:
path: /Audio/_Goobstation/Items/Fishing/fishing_rod_reel.ogg
itemIconStyle: NoItem
- type: InstantAction
event: !type:PullFishingLureActionEvent

View File

@ -0,0 +1,14 @@
# SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
# SPDX-FileCopyrightText: 2025 Roudenn <romabond091@gmail.com>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
- type: cargoProduct
id: FishingGoodies
icon:
sprite: _Goobstation/Objects/Specific/Fishing/fishing_rod.rsi
state: icon
product: CrateFishingGoodies
cost: 1000
category: cargoproduct-category-name-service
group: market

View File

@ -0,0 +1,17 @@
# SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
# SPDX-FileCopyrightText: 2025 Roudenn <romabond091@gmail.com>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
- type: entity
id: CrateFishingGoodies
parent: CratePlastic
name: fishing goodies crate
description: A couple of fishing rods, a bear, and a hat. Everything you need to catch all fish on the station!
components:
- type: StorageFill
contents:
- id: FishingRod
- id: FishingRodGoon
- id: ClothingHeadFishCap
- id: DrinkBeerBottleFull

View File

@ -0,0 +1,294 @@
# SPDX-FileCopyrightText: 2025 Evige <jussi.heikkila1@gmail.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
- type: entity
id: BaseFish
parent: [SimpleMobBase, BaseItem]
abstract: true
name: fish
description: Water-born creature from the infinite sea... Or toilet.
components:
- type: Fish
difficulty: 0.029
- type: StaticPrice
price: 250
- type: MobThresholds
thresholds:
0: Alive
1: Critical
5: Dead
- type: MovementSpeedModifier
baseWalkSpeed: 0.1
baseSprintSpeed: 0.1
- type: PassiveDamage
allowedStates:
- Alive
damageCap: 20
damage:
types:
Asphyxiation: 1
- type: Respirator
damage:
types:
Asphyxiation: 1.0
damageRecovery:
types:
Asphyxiation: 0
- type: Butcherable
spawned:
- id: FoodMeatFish
amount: 2
- type: Bloodstream
bloodMaxVolume: 20
bloodReagent: Blood
- type: ZombieImmune
- type: entity
id: BaseFishRare
parent: BaseFish
abstract: true
name: rare fish
description: Water-born creature from the infinite sea... Or toilet.
components:
- type: Fish
difficulty: 0.038
- type: StaticPrice
price: 750
- type: entity
id: FishAlien
parent: BaseFishRare
name: alien fish
description: Looks pretty... Abductory.
components:
- type: Sprite
sprite: _Goobstation/Objects/Specific/Fishing/fish.rsi
state: alien
- type: Item
sprite: _Goobstation/Objects/Specific/Fishing/fish.rsi
- type: entity
id: FishAngler
parent: BaseFish
name: angler fish
description: Scariest thing in existence, after bingles.
components:
- type: Sprite
sprite: _Goobstation/Objects/Specific/Fishing/fish.rsi
state: angler
- type: Item
sprite: _Goobstation/Objects/Specific/Fishing/fish.rsi
- type: entity
id: FishBass
parent: BaseFish
name: bass fish
description: Probably the most normal fish in existence. Unless you use it as bass guitar.
components:
- type: Sprite
sprite: _Goobstation/Objects/Specific/Fishing/fish.rsi
state: bass
- type: Item
sprite: _Goobstation/Objects/Specific/Fishing/fish.rsi
- type: entity
id: FishBingle
parent: BaseFishRare
name: bingle fish
description: bingle
components:
- type: Sprite
sprite: _Goobstation/Objects/Specific/Fishing/fish.rsi
state: bingle
- type: Item
sprite: _Goobstation/Objects/Specific/Fishing/fish.rsi
- type: entity
id: FishBlob
parent: BaseFish
name: blob fish
description: Unfortuantly, this is not a 5th level bio-hazard threat.
components:
- type: Sprite
sprite: _Goobstation/Objects/Specific/Fishing/fish.rsi
state: blob
- type: Item
sprite: _Goobstation/Objects/Specific/Fishing/fish.rsi
- type: entity
id: FishBlueFintuna
parent: BaseFish
name: blue fintuna
description: It's looking straight to you...
components:
- type: Sprite
sprite: _Goobstation/Objects/Specific/Fishing/fish.rsi
state: blue_fintuna
- type: Item
sprite: _Goobstation/Objects/Specific/Fishing/fish.rsi
- type: entity
id: FishBoyFisher
parent: BaseFishRare
name: boy fisher
description: You like kissing fish, don't you?
components:
- type: Sprite
sprite: _Goobstation/Objects/Specific/Fishing/fish.rsi
state: boy_fisher
- type: Item
sprite: _Goobstation/Objects/Specific/Fishing/fish.rsi
- type: entity
id: FishCat
parent: BaseFish
name: cat fish
description: Ironically this looks nothing like a cat. And it doesn't meow.
components:
- type: Sprite
sprite: _Goobstation/Objects/Specific/Fishing/fish.rsi
state: catfish
- type: Item
sprite: _Goobstation/Objects/Specific/Fishing/fish.rsi
- type: entity
id: FishTropicalClown
parent: BaseFish
name: clown clown fish
description: That is almost certanly some kind of joke.
components:
- type: Sprite
sprite: _Goobstation/Objects/Specific/Fishing/fish.rsi
state: clown
- type: Item
sprite: _Goobstation/Objects/Specific/Fishing/fish.rsi
- type: Bloodstream
bloodMaxVolume: 20
bloodReagent: Laughter
- type: entity
id: FishNukeDisk
parent: BaseFishRare
name: nuke disk fish
description: Erm, Actually, it's a bad idea trying to fit a fish inside a nuclear explosion device.
components:
- type: Sprite
sprite: _Goobstation/Objects/Specific/Fishing/fish.rsi
state: disk
- type: Item
sprite: _Goobstation/Objects/Specific/Fishing/fish.rsi
- type: entity
id: FishGib
parent: BaseFishRare
name: gib stick fish
description: "My face when i open a present and get gibstick:"
components:
- type: Sprite
sprite: _Goobstation/Objects/Specific/Fishing/fish.rsi
state: gib
- type: Item
sprite: _Goobstation/Objects/Specific/Fishing/fish.rsi
- type: entity
id: FishMutant
parent: BaseFishRare
name: mutant fish
description: Anomalous fish that looks like it ate some nuclear waste.
components:
- type: Sprite
sprite: _Goobstation/Objects/Specific/Fishing/fish.rsi
state: mutant
- type: Item
sprite: _Goobstation/Objects/Specific/Fishing/fish.rsi
- type: entity
id: FishNuker
parent: BaseFishRare
name: nuclear operative fish
description: Predator of the nuke disk fish.
components:
- type: Sprite
sprite: _Goobstation/Objects/Specific/Fishing/fish.rsi
state: nuker
- type: Item
sprite: _Goobstation/Objects/Specific/Fishing/fish.rsi
- type: entity
id: FishPuffer
parent: BaseFish
name: puffer fish
description: It is poisonous... in many ways.
components:
- type: Sprite
sprite: _Goobstation/Objects/Specific/Fishing/fish.rsi
state: pufferfish
- type: Item
sprite: _Goobstation/Objects/Specific/Fishing/fish.rsi
- type: entity
id: FishSilver
parent: BaseFish
name: silver fish
description: Actually contains silver.
components:
- type: Sprite
sprite: _Goobstation/Objects/Specific/Fishing/fish.rsi
state: silverfish
- type: Item
sprite: _Goobstation/Objects/Specific/Fishing/fish.rsi
- type: entity
id: FishTropicalSun
parent: BaseFish
name: tropical sun fish
description: It probably looked at the sun for too long...?
components:
- type: Sprite
sprite: _Goobstation/Objects/Specific/Fishing/fish.rsi
state: sun_tropical
- type: Item
sprite: _Goobstation/Objects/Specific/Fishing/fish.rsi
- type: entity
id: FishToxic
parent: BaseFishRare
name: toxic waste fish
description: "Looking at this fish, you understand how much it was a bad idea to allow nuclear waste to be dumped into rivers."
components:
- type: Sprite
sprite: _Goobstation/Objects/Specific/Fishing/fish.rsi
state: toxic
- type: Item
sprite: _Goobstation/Objects/Specific/Fishing/fish.rsi
- type: entity
id: FishTropical
parent: BaseFish
name: clown fish
description: This fish is always searching for something... Or maybe someone?
components:
- type: Sprite
sprite: _Goobstation/Objects/Specific/Fishing/fish.rsi
state: tropical
- type: Item
sprite: _Goobstation/Objects/Specific/Fishing/fish.rsi
- type: Bloodstream
bloodMaxVolume: 10
bloodReagent: Laughter
- type: entity
id: FishIan
parent: BaseFishRare
name: ian fish
description: Water-born ian from the sea.
components:
- type: Sprite
sprite: _Goobstation/Objects/Specific/Fishing/fish.rsi
state: ian
- type: Item
sprite: _Goobstation/Objects/Specific/Fishing/fish.rsi

View File

@ -0,0 +1,49 @@
# 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
- type: entity
id: BaseFishingSpot
abstract: true
name: suspicious waves
description: Something is floating here...
placement:
mode: SnapgridCenter
components:
- type: FishingSpot
fishList: !type:NestedSelector
tableId: WaterFishingLootTable
fishDefaultTimer: 25.0
fishTimerVariety: 15.0
- type: Physics
bodyType: Static
- type: Fixtures
fixtures:
fix1:
shape:
!type:PhysShapeAabb
bounds: "-0.25,-0.25,0.25,0.25"
layer:
- ItemMask
mask:
- HighImpassable
density: 1000
hard: false
- type: Transform
anchored: true
noRot: true
- type: Clickable
- type: InteractionOutline
# TODO: change visuals when it's active
- type: Sprite
sprite: _Goobstation/Objects/Specific/Fishing/fishing_spot.rsi
state: water
- type: Icon
sprite: _Goobstation/Objects/Specific/Fishing/fishing_spot.rsi
state: water
- type: entity
id: FishingSpotWater
parent: BaseFishingSpot

View File

@ -0,0 +1,109 @@
# SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
# SPDX-FileCopyrightText: 2025 Roudenn <romabond091@gmail.com>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
# This should be on every other fishing table
- type: entityTable
id: BasicFishingLootTable
table: !type:GroupSelector
children:
# 75% chance of scrap of some kind
- !type:GroupSelector
weight: 75
children:
- !type:NestedSelector
tableId: SalvageScrapLowValue
weight: 65
- !type:NestedSelector
tableId: SalvageScrapHighValue
weight: 35
# 15% chance of some trash
- !type:NestedSelector
tableId: GenericTrashItems
weight: 15
# 10% chance of low-value treasure or maintenance tools
- !type:GroupSelector
weight: 10
children:
- !type:NestedSelector
tableId: SalvageTreasureCommon
- !type:NestedSelector
tableId: MaintToolsTable
# All types of the rare and unique fishes
- type: entityTable
id: RareFishTable
table: !type:GroupSelector
children:
- id: FishAlien
- id: FishBingle
- id: FishBoyFisher
- id: FishNukeDisk
- id: FishGib
- id: FishMutant
- id: FishNuker
- id: FishToxic
- id: FishIan
# Fish from water
- type: entityTable
id: WaterFishTable
table: !type:GroupSelector
children:
- id: FishAngler
- id: FishBass
- id: FishBlob
- id: FishBlueFintuna
- id: FishCat
- id: FishTropicalClown
- id: FishPuffer
- id: FishSilver
- id: FishTropicalSun
- id: FishTropical
# Items from Water
- type: entityTable
id: WaterFishingLootTable
table: !type:GroupSelector
children:
- !type:NestedSelector
tableId: BasicFishingLootTable
weight: 70
- !type:NestedSelector
tableId: WaterFishTable
weight: 20
- !type:NestedSelector
tableId: RareFishTable
weight: 10
# Items from Lava
- type: entityTable
id: LavaFishingLootTable
table: !type:GroupSelector
children:
# TODO: lava/plasma fish
- !type:NestedSelector
tableId: BasicFishingLootTable
# Items from Plasma
- type: entityTable
id: PlasmaFishingLootTable
table: !type:GroupSelector
children:
# TODO: lava/plasma fish
- !type:NestedSelector
tableId: BasicFishingLootTable
# Items from Toilets
- type: entityTable
id: ToiletFishingLootTable
table: !type:GroupSelector
children:
# like water but no rare fishes
- !type:NestedSelector
tableId: BasicFishingLootTable
weight: 80
- !type:NestedSelector
tableId: WaterFishTable
weight: 20

View File

@ -0,0 +1,117 @@
# 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
# Basic rod
- type: entity
id: FishingRod
parent: BaseItem
name: fishing rod
description: It's time to go fishing!
components:
- type: Sprite
sprite: _Goobstation/Objects/Specific/Fishing/fishing_rod.rsi
state: icon
- type: Item
sprite: _Goobstation/Objects/Specific/Fishing/fishing_rod.rsi
size: Normal
- type: UseDelay # Just for visuals and to prevent autoclickers
delay: 0.08 # 12,5 CPS at max
- type: MeleeWeapon
wideAnimationRotation: 45
attackRate: 1.0
damage:
types:
Piercing: 4
- type: FishingRod
# Variation of a normal rod
- type: entity
id: FishingRodGoon
suffix: Goon
parent: FishingRod
name: fishing rod
description: It's time to go fishing!
components:
- type: Sprite
sprite: _Goobstation/Objects/Specific/Fishing/goon_rod.rsi
state: icon
- type: Item
sprite: _Goobstation/Objects/Specific/Fishing/goon_rod.rsi
size: Normal
# Makeshift rod
- type: entity
id: FishingRodMakeshift
parent: FishingRod
name: makeshift fishing rod
description: Probably would be hard to catch a fish using that.
components:
- type: Sprite
sprite: _Goobstation/Objects/Specific/Fishing/makeshift_rod.rsi
state: icon
- type: Item
sprite: _Goobstation/Objects/Specific/Fishing/makeshift_rod.rsi
size: Normal
- type: FishingRod
floatPrototype: FishingLureMakeshift
efficiency: 0.8
- type: Construction
graph: FishingRodMakeshift
node: makeshiftRod
# Golden rod
- type: entity
id: FishingRodGolden
parent: FishingRod
name: golden fishing rod
description: Finally, you caught 250 fishes. Here's your trophey.
components:
- type: Sprite
sprite: _Goobstation/Objects/Specific/Fishing/golden_rod.rsi
state: icon
- type: Item
sprite: _Goobstation/Objects/Specific/Fishing/golden_rod.rsi
size: Normal
- type: FishingRod
efficiency: 1.3
- type: MeleeWeapon
wideAnimationRotation: 45
attackRate: 1.0
damage:
types:
Piercing: 10
# Normal fishing lure
- type: entity
id: FishingLure
parent: BaseItem
name: fishing lure
description: fish come here
categories: [ HideSpawnMenu ]
components:
- type: Sprite
noRot: true
sprite: _Goobstation/Objects/Specific/Fishing/fishing_lure.rsi
state: icon
- type: Item
sprite: _Goobstation/Objects/Specific/Fishing/fishing_lure.rsi
size: Normal
- type: FishingLure
- type: entity
id: FishingLureMakeshift
parent: FishingLure
name: fishing lure
description: fish come here
categories: [ HideSpawnMenu ]
components:
- type: Sprite
noRot: true
sprite: _Goobstation/Objects/Specific/Fishing/makeshift_lure.rsi
state: icon
- type: Item
sprite: _Goobstation/Objects/Specific/Fishing/makeshift_lure.rsi
size: Normal

View File

@ -0,0 +1,21 @@
# SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
# SPDX-FileCopyrightText: 2025 Roudenn <romabond091@gmail.com>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
- type: constructionGraph
id: FishingRodMakeshift
start: start
graph:
- node: start
edges:
- to: makeshiftRod
steps:
- material: Cloth
amount: 3
doAfter: 2
- material: WoodPlank
amount: 5
doAfter: 2
- node: makeshiftRod
entity: FishingRodMakeshift

View File

@ -0,0 +1,12 @@
# SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
# SPDX-FileCopyrightText: 2025 Roudenn <romabond091@gmail.com>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
- type: construction
id: FishingRodMakeshift
graph: FishingRodMakeshift
startNode: start
targetNode: makeshiftRod
category: construction-category-misc
objectType: Item

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 B

View File

@ -0,0 +1,14 @@
{
"version": 1,
"size": {
"x": 8,
"y": 32
},
"license": "CC-BY-SA-3.0",
"copyright": "https://github.com/tgstation/tgstation/blob/886ca0f8dddf83ecaf10c92ff106172722352192/icons/effects/progessbar.dmi, resized and edited by rouden_ (discord)",
"states": [
{
"name": "icon"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 676 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 783 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 813 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 638 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 687 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 748 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 511 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 575 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 B

View File

@ -0,0 +1,95 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "bingle, boy_fisher, gib, disk, nuker, pufferfish by arraydeess (discord), clown, tropical, mutant, alien, bluefintuna, blob, toxic by imasleeping (discord), ian fish by latibulate_ (discord)",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "alien"
},
{
"name": "angler"
},
{
"name": "bass"
},
{
"name": "bingle"
},
{
"name": "blob"
},
{
"name": "blue_fintuna"
},
{
"name": "boy_fisher",
"delays": [
[
0.1,
0.1,
0.1,
0.1,
0.1
]
]
},
{
"name": "catfish"
},
{
"name": "clown"
},
{
"name": "disk",
"delays": [
[
0.1,
0.1,
0.1,
0.1
]
]
},
{
"name": "gib",
"delays": [
[
0.1,
0.1,
0.1,
0.1,
0.1,
0.1
]
]
},
{
"name": "mutant"
},
{
"name": "nuker"
},
{
"name": "pufferfish"
},
{
"name": "silverfish"
},
{
"name": "sun_tropical"
},
{
"name": "toxic"
},
{
"name": "tropical"
},
{
"name": "ian"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 845 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 694 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 B

View File

@ -0,0 +1,17 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "Sprites by arraydeess (discord)",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "icon"
},
{
"name": "rope"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 736 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 623 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 435 B

View File

@ -0,0 +1,33 @@
{
"version": 1,
"license": "CC-BY-NC-SA-3.0",
"copyright": "Goonstation, taken at commit 39ddf6bbd54c9f27fe49f5cea1a3119738b09596",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "inhand-left",
"directions": 4
},
{
"name": "inhand-left-active",
"directions": 4
},
{
"name": "inhand-right",
"directions": 4
},
{
"name": "inhand-right-active",
"directions": 4
},
{
"name": "icon"
},
{
"name": "icon-active"
}
]
}

View File

@ -0,0 +1,21 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "water by Rouge2t7 on Discord",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "water",
"delays": [
[
0.25,
0.25,
0.25
]
]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 463 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 B

View File

@ -0,0 +1,33 @@
{
"version": 1,
"license": "CC-BY-NC-SA-3.0",
"copyright": "Goonstation, taken at commit 39ddf6bbd54c9f27fe49f5cea1a3119738b09596, modified by rouden_ (discord)",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "inhand-left",
"directions": 4
},
{
"name": "inhand-left-active",
"directions": 4
},
{
"name": "inhand-right",
"directions": 4
},
{
"name": "inhand-right-active",
"directions": 4
},
{
"name": "icon"
},
{
"name": "icon-active"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 591 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 559 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 435 B

View File

@ -0,0 +1,33 @@
{
"version": 1,
"license": "CC-BY-NC-SA-3.0",
"copyright": "Goonstation, taken at commit 39ddf6bbd54c9f27fe49f5cea1a3119738b09596",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "inhand-left",
"directions": 4
},
{
"name": "inhand-left-active",
"directions": 4
},
{
"name": "inhand-right",
"directions": 4
},
{
"name": "inhand-right-active",
"directions": 4
},
{
"name": "icon"
},
{
"name": "icon-active"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

View File

@ -0,0 +1,17 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "Sprites by arraydeess (discord)",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "icon"
},
{
"name": "rope"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 430 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 B

View File

@ -0,0 +1,33 @@
{
"version": 1,
"license": "CC-BY-NC-SA-3.0",
"copyright": "Sprites by arraydeess (discord) and rouden_ (discord)",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "inhand-left",
"directions": 4
},
{
"name": "inhand-left-active",
"directions": 4
},
{
"name": "inhand-right",
"directions": 4
},
{
"name": "inhand-right-active",
"directions": 4
},
{
"name": "icon"
},
{
"name": "icon-active"
}
]
}