From 29f1a2c89b2c2e053d0a22fe48e52bde98b108e2 Mon Sep 17 00:00:00 2001 From: mikhailbovt Date: Thu, 12 Feb 2026 10:33:15 +0300 Subject: [PATCH] new-ghost --- Content.Client/Ghost/GhostSystem.cs | 61 +- .../Components/InteractionOutlineComponent.cs | 26 +- .../Psionics/PsionicAbilitiesSystem.cs | 2 +- .../GameTicking/GameTicker.Spawning.cs | 2 +- Content.Server/Ghost/GhostSystem.cs | 428 +++++- .../Entities/Mobs/Player/observer.yml | 1269 +++++++++++++++++ Resources/Prototypes/Shaders/outline.yml | 2 + Resources/Prototypes/Shaders/shaders.yml | 7 +- .../Shaders/ghost_composite_tint.swsl | 12 + Resources/Textures/Shaders/outline.swsl | 39 +- 10 files changed, 1796 insertions(+), 52 deletions(-) create mode 100644 Resources/Textures/Shaders/ghost_composite_tint.swsl diff --git a/Content.Client/Ghost/GhostSystem.cs b/Content.Client/Ghost/GhostSystem.cs index 080233ec40b..6b3dea8a3e1 100644 --- a/Content.Client/Ghost/GhostSystem.cs +++ b/Content.Client/Ghost/GhostSystem.cs @@ -1,21 +1,31 @@ +using System; using Content.Client.Movement.Systems; using Content.Shared.Actions; using Content.Shared.Ghost; using Robust.Client.Console; using Robust.Client.GameObjects; +using Robust.Client.Graphics; using Robust.Client.Player; using Robust.Shared.Player; +using Robust.Shared.Prototypes; namespace Content.Client.Ghost { public sealed class GhostSystem : SharedGhostSystem { + private const string VisualObserverPrototypePrefix = "MobObserverVisual"; + private const string CompositeGhostShaderId = "GhostCompositeTint"; + private const float VisualObserverAlphaMultiplier = 0.50f; + [Dependency] private readonly IClientConsoleHost _console = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly SharedActionsSystem _actions = default!; [Dependency] private readonly PointLightSystem _pointLightSystem = default!; [Dependency] private readonly ContentEyeSystem _contentEye = default!; + private readonly Dictionary _compositeGhostShaders = new(); + public int AvailableGhostRoleCount { get; private set; } private bool _ghostVisibility = true; @@ -72,7 +82,10 @@ public override void Initialize() private void OnStartup(EntityUid uid, GhostComponent component, ComponentStartup args) { if (TryComp(uid, out SpriteComponent? sprite)) + { sprite.Visible = GhostVisibility || uid == _playerManager.LocalEntity; + ApplyGhostVisuals(uid, component, sprite); + } } private void OnToggleLighting(EntityUid uid, EyeComponent component, ToggleLightingActionEvent args) @@ -129,6 +142,8 @@ private void OnToggleGhosts(EntityUid uid, GhostComponent component, ToggleGhost private void OnGhostRemove(EntityUid uid, GhostComponent component, ComponentRemove args) { + RemoveGhostCompositeShader(uid); + _actions.RemoveAction(uid, component.ToggleLightingActionEntity); _actions.RemoveAction(uid, component.ToggleFoVActionEntity); _actions.RemoveAction(uid, component.ToggleGhostsActionEntity); @@ -150,7 +165,7 @@ private void OnGhostPlayerAttach(EntityUid uid, GhostComponent component, LocalP private void OnGhostState(EntityUid uid, GhostComponent component, ref AfterAutoHandleStateEvent args) { if (TryComp(uid, out var sprite)) - sprite.LayerSetColor(0, component.color); + ApplyGhostVisuals(uid, component, sprite); if (uid != _playerManager.LocalEntity) return; @@ -211,5 +226,49 @@ public void ReturnToRound() var msg = new GhostReturnToRoundRequest(); RaiseNetworkEvent(msg); } + + private void ApplyGhostVisuals(EntityUid uid, GhostComponent component, SpriteComponent sprite) + { + if (TryComp(uid, out MetaDataComponent? metaData) && + metaData.EntityPrototype?.ID.StartsWith(VisualObserverPrototypePrefix, StringComparison.Ordinal) == true) + { + var shader = EnsureGhostCompositeShader(uid, sprite); + shader.SetParameter("ghost_tint", new Robust.Shared.Maths.Vector3(component.color.R, component.color.G, component.color.B)); + shader.SetParameter("ghost_alpha", Math.Clamp(component.color.A * VisualObserverAlphaMultiplier, 0f, 1f)); + sprite.Color = Color.White; + return; + } + + RemoveGhostCompositeShader(uid, sprite); + sprite.Color = component.color; + } + + private ShaderInstance EnsureGhostCompositeShader(EntityUid uid, SpriteComponent sprite) + { + if (!_compositeGhostShaders.TryGetValue(uid, out var shader)) + { + shader = _prototype.Index(CompositeGhostShaderId).InstanceUnique(); + _compositeGhostShaders[uid] = shader; + } + + if (sprite.PostShader != shader) + sprite.PostShader = shader; + + return shader; + } + + private void RemoveGhostCompositeShader(EntityUid uid, SpriteComponent? sprite = null) + { + if (!_compositeGhostShaders.Remove(uid, out var shader)) + return; + + if (sprite == null) + TryComp(uid, out sprite); + + if (sprite != null && sprite.PostShader == shader) + sprite.PostShader = null; + + shader.Dispose(); + } } } diff --git a/Content.Client/Interactable/Components/InteractionOutlineComponent.cs b/Content.Client/Interactable/Components/InteractionOutlineComponent.cs index b2f00dbe21d..d2186370633 100644 --- a/Content.Client/Interactable/Components/InteractionOutlineComponent.cs +++ b/Content.Client/Interactable/Components/InteractionOutlineComponent.cs @@ -1,6 +1,7 @@ using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Shared.Prototypes; +using Content.Shared.Ghost; namespace Content.Client.Interactable.Components { @@ -11,6 +12,8 @@ public sealed partial class InteractionOutlineComponent : Component [Dependency] private readonly IEntityManager _entMan = default!; private const float DefaultWidth = 1; + private const float DefaultAlphaCutoff = 0f; + private const float GhostAlphaCutoff = 0.02f; [ValidatePrototypeId] private const string ShaderInRange = "SelectionOutlineInrange"; @@ -20,18 +23,24 @@ public sealed partial class InteractionOutlineComponent : Component private bool _inRange; private ShaderInstance? _shader; + private ShaderInstance? _previousPostShader; private int _lastRenderScale; public void OnMouseEnter(EntityUid uid, bool inInteractionRange, int renderScale) { _lastRenderScale = renderScale; _inRange = inInteractionRange; - if (_entMan.TryGetComponent(uid, out SpriteComponent? sprite) && sprite.PostShader == null) - { - // TODO why is this creating a new instance of the outline shader every time the mouse enters??? - _shader = MakeNewShader(inInteractionRange, renderScale); - sprite.PostShader = _shader; - } + + if (!_entMan.TryGetComponent(uid, out SpriteComponent? sprite)) + return; + + if (_shader != null && sprite.PostShader == _shader) + return; + + _previousPostShader = sprite.PostShader; + _shader?.Dispose(); + _shader = MakeNewShader(inInteractionRange, renderScale); + sprite.PostShader = _shader; } public void OnMouseLeave(EntityUid uid) @@ -39,11 +48,12 @@ public void OnMouseLeave(EntityUid uid) if (_entMan.TryGetComponent(uid, out SpriteComponent? sprite)) { if (sprite.PostShader == _shader) - sprite.PostShader = null; + sprite.PostShader = _previousPostShader; } _shader?.Dispose(); _shader = null; + _previousPostShader = null; } public void UpdateInRange(EntityUid uid, bool inInteractionRange, int renderScale) @@ -55,6 +65,7 @@ public void UpdateInRange(EntityUid uid, bool inInteractionRange, int renderScal _inRange = inInteractionRange; _lastRenderScale = renderScale; + _shader?.Dispose(); _shader = MakeNewShader(_inRange, _lastRenderScale); sprite.PostShader = _shader; } @@ -66,6 +77,7 @@ private ShaderInstance MakeNewShader(bool inRange, int renderScale) var instance = _prototypeManager.Index(shaderName).InstanceUnique(); instance.SetParameter("outline_width", DefaultWidth * renderScale); + instance.SetParameter("alpha_cutoff", _entMan.HasComponent(Owner) ? GhostAlphaCutoff : DefaultAlphaCutoff); return instance; } } diff --git a/Content.Server/Abilities/Psionics/PsionicAbilitiesSystem.cs b/Content.Server/Abilities/Psionics/PsionicAbilitiesSystem.cs index 2a6ac0df4e5..727939c7914 100644 --- a/Content.Server/Abilities/Psionics/PsionicAbilitiesSystem.cs +++ b/Content.Server/Abilities/Psionics/PsionicAbilitiesSystem.cs @@ -232,7 +232,7 @@ public void ScarierMindbreak(EntityUid uid, PsionicComponent component) if (!_mind.TryGetMind(session, out var mindId, out var mind)) return; - _ghost.SpawnGhost((mindId, mind), Transform(uid).Coordinates, false); + _ghost.SpawnGhost((mindId, mind), Transform(uid).Coordinates, false, uid); _npcFaction.AddFaction(uid, "SimpleNeutral"); var htn = EnsureComp(uid); htn.RootTask = new HTNCompoundTask() { Task = "IdleCompound" }; diff --git a/Content.Server/GameTicking/GameTicker.Spawning.cs b/Content.Server/GameTicking/GameTicker.Spawning.cs index cf74892048c..9c42f505e73 100644 --- a/Content.Server/GameTicking/GameTicker.Spawning.cs +++ b/Content.Server/GameTicking/GameTicker.Spawning.cs @@ -434,7 +434,7 @@ public void SpawnObserver(ICommonSession player) _roles.MindAddRole(mind.Value, "MindRoleObserver"); } - var ghost = _ghost.SpawnGhost(mind.Value); + var ghost = _ghost.SpawnLobbyObserverGhost(mind.Value); _adminLogger.Add(LogType.LateJoin, LogImpact.Low, $"{player.Name} late joined the round as an Observer with {ToPrettyString(ghost):entity}."); diff --git a/Content.Server/Ghost/GhostSystem.cs b/Content.Server/Ghost/GhostSystem.cs index 153604e055c..e0eba9d71ee 100644 --- a/Content.Server/Ghost/GhostSystem.cs +++ b/Content.Server/Ghost/GhostSystem.cs @@ -1,36 +1,53 @@ using System.Linq; using System.Numerics; using Content.Server.Administration.Logs; +using Content.Server.Body.Components; using Content.Server.Chat.Managers; +using Content.Server.Clothing.Systems; using Content.Server.GameTicking; using Content.Server.Ghost.Components; +using Content.Server.Humanoid; using Content.Server.Mind; +using Content.Server.Players.PlayTimeTracking; using Content.Server.Preferences.Managers; -using Content.Server.Roles; using Content.Server.Roles.Jobs; +using Content.Server.Station.Systems; using Content.Shared._White.CustomGhostSystem; +using Content.Shared.Clothing.Components; +using Content.Shared.Clothing.EntitySystems; using Content.Shared._White.Roles; using Content.Shared.Actions; using Content.Shared.CCVar; using Content.Shared.Damage; +using Content.Shared.Damage.Components; using Content.Shared.Damage.Prototypes; using Content.Shared.Database; using Content.Shared.Examine; using Content.Shared.Eye; using Content.Shared.FixedPoint; using Content.Shared.Follower; +using Content.Shared.GameTicking; using Content.Shared.Ghost; +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Prototypes; +using Content.Shared.Inventory; +using Content.Shared.Item; using Content.Shared.Mind; using Content.Shared.Mind.Components; using Content.Shared.Mobs; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; +using Content.Shared.Movement.Components; using Content.Shared.Movement.Events; using Content.Shared.Movement.Systems; +using Content.Shared.Players; +using Content.Shared.Preferences; using Content.Shared.Popups; +using Content.Shared.Roles; using Content.Shared.SSDIndicator; using Content.Shared.Storage.Components; using Content.Shared.Tag; +using Content.Shared.Traits.Assorted.Components; using Content.Shared.Warps; using Robust.Server.GameObjects; using Robust.Server.Player; @@ -49,15 +66,23 @@ public sealed class GhostSystem : SharedGhostSystem { [Dependency] private readonly SharedActionsSystem _actions = default!; [Dependency] private readonly IAdminLogManager _adminLog = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly SharedEyeSystem _eye = default!; + [Dependency] private readonly ClothingSystem _clothing = default!; [Dependency] private readonly FollowerSystem _followerSystem = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly HumanoidAppearanceSystem _humanoid = default!; + [Dependency] private readonly InventorySystem _inventory = default!; + [Dependency] private readonly SharedItemSystem _item = default!; [Dependency] private readonly JobSystem _jobs = default!; [Dependency] private readonly EntityLookupSystem _lookup = default!; + [Dependency] private readonly LoadoutSystem _loadout = default!; [Dependency] private readonly MindSystem _minds = default!; [Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly SharedPhysicsSystem _physics = default!; + [Dependency] private readonly PlayTimeTrackingManager _playTimeTracking = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly StationSpawningSystem _stationSpawning = default!; [Dependency] private readonly TransformSystem _transformSystem = default!; [Dependency] private readonly VisibilitySystem _visibilitySystem = default!; [Dependency] private readonly MetaDataSystem _metaData = default!; @@ -71,16 +96,43 @@ public sealed class GhostSystem : SharedGhostSystem [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly TagSystem _tag = default!; - // WD EDIT START + [Dependency] private readonly MovementSpeedModifierSystem _movementSpeed = default!; [Dependency] private readonly IServerPreferencesManager _prefs = default!; - [Dependency] private readonly RoleSystem _roles = default!; - // WD EDIT END private EntityQuery _ghostQuery; private EntityQuery _physicsQuery; + private readonly Dictionary _mindHumanoidSnapshotSources = new(); public static readonly Color AntagonistButtonColor = Color.FromHex("#7F4141"); // WWDP EDIT + [ValidatePrototypeId] + private const string VisualObserverPrototypeName = "MobObserverVisualHumanoid"; + + private enum GhostSpawnMode + { + Default, + LobbyObserver + } + + private static readonly HashSet VisualSnapshotSlots = new(StringComparer.Ordinal) + { + "shoes", + "jumpsuit", + "outerClothing", + "gloves", + "neck", + "mask", + "eyes", + "ears", + "head", + "id", + "belt", + "back", + "suitstorage", + "innerBelt", + "innerNeck" + }; + public override void Initialize() { base.Initialize(); @@ -97,6 +149,8 @@ public override void Initialize() SubscribeLocalEvent(OnMindRemovedMessage); SubscribeLocalEvent(OnMindUnvisitedMessage); SubscribeLocalEvent(OnPlayerDetached); + SubscribeLocalEvent(OnMindGotRemoved); + SubscribeLocalEvent(OnMindTerminating); SubscribeLocalEvent(OnRelayMoveInput); @@ -269,6 +323,16 @@ private void OnPlayerDetached(EntityUid uid, GhostComponent component, PlayerDet DeleteEntity(uid); } + private void OnMindGotRemoved(EntityUid uid, MindComponent component, MindGotRemovedEvent args) + { + TryCaptureMindHumanoidSnapshot(uid, args.Container.Owner); + } + + private void OnMindTerminating(EntityUid uid, MindComponent component, ref EntityTerminatingEvent args) + { + ClearMindHumanoidSnapshot(uid); + } + private void DeleteEntity(EntityUid uid) { if (Deleted(uid) || Terminating(uid)) @@ -535,7 +599,13 @@ public bool DoGhostBooEvent(EntityUid target) bool canReturn = false) { _transformSystem.TryGetMapOrGridCoordinates(targetEntity, out var spawnPosition); - return SpawnGhost(mind, spawnPosition, canReturn); + return SpawnGhost(mind, spawnPosition, canReturn, targetEntity); + } + + public EntityUid? SpawnLobbyObserverGhost(Entity mind, EntityCoordinates? spawnPosition = null, + bool canReturn = false) + { + return SpawnGhostInternal(mind, spawnPosition, canReturn, GhostSpawnMode.LobbyObserver); } private bool IsValidSpawnPosition(EntityCoordinates? spawnPosition) @@ -556,7 +626,13 @@ private bool IsValidSpawnPosition(EntityCoordinates? spawnPosition) } public EntityUid? SpawnGhost(Entity mind, EntityCoordinates? spawnPosition = null, - bool canReturn = false) + bool canReturn = false, EntityUid? sourceEntity = null) + { + return SpawnGhostInternal(mind, spawnPosition, canReturn, GhostSpawnMode.Default, sourceEntity); + } + + private EntityUid? SpawnGhostInternal(Entity mind, EntityCoordinates? spawnPosition, + bool canReturn, GhostSpawnMode mode, EntityUid? sourceEntity = null) { if (!Resolve(mind, ref mind.Comp)) return null; @@ -577,23 +653,43 @@ private bool IsValidSpawnPosition(EntityCoordinates? spawnPosition) return null; } - // WWDP EDIT START - CustomGhostPrototype? customGhost = null; - if (_prototypeManager.TryIndex(_prefs.GetPreferencesOrNull(mind.Comp.UserId)?.CustomGhost, out var ghostProto)) - customGhost = ghostProto; + EntityUid ghost = EntityUid.Invalid; + var spawnedVisualGhost = false; + var snapshotSource = mode == GhostSpawnMode.Default + ? ResolveVisualSnapshotSource(mind.Owner, sourceEntity) + : null; - var ghost = SpawnAtPosition(customGhost?.GhostEntityPrototype ?? GameTicker.ObserverPrototypeName, spawnPosition.Value); - // WWDP EDIT END + if (mode == GhostSpawnMode.LobbyObserver) + spawnedVisualGhost = TrySpawnLobbyObserverGhost((mind.Owner, mind.Comp), spawnPosition.Value, out ghost); + else if (snapshotSource is { } source) + spawnedVisualGhost = TrySpawnBodySnapshotGhost(source, spawnPosition.Value, out ghost); + + if (!spawnedVisualGhost) + ghost = SpawnFallbackGhost((mind.Owner, mind.Comp), spawnPosition.Value); + + if (!TryComp(ghost, out var ghostComponent)) + { + Log.Error($"Spawned ghost prototype without {nameof(GhostComponent)}: {ToPrettyString(ghost)}"); + QueueDel(ghost); + _minds.TransferTo(mind.Owner, null, createGhost: false, mind: mind.Comp); + return null; + } + + CopyMovementDefaultsFromCurrentEntity(mind.Comp.CurrentEntity, ghost); - var ghostComponent = Comp(ghost); + if (spawnedVisualGhost) + { + ghostComponent.CanGhostInteract = false; + EnsureComp(ghost); + EnsureComp(ghost); + _movementSpeed.RefreshMovementSpeedModifiers(ghost); + } // Try setting the ghost entity name to either the character name or the player name. // If all else fails, it'll default to the default entity prototype name, "observer". // However, that should rarely happen. - // WWDP EDIT START if (FirstNonNullNonEmpty(mind.Comp.CharacterName, mind.Comp.Session?.Name) is string ghostName) _metaData.SetEntityName(ghost, ghostName); - // WWDP EDIT END if (mind.Comp.TimeOfDeath.HasValue) { @@ -608,16 +704,304 @@ private bool IsValidSpawnPosition(EntityCoordinates? spawnPosition) _minds.TransferTo(mind.Owner, ghost, mind: mind.Comp); Log.Debug($"Spawned ghost \"{ToPrettyString(ghost)}\" for {mind.Comp.CharacterName}."); return ghost; + } - // WWDP EDIT START - static string? FirstNonNullNonEmpty(params string?[] strings) + private void CopyMovementDefaultsFromCurrentEntity(EntityUid? sourceEntity, EntityUid ghost) + { + if (sourceEntity is not { } source || + !TryComp(source, out var sourceMover) || + !TryComp(ghost, out var ghostMover)) + { + return; + } + + if (ghostMover.DefaultSprinting == sourceMover.DefaultSprinting) + return; + + ghostMover.DefaultSprinting = sourceMover.DefaultSprinting; + Dirty(ghost, ghostMover); + } + + private EntityUid SpawnFallbackGhost(Entity mind, EntityCoordinates spawnPosition) + { + CustomGhostPrototype? customGhost = null; + if (_prototypeManager.TryIndex(_prefs.GetPreferencesOrNull(mind.Comp.UserId)?.CustomGhost, out var ghostProto)) + customGhost = ghostProto; + + return SpawnAtPosition(customGhost?.GhostEntityPrototype ?? GameTicker.ObserverPrototypeName, spawnPosition); + } + + private bool TrySpawnLobbyObserverGhost(Entity mind, EntityCoordinates spawnPosition, out EntityUid ghost) + { + ghost = default; + + if (mind.Comp.UserId is not { } userId) + return false; + + var prefs = _prefs.GetPreferencesOrNull(userId); + if (prefs?.SelectedCharacter is not HumanoidCharacterProfile profile) + return false; + + if (!TryGetVisualObserverPrototype(profile.Species, out var ghostPrototype)) + return false; + + ghost = SpawnAtPosition(ghostPrototype, spawnPosition); + + if (!HasComp(ghost)) + { + QueueDel(ghost); + ghost = default; + return false; + } + + _humanoid.LoadProfile(ghost, profile, loadExtensions: false, generateLoadouts: false); + _metaData.SetEntityName(ghost, profile.Name); + + if (!TryPickLobbyObserverJob(profile, out var jobId) || + !_prototypeManager.TryIndex(jobId, out var jobProto)) + { + return true; + } + + if (jobProto.StartingGear != null && + _prototypeManager.TryIndex(jobProto.StartingGear, out var startingGear)) + { + startingGear = _stationSpawning.ApplySubGear(startingGear, profile, jobProto); + _stationSpawning.EquipStartingGear(ghost, startingGear, raiseEvent: false); + } + + if (!_configurationManager.GetCVar(CCVars.GameLoadoutsEnabled)) + return true; + + Dictionary playTimes = new(); + var whitelisted = false; + if (_playerManager.TryGetSessionById(userId, out var session)) + { + if (_playTimeTracking.TryGetTrackerTimes(session, out var trackedTimes)) + playTimes = trackedTimes; + + whitelisted = session.ContentData()?.Whitelisted ?? false; + } + + _loadout.ApplyCharacterLoadout(ghost, jobId, profile, playTimes, whitelisted, deleteFailed: true, jobProto: jobProto); + return true; + } + + private bool TryPickLobbyObserverJob(HumanoidCharacterProfile profile, out ProtoId jobId) + { + foreach (var (job, priority) in profile.JobPriorities) + { + if (priority != JobPriority.High || !_prototypeManager.HasIndex(job)) + continue; + + jobId = job; + return true; + } + + var overflowJob = new ProtoId(SharedGameTicker.FallbackOverflowJob); + if (_prototypeManager.HasIndex(overflowJob)) + { + jobId = overflowJob; + return true; + } + + jobId = default; + return false; + } + + private bool TrySpawnBodySnapshotGhost(EntityUid sourceEntity, EntityCoordinates spawnPosition, out EntityUid ghost) + { + ghost = default; + + if (!TryGetVisualObserverPrototype(sourceEntity, out var ghostPrototype)) + return false; + + ghost = SpawnAtPosition(ghostPrototype, spawnPosition); + + if (!HasComp(ghost)) + { + QueueDel(ghost); + ghost = default; + return false; + } + + _humanoid.CloneAppearance(sourceEntity, ghost); + CopyDamageSnapshot(sourceEntity, ghost); + CopyInventorySnapshot(sourceEntity, ghost); + + return true; + } + + private EntityUid? ResolveVisualSnapshotSource(EntityUid mindId, EntityUid? sourceEntity) + { + if (sourceEntity is { } source && Exists(source) && HasComp(source)) + return source; + + return TryGetCachedMindHumanoidSnapshot(mindId, out var cached) ? cached : null; + } + + private bool TryGetCachedMindHumanoidSnapshot(EntityUid mindId, out EntityUid cached) + { + cached = default; + if (!_mindHumanoidSnapshotSources.TryGetValue(mindId, out var snapshot)) + return false; + + if (!Exists(snapshot) || !HasComp(snapshot)) + { + _mindHumanoidSnapshotSources.Remove(mindId); + return false; + } + + cached = snapshot; + return true; + } + + private bool TryCaptureMindHumanoidSnapshot(EntityUid mindId, EntityUid sourceEntity) + { + if (!Exists(sourceEntity) || !TryGetVisualObserverPrototype(sourceEntity, out var ghostPrototype)) + return false; + + ClearMindHumanoidSnapshot(mindId); + + var snapshot = Spawn(ghostPrototype, MapCoordinates.Nullspace); + if (!HasComp(snapshot)) + { + QueueDel(snapshot); + return false; + } + + _mindHumanoidSnapshotSources[mindId] = snapshot; + _humanoid.CloneAppearance(sourceEntity, snapshot); + CopyDamageSnapshot(sourceEntity, snapshot); + CopyInventorySnapshot(sourceEntity, snapshot); + return true; + } + + private bool TryGetVisualObserverPrototype(EntityUid sourceEntity, out string prototype) + { + prototype = default!; + if (!TryComp(sourceEntity, out var humanoid)) + return false; + + return TryGetVisualObserverPrototype(humanoid.Species, out prototype); + } + + private bool TryGetVisualObserverPrototype(ProtoId species, out string prototype) + { + prototype = VisualObserverPrototypeName; + if (!_prototypeManager.TryIndex(species, out var speciesPrototype)) + return _prototypeManager.HasIndex(prototype); + + var bySpecies = $"MobObserverVisual{speciesPrototype.ID}"; + if (_prototypeManager.HasIndex(bySpecies)) + { + prototype = bySpecies; + return true; + } + + var byDoll = TryGetVisualObserverPrototypeFromDoll(speciesPrototype.DollPrototype); + if (byDoll != null && _prototypeManager.HasIndex(byDoll)) + { + prototype = byDoll; + return true; + } + + return _prototypeManager.HasIndex(prototype); + } + + private static string? TryGetVisualObserverPrototypeFromDoll(EntProtoId dollPrototype) + { + const string dollPrefix = "Mob"; + const string dollSuffix = "Dummy"; + + var dollId = dollPrototype.Id; + if (!dollId.StartsWith(dollPrefix, StringComparison.Ordinal) || + !dollId.EndsWith(dollSuffix, StringComparison.Ordinal)) { - foreach (var str in strings) - if (!string.IsNullOrWhiteSpace(str)) - return str; return null; } - // WWDP EDIT END + + var speciesIdLength = dollId.Length - dollPrefix.Length - dollSuffix.Length; + if (speciesIdLength <= 0) + return null; + + var speciesId = dollId.Substring(dollPrefix.Length, speciesIdLength); + return $"MobObserverVisual{speciesId}"; + } + + private void ClearMindHumanoidSnapshot(EntityUid mindId) + { + if (!_mindHumanoidSnapshotSources.Remove(mindId, out var snapshot)) + return; + + if (Exists(snapshot) && !TerminatingOrDeleted(snapshot)) + QueueDel(snapshot); + } + + private void CopyDamageSnapshot(EntityUid sourceEntity, EntityUid ghost) + { + if (!TryComp(sourceEntity, out var sourceDamageable) || + !TryComp(ghost, out var ghostDamageable)) + { + return; + } + + _damageable.SetDamage(ghost, ghostDamageable, new DamageSpecifier(sourceDamageable.Damage)); + } + + private void CopyInventorySnapshot(EntityUid sourceEntity, EntityUid ghost) + { + if (!_inventory.TryGetSlots(sourceEntity, out var slots)) + return; + + var ghostCoordinates = Transform(ghost).Coordinates; + foreach (var slot in slots) + { + if (!VisualSnapshotSlots.Contains(slot.Name)) + continue; + + if (!_inventory.TryGetSlotEntity(sourceEntity, slot.Name, out var sourceItem)) + continue; + + CopyInventorySlotVisualSnapshot(sourceItem.Value, ghost, slot.Name, ghostCoordinates); + } + } + + private void CopyInventorySlotVisualSnapshot(EntityUid sourceItem, EntityUid ghost, string slotName, EntityCoordinates ghostCoordinates) + { + var prototype = MetaData(sourceItem).EntityPrototype?.ID; + if (prototype == null || !_prototypeManager.HasIndex(prototype)) + return; + + var clone = SpawnAtPosition(prototype, ghostCoordinates); + + if (TryComp(sourceItem, out var sourceItemComp) && + TryComp(clone, out var cloneItemComp)) + { + _item.CopyVisuals(clone, sourceItemComp, cloneItemComp); + } + + if (TryComp(sourceItem, out var sourceClothingComp) && + TryComp(clone, out var cloneClothingComp)) + { + _clothing.CopyVisuals(clone, sourceClothingComp, cloneClothingComp); + } + + _appearance.CopyData(sourceItem, clone); + + if (!_inventory.TryEquip(ghost, clone, slotName, silent: true, force: true)) + QueueDel(clone); + } + + private static string? FirstNonNullNonEmpty(params string?[] strings) + { + foreach (var str in strings) + { + if (!string.IsNullOrWhiteSpace(str)) + return str; + } + + return null; } public bool OnGhostAttempt(EntityUid mindId, bool canReturnGlobal, bool viaCommand = false, MindComponent? mind = null) @@ -702,7 +1086,7 @@ public bool OnGhostAttempt(EntityUid mindId, bool canReturnGlobal, bool viaComma if (playerEntity != null) _adminLog.Add(LogType.Mind, $"{EntityManager.ToPrettyString(playerEntity.Value):player} ghosted{(!canReturn ? " (non-returnable)" : "")}"); - var ghost = SpawnGhost((mindId, mind), position, canReturn); + var ghost = SpawnGhost((mindId, mind), position, canReturn, playerEntity); if (ghost == null) return false; diff --git a/Resources/Prototypes/Entities/Mobs/Player/observer.yml b/Resources/Prototypes/Entities/Mobs/Player/observer.yml index c7ed53bd448..3de64b42e6e 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/observer.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/observer.yml @@ -92,6 +92,1275 @@ tags: - AllowGhostShownByEvent +- type: entity + parent: + - Incorporeal + - BaseMob + id: MobObserverVisualBase + abstract: true + name: observer + description: Boo! + categories: [ HideSpawnMenu ] + components: + - type: ContentEye + maxZoom: 1.44,1.44 + - type: Eye + drawFov: false + visMask: + - TelegnosticProjection + - PsionicInvisibility + - Ghost + - Normal + - Ethereal + - type: Input + context: "ghost" + - type: Examiner + skipChecks: true + - type: Ghost + - type: GhostHearing + - type: ShowElectrocutionHUD + - type: IntrinsicRadioReceiver + - type: ActiveRadio + receiveAllChannels: true + globalReceive: true + - type: UniversalLanguageSpeaker # Ghosts should understand any language. + - type: PointLight + netsync: false + radius: 6 + castShadows: false + enabled: false + - type: UserInterface + interfaces: + enum.RevivalContractUiKey.Key: # Goobstation - Devil + type: RevivalContractBoundUserInterface + requireInputValidation: false + - type: NoNormalInteraction + - type: Spectral + - type: Tag + tags: + - BypassInteractionRangeChecks + - AllowGhostShownByEvent + +- type: entity + parent: MobObserverVisualBase + id: MobObserverVisualHumanoid + categories: [ HideSpawnMenu ] + components: + - type: Sprite + noRot: true + overrideContainerOcclusion: true + drawdepth: Ghosts + color: "#fff8" + layers: + - map: [ "enum.HumanoidVisualLayers.Chest" ] + - map: [ "enum.HumanoidVisualLayers.Head" ] + - map: [ "enum.HumanoidVisualLayers.Snout" ] + - map: [ "enum.HumanoidVisualLayers.Eyes" ] + - map: [ "enum.HumanoidVisualLayers.Face" ] + - map: [ "enum.HumanoidVisualLayers.RArm" ] + - map: [ "enum.HumanoidVisualLayers.LArm" ] + - map: [ "enum.HumanoidVisualLayers.RLeg" ] + - map: [ "enum.HumanoidVisualLayers.LLeg" ] + - map: [ "enum.HumanoidVisualLayers.Underwear" ] + - map: [ "enum.HumanoidVisualLayers.Undershirt" ] + - map: ["jumpsuit"] + - map: ["enum.HumanoidVisualLayers.LFoot"] + - map: ["enum.HumanoidVisualLayers.RFoot"] + - map: ["enum.HumanoidVisualLayers.LHand"] + - map: ["enum.HumanoidVisualLayers.RHand"] + - map: ["enum.HumanoidVisualLayers.Handcuffs"] + color: "#ffffff" + sprite: Objects/Misc/handcuffs.rsi + state: body-overlay-2 + visible: false + - map: [ "gloves" ] + - map: [ "shoes" ] + - map: [ "ears" ] + - map: [ "innerBelt" ] + - map: [ "innerNeck" ] + - map: [ "outerClothing" ] + - map: [ "eyes" ] + - map: [ "belt" ] + - map: [ "id" ] + - map: [ "neck" ] + - map: [ "back" ] + - map: [ "suitstorage" ] + - map: [ "enum.HumanoidVisualLayers.FacialHair" ] + - map: [ "enum.HumanoidVisualLayers.Hair" ] + - map: [ "enum.HumanoidVisualLayers.HeadSide" ] + - map: [ "enum.HumanoidVisualLayers.HeadTop" ] + - map: [ "enum.HumanoidVisualLayers.Tail" ] + - map: [ "enum.HumanoidVisualLayers.Wings" ] + - map: [ "mask" ] + - map: [ "head" ] + - type: Appearance + - type: Inventory + - type: InventorySlots + - type: HumanoidAppearance + species: Human + - type: Damageable + - type: DamageVisuals + thresholds: [ 10, 20, 30, 50, 70, 100 ] + targetLayers: + - "enum.HumanoidVisualLayers.Chest" + - "enum.HumanoidVisualLayers.Head" + - "enum.HumanoidVisualLayers.LArm" + - "enum.HumanoidVisualLayers.LLeg" + - "enum.HumanoidVisualLayers.RArm" + - "enum.HumanoidVisualLayers.RLeg" + damageOverlayGroups: + Brute: + sprite: Mobs/Effects/brute_damage.rsi + color: "#FF0000" + Burn: + sprite: Mobs/Effects/burn_damage.rsi + - type: IgnoreSlowOnDamage + - type: SpeedModifierImmunity + +- type: entity + parent: MobObserverVisualBase + id: MobObserverVisualArachne + categories: [ HideSpawnMenu ] + components: + - type: Sprite + noRot: true + overrideContainerOcclusion: true + drawdepth: Ghosts + color: "#fff8" + layers: + - map: [ "enum.HumanoidVisualLayers.LLeg" ] + sprite: Mobs/Species/arachne.rsi + state: spider_body + - map: [ "enum.HumanoidVisualLayers.Chest" ] + - map: [ "enum.HumanoidVisualLayers.RLeg" ] + sprite: Mobs/Species/arachne.rsi + state: spider_body_front + - map: [ "enum.HumanoidVisualLayers.Head" ] + - map: [ "enum.HumanoidVisualLayers.Snout" ] + - map: [ "enum.HumanoidVisualLayers.Eyes" ] + - map: [ "enum.HumanoidVisualLayers.Face" ] + - map: [ "enum.HumanoidVisualLayers.RArm" ] + - map: [ "enum.HumanoidVisualLayers.LArm" ] + - shader: StencilMask + map: [ "enum.HumanoidVisualLayers.StencilMask" ] + sprite: Mobs/Customization/anytaur_masking_helpers.rsi + state: unisex_full + visible: false + - map: [ "jumpsuit" ] + - map: [ "enum.HumanoidVisualLayers.LHand" ] + - map: [ "enum.HumanoidVisualLayers.RHand" ] + - map: [ "enum.HumanoidVisualLayers.Handcuffs" ] + color: "#ffffff" + sprite: Objects/Misc/handcuffs.rsi + state: body-overlay-2 + visible: false + - map: [ "id" ] + - map: [ "gloves" ] + - map: [ "shoes" ] + - map: [ "ears" ] + - map: [ "innerBelt" ] + - map: [ "innerNeck" ] + - map: [ "outerClothing" ] + - map: [ "eyes" ] + - map: [ "belt" ] + - map: [ "neck" ] + - map: [ "back" ] + - map: [ "suitstorage" ] + - map: [ "enum.HumanoidVisualLayers.FacialHair" ] + - map: [ "enum.HumanoidVisualLayers.Hair" ] + state: bald + sprite: Mobs/Customization/human_hair.rsi + - map: [ "mask" ] + - map: [ "enum.HumanoidVisualLayers.HeadSide" ] + - map: [ "head" ] + - map: [ "enum.HumanoidVisualLayers.Tail" ] + sprite: Mobs/Customization/masking_helpers.rsi + state: none + visible: false + - type: Inventory + templateId: anytaur + - type: InventorySlots + - type: Appearance + - type: HumanoidAppearance + species: Arachne + - type: Damageable + - type: DamageVisuals + thresholds: [ 10, 20, 30, 50, 70, 100 ] + targetLayers: + - "enum.HumanoidVisualLayers.Chest" + - "enum.HumanoidVisualLayers.Head" + - "enum.HumanoidVisualLayers.LArm" + - "enum.HumanoidVisualLayers.RArm" + damageOverlayGroups: + Brute: + sprite: Mobs/Effects/brute_damage.rsi + color: "#FF0000" + Burn: + sprite: Mobs/Effects/burn_damage.rsi + - type: IgnoreSlowOnDamage + - type: SpeedModifierImmunity + +- type: entity + parent: MobObserverVisualHumanoid + id: MobObserverVisualMoth + categories: [ HideSpawnMenu ] + components: + - type: Sprite + noRot: true + overrideContainerOcclusion: true + drawdepth: Ghosts + color: "#fff8" + layers: + - map: [ "enum.HumanoidVisualLayers.Chest" ] + - map: [ "enum.HumanoidVisualLayers.Snout" ] + - map: [ "enum.HumanoidVisualLayers.Head" ] + - map: [ "enum.HumanoidVisualLayers.Eyes" ] + - map: [ "enum.HumanoidVisualLayers.Face" ] + - map: [ "enum.HumanoidVisualLayers.RArm" ] + - map: [ "enum.HumanoidVisualLayers.LArm" ] + - map: [ "enum.HumanoidVisualLayers.RLeg" ] + - map: [ "enum.HumanoidVisualLayers.LLeg" ] + - map: [ "enum.HumanoidVisualLayers.Underwear" ] + - map: [ "enum.HumanoidVisualLayers.Undershirt" ] + - map: [ "jumpsuit" ] + - map: [ "enum.HumanoidVisualLayers.LHand" ] + - map: [ "enum.HumanoidVisualLayers.RHand" ] + - map: [ "enum.HumanoidVisualLayers.LFoot" ] + - map: [ "enum.HumanoidVisualLayers.RFoot" ] + - map: [ "enum.HumanoidVisualLayers.Handcuffs" ] + color: "#ffffff" + sprite: Objects/Misc/handcuffs.rsi + state: body-overlay-2 + visible: false + - map: [ "gloves" ] + - map: [ "shoes" ] + - map: [ "ears" ] + - map: [ "innerBelt" ] + - map: [ "innerNeck" ] + - map: [ "outerClothing" ] + - map: [ "eyes" ] + - map: [ "belt" ] + - map: [ "id" ] + - map: [ "enum.HumanoidVisualLayers.Tail" ] + - map: [ "neck" ] + - map: [ "back" ] + - map: [ "suitstorage" ] + - map: [ "enum.HumanoidVisualLayers.FacialHair" ] + - map: [ "enum.HumanoidVisualLayers.Hair" ] + - map: [ "enum.HumanoidVisualLayers.HeadSide" ] + - map: [ "enum.HumanoidVisualLayers.HeadTop" ] + - map: [ "mask" ] + - map: [ "head" ] + - map: [ "clownedon" ] + sprite: Effects/creampie.rsi + state: creampie_moth + visible: false + - type: Inventory + femaleDisplacements: + jumpsuit: + sizeMaps: + 32: + sprite: Mobs/Species/Human/displacement.rsi + state: jumpsuit-female + - type: HumanoidAppearance + species: Moth + - type: DamageVisuals + thresholds: [ 10, 20, 30, 50, 70, 100 ] + targetLayers: + - "enum.HumanoidVisualLayers.Chest" + - "enum.HumanoidVisualLayers.Head" + - "enum.HumanoidVisualLayers.LArm" + - "enum.HumanoidVisualLayers.LLeg" + - "enum.HumanoidVisualLayers.RArm" + - "enum.HumanoidVisualLayers.RLeg" + damageOverlayGroups: + Brute: + sprite: Mobs/Effects/brute_damage.rsi + color: "#808A51" + Burn: + sprite: Mobs/Effects/burn_damage.rsi + +- type: entity + parent: MobObserverVisualHumanoid + id: MobObserverVisualHarpy + categories: [ HideSpawnMenu ] + components: + - type: Sprite + noRot: true + overrideContainerOcclusion: true + drawdepth: Ghosts + color: "#fff8" + scale: 0.9, 0.9 + layers: + - map: [ "enum.HumanoidVisualLayers.Chest" ] + - map: [ "enum.HumanoidVisualLayers.Head" ] + - map: [ "enum.HumanoidVisualLayers.Snout" ] + - map: [ "enum.HumanoidVisualLayers.Eyes" ] + - map: [ "enum.HumanoidVisualLayers.Face" ] + - map: [ "enum.HumanoidVisualLayers.LArm" ] + - map: [ "enum.HumanoidVisualLayers.RLeg" ] + - map: [ "enum.HumanoidVisualLayers.LLeg" ] + - map: [ "underpants" ] + - map: [ "undershirt" ] + - map: [ "socks" ] + - map: [ "jumpsuit" ] + - map: [ "enum.HumanoidVisualLayers.LFoot" ] + - map: [ "enum.HumanoidVisualLayers.RFoot" ] + - map: [ "enum.HumanoidVisualLayers.LHand" ] + - map: [ "enum.HumanoidVisualLayers.RHand" ] + - map: [ "id" ] + - map: [ "gloves" ] + - map: [ "ears" ] + - map: [ "innerBelt" ] + - map: [ "innerNeck" ] + - map: [ "outerClothing" ] + - map: [ "eyes" ] + - map: [ "belt" ] + - map: [ "neck" ] + - map: [ "back" ] + - map: [ "suitstorage" ] + - map: [ "enum.HumanoidVisualLayers.FacialHair" ] + - map: [ "enum.HumanoidVisualLayers.HeadSide" ] + - map: [ "enum.HumanoidVisualLayers.HeadTop" ] + - map: [ "enum.HumanoidVisualLayers.Tail" ] + - map: [ "clownedon" ] + sprite: Effects/creampie.rsi + state: creampie_human + visible: false + - map: [ "enum.HumanoidVisualLayers.RArm" ] + - map: [ "enum.HumanoidVisualLayers.Hair" ] + - map: [ "mask" ] + - map: [ "head" ] + - map: [ "singingLayer" ] + sprite: Effects/harpysinger.rsi + state: singing_music_notes + visible: false + - type: Inventory + speciesId: harpy + templateId: digitigrade + displacements: + jumpsuit: + sizeMaps: + 32: + sprite: Mobs/Species/Harpy/displacement.rsi + state: jumpsuit + copyToShaderParameters: + layerKey: dummy + parameterTexture: displacementMap + parameterUV: displacementUV + femaleDisplacements: + jumpsuit-body-slim: + sizeMaps: + 32: + sprite: _White/Mobs/Species/Harpy/displacement.rsi + state: jumpsuit-slim + - type: HumanoidAppearance + species: Harpy + +- type: entity + parent: MobObserverVisualHumanoid + id: MobObserverVisualShadowkin + categories: [ HideSpawnMenu ] + components: + - type: Sprite + noRot: true + overrideContainerOcclusion: true + drawdepth: Ghosts + color: "#fff8" + scale: 0.85, 0.85 + layers: + - map: [ "enum.HumanoidVisualLayers.Chest" ] + - map: [ "enum.HumanoidVisualLayers.Head" ] + - map: [ "enum.HumanoidVisualLayers.Snout" ] + - map: [ "enum.HumanoidVisualLayers.Eyes" ] + shader: unshaded + - map: [ "enum.HumanoidVisualLayers.Face" ] + - map: [ "enum.HumanoidVisualLayers.RArm" ] + - map: [ "enum.HumanoidVisualLayers.LArm" ] + - map: [ "enum.HumanoidVisualLayers.RLeg" ] + - map: [ "enum.HumanoidVisualLayers.LLeg" ] + - shader: StencilMask + map: [ "enum.HumanoidVisualLayers.StencilMask" ] + sprite: Mobs/Customization/masking_helpers.rsi + state: full + visible: false + - map: [ "enum.HumanoidVisualLayers.LFoot" ] + - map: [ "enum.HumanoidVisualLayers.RFoot" ] + - map: [ "socks" ] + - map: [ "underpants" ] + - map: [ "undershirt" ] + - map: [ "jumpsuit" ] + - map: [ "enum.HumanoidVisualLayers.LHand" ] + - map: [ "enum.HumanoidVisualLayers.RHand" ] + - map: [ "enum.HumanoidVisualLayers.Handcuffs" ] + color: "#ffffff" + sprite: Objects/Misc/handcuffs.rsi + state: body-overlay-2 + visible: false + - map: [ "id" ] + - map: [ "gloves" ] + - map: [ "shoes" ] + - map: [ "ears" ] + - map: [ "innerBelt" ] + - map: [ "innerNeck" ] + - map: [ "outerClothing" ] + - map: [ "eyes" ] + - map: [ "belt" ] + - map: [ "neck" ] + - map: [ "back" ] + - map: [ "suitstorage" ] + - map: [ "enum.HumanoidVisualLayers.FacialHair" ] + - map: [ "enum.HumanoidVisualLayers.Hair" ] + - map: [ "enum.HumanoidVisualLayers.HeadSide" ] + - map: [ "enum.HumanoidVisualLayers.HeadTop" ] + - map: [ "mask" ] + - map: [ "head" ] + - map: [ "enum.HumanoidVisualLayers.Tail" ] + - type: HumanoidAppearance + species: Shadowkin + - type: DamageVisuals + thresholds: [ 10, 20, 30, 50, 70, 100 ] + damageOverlayGroups: + Brute: + sprite: Mobs/Effects/brute_damage.rsi + color: "#1c1624" + +- type: entity + parent: MobObserverVisualHumanoid + id: MobObserverVisualLamia + categories: [ HideSpawnMenu ] + components: + - type: Sprite + noRot: true + overrideContainerOcclusion: true + drawdepth: Ghosts + color: "#fff8" + scale: 1, 1 + layers: + - map: [ "enum.HumanoidVisualLayers.Chest" ] + - map: [ "enum.HumanoidVisualLayers.Head" ] + - map: [ "enum.HumanoidVisualLayers.Eyes" ] + - map: [ "enum.HumanoidVisualLayers.Face" ] + - map: [ "enum.HumanoidVisualLayers.RArm" ] + - map: [ "enum.HumanoidVisualLayers.LArm" ] + - map: [ "jumpsuit" ] + shader: StencilDraw + - map: [ "enum.HumanoidVisualLayers.LHand" ] + - map: [ "enum.HumanoidVisualLayers.RHand" ] + - map: [ "enum.HumanoidVisualLayers.Handcuffs" ] + color: "#ffffff" + sprite: Objects/Misc/handcuffs.rsi + state: body-overlay-2 + visible: false + - map: [ "id" ] + - map: [ "gloves" ] + - map: [ "ears" ] + - map: [ "innerBelt" ] + - map: [ "innerNeck" ] + - map: [ "outerClothing" ] + - map: [ "eyes" ] + - map: [ "belt" ] + - map: [ "belt2" ] + - map: [ "neck" ] + - map: [ "back" ] + - map: [ "suitstorage" ] + - map: [ "enum.HumanoidVisualLayers.FacialHair" ] + - map: [ "enum.HumanoidVisualLayers.Hair" ] + - map: [ "enum.HumanoidVisualLayers.HeadSide" ] + - map: [ "enum.HumanoidVisualLayers.HeadTop" ] + - map: [ "mask" ] + - map: [ "head" ] + - map: [ "enum.HumanoidVisualLayers.Tail" ] + - map: [ "clownedon" ] + sprite: Effects/creampie.rsi + state: creampie_human + visible: false + - type: Inventory + speciesId: lamia + templateId: lamia + femaleDisplacements: + jumpsuit: + sizeMaps: + 32: + sprite: Mobs/Species/Human/displacement.rsi + state: jumpsuit-female + - type: HumanoidAppearance + species: Lamia + - type: DamageVisuals + thresholds: [ 60, 120, 200 ] + targetLayers: + - "enum.HumanoidVisualLayers.Chest" + - "enum.HumanoidVisualLayers.Head" + - "enum.HumanoidVisualLayers.LArm" + - "enum.HumanoidVisualLayers.RArm" + damageOverlayGroups: + Brute: + sprite: Nyanotrasen/Mobs/Effects/Lamia/brute_damage.rsi + color: "#FF0000" + Burn: + sprite: Nyanotrasen/Mobs/Effects/Lamia/burn_damage.rsi + +- type: entity + parent: MobObserverVisualHumanoid + id: MobObserverVisualThaven + categories: [ HideSpawnMenu ] + components: + - type: Inventory + templateId: thaven + displacements: + jumpsuit: + sizeMaps: + 32: + sprite: _Impstation/Mobs/Species/Thaven/displacement.rsi + state: jumpsuit + copyToShaderParameters: + layerKey: dummy + parameterTexture: displacementMap + parameterUV: displacementUV + head: + sizeMaps: + 32: + sprite: _Impstation/Mobs/Species/Thaven/displacement.rsi + state: head + copyToShaderParameters: + layerKey: dummy + parameterTexture: displacementMap + parameterUV: displacementUV + eyes: + sizeMaps: + 32: + sprite: _Impstation/Mobs/Species/Thaven/displacement.rsi + state: eyes + copyToShaderParameters: + layerKey: dummy + parameterTexture: displacementMap + parameterUV: displacementUV + ears: + sizeMaps: + 32: + sprite: _Impstation/Mobs/Species/Thaven/displacement.rsi + state: head + copyToShaderParameters: + layerKey: dummy + parameterTexture: displacementMap + parameterUV: displacementUV + mask: + sizeMaps: + 32: + sprite: _Impstation/Mobs/Species/Thaven/displacement.rsi + state: mask + copyToShaderParameters: + layerKey: dummy + parameterTexture: displacementMap + parameterUV: displacementUV + neck: + sizeMaps: + 32: + sprite: _Impstation/Mobs/Species/Thaven/displacement.rsi + state: neck + copyToShaderParameters: + layerKey: dummy + parameterTexture: displacementMap + parameterUV: displacementUV + outerClothing: + sizeMaps: + 32: + sprite: _Impstation/Mobs/Species/Thaven/displacement.rsi + state: outerclothing_hardsuit + copyToShaderParameters: + layerKey: dummy + parameterTexture: displacementMap + parameterUV: displacementUV + gloves: + sizeMaps: + 32: + sprite: _Impstation/Mobs/Species/Thaven/displacement.rsi + state: hands + copyToShaderParameters: + layerKey: dummy + parameterTexture: displacementMap + parameterUV: displacementUV + - type: HumanoidAppearance + species: Thaven + +- type: entity + parent: MobObserverVisualHumanoid + id: MobObserverVisualHuman + categories: [ HideSpawnMenu ] + components: + - type: HumanoidAppearance + species: Human + +- type: entity + parent: MobObserverVisualHumanoid + id: MobObserverVisualAbductor + categories: [ HideSpawnMenu ] + components: + - type: HumanoidAppearance + species: Abductor + +- type: entity + parent: MobObserverVisualHumanoid + id: MobObserverVisualArachnid + categories: [ HideSpawnMenu ] + components: + - type: HumanoidAppearance + species: Arachnid + - type: Inventory + templateId: arachnid + - type: DamageVisuals + thresholds: [ 10, 20, 30, 50, 70, 100 ] + targetLayers: + - "enum.HumanoidVisualLayers.Chest" + - "enum.HumanoidVisualLayers.Head" + - "enum.HumanoidVisualLayers.LArm" + - "enum.HumanoidVisualLayers.LLeg" + - "enum.HumanoidVisualLayers.RArm" + - "enum.HumanoidVisualLayers.RLeg" + damageOverlayGroups: + Brute: + sprite: Mobs/Effects/brute_damage.rsi + color: "#162581" + Burn: + sprite: Mobs/Effects/burn_damage.rsi + +- type: entity + parent: MobObserverVisualHumanoid + id: MobObserverVisualChitinid + categories: [ HideSpawnMenu ] + components: + - type: HumanoidAppearance + species: Chitinid + - type: Inventory + speciesId: Chitinid + femaleDisplacements: + jumpsuit: + sizeMaps: + 32: + sprite: Mobs/Species/Human/displacement.rsi + state: jumpsuit-female + +- type: entity + parent: MobObserverVisualHumanoid + id: MobObserverVisualDiona + categories: [ HideSpawnMenu ] + components: + - type: HumanoidAppearance + species: Diona + - type: Inventory + templateId: diona + femaleDisplacements: + jumpsuit: + sizeMaps: + 32: + sprite: Mobs/Species/Human/displacement.rsi + state: jumpsuit-female + - type: DamageVisuals + thresholds: [ 10, 20, 30, 50, 70, 100 ] + targetLayers: + - "enum.HumanoidVisualLayers.Chest" + - "enum.HumanoidVisualLayers.Head" + - "enum.HumanoidVisualLayers.LArm" + - "enum.HumanoidVisualLayers.LLeg" + - "enum.HumanoidVisualLayers.RArm" + - "enum.HumanoidVisualLayers.RLeg" + damageOverlayGroups: + Brute: + sprite: Mobs/Effects/brute_damage.rsi + color: "#cd7314" + Burn: + sprite: Mobs/Effects/burn_damage.rsi + +- type: entity + parent: MobObserverVisualHumanoid + id: MobObserverVisualDwarf + categories: [ HideSpawnMenu ] + components: + - type: HumanoidAppearance + species: Dwarf + - type: Inventory + femaleDisplacements: + jumpsuit: + sizeMaps: + 32: + sprite: Mobs/Species/Human/displacement.rsi + state: jumpsuit-female + +- type: entity + parent: MobObserverVisualHumanoid + id: MobObserverVisualFelinid + categories: [ HideSpawnMenu ] + components: + - type: Sprite + scale: 0.8, 0.8 + - type: HumanoidAppearance + species: Felinid + - type: Inventory + femaleDisplacements: + jumpsuit: + sizeMaps: + 32: + sprite: Mobs/Species/Human/displacement.rsi + state: jumpsuit-female + +- type: entity + parent: MobObserverVisualHumanoid + id: MobObserverVisualGingerbread + categories: [ HideSpawnMenu ] + components: + - type: HumanoidAppearance + species: Gingerbread + - type: Inventory + femaleDisplacements: + jumpsuit: + sizeMaps: + 32: + sprite: Mobs/Species/Human/displacement.rsi + state: jumpsuit-female + - type: DamageVisuals + thresholds: [ 10, 20, 30, 50, 70, 100 ] + targetLayers: + - "enum.HumanoidVisualLayers.Chest" + - "enum.HumanoidVisualLayers.Head" + - "enum.HumanoidVisualLayers.LArm" + - "enum.HumanoidVisualLayers.LLeg" + - "enum.HumanoidVisualLayers.RArm" + - "enum.HumanoidVisualLayers.RLeg" + damageOverlayGroups: + Brute: + sprite: Mobs/Effects/brute_damage.rsi + color: "#896e55" + Burn: + sprite: Mobs/Effects/burn_damage.rsi + +- type: entity + parent: MobObserverVisualHumanoid + id: MobObserverVisualIPC + categories: [ HideSpawnMenu ] + components: + - type: HumanoidAppearance + species: IPC + - type: Inventory + templateId: ipc + +- type: entity + parent: MobObserverVisualHumanoid + id: MobObserverVisualKobold + categories: [ HideSpawnMenu ] + components: + - type: HumanoidAppearance + species: Kobold + +- type: entity + parent: MobObserverVisualHumanoid + id: MobObserverVisualMonkey + categories: [ HideSpawnMenu ] + components: + - type: HumanoidAppearance + species: Monkey + +- type: entity + parent: MobObserverVisualHumanoid + id: MobObserverVisualOni + categories: [ HideSpawnMenu ] + components: + - type: Sprite + scale: 1.2, 1.2 + - type: HumanoidAppearance + species: Oni + - type: Inventory + femaleDisplacements: + jumpsuit: + sizeMaps: + 32: + sprite: Mobs/Species/Human/displacement.rsi + state: jumpsuit-female + +- type: entity + parent: MobObserverVisualHumanoid + id: MobObserverVisualPlasmaman + categories: [ HideSpawnMenu ] + components: + - type: HumanoidAppearance + species: Plasmaman + - type: Inventory + templateId: plasmaman + femaleDisplacements: + jumpsuit: + sizeMaps: + 32: + sprite: Mobs/Species/Human/displacement.rsi + state: jumpsuit-female + - type: DamageVisuals + thresholds: [ 10, 20, 30, 50, 70, 100 ] + targetLayers: + - "enum.HumanoidVisualLayers.Chest" + - "enum.HumanoidVisualLayers.Head" + - "enum.HumanoidVisualLayers.LArm" + - "enum.HumanoidVisualLayers.LLeg" + - "enum.HumanoidVisualLayers.RArm" + - "enum.HumanoidVisualLayers.RLeg" + damageOverlayGroups: + Brute: + sprite: Mobs/Effects/brute_damage.rsi + color: "#555555AA" + Burn: + sprite: Mobs/Effects/burn_damage.rsi + +- type: entity + parent: MobObserverVisualHumanoid + id: MobObserverVisualReptilian + categories: [ HideSpawnMenu ] + components: + - type: HumanoidAppearance + species: Reptilian + - type: Inventory + speciesId: reptilian + femaleDisplacements: + jumpsuit: + sizeMaps: + 32: + sprite: _White/Mobs/Species/Reptilian/displacement.rsi + state: jumpsuit-female-digi + shoes: + sizeMaps: + 32: + sprite: _White/Mobs/Species/Reptilian/displacement.rsi + state: shoes-digi + displacements: + jumpsuit: + sizeMaps: + 32: + sprite: _White/Mobs/Species/Reptilian/displacement.rsi + state: jumpsuit-digi + shoes: + sizeMaps: + 32: + sprite: _White/Mobs/Species/Reptilian/displacement.rsi + state: shoes-digi + +- type: entity + parent: MobObserverVisualHumanoid + id: MobObserverVisualResomi + categories: [ HideSpawnMenu ] + components: + - type: HumanoidAppearance + species: Resomi + - type: Inventory + speciesId: resomi + displacements: + jumpsuit: + sizeMaps: + 32: + sprite: _White/Mobs/Species/Resomi/displacement.rsi + state: jumpsuit + eyes: + sizeMaps: + 32: + sprite: _White/Mobs/Species/Resomi/displacement.rsi + state: eyes + gloves: + sizeMaps: + 32: + sprite: _White/Mobs/Species/Resomi/displacement.rsi + state: hands + head: + sizeMaps: + 32: + sprite: _White/Mobs/Species/Resomi/displacement.rsi + state: head + back: + sizeMaps: + 32: + sprite: _White/Mobs/Species/Resomi/displacement.rsi + state: back + ears: + sizeMaps: + 32: + sprite: _White/Mobs/Species/Resomi/displacement.rsi + state: ears + shoes: + sizeMaps: + 32: + sprite: _White/Mobs/Species/Resomi/displacement.rsi + state: feet + neck: + sizeMaps: + 32: + sprite: _White/Mobs/Species/Resomi/displacement.rsi + state: neck + mask: + sizeMaps: + 32: + sprite: _White/Mobs/Species/Resomi/displacement.rsi + state: mask + suitstorage: + sizeMaps: + 32: + sprite: _White/Mobs/Species/Resomi/displacement.rsi + state: suitStorage + belt: + sizeMaps: + 32: + sprite: _White/Mobs/Species/Resomi/displacement.rsi + state: belt + - type: DamageVisuals + thresholds: [ 10, 30, 50, 70 ] + targetLayers: + - "enum.HumanoidVisualLayers.Chest" + - "enum.HumanoidVisualLayers.Head" + - "enum.HumanoidVisualLayers.LArm" + - "enum.HumanoidVisualLayers.LLeg" + - "enum.HumanoidVisualLayers.RArm" + - "enum.HumanoidVisualLayers.RLeg" + damageOverlayGroups: + Brute: + sprite: _White/Mobs/Effects/Resomi/brute_damage.rsi + color: "#C048C2" + Burn: + sprite: _White/Mobs/Effects/Resomi/burn_damage.rsi + +- type: entity + parent: MobObserverVisualHumanoid + id: MobObserverVisualShadow + categories: [ HideSpawnMenu ] + components: + - type: HumanoidAppearance + species: Shadow + - type: DamageVisuals + thresholds: [ 10, 20, 30, 50, 70, 100 ] + targetLayers: + - "enum.HumanoidVisualLayers.Chest" + - "enum.HumanoidVisualLayers.Head" + - "enum.HumanoidVisualLayers.LArm" + - "enum.HumanoidVisualLayers.LLeg" + - "enum.HumanoidVisualLayers.RArm" + - "enum.HumanoidVisualLayers.RLeg" + damageOverlayGroups: + Brute: + sprite: Mobs/Effects/brute_damage.rsi + color: "#5D3FD3" + Burn: + sprite: Mobs/Effects/burn_damage.rsi + +- type: entity + parent: MobObserverVisualHumanoid + id: MobObserverVisualShadowling + categories: [ HideSpawnMenu ] + components: + - type: HumanoidAppearance + species: Shadowling + - type: DamageVisuals + thresholds: [ 10, 20, 30, 50, 70, 100 ] + targetLayers: + - "enum.HumanoidVisualLayers.Chest" + - "enum.HumanoidVisualLayers.Head" + - "enum.HumanoidVisualLayers.LArm" + - "enum.HumanoidVisualLayers.LLeg" + - "enum.HumanoidVisualLayers.RArm" + - "enum.HumanoidVisualLayers.RLeg" + damageOverlayGroups: + Brute: + sprite: Mobs/Effects/brute_damage.rsi + color: "#5D3FD3" + Burn: + sprite: Mobs/Effects/burn_damage.rsi + +- type: entity + parent: MobObserverVisualHumanoid + id: MobObserverVisualSkeleton + categories: [ HideSpawnMenu ] + components: + - type: HumanoidAppearance + species: Skeleton + - type: Inventory + femaleDisplacements: + jumpsuit: + sizeMaps: + 32: + sprite: Mobs/Species/Human/displacement.rsi + state: jumpsuit-female + - type: DamageVisuals + thresholds: [ 10, 20, 30, 50, 70, 100 ] + targetLayers: + - "enum.HumanoidVisualLayers.Chest" + - "enum.HumanoidVisualLayers.Head" + - "enum.HumanoidVisualLayers.LArm" + - "enum.HumanoidVisualLayers.LLeg" + - "enum.HumanoidVisualLayers.RArm" + - "enum.HumanoidVisualLayers.RLeg" + damageOverlayGroups: + Brute: + sprite: Mobs/Effects/brute_damage.rsi + color: "#555555AA" + Burn: + sprite: Mobs/Effects/burn_damage.rsi + +- type: entity + parent: MobObserverVisualHumanoid + id: MobObserverVisualSlimePerson + categories: [ HideSpawnMenu ] + components: + - type: HumanoidAppearance + species: SlimePerson + - type: Inventory + femaleDisplacements: + jumpsuit: + sizeMaps: + 32: + sprite: Mobs/Species/Human/displacement.rsi + state: jumpsuit-female + - type: DamageVisuals + thresholds: [ 10, 20, 30, 50, 70, 100 ] + targetLayers: + - "enum.HumanoidVisualLayers.Chest" + - "enum.HumanoidVisualLayers.Head" + - "enum.HumanoidVisualLayers.LArm" + - "enum.HumanoidVisualLayers.LLeg" + - "enum.HumanoidVisualLayers.RArm" + - "enum.HumanoidVisualLayers.RLeg" + damageOverlayGroups: + Brute: + sprite: Mobs/Effects/brute_damage.rsi + color: "#2cf274" + Burn: + sprite: Mobs/Effects/burn_damage.rsi + +- type: entity + parent: MobObserverVisualHumanoid + id: MobObserverVisualSynthHuman + categories: [ HideSpawnMenu ] + components: + - type: HumanoidAppearance + species: SynthHuman + +- type: entity + parent: MobObserverVisualHumanoid + id: MobObserverVisualTajaran + categories: [ HideSpawnMenu ] + components: + - type: Sprite + scale: 0.8, 0.8 + - type: HumanoidAppearance + species: Tajaran + - type: Inventory + femaleDisplacements: + jumpsuit: + sizeMaps: + 32: + sprite: Mobs/Species/Human/displacement.rsi + state: jumpsuit-female + +- type: entity + parent: MobObserverVisualHumanoid + id: MobObserverVisualVox + categories: [ HideSpawnMenu ] + components: + - type: HumanoidAppearance + species: Vox + - type: Inventory + speciesId: vox + displacements: + jumpsuit: + sizeMaps: + 32: + sprite: Mobs/Species/Vox/displacement.rsi + state: jumpsuit + eyes: + sizeMaps: + 32: + sprite: Mobs/Species/Vox/displacement.rsi + state: eyes + gloves: + sizeMaps: + 32: + sprite: Mobs/Species/Vox/displacement.rsi + state: hand + head: + sizeMaps: + 32: + sprite: Mobs/Species/Vox/displacement.rsi + state: head + back: + sizeMaps: + 32: + sprite: Mobs/Species/Vox/displacement.rsi + state: back + ears: + sizeMaps: + 32: + sprite: Mobs/Species/Vox/displacement.rsi + state: ears + shoes: + sizeMaps: + 32: + sprite: Mobs/Species/Vox/displacement.rsi + state: shoes + - type: DamageVisuals + thresholds: [ 10, 20, 30, 50, 70, 100 ] + targetLayers: + - "enum.HumanoidVisualLayers.Chest" + - "enum.HumanoidVisualLayers.Head" + - "enum.HumanoidVisualLayers.LArm" + - "enum.HumanoidVisualLayers.LLeg" + - "enum.HumanoidVisualLayers.RArm" + - "enum.HumanoidVisualLayers.RLeg" + damageOverlayGroups: + Brute: + sprite: Mobs/Effects/brute_damage.rsi + color: "#7a8bf2" + Burn: + sprite: Mobs/Effects/burn_damage.rsi + +- type: entity + parent: MobObserverVisualHumanoid + id: MobObserverVisualVulpkanin + categories: [ HideSpawnMenu ] + components: + - type: HumanoidAppearance + species: Vulpkanin + - type: Inventory + speciesId: vulpkanin + femaleDisplacements: + jumpsuit: + sizeMaps: + 32: + sprite: Mobs/Species/Human/displacement.rsi + state: jumpsuit-female + +- type: entity + parent: MobObserverVisualHumanoid + id: MobObserverVisualXelthia + categories: [ HideSpawnMenu ] + components: + - type: HumanoidAppearance + species: Xelthia + - type: Inventory + templateId: xelthia + speciesId: Xelthia + displacements: + jumpsuit: + sizeMaps: + 32: + sprite: _EE/Mobs/Species/Xelthia/displacement.rsi + state: jumpsuit + copyToShaderParameters: + layerKey: dummy + parameterTexture: displacementMap + parameterUV: displacementUV + head: + sizeMaps: + 32: + sprite: _EE/Mobs/Species/Xelthia/displacement.rsi + state: jumpsuit + copyToShaderParameters: + layerKey: dummy + parameterTexture: displacementMap + parameterUV: displacementUV + shoes: + sizeMaps: + 32: + sprite: _EE/Mobs/Species/Xelthia/displacement.rsi + state: jumpsuit + copyToShaderParameters: + layerKey: dummy + parameterTexture: displacementMap + parameterUV: displacementUV + mask: + sizeMaps: + 32: + sprite: _EE/Mobs/Species/Xelthia/displacement.rsi + state: jumpsuit + copyToShaderParameters: + layerKey: dummy + parameterTexture: displacementMap + parameterUV: displacementUV + outerClothing: + sizeMaps: + 32: + sprite: _EE/Mobs/Species/Xelthia/displacement.rsi + state: jumpsuit + copyToShaderParameters: + layerKey: dummy + parameterTexture: displacementMap + parameterUV: displacementUV + suitstorage: + sizeMaps: + 32: + sprite: _EE/Mobs/Species/Xelthia/displacement.rsi + state: jumpsuit + copyToShaderParameters: + layerKey: dummy + parameterTexture: displacementMap + parameterUV: displacementUV + eyes: + sizeMaps: + 32: + sprite: _EE/Mobs/Species/Xelthia/displacement.rsi + state: jumpsuit + copyToShaderParameters: + layerKey: dummy + parameterTexture: displacementMap + parameterUV: displacementUV + gloves: + sizeMaps: + 32: + sprite: _EE/Mobs/Species/Xelthia/displacement.rsi + state: jumpsuit + copyToShaderParameters: + layerKey: dummy + parameterTexture: displacementMap + parameterUV: displacementUV + ears: + sizeMaps: + 32: + sprite: _EE/Mobs/Species/Xelthia/displacement.rsi + state: jumpsuit + copyToShaderParameters: + layerKey: dummy + parameterTexture: displacementMap + parameterUV: displacementUV + neck: + sizeMaps: + 32: + sprite: _EE/Mobs/Species/Xelthia/displacement.rsi + state: jumpsuit + copyToShaderParameters: + layerKey: dummy + parameterTexture: displacementMap + parameterUV: displacementUV + belt: + sizeMaps: + 32: + sprite: _EE/Mobs/Species/Xelthia/displacement.rsi + state: jumpsuit + copyToShaderParameters: + layerKey: dummy + parameterTexture: displacementMap + parameterUV: displacementUV + back: + sizeMaps: + 32: + sprite: _EE/Mobs/Species/Xelthia/displacement.rsi + state: backpack + copyToShaderParameters: + layerKey: dummy + parameterTexture: displacementMap + parameterUV: displacementUV + - type: DamageVisuals + thresholds: [ 10, 20, 30, 50, 70, 100 ] + targetLayers: + - "enum.HumanoidVisualLayers.Chest" + - "enum.HumanoidVisualLayers.Head" + - "enum.HumanoidVisualLayers.LArm" + - "enum.HumanoidVisualLayers.LLeg" + - "enum.HumanoidVisualLayers.RArm" + - "enum.HumanoidVisualLayers.RLeg" + damageOverlayGroups: + Brute: + sprite: Mobs/Effects/brute_damage.rsi + color: "#00FF69" + - type: entity id: ActionGhostBoo name: Boo! diff --git a/Resources/Prototypes/Shaders/outline.yml b/Resources/Prototypes/Shaders/outline.yml index 57ea6a485a9..35046e147a4 100644 --- a/Resources/Prototypes/Shaders/outline.yml +++ b/Resources/Prototypes/Shaders/outline.yml @@ -7,6 +7,7 @@ light_boost: 2 light_gamma: 1.5 light_whitepoint: 48 + alpha_cutoff: 0.0 - type: shader id: SelectionOutlineInrange @@ -17,3 +18,4 @@ light_boost: 2 light_gamma: 0.9 light_whitepoint: 1 + alpha_cutoff: 0.0 diff --git a/Resources/Prototypes/Shaders/shaders.yml b/Resources/Prototypes/Shaders/shaders.yml index 04623cd0b4a..c2407069ab5 100644 --- a/Resources/Prototypes/Shaders/shaders.yml +++ b/Resources/Prototypes/Shaders/shaders.yml @@ -124,6 +124,11 @@ kind: source path: "/Textures/Shaders/color_tint.swsl" +- type: shader + id: GhostCompositeTint + kind: source + path: "/Textures/Shaders/ghost_composite_tint.swsl" + - type: shader id: Ethereal kind: source @@ -137,4 +142,4 @@ - type: shader id: Hologram kind: source - path: "/Textures/Shaders/hologram.swsl" \ No newline at end of file + path: "/Textures/Shaders/hologram.swsl" diff --git a/Resources/Textures/Shaders/ghost_composite_tint.swsl b/Resources/Textures/Shaders/ghost_composite_tint.swsl new file mode 100644 index 00000000000..023030a1ef8 --- /dev/null +++ b/Resources/Textures/Shaders/ghost_composite_tint.swsl @@ -0,0 +1,12 @@ +light_mode unshaded; + +uniform lowp vec3 ghost_tint; // RGB between 0 and 1. +uniform lowp float ghost_alpha; // Alpha between 0 and 1. + +void fragment() +{ + highp vec4 col = zTexture(UV); + col.rgb *= ghost_tint; + col.a *= ghost_alpha; + COLOR = col; +} diff --git a/Resources/Textures/Shaders/outline.swsl b/Resources/Textures/Shaders/outline.swsl index df031d7d6e7..50d9946b4f9 100644 --- a/Resources/Textures/Shaders/outline.swsl +++ b/Resources/Textures/Shaders/outline.swsl @@ -33,48 +33,49 @@ uniform bool outline_fullbright; // =false; uniform highp float light_boost; // = 4.0; uniform highp float light_gamma; // = 1.0; uniform highp float light_whitepoint; // = 1.0; +uniform highp float alpha_cutoff; // = 0.0; + +highp float mask_alpha(highp float alpha) +{ + if (alpha_cutoff <= 0.0) + return alpha; + + return alpha > alpha_cutoff ? 1.0 : 0.0; +} void fragment() { highp vec4 col = zTexture(UV); highp vec2 ps = TEXTURE_PIXEL_SIZE; highp float a; - highp float maxa = col.a; - highp float mina = col.a; + highp float cola = mask_alpha(col.a); + highp float maxa = cola; // note: these bypass zTexture because only alpha is queried. - a = texture2D(TEXTURE, UV + vec2(0.0, -outline_width)*ps).a; + a = mask_alpha(texture2D(TEXTURE, UV + vec2(0.0, -outline_width)*ps).a); maxa = max(a, maxa); - mina = min(a, mina); - a = texture2D(TEXTURE, UV + vec2(-outline_width, -outline_width)*ps).a; + a = mask_alpha(texture2D(TEXTURE, UV + vec2(-outline_width, -outline_width)*ps).a); maxa = max(a, maxa); - mina = min(a, mina); - a = texture2D(TEXTURE, UV + vec2(0.0, outline_width)*ps).a; + a = mask_alpha(texture2D(TEXTURE, UV + vec2(0.0, outline_width)*ps).a); maxa = max(a, maxa); - mina = min(a, mina); - a = texture2D(TEXTURE, UV + vec2(outline_width, -outline_width)*ps).a; + a = mask_alpha(texture2D(TEXTURE, UV + vec2(outline_width, -outline_width)*ps).a); maxa = max(a, maxa); - mina = min(a, mina); - a = texture2D(TEXTURE, UV + vec2(-outline_width,0.0)*ps).a; + a = mask_alpha(texture2D(TEXTURE, UV + vec2(-outline_width,0.0)*ps).a); maxa = max(a, maxa); - mina = min(a, mina); - a = texture2D(TEXTURE, UV + vec2(-outline_width, outline_width)*ps).a; + a = mask_alpha(texture2D(TEXTURE, UV + vec2(-outline_width, outline_width)*ps).a); maxa = max(a, maxa); - mina = min(a, mina); - a = texture2D(TEXTURE, UV + vec2(outline_width, 0.0)*ps).a; + a = mask_alpha(texture2D(TEXTURE, UV + vec2(outline_width, 0.0)*ps).a); maxa = max(a, maxa); - mina = min(a, mina); - a = texture2D(TEXTURE, UV + vec2(outline_width, outline_width)*ps).a; + a = mask_alpha(texture2D(TEXTURE, UV + vec2(outline_width, outline_width)*ps).a); maxa = max(a, maxa); - mina = min(a, mina); lowp float sampledLight = outline_fullbright ? 1.0 : clamp( (pow( zGrayscale_BT601(lightSample.rgb) * light_whitepoint, light_gamma) / light_whitepoint ) * light_boost, 0.0, 1.0); - COLOR = mix(col, outline_color * vec4(vec3(1.0), sampledLight), maxa - col.a); + COLOR = mix(col, outline_color * vec4(vec3(1.0), sampledLight), maxa - cola); lightSample = vec3(1.0); }