# Conflicts: # .github/workflows/publish.yml # Content.Client/Clothing/ClientClothingSystem.cs # Content.Client/Clothing/Systems/ChameleonClothingSystem.cs # Content.Client/Fax/UI/FaxWindow.xaml # Content.Client/Ghost/GhostSystem.cs # Content.Client/Holiday/HolidaySystem.cs # Content.Client/Humanoid/HumanoidAppearanceSystem.cs # Content.Client/Lobby/UI/Loadouts/LoadoutWindow.xaml.cs # Content.Client/Lobby/UI/LobbyGui.xaml # Content.Client/Lobby/UI/LobbyGui.xaml.cs # Content.Client/Mech/MechSystem.cs # Content.Client/Stealth/StealthSystem.cs # Content.Client/Weapons/Ranged/Systems/GunSystem.cs # Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs # Content.Server/Bed/BedSystem.cs # Content.Server/Body/Systems/BodySystem.cs # Content.Server/Body/Systems/RespiratorSystem.cs # Content.Server/Cargo/Systems/CargoSystem.Orders.cs # Content.Server/Communications/CommunicationsConsoleComponent.cs # Content.Server/EntityEffects/Effects/AreaReactionEffect.cs # Content.Server/EntityEffects/Effects/MakeSentient.cs # Content.Server/EntityEffects/Effects/PlantMetabolism/PlantAdjustPests.cs # Content.Server/EntityEffects/Effects/PlantMetabolism/PlantAdjustToxins.cs # Content.Server/EntityEffects/Effects/PlantMetabolism/PlantAdjustWeeds.cs # Content.Server/EntityEffects/Effects/PlantMutateGases.cs # Content.Server/Fluids/EntitySystems/AbsorbentSystem.cs # Content.Server/GameTicking/GameTicker.Spawning.cs # Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs # Content.Server/IoC/ServerContentIoC.cs # Content.Server/Nutrition/EntitySystems/FoodSystem.cs # Content.Server/Power/EntitySystems/PowerNetSystem.cs # Content.Server/Preferences/Managers/ServerPreferencesManager.cs # Content.Server/RoundEnd/RoundEndSystem.cs # Content.Server/Shuttles/Systems/EmergencyShuttleSystem.cs # Content.Server/Station/Systems/StationSpawningSystem.cs # Content.Server/VoiceMask/VoiceMaskSystem.cs # Content.Server/Weapons/Ranged/Systems/GunSystem.cs # Content.Shared/Bed/Components/HealOnBuckleComponent.cs # Content.Shared/Clothing/EntitySystems/ClothingSystem.cs # Content.Shared/EntityEffects/Effects/ExplosionReactionEffect.cs # Content.Shared/Inventory/InventorySystem.Relay.cs # Content.Shared/Item/ItemToggle/Components/ComponentTogglerComponent.cs # Content.Shared/Preferences/HumanoidCharacterProfile.cs # Content.Shared/Preferences/Loadouts/RoleLoadout.cs # Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs # Content.Shared/Wieldable/SharedWieldableSystem.cs # Resources/Audio/Machines/attributions.yml # Resources/Locale/en-US/_strings/atmos/plaque-component.ftl # Resources/Locale/en-US/_strings/chat/chat-channel.ftl # Resources/Locale/en-US/_strings/components/screen-component.ftl # Resources/Locale/en-US/_strings/inventory/components/slot-block-component.ftl # Resources/Locale/en-US/_strings/mass-media/news-discord.ftl # Resources/Locale/en-US/_strings/objectives/commands/stripall.ftl # Resources/Locale/en-US/_strings/salvage/job-board.ftl # Resources/Locale/en-US/_strings/ui/spray-painter-window.ftl # Resources/Locale/en-US/_strings/ui/turret-controls.ftl # Resources/Maps/_Sunrise/Station/box.yml # Resources/Prototypes/Access/misc.yml # Resources/Prototypes/Catalog/Fills/Boxes/emergency.yml # Resources/Prototypes/Catalog/Fills/Lockers/engineer.yml # Resources/Prototypes/Catalog/Fills/Lockers/heads.yml # Resources/Prototypes/Catalog/Fills/Lockers/service.yml # Resources/Prototypes/Catalog/Fills/Lockers/suit_storage.yml # Resources/Prototypes/Entities/Clothing/Head/helmets.yml # Resources/Prototypes/Entities/Clothing/Masks/masks.yml # Resources/Prototypes/Entities/Clothing/OuterClothing/hardsuits.yml # Resources/Prototypes/Entities/Markers/Spawners/mobs.yml # Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml # Resources/Prototypes/Entities/Mobs/NPCs/animals.yml # Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml # Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml # Resources/Prototypes/Entities/Mobs/Player/dragon.yml # Resources/Prototypes/Entities/Mobs/Player/silicon.yml # Resources/Prototypes/Entities/Mobs/Species/arachnid.yml # Resources/Prototypes/Entities/Mobs/Species/moth.yml # Resources/Prototypes/Entities/Objects/Specific/Robotics/borg_modules.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Shotguns/shotguns.yml # Resources/Prototypes/LootTables/suspicion_loot_table.yml # Resources/Prototypes/Reagents/Consumable/Drink/drinks.yml # Resources/Prototypes/Reagents/medicine.yml # Resources/Prototypes/Recipes/Lathes/Packs/science.yml # Resources/Prototypes/Recipes/Lathes/medical.yml # Resources/Prototypes/SoundCollections/NukeMusic.yml # Resources/ServerInfo/Guidebook/Cargo/Salvage.xml # Resources/Textures/Clothing/Hands/Gloves/KnuckleDusters/brassknuckleduster.rsi/equipped-OUTERCLOTHING-reptilian.png # Resources/Textures/Clothing/OuterClothing/Hardsuits/basic.rsi/meta.json # Resources/Textures/Clothing/OuterClothing/Suits/eva.rsi/meta.json # Resources/Textures/Clothing/OuterClothing/Suits/eva_emergency.rsi/meta.json # Resources/Textures/Clothing/OuterClothing/Suits/eva_prisoner.rsi/meta.json # Resources/Textures/Effects/speech.rsi/alien3.png # Resources/Textures/Effects/speech.rsi/alienroyal3.png # Resources/Textures/Effects/speech.rsi/default3.png # Resources/Textures/Effects/speech.rsi/guardian3.png # Resources/Textures/Effects/speech.rsi/holo3.png # Resources/Textures/Effects/speech.rsi/lawyer3.png # Resources/Textures/Effects/speech.rsi/lizard3.png # Resources/Textures/Effects/speech.rsi/meta.json # Resources/Textures/Effects/speech.rsi/moth3.png # Resources/Textures/Effects/speech.rsi/robot3.png # Resources/Textures/Effects/speech.rsi/slime3.png # Resources/Textures/Effects/speech.rsi/spider3.png # Resources/Textures/Effects/speech.rsi/syndibot3.png # Resources/Textures/Interface/Misc/job_icons.rsi/meta.json # Resources/Textures/Mobs/Silicon/chassis.rsi/meta.json # Resources/Textures/Mobs/Species/Reptilian/parts.rsi/head_f.png # Resources/Textures/Mobs/Species/Reptilian/parts.rsi/head_m.png # Resources/Textures/Objects/Misc/id_cards.rsi/meta.json # Resources/Textures/Structures/Doors/Airlocks/Glass/external.rsi/emergency_unlit.png # Resources/Textures/Structures/Piping/disposal.rsi/meta.json # Resources/Textures/Tiles/attributions.yml # Resources/Textures/Tiles/steel_dirty.png # Resources/migration.yml # runclient-Tools.bat # runclient.bat
505 lines
19 KiB
C#
505 lines
19 KiB
C#
using System.Linq;
|
|
using System.Numerics;
|
|
using Content.Client.Animations;
|
|
using Content.Client.Gameplay;
|
|
using Content.Client.Items;
|
|
using Content.Client.Weapons.Ranged.Components;
|
|
using Content.Shared.Camera;
|
|
using Content.Shared.CombatMode;
|
|
using Content.Shared.Mech.Components;
|
|
using Content.Shared.Weapons.Ranged;
|
|
using Content.Shared.Weapons.Ranged.Components;
|
|
using Content.Shared.Weapons.Ranged.Events;
|
|
using Content.Shared.Weapons.Ranged.Systems;
|
|
using Robust.Client.Animations;
|
|
using Robust.Client.ComponentTrees;
|
|
using Robust.Client.GameObjects;
|
|
using Robust.Client.Graphics;
|
|
using Robust.Client.Input;
|
|
using Robust.Client.Player;
|
|
using Robust.Client.State;
|
|
using Robust.Shared.Animations;
|
|
using Robust.Shared.Input;
|
|
using Robust.Shared.Map;
|
|
using Robust.Shared.Map.Components;
|
|
using Robust.Shared.Prototypes;
|
|
using Robust.Shared.Timing;
|
|
using Robust.Shared.Utility;
|
|
using SharedGunSystem = Content.Shared.Weapons.Ranged.Systems.SharedGunSystem;
|
|
using TimedDespawnComponent = Robust.Shared.Spawners.TimedDespawnComponent;
|
|
|
|
namespace Content.Client.Weapons.Ranged.Systems;
|
|
|
|
public sealed partial class GunSystem : SharedGunSystem
|
|
{
|
|
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
|
[Dependency] private readonly IInputManager _inputManager = default!;
|
|
[Dependency] private readonly IPlayerManager _player = default!;
|
|
[Dependency] private readonly IStateManager _state = default!;
|
|
[Dependency] private readonly AnimationPlayerSystem _animPlayer = default!;
|
|
[Dependency] private readonly InputSystem _inputSystem = default!;
|
|
[Dependency] private readonly SharedCameraRecoilSystem _recoil = default!;
|
|
[Dependency] private readonly SharedMapSystem _maps = default!;
|
|
[Dependency] private readonly SharedTransformSystem _xform = default!;
|
|
[Dependency] private readonly SpriteSystem _sprite = default!;
|
|
|
|
[ValidatePrototypeId<EntityPrototype>]
|
|
public const string HitscanProto = "HitscanEffect";
|
|
|
|
[ValidatePrototypeId<EntityPrototype>]
|
|
public const string HitscanTracerProto = "HitscanEffectTracer";
|
|
|
|
public bool SpreadOverlay
|
|
{
|
|
get => _spreadOverlay;
|
|
set
|
|
{
|
|
if (_spreadOverlay == value)
|
|
return;
|
|
|
|
_spreadOverlay = value;
|
|
var overlayManager = IoCManager.Resolve<IOverlayManager>();
|
|
|
|
if (_spreadOverlay)
|
|
{
|
|
overlayManager.AddOverlay(new GunSpreadOverlay(
|
|
EntityManager,
|
|
_eyeManager,
|
|
Timing,
|
|
_inputManager,
|
|
_player,
|
|
this,
|
|
TransformSystem));
|
|
}
|
|
else
|
|
{
|
|
overlayManager.RemoveOverlay<GunSpreadOverlay>();
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool _spreadOverlay;
|
|
|
|
public override void Initialize()
|
|
{
|
|
base.Initialize();
|
|
UpdatesOutsidePrediction = true;
|
|
SubscribeLocalEvent<AmmoCounterComponent, ItemStatusCollectMessage>(OnAmmoCounterCollect);
|
|
SubscribeLocalEvent<AmmoCounterComponent, UpdateClientAmmoEvent>(OnUpdateClientAmmo);
|
|
SubscribeAllEvent<MuzzleFlashEvent>(OnMuzzleFlash);
|
|
|
|
// Plays animated effects on the client.
|
|
SubscribeNetworkEvent<HitscanEvent>(OnHitscan);
|
|
|
|
InitializeMagazineVisuals();
|
|
InitializeSpentAmmo();
|
|
}
|
|
|
|
private void OnUpdateClientAmmo(EntityUid uid, AmmoCounterComponent ammoComp, ref UpdateClientAmmoEvent args)
|
|
{
|
|
UpdateAmmoCount(uid, ammoComp);
|
|
}
|
|
|
|
private void OnMuzzleFlash(MuzzleFlashEvent args)
|
|
{
|
|
var gunUid = GetEntity(args.Uid);
|
|
|
|
CreateEffect(gunUid, args, gunUid);
|
|
}
|
|
|
|
private IEnumerable<EntityCoordinates> GenerateTracerPath(EntityCoordinates from, float distance, Angle angle, float stepDistance = 1f)
|
|
{
|
|
var steps = Math.Max(1, (int)Math.Ceiling(distance / stepDistance));
|
|
|
|
for (var i = 0; i <= steps; i++)
|
|
{
|
|
yield return from.Offset(angle.ToVec() * (distance / steps) * i);
|
|
}
|
|
}
|
|
|
|
private void OnHitscan(HitscanEvent ev)
|
|
{
|
|
const double tracerInterval = 0.01f;
|
|
|
|
foreach (var a in ev.Sprites)
|
|
{
|
|
if (a.sprite is not SpriteSpecifier.Rsi rsi)
|
|
continue;
|
|
|
|
var startCoords = GetCoordinates(a.coordinates);
|
|
|
|
if (!TryComp(startCoords.EntityId, out TransformComponent? relativeXform))
|
|
continue;
|
|
|
|
if (a.effectType == EffectType.Tracer)
|
|
{
|
|
var tracePath = GenerateTracerPath(startCoords, a.distance, a.angle);
|
|
var entityCoordinatesEnumerable = tracePath.ToList();
|
|
|
|
var stepIndex = 0;
|
|
foreach (var stepCoords in entityCoordinatesEnumerable)
|
|
{
|
|
var ent = CreateTracerEffect(stepCoords, a.angle, rsi, relativeXform);
|
|
|
|
var anim = new Animation()
|
|
{
|
|
Length = TimeSpan.FromSeconds(3.00f),
|
|
AnimationTracks =
|
|
{
|
|
new AnimationTrackSpriteFlick()
|
|
{
|
|
LayerKey = EffectLayers.Unshaded,
|
|
KeyFrames =
|
|
{
|
|
new AnimationTrackSpriteFlick.KeyFrame("empty", stepIndex * (float)tracerInterval),
|
|
new AnimationTrackSpriteFlick.KeyFrame(rsi.RsiState, (stepIndex + 1) * (float)tracerInterval),
|
|
new AnimationTrackSpriteFlick.KeyFrame("empty", (stepIndex + 2) * (float)tracerInterval),
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
_animPlayer.Play(ent, anim, "hitscan-effect");
|
|
stepIndex++;
|
|
}
|
|
}
|
|
else if (a.effectType == EffectType.Static)
|
|
{
|
|
CreateStaticEffect(startCoords, a.angle, rsi, a.distance, relativeXform);
|
|
}
|
|
}
|
|
}
|
|
|
|
private EntityUid CreateTracerEffect(EntityCoordinates coords, Angle angle, SpriteSpecifier.Rsi rsi, TransformComponent relativeXform)
|
|
{
|
|
var ent = Spawn(HitscanTracerProto, coords);
|
|
var sprite = Comp<SpriteComponent>(ent);
|
|
|
|
var xform = Transform(ent);
|
|
var targetWorldRot = angle + _xform.GetWorldRotation(relativeXform);
|
|
var delta = targetWorldRot - _xform.GetWorldRotation(xform);
|
|
_xform.SetLocalRotationNoLerp(ent, xform.LocalRotation + delta, xform);
|
|
|
|
sprite[EffectLayers.Unshaded].AutoAnimated = false;
|
|
_sprite.LayerSetSprite((ent, sprite), EffectLayers.Unshaded, rsi);
|
|
_sprite.LayerSetRsiState((ent, sprite), EffectLayers.Unshaded, rsi.RsiState);
|
|
sprite.Scale = new Vector2(1f, 1f);
|
|
sprite[EffectLayers.Unshaded].Visible = true;
|
|
return ent;
|
|
}
|
|
|
|
private void CreateStaticEffect(EntityCoordinates coords, Angle angle, SpriteSpecifier.Rsi rsi, float distance, TransformComponent relativeXform)
|
|
{
|
|
var ent = Spawn(HitscanProto, coords);
|
|
var sprite = Comp<SpriteComponent>(ent);
|
|
var xform = Transform(ent);
|
|
xform.LocalRotation = angle;
|
|
sprite[EffectLayers.Unshaded].AutoAnimated = false;
|
|
_sprite.LayerSetSprite((ent, sprite), EffectLayers.Unshaded, rsi);
|
|
_sprite.LayerSetRsiState((ent, sprite), EffectLayers.Unshaded, rsi.RsiState);
|
|
sprite.Scale = new Vector2(distance, 1f);
|
|
sprite[EffectLayers.Unshaded].Visible = true;
|
|
|
|
var anim = new Animation()
|
|
{
|
|
Length = TimeSpan.FromSeconds(0.48f),
|
|
AnimationTracks =
|
|
{
|
|
new AnimationTrackSpriteFlick()
|
|
{
|
|
LayerKey = EffectLayers.Unshaded,
|
|
KeyFrames =
|
|
{
|
|
new AnimationTrackSpriteFlick.KeyFrame(rsi.RsiState, 0f),
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
_animPlayer.Play(ent, anim, "hitscan-effect");
|
|
}
|
|
|
|
public override void Update(float frameTime)
|
|
{
|
|
if (!Timing.IsFirstTimePredicted)
|
|
return;
|
|
|
|
var entityNull = _player.LocalEntity;
|
|
|
|
if (entityNull == null || !TryComp<CombatModeComponent>(entityNull, out var combat) || !combat.IsInCombatMode)
|
|
return;
|
|
|
|
var entity = entityNull.Value;
|
|
|
|
if (TryComp<MechPilotComponent>(entity, out var mechPilot))
|
|
{
|
|
entity = mechPilot.Mech;
|
|
}
|
|
|
|
if (!TryGetGun(entity, out var gunUid, out var gun))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var useKey = gun.UseKey ? EngineKeyFunctions.Use : EngineKeyFunctions.UseSecondary;
|
|
|
|
if (_inputSystem.CmdStates.GetState(useKey) != BoundKeyState.Down && !gun.BurstActivated)
|
|
{
|
|
if (gun.ShotCounter != 0)
|
|
EntityManager.RaisePredictiveEvent(new RequestStopShootEvent { Gun = GetNetEntity(gunUid) });
|
|
return;
|
|
}
|
|
|
|
if (gun.NextFire > Timing.CurTime)
|
|
return;
|
|
|
|
var mousePos = _eyeManager.PixelToMap(_inputManager.MouseScreenPosition);
|
|
|
|
if (mousePos.MapId == MapId.Nullspace)
|
|
{
|
|
if (gun.ShotCounter != 0)
|
|
EntityManager.RaisePredictiveEvent(new RequestStopShootEvent { Gun = GetNetEntity(gunUid) });
|
|
|
|
return;
|
|
}
|
|
|
|
// Define target coordinates relative to gun entity, so that network latency on moving grids doesn't fuck up the target location.
|
|
var coordinates = TransformSystem.ToCoordinates(entity, mousePos);
|
|
|
|
var targets = new List<NetEntity>();
|
|
if (_state.CurrentState is GameplayStateBase screen)
|
|
{
|
|
var spriteTree = EntityManager.EntitySysManager.GetEntitySystem<SpriteTreeSystem>();
|
|
var entities = spriteTree.QueryAabb(mousePos.MapId, Box2.CenteredAround(mousePos.Position, new Vector2(1.5f, 1.5f)));
|
|
|
|
foreach (var ent in entities)
|
|
{
|
|
targets.Add(GetNetEntity(ent.Uid));
|
|
}
|
|
}
|
|
|
|
Log.Debug($"Sending shoot request tick {Timing.CurTick} / {Timing.CurTime}");
|
|
|
|
EntityManager.RaisePredictiveEvent(new RequestShootEvent
|
|
{
|
|
Targets = targets,
|
|
Coordinates = GetNetCoordinates(coordinates),
|
|
Gun = GetNetEntity(gunUid),
|
|
});
|
|
}
|
|
|
|
public override void Shoot(EntityUid gunUid, GunComponent gun, List<(EntityUid? Entity, IShootable Shootable)> ammo,
|
|
EntityCoordinates fromCoordinates, EntityCoordinates toCoordinates, out bool userImpulse, EntityUid? user = null, bool throwItems = false)
|
|
{
|
|
userImpulse = true;
|
|
|
|
// Rather than splitting client / server for every ammo provider it's easier
|
|
// to just delete the spawned entities. This is for programmer sanity despite the wasted perf.
|
|
// This also means any ammo specific stuff can be grabbed as necessary.
|
|
var direction = TransformSystem.ToMapCoordinates(fromCoordinates).Position - TransformSystem.ToMapCoordinates(toCoordinates).Position;
|
|
var worldAngle = direction.ToAngle().Opposite();
|
|
|
|
foreach (var (ent, shootable) in ammo)
|
|
{
|
|
if (throwItems)
|
|
{
|
|
Recoil(user, direction, gun.CameraRecoilScalarModified);
|
|
if (IsClientSide(ent!.Value))
|
|
Del(ent.Value);
|
|
else
|
|
RemoveShootable(ent.Value);
|
|
continue;
|
|
}
|
|
|
|
switch (shootable)
|
|
{
|
|
case CartridgeAmmoComponent cartridge:
|
|
if (!cartridge.Spent)
|
|
{
|
|
SetCartridgeSpent(ent!.Value, cartridge, true);
|
|
MuzzleFlash(gunUid, cartridge, worldAngle, user);
|
|
Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
|
|
Recoil(user, direction, gun.CameraRecoilScalarModified);
|
|
// TODO: Can't predict entity deletions.
|
|
//if (cartridge.DeleteOnSpawn)
|
|
// Del(cartridge.Owner);
|
|
}
|
|
else
|
|
{
|
|
userImpulse = false;
|
|
Audio.PlayPredicted(gun.SoundEmpty, gunUid, user);
|
|
}
|
|
|
|
if (IsClientSide(ent!.Value))
|
|
Del(ent.Value);
|
|
|
|
break;
|
|
case HitScanCartridgeAmmoComponent hitScanCartridge:
|
|
if (!hitScanCartridge.Spent)
|
|
{
|
|
SetHitscanCartridgeSpent(ent!.Value, hitScanCartridge, true);
|
|
MuzzleFlash(gunUid, hitScanCartridge, worldAngle, user);
|
|
Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
|
|
Recoil(user, direction, gun.CameraRecoilScalarModified);
|
|
// TODO: Can't predict entity deletions.
|
|
//if (cartridge.DeleteOnSpawn)
|
|
// Del(cartridge.Owner);
|
|
}
|
|
else
|
|
{
|
|
userImpulse = false;
|
|
Audio.PlayPredicted(gun.SoundEmpty, gunUid, user);
|
|
}
|
|
|
|
if (IsClientSide(ent!.Value))
|
|
Del(ent.Value);
|
|
|
|
break;
|
|
case AmmoComponent newAmmo:
|
|
MuzzleFlash(gunUid, newAmmo, worldAngle, user);
|
|
Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
|
|
Recoil(user, direction, gun.CameraRecoilScalarModified);
|
|
if (IsClientSide(ent!.Value))
|
|
Del(ent.Value);
|
|
else
|
|
RemoveShootable(ent.Value);
|
|
break;
|
|
case HitscanPrototype:
|
|
Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
|
|
Recoil(user, direction, gun.CameraRecoilScalarModified);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void Recoil(EntityUid? user, Vector2 recoil, float recoilScalar)
|
|
{
|
|
if (!Timing.IsFirstTimePredicted || user == null || recoil == Vector2.Zero || recoilScalar == 0)
|
|
return;
|
|
|
|
_recoil.KickCamera(user.Value, recoil.Normalized() * 0.5f * recoilScalar);
|
|
}
|
|
|
|
protected override void Popup(string message, EntityUid? uid, EntityUid? user)
|
|
{
|
|
if (uid == null || user == null || !Timing.IsFirstTimePredicted)
|
|
return;
|
|
|
|
PopupSystem.PopupEntity(message, uid.Value, user.Value);
|
|
}
|
|
|
|
protected override void CreateEffect(EntityUid gunUid, MuzzleFlashEvent message, EntityUid? tracked = null)
|
|
{
|
|
if (!Timing.IsFirstTimePredicted)
|
|
return;
|
|
|
|
// EntityUid check added to stop throwing exceptions due to https://github.com/space-wizards/space-station-14/issues/28252
|
|
// TODO: Check to see why invalid entities are firing effects.
|
|
if (gunUid == EntityUid.Invalid)
|
|
{
|
|
Log.Debug($"Invalid Entity sent MuzzleFlashEvent (proto: {message.Prototype}, gun: {ToPrettyString(gunUid)})");
|
|
return;
|
|
}
|
|
|
|
var gunXform = Transform(gunUid);
|
|
var gridUid = gunXform.GridUid;
|
|
EntityCoordinates coordinates;
|
|
|
|
if (TryComp(gridUid, out MapGridComponent? mapGrid))
|
|
{
|
|
coordinates = new EntityCoordinates(gridUid.Value, _maps.LocalToGrid(gridUid.Value, mapGrid, gunXform.Coordinates));
|
|
}
|
|
else if (gunXform.MapUid != null)
|
|
{
|
|
coordinates = new EntityCoordinates(gunXform.MapUid.Value, TransformSystem.GetWorldPosition(gunXform));
|
|
}
|
|
else
|
|
{
|
|
return;
|
|
}
|
|
|
|
var ent = Spawn(message.Prototype, coordinates);
|
|
TransformSystem.SetWorldRotationNoLerp(ent, message.Angle);
|
|
|
|
if (tracked != null)
|
|
{
|
|
var track = EnsureComp<TrackUserComponent>(ent);
|
|
track.User = tracked;
|
|
track.Offset = Vector2.UnitX / 2f;
|
|
}
|
|
|
|
var lifetime = 0.4f;
|
|
|
|
if (TryComp<TimedDespawnComponent>(gunUid, out var despawn))
|
|
{
|
|
lifetime = despawn.Lifetime;
|
|
}
|
|
|
|
var anim = new Animation()
|
|
{
|
|
Length = TimeSpan.FromSeconds(lifetime),
|
|
AnimationTracks =
|
|
{
|
|
new AnimationTrackComponentProperty
|
|
{
|
|
ComponentType = typeof(SpriteComponent),
|
|
Property = nameof(SpriteComponent.Color),
|
|
InterpolationMode = AnimationInterpolationMode.Linear,
|
|
KeyFrames =
|
|
{
|
|
new AnimationTrackProperty.KeyFrame(Color.White.WithAlpha(1f), 0),
|
|
new AnimationTrackProperty.KeyFrame(Color.White.WithAlpha(0f), lifetime)
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
_animPlayer.Play(ent, anim, "muzzle-flash");
|
|
if (!TryComp(gunUid, out PointLightComponent? light))
|
|
{
|
|
light = Factory.GetComponent<PointLightComponent>();
|
|
light.NetSyncEnabled = false;
|
|
AddComp(gunUid, light);
|
|
}
|
|
|
|
Lights.SetEnabled(gunUid, true, light);
|
|
Lights.SetRadius(gunUid, 2f, light);
|
|
Lights.SetColor(gunUid, Color.FromHex("#cc8e2b"), light);
|
|
Lights.SetEnergy(gunUid, 5f, light);
|
|
|
|
var animTwo = new Animation()
|
|
{
|
|
Length = TimeSpan.FromSeconds(lifetime),
|
|
AnimationTracks =
|
|
{
|
|
new AnimationTrackComponentProperty
|
|
{
|
|
ComponentType = typeof(PointLightComponent),
|
|
Property = nameof(PointLightComponent.Energy),
|
|
InterpolationMode = AnimationInterpolationMode.Linear,
|
|
KeyFrames =
|
|
{
|
|
new AnimationTrackProperty.KeyFrame(5f, 0),
|
|
new AnimationTrackProperty.KeyFrame(0f, lifetime)
|
|
}
|
|
},
|
|
new AnimationTrackComponentProperty
|
|
{
|
|
ComponentType = typeof(PointLightComponent),
|
|
Property = nameof(PointLightComponent.AnimatedEnable),
|
|
InterpolationMode = AnimationInterpolationMode.Linear,
|
|
KeyFrames =
|
|
{
|
|
new AnimationTrackProperty.KeyFrame(true, 0),
|
|
new AnimationTrackProperty.KeyFrame(false, lifetime)
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
var uidPlayer = EnsureComp<AnimationPlayerComponent>(gunUid);
|
|
|
|
_animPlayer.Stop(gunUid, uidPlayer, "muzzle-flash-light");
|
|
_animPlayer.Play((gunUid, uidPlayer), animTwo, "muzzle-flash-light");
|
|
}
|
|
}
|