--- /dev/null
+using Content.Client.Chemistry.Visualizers;
+
+namespace Content.Client.Storage.Components;
+
+/// <summary>
+/// Essentially a version of <see cref="SolutionContainerVisualsComponent"/> fill level handling but for item storage.
+/// Depending on the fraction of storage that's filled, will change the sprite at <see cref="FillLayer"/> to the nearest
+/// fill level, up to <see cref="MaxFillLevels"/>.
+/// </summary>
+[RegisterComponent]
+public sealed partial class StorageContainerVisualsComponent : Component
+{
+ [DataField("maxFillLevels")]
+ public int MaxFillLevels = 0;
+
+ /// <summary>
+ /// A prefix to use for the fill states., i.e. {FillBaseName}{fill level} for the state
+ /// </summary>
+ [DataField("fillBaseName")]
+ public string? FillBaseName;
+
+ [DataField("layer")]
+ public StorageContainerVisualLayers FillLayer = StorageContainerVisualLayers.Fill;
+}
+
+public enum StorageContainerVisualLayers : byte
+{
+ Fill
+}
--- /dev/null
+using Content.Client.Storage.Components;
+using Content.Shared.Rounding;
+using Content.Shared.Storage;
+using Robust.Client.GameObjects;
+
+namespace Content.Client.Storage.Systems;
+
+/// <inheritdoc cref="StorageContainerVisualsComponent"/>
+public sealed class StorageContainerVisualsSystem : VisualizerSystem<StorageContainerVisualsComponent>
+{
+ protected override void OnAppearanceChange(EntityUid uid, StorageContainerVisualsComponent component, ref AppearanceChangeEvent args)
+ {
+ if (args.Sprite == null)
+ return;
+
+ if (!AppearanceSystem.TryGetData<int>(uid, StorageVisuals.StorageUsed, out var used, args.Component))
+ return;
+
+ if (!AppearanceSystem.TryGetData<int>(uid, StorageVisuals.Capacity, out var capacity, args.Component))
+ return;
+
+ var fraction = used / (float) capacity;
+
+ if (!args.Sprite.LayerMapTryGet(component.FillLayer, out var fillLayer))
+ return;
+
+ var closestFillSprite = Math.Min(ContentHelpers.RoundToNearestLevels(fraction, 1, component.MaxFillLevels + 1),
+ component.MaxFillLevels);
+
+ if (closestFillSprite > 0)
+ {
+ if (component.FillBaseName == null)
+ return;
+
+ args.Sprite.LayerSetVisible(fillLayer, true);
+ var stateName = component.FillBaseName + closestFillSprite;
+ args.Sprite.LayerSetState(fillLayer, stateName);
+ }
+ else
+ {
+ args.Sprite.LayerSetVisible(fillLayer, false);
+ }
+ }
+}
-using Content.Server.Storage.EntitySystems;
+using Content.Server.Storage.Components;
+using Content.Server.Storage.EntitySystems;
using Content.Shared.Item;
using Content.Shared.Stacks;
using Content.Shared.Storage;
if (!Container.TryGetContainingContainer(uid, out var container) ||
!TryComp<StorageComponent>(container.Owner, out var storage))
- {
return;
- }
- _storage.RecalculateStorageUsed(storage);
+ _storage.RecalculateStorageUsed(container.Owner, storage);
_storage.UpdateUI(container.Owner, storage);
}
}
using Robust.Server.GameObjects;
using Robust.Shared.Player;
using Robust.Shared.Physics.Events;
-using Content.Shared.Effects;
namespace Content.Server.Projectiles;
private void OnStartCollide(EntityUid uid, ProjectileComponent component, ref StartCollideEvent args)
{
// This is so entities that shouldn't get a collision are ignored.
- if (args.OurFixtureId != ProjectileFixture || !args.OtherFixture.Hard || component.DamagedEntity)
+ if (args.OurFixtureId != ProjectileFixture || !args.OtherFixture.Hard
+ || component.DamagedEntity || component is { Weapon: null, OnlyCollideWhenShot: true })
return;
var target = args.OtherEntity;
_adminLogger.Add(LogType.BulletHit,
HasComp<ActorComponent>(target) ? LogImpact.Extreme : LogImpact.High,
- $"Projectile {ToPrettyString(uid):projectile} shot by {ToPrettyString(component.Shooter):user} hit {otherName:target} and dealt {modifiedDamage.Total:damage} damage");
+ $"Projectile {ToPrettyString(uid):projectile} shot by {ToPrettyString(component.Shooter!.Value):user} hit {otherName:target} and dealt {modifiedDamage.Total:damage} damage");
}
if (!deleted)
[ViewVariables(VVAccess.ReadWrite), DataField("removalTime"), AutoNetworkedField]
public float? RemovalTime = 3f;
+ /// <summary>
+ /// Whether this entity will embed when thrown, or only when shot as a projectile.
+ /// </summary>
+ [ViewVariables(VVAccess.ReadWrite), DataField("embedOnThrow"), AutoNetworkedField]
+ public bool EmbedOnThrow = true;
+
/// <summary>
/// How far into the entity should we offset (0 is wherever we collided).
/// </summary>
/// <summary>
/// User that shot this projectile.
/// </summary>
- [DataField("shooter"), AutoNetworkedField] public EntityUid Shooter;
+ [DataField("shooter"), AutoNetworkedField]
+ public EntityUid? Shooter;
/// <summary>
/// Weapon used to shoot.
/// </summary>
[DataField("weapon"), AutoNetworkedField]
- public EntityUid Weapon;
+ public EntityUid? Weapon;
[DataField("ignoreShooter"), AutoNetworkedField]
public bool IgnoreShooter = true;
[DataField("soundForce")]
public bool ForceSound = false;
+ /// <summary>
+ /// Whether this projectile will only collide with entities if it was shot from a gun (if <see cref="Weapon"/> is not null)
+ /// </summary>
+ [DataField("onlyCollideWhenShot")]
+ public bool OnlyCollideWhenShot = false;
+
+ /// <summary>
+ /// Whether this projectile has already damaged an entity.
+ /// </summary>
public bool DamagedEntity;
}
_physics.SetBodyType(uid, BodyType.Dynamic, body: physics, xform: xform);
_transform.AttachToGridOrMap(uid, xform);
+ // Reset whether the projectile has damaged anything if it successfully was removed
+ if (TryComp<ProjectileComponent>(uid, out var projectile))
+ {
+ projectile.Shooter = null;
+ projectile.Weapon = null;
+ projectile.DamagedEntity = false;
+ }
+
// Land it just coz uhhh yeah
var landEv = new LandEvent(args.User, true);
RaiseLocalEvent(uid, ref landEv);
private void OnEmbedThrowDoHit(EntityUid uid, EmbeddableProjectileComponent component, ThrowDoHitEvent args)
{
+ if (!component.EmbedOnThrow)
+ return;
+
Embed(uid, args.Target, component);
}
// Raise a specific event for projectiles.
if (TryComp<ProjectileComponent>(uid, out var projectile))
{
- var ev = new ProjectileEmbedEvent(projectile.Shooter, projectile.Weapon, args.Target);
+ var ev = new ProjectileEmbedEvent(projectile.Shooter!.Value, projectile.Weapon!.Value, args.Target);
RaiseLocalEvent(uid, ref ev);
}
}
[DataField("containerWhitelist")]
public HashSet<string>? ContainerWhitelist;
- public readonly List<string> SpriteLayers = new();
+ /// <summary>
+ /// The list of map layer keys that are valid targets for changing in <see cref="MapLayers"/>
+ /// Can be initialized if already existing on the sprite, or inferred automatically
+ /// </summary>
+ [DataField("spriteLayers")]
+ public List<string> SpriteLayers = new();
}
}
if (component.Container == default)
return;
- RecalculateStorageUsed(component);
+ RecalculateStorageUsed(uid, component);
UpdateStorageVisualization(uid, component);
UpdateUI(uid, component);
Dirty(uid, component);
_appearance.SetData(uid, StackVisuals.Hide, !storageComp.IsUiOpen);
}
- public void RecalculateStorageUsed(StorageComponent storageComp)
+ public void RecalculateStorageUsed(EntityUid uid, StorageComponent storageComp)
{
storageComp.StorageUsed = 0;
var size = itemComp.Size;
storageComp.StorageUsed += size;
}
+
+ _appearance.SetData(uid, StorageVisuals.StorageUsed, storageComp.StorageUsed);
+ _appearance.SetData(uid, StorageVisuals.Capacity, storageComp.StorageCapacityMax);
}
public int GetAvailableSpace(EntityUid uid, StorageComponent? component = null)
Open,
HasContents,
CanLock,
- Locked
+ Locked,
+ StorageUsed,
+ Capacity
}
}
return;
}
- if (projectileQuery.HasComponent(uid))
+ // Allow throwing if this projectile only acts as a projectile when shot, otherwise disallow
+ if (projectileQuery.TryGetComponent(uid, out var proj) && !proj.OnlyCollideWhenShot)
return;
var comp = EnsureComp<ThrownItemComponent>(uid);
component.Amount <= 0 ||
component.Whitelist?.IsValid(args.OtherEntity, EntityManager) == false ||
!TryComp<ProjectileComponent>(uid, out var projectile) ||
- !projectile.Weapon.IsValid())
+ projectile.Weapon == null)
{
return;
}
// Markers are exclusive, deal with it.
var marker = EnsureComp<DamageMarkerComponent>(args.OtherEntity);
marker.Damage = new DamageSpecifier(component.Damage);
- marker.Marker = projectile.Weapon;
+ marker.Marker = projectile.Weapon.Value;
marker.EndTime = _timing.CurTime + component.Duration;
component.Amount--;
Dirty(marker);
if (Resolve(projectile, ref projectileComp, false))
{
- _adminLogger.Add(LogType.BulletHit, LogImpact.Medium, $"{ToPrettyString(user)} reflected {ToPrettyString(projectile)} from {ToPrettyString(projectileComp.Weapon)} shot by {projectileComp.Shooter}");
+ _adminLogger.Add(LogType.BulletHit, LogImpact.Medium, $"{ToPrettyString(user)} reflected {ToPrettyString(projectile)} from {ToPrettyString(projectileComp.Weapon!.Value)} shot by {projectileComp.Shooter!.Value}");
projectileComp.Shooter = user;
projectileComp.Weapon = user;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
namespace Content.Shared.Wieldable.Components;
[DataField("wieldTime")]
public float WieldTime = 1.5f;
}
+
+[Serializable, NetSerializable]
+public enum WieldableVisuals : byte
+{
+ Wielded
+}
[Dependency] private readonly SharedItemSystem _itemSystem = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
+ [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
public override void Initialize()
{
var ev = new ItemWieldedEvent();
RaiseLocalEvent(uid, ref ev);
+ _appearance.SetData(uid, WieldableVisuals.Wielded, true);
Dirty(component);
args.Handled = true;
("user", args.User.Value), ("item", uid)), args.User.Value, Filter.PvsExcept(args.User.Value), true);
}
+ _appearance.SetData(uid, WieldableVisuals.Wielded, false);
+
Dirty(component);
_virtualItemSystem.DeleteInHandsMatching(args.User.Value, uid);
}
license: "CC-BY-4.0"
copyright: "User volivieri on freesound.org. Modified by Velcroboy on github."
source: "https://freesound.org/people/volivieri/sounds/37190/"
+
+- files: ["bow_pull.ogg"]
+ license: "CC-BY-3.0"
+ copyright: "User jzdnvdoosj on freesound.org. Converted to ogg by mirrorcult"
+ source: "https://freesound.org/people/jzdnvdoosj/sounds/626262/"
\ No newline at end of file
--- /dev/null
+- files: ["arrow_nock.ogg"]
+ license: "CC-BY-NC-4.0"
+ copyright: "Created by LiamG_SFX, converted to ogg by mirrorcult"
+ source: "https://freesound.org/people/LiamG_SFX/sounds/322224/"
\ No newline at end of file
prob: 0.2
- id: ClothingHandsGlovesColorYellow
prob: 0.05
+ - id: ClothingBeltQuiver
+ prob: 0.02
- id: ClothingBeltUtility
prob: 0.10
- id: ClothingHeadHatCone
--- /dev/null
+- type: entity
+ parent: ClothingBeltStorageBase
+ id: ClothingBeltQuiver
+ name: quiver
+ description: Can hold up to 15 arrows, and fits snug around your waist.
+ components:
+ - type: Sprite
+ sprite: Clothing/Belt/quiver.rsi
+ layers:
+ - state: icon
+ - map: [ "enum.StorageContainerVisualLayers.Fill" ]
+ visible: false
+ - type: Clothing
+ - type: Storage
+ capacity: 150
+ whitelist:
+ tags:
+ - Arrow
+ - type: Appearance
+ - type: StorageContainerVisuals
+ maxFillLevels: 3
+ fillBaseName: fill-
--- /dev/null
+- type: entity
+ name: bow
+ parent: BaseItem
+ id: BaseBow
+ description: The original rooty tooty point and shooty.
+ abstract: true
+ components:
+ - type: Sprite
+ sprite: Objects/Weapons/Guns/Bow/bow.rsi
+ - type: Item
+ size: 60
+ - type: Clothing
+ quickEquip: false
+ slots:
+ - Back
+ - type: Wieldable
+ wieldTime: 0.5
+ wieldSound:
+ path: /Audio/Items/bow_pull.ogg
+ - type: GunRequiresWield
+ - type: Gun
+ minAngle: 0
+ maxAngle: 5
+ fireRate: 1
+ selectedMode: SemiAuto
+ availableModes:
+ - SemiAuto
+ soundGunshot:
+ collection: BulletMiss
+ soundEmpty: null
+ - type: ItemSlots
+ slots:
+ arrow:
+ name: Arrow
+ startingItem: null
+ insertSound: /Audio/Weapons/Guns/Misc/arrow_nock.ogg
+ whitelist:
+ tags:
+ - Arrow
+ - type: ContainerContainer
+ containers:
+ arrow: !type:ContainerSlot
+ - type: ContainerAmmoProvider
+ container: arrow
+
+- type: entity
+ id: BowImprovised
+ parent: BaseBow
+ components:
+ - type: Sprite
+ layers:
+ - state: unwielded
+ map: [ base ]
+ - state: unwielded-arrow
+ map: [ arrow ]
+ visible: false
+ # to elucidate whats intended here:
+ # arrow is inserted -> ItemMapper sets layer with map `arrow` to visible
+ # bow is wielded -> generic vis sets states of layers with map `arrow` and `base`
+ # arrow is removed -> itemmapper sets layer with map `arrow` to invisible
+ - type: Appearance
+ - type: ItemMapper
+ spriteLayers:
+ - arrow
+ mapLayers:
+ arrow:
+ whitelist:
+ tags:
+ - Arrow
+ - type: GenericVisualizer
+ visuals:
+ enum.WieldableVisuals.Wielded:
+ arrow:
+ True: { state: wielded-arrow }
+ False: { state: unwielded-arrow }
+ base:
+ True: { state: wielded }
+ False: { state: unwielded }
+ - type: Construction
+ graph: ImprovisedBow
+ node: ImprovisedBow
--- /dev/null
+- type: entity
+ parent: BaseItem
+ id: BaseArrow
+ abstract: true
+ components:
+ - type: Item
+ size: 10
+ - type: Sprite
+ sprite: Objects/Weapons/Guns/Projectiles/arrows.rsi
+ - type: Fixtures
+ fixtures:
+ fix1:
+ shape: !type:PhysShapeCircle
+ radius: 0.2
+ density: 5
+ mask:
+ - ItemMask
+ restitution: 0.3
+ friction: 0.2
+ projectile:
+ shape:
+ !type:PhysShapeAabb
+ bounds: "-0.1,-0.1,0.1,0.1"
+ hard: false
+ mask:
+ - Impassable
+ - BulletImpassable
+ - type: EmbeddableProjectile
+ sound: /Audio/Weapons/star_hit.ogg
+ embedOnThrow: false
+ - type: ThrowingAngle
+ angle: 0
+ - type: Ammo
+ muzzleFlash: null
+ - type: Tag
+ tags:
+ - Arrow
+ - type: Projectile
+ deleteOnCollide: false
+ onlyCollideWhenShot: true
+ damage:
+ types:
+ Piercing: 25
+
+- type: entity
+ parent: BaseArrow
+ id: ArrowRegular
+ name: arrow
+ description: You can feel the power of the steppe within you.
+ components:
+ - type: Sprite
+ layers:
+ - state: tail
+ color: red
+ - state: rod
+ color: brown
+ - state: tip
+ - type: Projectile
+ damage:
+ types:
+ Piercing: 35
+
+- type: entity
+ parent: BaseArrow
+ id: ArrowImprovised
+ name: glass shard arrow
+ description: The greyshirt's preferred projectile.
+ components:
+ - type: Sprite
+ sprite: Objects/Weapons/Guns/Projectiles/arrows.rsi
+ layers:
+ - state: tail
+ color: white
+ - state: rod
+ color: darkgray
+ - state: tip
+ color: lightblue
+ - type: Projectile
+ damage:
+ types:
+ Piercing: 25
+ - type: Construction
+ graph: ImprovisedArrow
+ node: ImprovisedArrow
--- /dev/null
+- type: constructionGraph
+ id: ImprovisedArrow
+ start: start
+ graph:
+ - node: start
+ edges:
+ - to: ImprovisedArrow
+ steps:
+ - material: MetalRod
+ amount: 1
+ doAfter: 0.5
+ - material: Cloth
+ amount: 1
+ doAfter: 0.5
+ - tag: GlassShard
+ name: Glass Shard
+ icon:
+ sprite: Objects/Materials/Shards/shard.rsi
+ state: shard1
+ doAfter: 0.5
+
+ - node: ImprovisedArrow
+ entity: ArrowImprovised
--- /dev/null
+- type: constructionGraph
+ id: ImprovisedBow
+ start: start
+ graph:
+ - node: start
+ edges:
+ - to: ImprovisedBow
+ steps:
+ - material: WoodPlank
+ amount: 10
+ doAfter: 4
+ - material: Cloth
+ amount: 5
+ doAfter: 4
+
+ - node: ImprovisedBow
+ entity: BowImprovised
category: construction-category-weapons
description: A uranium shard with a piece of cloth wrapped around it.
icon: { sprite: Objects/Weapons/Melee/uranium_shiv.rsi, state: icon }
- objectType: Item
+ objectType: Item
- type: construction
name: crude spear
description: Crude and falling apart. Why would you make this?
icon: { sprite: Objects/Weapons/Melee/shields.rsi, state: makeshift-icon }
objectType: Item
+
+- type: construction
+ name: glass shard arrow
+ id: ImprovisedArrow
+ graph: ImprovisedArrow
+ startNode: start
+ targetNode: ImprovisedArrow
+ category: construction-category-weapons
+ description: An arrow tipped with pieces of a glass shard, for use with a bow.
+ icon: { sprite: Objects/Weapons/Guns/Bow/bow.rsi, state: wielded-arrow }
+ objectType: Item
+
+- type: construction
+ name: improvised bow
+ id: ImprovisedBow
+ graph: ImprovisedBow
+ startNode: start
+ targetNode: ImprovisedBow
+ category: construction-category-weapons
+ description: A shoddily constructed bow made out of wood and cloth. It's not much, but it's gotten the job done for millennia.
+ icon: { sprite: Objects/Weapons/Guns/Bow/bow.rsi, state: unwielded }
+ objectType: Item
- type: Tag
id: ArtifactFragment
+- type: Tag
+ id: Arrow
+
- type: Tag
id: ATVKeys
id: Balloon
- type: Tag
- id: BaseballBat
+ id: BaseballBat
- type: Tag
id: BBQsauce
id: CluwneHorn
- type: Tag #Ohioans die happy
- id: Corn
+ id: Corn
- type: Tag
id: Coldsauce
- type: Tag
id: WallmountSubstationElectronics
-
+
- type: Tag
id: WeaponPistolCHIMPUpgradeKit
--- /dev/null
+{
+ "version": 1,
+ "license": "CC-BY-SA-3.0",
+ "copyright": "tgstation at a373b4cb08298523d40acc14f9c390a0c403fc31, modified by mirrorcult",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "icon"
+ },
+ {
+ "name": "equipped-BELT",
+ "directions": 4
+ },
+ {
+ "name": "fill-1"
+ },
+ {
+ "name": "fill-2"
+ },
+ {
+ "name": "fill-3"
+ }
+ ]
+}
\ No newline at end of file
--- /dev/null
+{
+ "version": 1,
+ "license": "CC-BY-SA-3.0",
+ "copyright": "tgstation at a373b4cb08298523d40acc14f9c390a0c403fc31, equipped-BACKPACK and wielded sprites added by mirrorcult",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "unwielded"
+ },
+ {
+ "name": "unwielded-arrow"
+ },
+ {
+ "name": "wielded"
+ },
+ {
+ "name": "wielded-arrow"
+ },
+ {
+ "name": "equipped-BACKPACK",
+ "directions": 4
+ },
+ {
+ "name": "inhand-left",
+ "directions": 4
+ },
+ {
+ "name": "inhand-right",
+ "directions": 4
+ },
+ {
+ "name": "wielded-inhand-left",
+ "directions": 4
+ },
+ {
+ "name": "wielded-inhand-right",
+ "directions": 4
+ }
+ ]
+}
--- /dev/null
+{
+ "version": 1,
+ "license": "CC-BY-SA-3.0",
+ "copyright": "tgstation at a373b4cb08298523d40acc14f9c390a0c403fc31, sprites modified and cut into layers by mirrorcult",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "tail"
+ },
+ {
+ "name": "rod"
+ },
+ {
+ "name": "tip"
+ },
+ {
+ "name": "solution"
+ },
+ {
+ "name": "inhand-left",
+ "directions": 4
+ },
+ {
+ "name": "inhand-right",
+ "directions": 4
+ }
+ ]
+}