From 81e044758f73294176953b00d4a40c0737d36cd7 Mon Sep 17 00:00:00 2001 From: Red <96445749+TheShuEd@users.noreply.github.com> Date: Thu, 26 Jun 2025 13:49:53 +0300 Subject: [PATCH] Magical vision skill (#1467) * fix firewave error spamming * basic magic traces entities * Add magical vision system and mana trace entities Introduces the magical vision mechanic, including the CP14MagicVisionComponent, marker entities, and related systems for tracking and displaying magical traces. Adds new actions, skill integration, localization strings, and icons for magical vision and trace markers. Magic traces are now spawned on spell use and mob state changes, with directional pointers and localized descriptions. * Show time passed for magic vision markers Adds a display of the time elapsed since a magic vision marker was spawned, using a localized string. Updates English and Russian locale files with the new 'cp14-magic-vision-timed-past' entry. * aura imprints * Update critical and death messages for inclusivity Revised the 'critical' and 'dead' messages in both English and Russian locale files to use more inclusive language, replacing references to 'magical creature' with 'someone'. * Move magic vision spawn on mob state change to server Transferred the logic for spawning magic vision markers on mob state changes (Critical/Dead) from CP14SharedMagicVisionSystem to CP14AuraImprintSystem. This centralizes the event handling on the server side. Also increased the duration of magic vision markers for spell usage from 20 to 50 seconds. * Integrate magic vision with visibility mask system Added a CP14MagicVision flag to VisibilityFlags and updated the magic vision and religion systems to use the visibility mask system. Magic vision markers now use the Visibility component, and visibility is refreshed when relevant components are added or removed. Removed client-side toggling logic in favor of server-driven visibility. * drowsiness overlay * noir shader * sfx design --- .../CP14ClientMagicVisionSystem.cs | 159 ++++++++++++++++++ .../MagicVision/CP14MagicVisionNoirOverlay.cs | 33 ++++ .../MagicVision/CP14MagicVisionOverlay.cs | 75 +++++++++ .../AuraImprint/CP14AuraImprintSystem.cs | 69 ++++++++ .../MagicVision/CP14MagicVisionSystem.cs | 54 ++++++ .../_CP14/Religion/CP14ReligionSystem.cs | 3 + Content.Shared/Eye/VisibilityFlags.cs | 3 + .../_CP14/AuraDNA/CP14AuraImprintComponent.cs | 19 +++ .../AuraDNA/CP14SharedAuraImprintSystem.cs | 8 + .../_CP14/MagicSpell/CP14SharedMagicSystem.cs | 23 ++- .../MagicVision/CP14MagicVisionComponent.cs | 11 ++ .../CP14MagicVisionMarkerComponent.cs | 31 ++++ .../CP14SharedMagicVisionSystem.cs | 83 +++++++++ .../Locale/en-US/_CP14/magicVision/vision.ftl | 6 + .../Locale/ru-RU/_CP14/magicVision/vision.ftl | 6 + .../Entities/Actions/Spells/Fire/firewave.yml | 6 + .../Actions/Spells/Meta/mana_vision.yml | 53 ++++++ .../_CP14/Entities/Markers/misc.yml | 3 +- .../_CP14/Entities/Mobs/Species/base.yml | 1 + .../_CP14/Entities/Mobs/Species/carcat.yml | 2 + .../_CP14/Entities/Mobs/Species/dwarf.yml | 2 + .../_CP14/Entities/Mobs/Species/elf.yml | 3 + .../_CP14/Entities/Mobs/Species/goblin.yml | 3 + .../_CP14/Entities/Mobs/Species/silva.yml | 2 + .../_CP14/Entities/Mobs/Species/skeleton.yml | 2 + .../_CP14/Entities/Mobs/Species/tiefling.yml | 2 + .../Prototypes/_CP14/Shaders/shaders.yml | 8 +- .../_CP14/Skill/Basic/metamagic.yml | 14 ++ .../Actions/Spells/meta.rsi/magic_vision.png | Bin 0 -> 355 bytes .../_CP14/Actions/Spells/meta.rsi/meta.json | 3 + .../_CP14/Actions/Spells/misc.rsi/meta.json | 6 + .../_CP14/Actions/Spells/misc.rsi/skull.png | Bin 0 -> 269 bytes .../Actions/Spells/misc.rsi/skull_red.png | Bin 0 -> 302 bytes .../Magic/pointer_sphere.rsi/meta.json | 14 ++ .../Magic/pointer_sphere.rsi/sphere.png | Bin 0 -> 232 bytes .../Textures/_CP14/Shaders/blue_noir.swsl | 43 +++++ 36 files changed, 746 insertions(+), 4 deletions(-) create mode 100644 Content.Client/_CP14/MagicVision/CP14ClientMagicVisionSystem.cs create mode 100644 Content.Client/_CP14/MagicVision/CP14MagicVisionNoirOverlay.cs create mode 100644 Content.Client/_CP14/MagicVision/CP14MagicVisionOverlay.cs create mode 100644 Content.Server/_CP14/AuraImprint/CP14AuraImprintSystem.cs create mode 100644 Content.Server/_CP14/MagicVision/CP14MagicVisionSystem.cs create mode 100644 Content.Shared/_CP14/AuraDNA/CP14AuraImprintComponent.cs create mode 100644 Content.Shared/_CP14/AuraDNA/CP14SharedAuraImprintSystem.cs create mode 100644 Content.Shared/_CP14/MagicVision/CP14MagicVisionComponent.cs create mode 100644 Content.Shared/_CP14/MagicVision/CP14MagicVisionMarkerComponent.cs create mode 100644 Content.Shared/_CP14/MagicVision/CP14SharedMagicVisionSystem.cs create mode 100644 Resources/Locale/en-US/_CP14/magicVision/vision.ftl create mode 100644 Resources/Locale/ru-RU/_CP14/magicVision/vision.ftl create mode 100644 Resources/Prototypes/_CP14/Entities/Actions/Spells/Meta/mana_vision.yml create mode 100644 Resources/Textures/_CP14/Actions/Spells/meta.rsi/magic_vision.png create mode 100644 Resources/Textures/_CP14/Actions/Spells/misc.rsi/skull.png create mode 100644 Resources/Textures/_CP14/Actions/Spells/misc.rsi/skull_red.png create mode 100644 Resources/Textures/_CP14/Effects/Magic/pointer_sphere.rsi/meta.json create mode 100644 Resources/Textures/_CP14/Effects/Magic/pointer_sphere.rsi/sphere.png create mode 100644 Resources/Textures/_CP14/Shaders/blue_noir.swsl diff --git a/Content.Client/_CP14/MagicVision/CP14ClientMagicVisionSystem.cs b/Content.Client/_CP14/MagicVision/CP14ClientMagicVisionSystem.cs new file mode 100644 index 0000000000..7d840f09b4 --- /dev/null +++ b/Content.Client/_CP14/MagicVision/CP14ClientMagicVisionSystem.cs @@ -0,0 +1,159 @@ +using System.Numerics; +using Content.Shared._CP14.MagicVision; +using Content.Shared.Examine; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Client.Player; +using Robust.Client.Timing; +using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Map; +using Robust.Shared.Player; +using Robust.Shared.Utility; + +namespace Content.Client._CP14.MagicVision; + +public sealed class CP14ClientMagicVisionSystem : CP14SharedMagicVisionSystem +{ + [Dependency] private readonly IClientGameTiming _timing = default!; + [Dependency] private readonly SpriteSystem _sprite = default!; + [Dependency] private readonly IPlayerManager _player = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly IOverlayManager _overlayMan = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + + private CP14MagicVisionOverlay? _overlay; + private CP14MagicVisionNoirOverlay? _overlay2; + + private TimeSpan _nextUpdate = TimeSpan.Zero; + + private SoundSpecifier _startSound = new SoundPathSpecifier(new ResPath("/Audio/Effects/eye_open.ogg")); + private SoundSpecifier _endSound = new SoundPathSpecifier(new ResPath("/Audio/Effects/eye_close.ogg")); + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnHandleStateMarker); + + SubscribeLocalEvent(OnPlayerAttached); + SubscribeLocalEvent(OnPlayerDetached); + + SubscribeLocalEvent(OnComponentInit); + SubscribeLocalEvent(OnComponentShutdown); + } + + private void OnComponentShutdown(Entity ent, ref ComponentShutdown args) + { + if (_player.LocalEntity != ent) + return; + if (_overlay != null) + { + _overlayMan.RemoveOverlay(_overlay); + _overlay = null; + } + if (_overlay2 != null) + { + _overlayMan.RemoveOverlay(_overlay2); + _overlay2 = null; + } + + _audio.PlayGlobal(_endSound, ent); + } + + private void OnComponentInit(Entity ent, ref ComponentInit args) + { + if (_player.LocalEntity != ent) + return; + + _overlay = new CP14MagicVisionOverlay(); + _overlayMan.AddOverlay(_overlay); + _overlay.StartOverlay = _timing.CurTime; + + _overlay2 = new CP14MagicVisionNoirOverlay(); + _overlayMan.AddOverlay(_overlay2); + + _audio.PlayGlobal(_startSound, ent); + } + + private void OnPlayerAttached(Entity ent, ref LocalPlayerAttachedEvent args) + { + _overlay = new CP14MagicVisionOverlay(); + _overlayMan.AddOverlay(_overlay); + _overlay.StartOverlay = _timing.CurTime; + + _overlay2 = new CP14MagicVisionNoirOverlay(); + _overlayMan.AddOverlay(_overlay2); + + _audio.PlayGlobal(_startSound, ent); + } + + private void OnPlayerDetached(Entity ent, ref LocalPlayerDetachedEvent args) + { + if (_overlay != null) + { + _overlayMan.RemoveOverlay(_overlay); + _overlay = null; + } + if (_overlay2 != null) + { + _overlayMan.RemoveOverlay(_overlay2); + _overlay2 = null; + } + _audio.PlayGlobal(_endSound, ent); + } + + protected override void OnExamined(Entity ent, ref ExaminedEvent args) + { + base.OnExamined(ent, ref args); + + if (ent.Comp.TargetCoordinates is null) + return; + + var originPosition = _transform.GetWorldPosition(ent); + var targetPosition = _transform.ToWorldPosition(ent.Comp.TargetCoordinates.Value); + + if ((targetPosition - originPosition).Length() < 0.5f) + return; + + Angle angle = new(targetPosition - originPosition); + + var pointer = Spawn(ent.Comp.PointerProto, new MapCoordinates(originPosition, _transform.GetMapId(Transform(ent).Coordinates))); + + _transform.SetWorldRotation(pointer, angle + Angle.FromDegrees(90)); + } + + private void OnHandleStateMarker(Entity ent, ref AfterAutoHandleStateEvent args) + { + if (!TryComp(ent, out var sprite)) + return; + if (ent.Comp.Icon is null) + return; + + var layer = _sprite.AddLayer(ent.Owner, ent.Comp.Icon); + sprite.LayerSetShader(layer, "unshaded"); + _sprite.LayerSetScale(ent.Owner, layer, new Vector2(0.5f, 0.5f)); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + if (_timing.CurTime < _nextUpdate) + return; + + _nextUpdate = _timing.CurTime + TimeSpan.FromSeconds(0.5f); + var queryFade = EntityQueryEnumerator(); + while (queryFade.MoveNext(out var uid, out var fade, out var sprite)) + { + UpdateOpaque((uid, fade), sprite); + } + } + + private void UpdateOpaque(Entity ent, SpriteComponent sprite) + { + var progress = Math.Clamp((_timing.CurTime.TotalSeconds - ent.Comp.SpawnTime.TotalSeconds) / (ent.Comp.EndTime.TotalSeconds - ent.Comp.SpawnTime.TotalSeconds), 0, 1); + var alpha = 1 - progress; + _sprite.SetColor(ent.Owner, Color.White.WithAlpha((float)alpha)); + } +} diff --git a/Content.Client/_CP14/MagicVision/CP14MagicVisionNoirOverlay.cs b/Content.Client/_CP14/MagicVision/CP14MagicVisionNoirOverlay.cs new file mode 100644 index 0000000000..00a1de951f --- /dev/null +++ b/Content.Client/_CP14/MagicVision/CP14MagicVisionNoirOverlay.cs @@ -0,0 +1,33 @@ +using Robust.Client.Graphics; +using Robust.Shared.Enums; +using Robust.Shared.Prototypes; + +namespace Content.Client._CP14.MagicVision; + +public sealed class CP14MagicVisionNoirOverlay : Overlay +{ + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + + public override OverlaySpace Space => OverlaySpace.WorldSpace; + public override bool RequestScreenTexture => true; + private readonly ShaderInstance _noirShader; + + public CP14MagicVisionNoirOverlay() + { + IoCManager.InjectDependencies(this); + _noirShader = _prototypeManager.Index("CP14BlueNoir").InstanceUnique(); + ZIndex = 9; // draw this over the DamageOverlay, RainbowOverlay etc, but before the black and white shader + } + + protected override void Draw(in OverlayDrawArgs args) + { + if (ScreenTexture == null) + return; + + var handle = args.WorldHandle; + _noirShader.SetParameter("SCREEN_TEXTURE", ScreenTexture); + handle.UseShader(_noirShader); + handle.DrawRect(args.WorldBounds, Color.White); + handle.UseShader(null); + } +} diff --git a/Content.Client/_CP14/MagicVision/CP14MagicVisionOverlay.cs b/Content.Client/_CP14/MagicVision/CP14MagicVisionOverlay.cs new file mode 100644 index 0000000000..b49d714cec --- /dev/null +++ b/Content.Client/_CP14/MagicVision/CP14MagicVisionOverlay.cs @@ -0,0 +1,75 @@ +using Content.Shared._CP14.MagicVision; +using Robust.Client.Graphics; +using Robust.Client.Player; +using Robust.Shared.Enums; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Client._CP14.MagicVision; + +public sealed class CP14MagicVisionOverlay : Overlay +{ + [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IGameTiming _timing = default!; + + public override OverlaySpace Space => OverlaySpace.WorldSpace; + public override bool RequestScreenTexture => true; + + private readonly ShaderInstance _drowsinessShader; + + public float CurrentPower = 10.0f; + public TimeSpan StartOverlay = TimeSpan.Zero; // when the overlay started + + private const float PowerDivisor = 250.0f; + private const float Intensity = 0.2f; // for adjusting the visual scale + private float _visualScale = 0; // between 0 and 1 + + public CP14MagicVisionOverlay() + { + IoCManager.InjectDependencies(this); + _drowsinessShader = _prototypeManager.Index("Drowsiness").InstanceUnique(); + } + + protected override void FrameUpdate(FrameEventArgs args) + { + var playerEntity = _playerManager.LocalEntity; + + if (playerEntity == null) + return; + + if (!_entityManager.HasComponent(playerEntity)) + return; + + var curTime = _timing.CurTime; + var timeLeft = (float)(curTime - StartOverlay).TotalSeconds; + + CurrentPower = Math.Max(50f, 200f - (150f * Math.Min((float)(timeLeft / 3.0), 1.0f))); + } + + protected override bool BeforeDraw(in OverlayDrawArgs args) + { + if (!_entityManager.TryGetComponent(_playerManager.LocalEntity, out EyeComponent? eyeComp)) + return false; + + if (args.Viewport.Eye != eyeComp.Eye) + return false; + + _visualScale = Math.Clamp(CurrentPower / PowerDivisor, 0.0f, 1.0f); + return _visualScale > 0; + } + + protected override void Draw(in OverlayDrawArgs args) + { + if (ScreenTexture == null) + return; + + var handle = args.WorldHandle; + _drowsinessShader.SetParameter("SCREEN_TEXTURE", ScreenTexture); + _drowsinessShader.SetParameter("Strength", _visualScale * Intensity); + handle.UseShader(_drowsinessShader); + handle.DrawRect(args.WorldBounds, Color.White); + handle.UseShader(null); + } +} diff --git a/Content.Server/_CP14/AuraImprint/CP14AuraImprintSystem.cs b/Content.Server/_CP14/AuraImprint/CP14AuraImprintSystem.cs new file mode 100644 index 0000000000..98fef18151 --- /dev/null +++ b/Content.Server/_CP14/AuraImprint/CP14AuraImprintSystem.cs @@ -0,0 +1,69 @@ +using Content.Shared._CP14.AuraDNA; +using Content.Shared._CP14.MagicVision; +using Content.Shared.Mobs; +using Robust.Shared.Random; +using Robust.Shared.Utility; + +namespace Content.Server._CP14.AuraImprint; + +/// +/// This system handles the basic mechanics of spell use, such as doAfter, event invocation, and energy spending. +/// +public sealed partial class CP14AuraImprintSystem : CP14SharedAuraImprintSystem +{ + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly CP14SharedMagicVisionSystem _vision = default!; + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnMobStateChanged); + } + + private void OnMapInit(Entity ent, ref MapInitEvent args) + { + ent.Comp.Imprint = GenerateAuraImprint(ent); + Dirty(ent); + } + + public string GenerateAuraImprint(Entity ent) + { + var letters = new[] { "ä", "ã", "ç", "ø", "ђ", "œ", "Ї", "Ћ", "ў", "ž", "Ћ", "ö", "є", "þ"}; + var imprint = string.Empty; + + for (var i = 0; i < ent.Comp.ImprintLength; i++) + { + imprint += letters[_random.Next(letters.Length)]; + } + + return $"[color={ent.Comp.ImprintColor.ToHex()}]{imprint}[/color]"; + } + + private void OnMobStateChanged(Entity ent, ref MobStateChangedEvent args) + { + switch (args.NewMobState) + { + case MobState.Critical: + { + _vision.SpawnMagicVision( + Transform(ent).Coordinates, + new SpriteSpecifier.Rsi(new ResPath("_CP14/Actions/Spells/misc.rsi"), "skull"), + Loc.GetString("cp14-magic-vision-crit"), + TimeSpan.FromMinutes(10), + ent); + break; + } + case MobState.Dead: + { + _vision.SpawnMagicVision( + Transform(ent).Coordinates, + new SpriteSpecifier.Rsi(new ResPath("_CP14/Actions/Spells/misc.rsi"), "skull_red"), + Loc.GetString("cp14-magic-vision-dead"), + TimeSpan.FromMinutes(10), + ent); + break; + } + } + } +} diff --git a/Content.Server/_CP14/MagicVision/CP14MagicVisionSystem.cs b/Content.Server/_CP14/MagicVision/CP14MagicVisionSystem.cs new file mode 100644 index 0000000000..4511765154 --- /dev/null +++ b/Content.Server/_CP14/MagicVision/CP14MagicVisionSystem.cs @@ -0,0 +1,54 @@ +using Content.Shared._CP14.MagicVision; +using Content.Shared.Eye; +using Robust.Shared.Timing; + +namespace Content.Server._CP14.MagicVision; + +public sealed class CP14MagicVisionSystem : CP14SharedMagicVisionSystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly SharedEyeSystem _eye = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMagicVisionToggle); + SubscribeLocalEvent(OnGetVisMask); + } + + private void OnGetVisMask(Entity ent, ref GetVisMaskEvent args) + { + args.VisibilityMask |= (int)VisibilityFlags.CP14MagicVision; + } + + private void OnMagicVisionToggle(Entity ent, ref CP14MagicVisionToggleActionEvent args) + { + if (!HasComp(ent)) + { + AddComp(ent); + } + else + { + RemComp(ent); + } + _eye.RefreshVisibilityMask(ent.Owner); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var marker)) + { + if (marker.EndTime == TimeSpan.Zero) + continue; + + if (_timing.CurTime < marker.EndTime) + continue; + + QueueDel(uid); + } + } +} diff --git a/Content.Server/_CP14/Religion/CP14ReligionSystem.cs b/Content.Server/_CP14/Religion/CP14ReligionSystem.cs index aa3f830a1f..5bac92fd75 100644 --- a/Content.Server/_CP14/Religion/CP14ReligionSystem.cs +++ b/Content.Server/_CP14/Religion/CP14ReligionSystem.cs @@ -26,6 +26,7 @@ public sealed partial class CP14ReligionGodSystem : CP14SharedReligionGodSystem [Dependency] private readonly CP14MagicEnergySystem _magicEnergy = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly SharedEyeSystem _eye = default!; private EntityQuery _godQuery; @@ -158,11 +159,13 @@ public sealed partial class CP14ReligionGodSystem : CP14SharedReligionGodSystem private void OnGodInit(Entity ent, ref ComponentInit args) { AddPvsOverrides(ent); + _eye.RefreshVisibilityMask(ent.Owner); } private void OnGodShutdown(Entity ent, ref ComponentShutdown args) { RemovePvsOverrides(ent); + _eye.RefreshVisibilityMask(ent.Owner); } private void OnPlayerAttached(Entity ent, ref PlayerAttachedEvent args) diff --git a/Content.Shared/Eye/VisibilityFlags.cs b/Content.Shared/Eye/VisibilityFlags.cs index 432e80dd58..eb034ad74a 100644 --- a/Content.Shared/Eye/VisibilityFlags.cs +++ b/Content.Shared/Eye/VisibilityFlags.cs @@ -10,5 +10,8 @@ namespace Content.Shared.Eye Normal = 1 << 0, Ghost = 1 << 1, Subfloor = 1 << 2, + //CP14 zone + CP14MagicVision = 1 << 3, + //CP14 zone end } } diff --git a/Content.Shared/_CP14/AuraDNA/CP14AuraImprintComponent.cs b/Content.Shared/_CP14/AuraDNA/CP14AuraImprintComponent.cs new file mode 100644 index 0000000000..5af990e8fd --- /dev/null +++ b/Content.Shared/_CP14/AuraDNA/CP14AuraImprintComponent.cs @@ -0,0 +1,19 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared._CP14.AuraDNA; + +/// +/// A component that stores a “blueprint” of the aura, unique to each mind. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(CP14SharedAuraImprintSystem))] +public sealed partial class CP14AuraImprintComponent : Component +{ + [DataField, AutoNetworkedField] + public string Imprint = string.Empty; + + [DataField] + public int ImprintLength = 8; + + [DataField] + public Color ImprintColor = Color.White; +} diff --git a/Content.Shared/_CP14/AuraDNA/CP14SharedAuraImprintSystem.cs b/Content.Shared/_CP14/AuraDNA/CP14SharedAuraImprintSystem.cs new file mode 100644 index 0000000000..30b95cfbe1 --- /dev/null +++ b/Content.Shared/_CP14/AuraDNA/CP14SharedAuraImprintSystem.cs @@ -0,0 +1,8 @@ +namespace Content.Shared._CP14.AuraDNA; + +/// +/// This system handles the basic mechanics of spell use, such as doAfter, event invocation, and energy spending. +/// +public abstract partial class CP14SharedAuraImprintSystem : EntitySystem +{ +} diff --git a/Content.Shared/_CP14/MagicSpell/CP14SharedMagicSystem.cs b/Content.Shared/_CP14/MagicSpell/CP14SharedMagicSystem.cs index cdffe4cb6b..47a966f9cd 100644 --- a/Content.Shared/_CP14/MagicSpell/CP14SharedMagicSystem.cs +++ b/Content.Shared/_CP14/MagicSpell/CP14SharedMagicSystem.cs @@ -1,11 +1,12 @@ -using System.Linq; using System.Text; using Content.Shared._CP14.MagicEnergy; using Content.Shared._CP14.MagicEnergy.Components; using Content.Shared._CP14.MagicSpell.Components; using Content.Shared._CP14.MagicSpell.Events; using Content.Shared._CP14.MagicSpell.Spells; +using Content.Shared._CP14.MagicVision; using Content.Shared.Actions; +using Content.Shared.Actions.Components; using Content.Shared.Damage.Systems; using Content.Shared.DoAfter; using Content.Shared.FixedPoint; @@ -32,6 +33,7 @@ public abstract partial class CP14SharedMagicSystem : EntitySystem [Dependency] private readonly MovementSpeedModifierSystem _movement = default!; [Dependency] private readonly SharedStaminaSystem _stamina = default!; [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly CP14SharedMagicVisionSystem _magicVision = default!; private EntityQuery _magicContainerQuery; private EntityQuery _magicEffectQuery; @@ -162,6 +164,9 @@ public abstract partial class CP14SharedMagicSystem : EntitySystem private void CastTelegraphy(Entity ent, CP14SpellEffectBaseArgs args) { + if (!_timing.IsFirstTimePredicted) + return; + foreach (var effect in ent.Comp.TelegraphyEffects) { effect.Effect(EntityManager, args); @@ -170,6 +175,9 @@ public abstract partial class CP14SharedMagicSystem : EntitySystem private void CastSpell(Entity ent, CP14SpellEffectBaseArgs args) { + if (!_timing.IsFirstTimePredicted) + return; + var ev = new CP14MagicEffectConsumeResourceEvent(args.User); RaiseLocalEvent(ent, ref ev); @@ -177,6 +185,19 @@ public abstract partial class CP14SharedMagicSystem : EntitySystem { effect.Effect(EntityManager, args); } + + if (args.User is not null + && TryComp(ent, out var actionComp) + && TryComp(ent, out var manaCost)) + { + _magicVision.SpawnMagicVision( + Transform(args.User.Value).Coordinates, + actionComp.Icon, + Loc.GetString("cp14-magic-vision-used-spell", ("name", MetaData(ent).EntityName)), + TimeSpan.FromSeconds((float)manaCost.ManaCost * 50), + args.User, + args.Position); + } } protected FixedPoint2 CalculateManacost(Entity ent, EntityUid? caster) diff --git a/Content.Shared/_CP14/MagicVision/CP14MagicVisionComponent.cs b/Content.Shared/_CP14/MagicVision/CP14MagicVisionComponent.cs new file mode 100644 index 0000000000..08a7cac7b4 --- /dev/null +++ b/Content.Shared/_CP14/MagicVision/CP14MagicVisionComponent.cs @@ -0,0 +1,11 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared._CP14.MagicVision; + +/// +/// Allows to see magic vision trace entities +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class CP14MagicVisionComponent : Component +{ +} diff --git a/Content.Shared/_CP14/MagicVision/CP14MagicVisionMarkerComponent.cs b/Content.Shared/_CP14/MagicVision/CP14MagicVisionMarkerComponent.cs new file mode 100644 index 0000000000..bafdf4388d --- /dev/null +++ b/Content.Shared/_CP14/MagicVision/CP14MagicVisionMarkerComponent.cs @@ -0,0 +1,31 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Content.Shared._CP14.MagicVision; + +/// +/// Controls the visibility of this entity to the client, based on the length of time it has existed and the client's ability to see the magic +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true), AutoGenerateComponentPause] +public sealed partial class CP14MagicVisionMarkerComponent : Component +{ + [DataField, AutoPausedField, AutoNetworkedField] + public TimeSpan SpawnTime = TimeSpan.Zero; + + [DataField, AutoPausedField, AutoNetworkedField] + public TimeSpan EndTime = TimeSpan.Zero; + + [DataField, AutoNetworkedField] + public EntityCoordinates? TargetCoordinates; + + [DataField, AutoNetworkedField] + public SpriteSpecifier? Icon; + + [DataField] + public string? AuraImprint; + + [DataField, AutoNetworkedField] + public EntProtoId PointerProto = "CP14ManaVisionPointer"; +} diff --git a/Content.Shared/_CP14/MagicVision/CP14SharedMagicVisionSystem.cs b/Content.Shared/_CP14/MagicVision/CP14SharedMagicVisionSystem.cs new file mode 100644 index 0000000000..d107a7faa9 --- /dev/null +++ b/Content.Shared/_CP14/MagicVision/CP14SharedMagicVisionSystem.cs @@ -0,0 +1,83 @@ +using System.Text; +using Content.Shared._CP14.AuraDNA; +using Content.Shared.Actions; +using Content.Shared.Examine; +using Content.Shared.Mobs; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; +using Robust.Shared.Utility; + +namespace Content.Shared._CP14.MagicVision; + +public abstract class CP14SharedMagicVisionSystem : EntitySystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly MetaDataSystem _meta = default!; + + public readonly EntProtoId MagicTraceProto = "CP14MagicVisionMarker"; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnExamined); + SubscribeLocalEvent(OnAuraHolderExamine); + } + + private void OnAuraHolderExamine(Entity ent, ref ExaminedEvent args) + { + if (!HasComp(args.Examiner)) + return; + + args.PushMarkup($"{Loc.GetString("cp14-magic-vision-aura")} {ent.Comp.Imprint}"); + } + + protected virtual void OnExamined(Entity ent, ref ExaminedEvent args) + { + var sb = new StringBuilder(); + + var timePassed = _timing.CurTime - ent.Comp.SpawnTime; + sb.Append($"{Loc.GetString("cp14-magic-vision-timed-past")} {timePassed.Minutes}:{(timePassed.Seconds < 10 ? "0" : "")}{timePassed.Seconds}\n"); + + if (ent.Comp.AuraImprint is not null) + { + sb.Append($"{Loc.GetString("cp14-magic-vision-aura")} {ent.Comp.AuraImprint}"); + } + + args.AddMarkup(sb.ToString()); + } + + /// + /// Creates an invisible “magical trace” entity that can be seen with magical vision. + /// + /// Coordinates where the magic trail will be created + /// Magic trace icon + /// Description that can be obtained when examining the magical trace + /// Duration of the magical trace + /// Optional: The direction in which this trace “faces.” When studying the trace, + /// this direction can be seen in order to understand, for example, in which direction the spell was used. + public void SpawnMagicVision(EntityCoordinates position, SpriteSpecifier? icon, string description, TimeSpan duration, EntityUid? aura = null, EntityCoordinates? target = null) + { + var ent = PredictedSpawnAtPosition(MagicTraceProto, position); + var markerComp = EnsureComp(ent); + + markerComp.SpawnTime = _timing.CurTime; + markerComp.EndTime = _timing.CurTime + duration; + markerComp.TargetCoordinates = target; + markerComp.Icon = icon; + + if (aura is not null && TryComp(aura, out var auraImprint)) + { + markerComp.AuraImprint = auraImprint.Imprint; + } + + _meta.SetEntityDescription(ent, description); + + Dirty(ent, markerComp); + } +} + +public sealed partial class CP14MagicVisionToggleActionEvent : InstantActionEvent +{ +} diff --git a/Resources/Locale/en-US/_CP14/magicVision/vision.ftl b/Resources/Locale/en-US/_CP14/magicVision/vision.ftl new file mode 100644 index 0000000000..167808c20b --- /dev/null +++ b/Resources/Locale/en-US/_CP14/magicVision/vision.ftl @@ -0,0 +1,6 @@ +cp14-magic-vision-used-spell = The spell "{$name}" was used here. +cp14-magic-vision-crit = Someone here is in critical condition. +cp14-magic-vision-dead = Someone here has died. + +cp14-magic-vision-timed-past = Time has passed: +cp14-magic-vision-aura = Aura imprint: \ No newline at end of file diff --git a/Resources/Locale/ru-RU/_CP14/magicVision/vision.ftl b/Resources/Locale/ru-RU/_CP14/magicVision/vision.ftl new file mode 100644 index 0000000000..426a014138 --- /dev/null +++ b/Resources/Locale/ru-RU/_CP14/magicVision/vision.ftl @@ -0,0 +1,6 @@ +cp14-magic-vision-used-spell = Здесь было использовано заклинание "{$name}". +cp14-magic-vision-crit = Здесь кто-то упал в критическое состояние. +cp14-magic-vision-dead = Здесь кто-то умер. + +cp14-magic-vision-timed-past = Времени прошло: +cp14-magic-vision-aura = Слепок ауры: \ No newline at end of file diff --git a/Resources/Prototypes/_CP14/Entities/Actions/Spells/Fire/firewave.yml b/Resources/Prototypes/_CP14/Entities/Actions/Spells/Fire/firewave.yml index 8bce03ff96..f838d94c49 100644 --- a/Resources/Prototypes/_CP14/Entities/Actions/Spells/Fire/firewave.yml +++ b/Resources/Prototypes/_CP14/Entities/Actions/Spells/Fire/firewave.yml @@ -56,6 +56,12 @@ categories: [ HideSpawnMenu ] save: false components: + - type: Sprite + sprite: _CP14/Effects/fire.rsi + layers: + - state: small + visible: false + map: ["enum.EffectLayers.Unshaded"] - type: PointLight color: "#E25822" radius: 2.0 diff --git a/Resources/Prototypes/_CP14/Entities/Actions/Spells/Meta/mana_vision.yml b/Resources/Prototypes/_CP14/Entities/Actions/Spells/Meta/mana_vision.yml new file mode 100644 index 0000000000..f3be0c3a53 --- /dev/null +++ b/Resources/Prototypes/_CP14/Entities/Actions/Spells/Meta/mana_vision.yml @@ -0,0 +1,53 @@ +- type: entity + id: CP14ActionToggleMagicVision + parent: BaseMentalAction + name: Magical vision + description: You focus on magical flows to track recent events and scan the aura imprints of other living beings. + components: + - type: Action + useDelay: 5 + itemIconStyle: BigAction + checkCanInteract: false + sound: !type:SoundPathSpecifier + path: /Audio/Magic/rumble.ogg + icon: + sprite: _CP14/Actions/Spells/meta.rsi + state: magic_vision + - type: InstantAction + event: !type:CP14MagicVisionToggleActionEvent + +- type: entity + id: CP14ManaVisionPointer + name: pointer + categories: [ HideSpawnMenu ] + components: + - type: Sprite + color: "#42a4f5" + sprite: /Textures/_CP14/Effects/Magic/pointer.rsi + offset: 0, -1 + layers: + - state: pointer + shader: unshaded + drawdepth: LowFloors + - type: TimedDespawn + lifetime: 2 + - type: Tag + tags: + - HideContextMenu + +- type: entity + id: CP14MagicVisionMarker + categories: [ HideSpawnMenu ] + name: mana trace + components: + - type: Sprite + noRot: true + drawdepth: Effects + layers: + - state: sphere + sprite: /Textures/_CP14/Effects/Magic/pointer_sphere.rsi + shader: unshaded + color: "#42a4f5" + - type: Clickable + - type: Visibility + layer: 8 #magic vision only \ No newline at end of file diff --git a/Resources/Prototypes/_CP14/Entities/Markers/misc.yml b/Resources/Prototypes/_CP14/Entities/Markers/misc.yml index 39d364395e..d4a80a1643 100644 --- a/Resources/Prototypes/_CP14/Entities/Markers/misc.yml +++ b/Resources/Prototypes/_CP14/Entities/Markers/misc.yml @@ -9,5 +9,4 @@ - state: green - sprite: /Textures/_CP14/Structures/Dungeon/demiplan_rift.rsi state: pulse - - type: CP14DemiplaneRift - + - type: CP14DemiplaneRift \ No newline at end of file diff --git a/Resources/Prototypes/_CP14/Entities/Mobs/Species/base.yml b/Resources/Prototypes/_CP14/Entities/Mobs/Species/base.yml index 822bad3780..b4430b638c 100644 --- a/Resources/Prototypes/_CP14/Entities/Mobs/Species/base.yml +++ b/Resources/Prototypes/_CP14/Entities/Mobs/Species/base.yml @@ -232,6 +232,7 @@ Cold: 0.25 Bloodloss: 0.25 - type: CP14TradingReputation + - type: CP14AuraImprint - type: entity diff --git a/Resources/Prototypes/_CP14/Entities/Mobs/Species/carcat.yml b/Resources/Prototypes/_CP14/Entities/Mobs/Species/carcat.yml index b9961a9733..f911488875 100644 --- a/Resources/Prototypes/_CP14/Entities/Mobs/Species/carcat.yml +++ b/Resources/Prototypes/_CP14/Entities/Mobs/Species/carcat.yml @@ -42,6 +42,8 @@ - type: CP14NightVision #Night vision - type: FootstepModifier footstepSoundCollection: null # Silent footstep + - type: CP14AuraImprint + imprintColor: "#9e7e5d" - type: Inventory templateId: CP14Carcat # Cant wear shoes speciesId: carcat diff --git a/Resources/Prototypes/_CP14/Entities/Mobs/Species/dwarf.yml b/Resources/Prototypes/_CP14/Entities/Mobs/Species/dwarf.yml index 61c5df2263..ac942f9fd1 100644 --- a/Resources/Prototypes/_CP14/Entities/Mobs/Species/dwarf.yml +++ b/Resources/Prototypes/_CP14/Entities/Mobs/Species/dwarf.yml @@ -20,6 +20,8 @@ spawned: - id: CP14FoodMeatHuman amount: 5 + - type: CP14AuraImprint + imprintColor: "#91898d" - type: Body prototype: CP14Dwarf requiredLegs: 2 diff --git a/Resources/Prototypes/_CP14/Entities/Mobs/Species/elf.yml b/Resources/Prototypes/_CP14/Entities/Mobs/Species/elf.yml index 492fbbb1f6..68b88460ae 100644 --- a/Resources/Prototypes/_CP14/Entities/Mobs/Species/elf.yml +++ b/Resources/Prototypes/_CP14/Entities/Mobs/Species/elf.yml @@ -45,6 +45,9 @@ freeLearnedSkills: - MetamagicT1 - MetamagicT2 + - type: CP14AuraImprint + imprintLength: 10 #Long imprint hehe + imprintColor: "#42aaf5" - type: Inventory templateId: CP14Human displacements: diff --git a/Resources/Prototypes/_CP14/Entities/Mobs/Species/goblin.yml b/Resources/Prototypes/_CP14/Entities/Mobs/Species/goblin.yml index 7add9ee392..fbfe810979 100644 --- a/Resources/Prototypes/_CP14/Entities/Mobs/Species/goblin.yml +++ b/Resources/Prototypes/_CP14/Entities/Mobs/Species/goblin.yml @@ -128,6 +128,9 @@ - type: CP14MagicEnergyDraw #Cool x3 mana regen! energy: 1.5 delay: 3 + - type: CP14AuraImprint + imprintLength: 6 #Short imprint hehe + imprintColor: "#919e19" - type: Hands handDisplacement: sizeMaps: diff --git a/Resources/Prototypes/_CP14/Entities/Mobs/Species/silva.yml b/Resources/Prototypes/_CP14/Entities/Mobs/Species/silva.yml index f880521dcd..75430a7e01 100644 --- a/Resources/Prototypes/_CP14/Entities/Mobs/Species/silva.yml +++ b/Resources/Prototypes/_CP14/Entities/Mobs/Species/silva.yml @@ -83,6 +83,8 @@ - type: Body prototype: CP14Silva requiredLegs: 2 + - type: CP14AuraImprint + imprintColor: "#31cc3b" - type: Bloodstream bloodReagent: CP14BloodFlowerSap #Lol - type: Inventory diff --git a/Resources/Prototypes/_CP14/Entities/Mobs/Species/skeleton.yml b/Resources/Prototypes/_CP14/Entities/Mobs/Species/skeleton.yml index becd0b64d2..bc76715b6c 100644 --- a/Resources/Prototypes/_CP14/Entities/Mobs/Species/skeleton.yml +++ b/Resources/Prototypes/_CP14/Entities/Mobs/Species/skeleton.yml @@ -88,6 +88,8 @@ - MobMask layer: - MobLayer + - type: CP14AuraImprint + imprintColor: "#a81b5d" - type: Damageable damageContainer: CP14Biological damageModifierSet: CP14Skeleton diff --git a/Resources/Prototypes/_CP14/Entities/Mobs/Species/tiefling.yml b/Resources/Prototypes/_CP14/Entities/Mobs/Species/tiefling.yml index 6761754113..0d4ba160ba 100644 --- a/Resources/Prototypes/_CP14/Entities/Mobs/Species/tiefling.yml +++ b/Resources/Prototypes/_CP14/Entities/Mobs/Species/tiefling.yml @@ -44,6 +44,8 @@ damage: Heat: 1 #Magic regen from fire Cold: -1 + - type: CP14AuraImprint + imprintColor: "#e0762b" - type: CP14SkillStorage #Innate pyrokinetic freeLearnedSkills: - PyrokineticT1 diff --git a/Resources/Prototypes/_CP14/Shaders/shaders.yml b/Resources/Prototypes/_CP14/Shaders/shaders.yml index abe9eaf7b2..05142d28d8 100644 --- a/Resources/Prototypes/_CP14/Shaders/shaders.yml +++ b/Resources/Prototypes/_CP14/Shaders/shaders.yml @@ -23,4 +23,10 @@ - type: shader id: CP14ReligionVision kind: source - path: "/Textures/_CP14/Shaders/religion.swsl" \ No newline at end of file + path: "/Textures/_CP14/Shaders/religion.swsl" + + +- type: shader + id: CP14BlueNoir + kind: source + path: "/Textures/_CP14/Shaders/blue_noir.swsl" \ No newline at end of file diff --git a/Resources/Prototypes/_CP14/Skill/Basic/metamagic.yml b/Resources/Prototypes/_CP14/Skill/Basic/metamagic.yml index 09162fa08e..6d6457b3d6 100644 --- a/Resources/Prototypes/_CP14/Skill/Basic/metamagic.yml +++ b/Resources/Prototypes/_CP14/Skill/Basic/metamagic.yml @@ -138,6 +138,20 @@ - !type:NeedPrerequisite prerequisite: MetamagicT2 +- type: cp14Skill + id: CP14ActionToggleMagicVision + skillUiPosition: 6, 4 + tree: Metamagic + icon: + sprite: _CP14/Actions/Spells/meta.rsi + state: magic_vision + effects: + - !type:AddAction + action: CP14ActionToggleMagicVision + restrictions: + - !type:NeedPrerequisite + prerequisite: MetamagicT2 + # T3 - type: cp14Skill diff --git a/Resources/Textures/_CP14/Actions/Spells/meta.rsi/magic_vision.png b/Resources/Textures/_CP14/Actions/Spells/meta.rsi/magic_vision.png new file mode 100644 index 0000000000000000000000000000000000000000..638188faa1cc32f5843b460fc9dee86da4815022 GIT binary patch literal 355 zcmV-p0i6DcP)Px$9Z5t%R9J=WR>2JdArM64&-k<+EAV3hG;k%HPc6X){E`?FE3h4SUgV1pgIPB$ z>~W|UbMu5{XV}@jLx=)2(zYe%CR9NqZClfVd96f0!zdb3D(g#yeBFYqP#lmtAeHq) zZl6V}wb4l1N@cwe&kAVc-m35sI|8XT&THd5Tn_>uafll% z1`<;-unM6v5Jf^##5fVGOb`$wV+(;EW8ig29_s}vN3kuiBA6TtNZE$*`V#h;XKIfc zTXv4n*|9xXRy-NFw3G$fIDQ}GnvnXk4Qj%e5b8D@RQXo~TkyD1gfNyvJnJi_+ls?( zvyARvZg;V|iaoa({~w$OZ1zPoiZ|bc8knpcEgk~GcS%bQ?xz3%002ovPDHLkV1f)N Bj+X!c literal 0 HcmV?d00001 diff --git a/Resources/Textures/_CP14/Actions/Spells/meta.rsi/meta.json b/Resources/Textures/_CP14/Actions/Spells/meta.rsi/meta.json index 5f3ba98223..6f90d15b7a 100644 --- a/Resources/Textures/_CP14/Actions/Spells/meta.rsi/meta.json +++ b/Resources/Textures/_CP14/Actions/Spells/meta.rsi/meta.json @@ -24,6 +24,9 @@ }, { "name": "mana_trance" + }, + { + "name": "magic_vision" } ] } \ No newline at end of file diff --git a/Resources/Textures/_CP14/Actions/Spells/misc.rsi/meta.json b/Resources/Textures/_CP14/Actions/Spells/misc.rsi/meta.json index 1c0e5d594e..692d3c9e5e 100644 --- a/Resources/Textures/_CP14/Actions/Spells/misc.rsi/meta.json +++ b/Resources/Textures/_CP14/Actions/Spells/misc.rsi/meta.json @@ -12,6 +12,12 @@ }, { "name": "polymorph" + }, + { + "name": "skull" + }, + { + "name": "skull_red" } ] } \ No newline at end of file diff --git a/Resources/Textures/_CP14/Actions/Spells/misc.rsi/skull.png b/Resources/Textures/_CP14/Actions/Spells/misc.rsi/skull.png new file mode 100644 index 0000000000000000000000000000000000000000..38b559456661be8017e4582fde46400f707a4534 GIT binary patch literal 269 zcmV+o0rLKdP)Px#$4Nv%R9J=Wl(7+mFbqXMx$L1$sJxadk7X)tqDe@Rv(E=6NQPACHAcpuk3JIM zIF9plChG$+#`V|zl#=*3{cfR*-0kW(j$`2iAYiL) TeWg_X00000NkvXXu0mjf@(gh2 literal 0 HcmV?d00001 diff --git a/Resources/Textures/_CP14/Actions/Spells/misc.rsi/skull_red.png b/Resources/Textures/_CP14/Actions/Spells/misc.rsi/skull_red.png new file mode 100644 index 0000000000000000000000000000000000000000..a5823d06e2be34baaa3db06b8196e6e4860e86c5 GIT binary patch literal 302 zcmV+}0nz@6P)Px#=t)FDR9J=Wlra*5Knz6_$AN;v8LxmAlonQ=gfrj`xCWfTlTcYuQUF(QhEkig zf>|Rh34@h+4U;ACv-^_;P!vV^cToC45=O@LK5r}0KetlxG!EQJx5!9}*hs{$hv#fw z8V9qdWW%bIito<*$=vS#004lj@mY3=ETAH5z_y^Qy|1;b@gZyc$#~TQeuc<_vi9I~ zd=2r%eD)fRIk3ki2_s_**o9Ea+Jgd-)r4#d^0vYP5emdXWUMd7)?A1z7(^b literal 0 HcmV?d00001 diff --git a/Resources/Textures/_CP14/Effects/Magic/pointer_sphere.rsi/meta.json b/Resources/Textures/_CP14/Effects/Magic/pointer_sphere.rsi/meta.json new file mode 100644 index 0000000000..8deee3c1c6 --- /dev/null +++ b/Resources/Textures/_CP14/Effects/Magic/pointer_sphere.rsi/meta.json @@ -0,0 +1,14 @@ +{ + "version": 1, + "size": { + "x": 32, + "y": 32 + }, + "license": "CC-BY-SA-4.0", + "copyright": "Created by TheShuEd", + "states": [ + { + "name": "sphere" + } + ] +} diff --git a/Resources/Textures/_CP14/Effects/Magic/pointer_sphere.rsi/sphere.png b/Resources/Textures/_CP14/Effects/Magic/pointer_sphere.rsi/sphere.png new file mode 100644 index 0000000000000000000000000000000000000000..5f669730c5673123a55e120270922d6674e27275 GIT binary patch literal 232 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}i#%N%Ln2z= zPCLohV8G!byR z@LLVHjC(Wv-eo2qS$r^vq5SnyhQ!Si4K>|PiTQ5~m@Od3yy&mkwU-?4CW$4mU31*@ zZ;|^NMS}y@+Zl}743ujp%FOv8zV&r#2%|=z&6~T7v2`9xwq+)G2VS=b3@? 0.0) { + if (xMax == c.r) { + hue = mod((c.g - c.b) / delta, 6.0); + } else if (xMax == c.g) { + hue = (c.b - c.r) / delta + 2.0; + } else { + hue = (c.r - c.g) / delta + 4.0; + } + hue *= 60.0; + if (hue < 0.0) hue += 360.0; + } + + highp float sat = (xMax == 0.0) ? 0.0 : delta / xMax; + return vec3(hue, sat, xMax); +} + +void fragment() { + highp vec4 color = zTextureSpec(SCREEN_TEXTURE, UV); + highp vec3 gray = vec3(zGrayscale(color.rgb)); + highp vec3 hsv = rgb2hsv(color.rgb); + + bool is_blue = hsv.x > (BlueHue - HueRange) && hsv.x < (BlueHue + HueRange); + bool saturated_enough = hsv.y > MinSaturation; + + if (is_blue && saturated_enough) { + COLOR = color; + } else { + COLOR = vec4(gray, color.a); + } +}