using Content.Server.Bible.Components; using Content.Server.Speech.Components; using Content.Server.Storage.Components; using Content.Server.Objectives.Components; using Content.Shared.Bed.Sleep; using Content.Shared.Body.Components; using Content.Shared.Chat.Prototypes; using Content.Shared.Chemistry.Components; using Content.Shared.Cuffs.Components; using Content.Shared.Damage; using Content.Shared.Database; using Content.Shared.DoAfter; using Content.Shared.FixedPoint; using Content.Shared.Humanoid; using Content.Shared.Interaction; using Content.Shared.Mobs; using Content.Shared.Mobs.Components; using Content.Shared.Popups; using Content.Shared.Polymorph; using Content.Shared.Prying.Components; using Content.Shared.Stealth.Components; using Content.Shared.Stunnable; using Content.Shared.Vampire; using Content.Shared.Vampire.Components; using Content.Shared.Weapons.Melee; using Content.Shared._Sunrise.ThermalVision; using Robust.Shared.Audio; using Robust.Shared.Containers; using Robust.Shared.Utility; using System.Collections.Frozen; using System.Diagnostics.CodeAnalysis; using Content.Shared.Flash; using Content.Shared.Flash.Components; using Content.Shared.Storage.Components; using Content.Shared.Damage.Components; namespace Content.Server.Vampire; public sealed partial class VampireSystem { private FrozenDictionary _powerCache = default!; private FrozenDictionary _passiveCache = default!; private void InitializePowers() { _powerCache = BuildPowerCache(); _passiveCache = BuildPassiveCache(); //Abilities SubscribeLocalEvent(OnVampireOpenMutationsMenu); SubscribeLocalEvent(OnVampireToggleFangs); SubscribeLocalEvent(OnVampireGlare); SubscribeLocalEvent(OnVampireScreech); SubscribeLocalEvent(OnVampirePolymorph); SubscribeLocalEvent(OnVampireHypnotise); SubscribeLocalEvent(OnVampireBloodSteal); SubscribeLocalEvent(OnVampireUnholyStrength); SubscribeLocalEvent(OnVampireSupernaturalStrength); SubscribeLocalEvent(OnVampireCloakOfDarkness); SubscribeLocalEvent(OnVampireBloodScale); SubscribeLocalEvent(OnVampireThermalVision); //Hypnotise SubscribeLocalEvent(HypnotiseDoAfter); //Drink Blood SubscribeLocalEvent(OnInteractHandEvent); SubscribeLocalEvent(DrinkDoAfter); //Deaths embrace SubscribeLocalEvent(OnInsertedIntoContainer); SubscribeLocalEvent(OnRemovedFromContainer); SubscribeLocalEvent(OnVampireStateChanged); } #region Ability Entry Points private void OnVampireOpenMutationsMenu(EntityUid uid, VampireComponent component, VampireOpenMutationsMenu ev) { TryOpenUi(uid, ev.Performer, component); ev.Handled = true; } private void OnVampireToggleFangs(EntityUid entity, VampireComponent component, VampireToggleFangsEvent ev) { if (!TryGetPowerDefinition(ev.DefinitionName, out var def)) return; var vampire = new Entity(entity, component); if (!IsAbilityUsable(vampire, def)) return; var actionEntity = GetPowerEntity(vampire, def.ID); if (actionEntity == null) return; var toggled = ToggleFangs(vampire); _action.SetToggled(actionEntity, toggled); ev.Handled = true; } private void OnVampireGlare(EntityUid entity, VampireComponent component, VampireGlareEvent ev) { if (!TryGetPowerDefinition(ev.DefinitionName, out var def)) return; var vampire = new Entity(entity, component); if (!IsAbilityUsable(vampire, def)) return; Glare(vampire, ev.Target, def.Duration, def.Damage); ev.Handled = true; } private void OnVampireScreech(EntityUid entity, VampireComponent component, VampireScreechEvent ev) { if (!TryGetPowerDefinition(ev.DefinitionName, out var def)) return; var vampire = new Entity(entity, component); if (!IsAbilityUsable(vampire, def)) return; Screech(vampire, def.Duration, def.Damage); ev.Handled = true; } private void OnVampirePolymorph(EntityUid entity, VampireComponent component, VampirePolymorphEvent ev) { if (!TryGetPowerDefinition(ev.DefinitionName, out var def)) return; var vampire = new Entity(entity, component); if (!IsAbilityUsable(vampire, def)) return; PolymorphSelf(vampire, def.PolymorphTarget); ev.Handled = true; } private void OnVampireHypnotise(EntityUid entity, VampireComponent component, VampireHypnotiseEvent ev) { if (!TryGetPowerDefinition(ev.DefinitionName, out var def)) return; var vampire = new Entity(entity, component); if (!IsAbilityUsable(vampire, def)) return; ev.Handled = TryHypnotise(vampire, ev.Target, def.Duration, def.DoAfterDelay); } private void OnVampireBloodSteal(EntityUid entity, VampireComponent component, VampireBloodStealEvent ev) { if (!TryGetPowerDefinition(ev.DefinitionName, out var def)) return; var vampire = new Entity(entity, component); if (!IsAbilityUsable(vampire, def)) return; BloodSteal(vampire); ev.Handled = true; } private void OnVampireUnholyStrength(EntityUid entity, VampireComponent component, VampireUnholyStrengthEvent ev) { if (!TryGetPowerDefinition(ev.DefinitionName, out var def)) return; var vampire = new Entity(entity, component); if (!IsAbilityUsable(vampire, def)) return; UnnaturalStrength(vampire); ev.Handled = true; } private void OnVampireSupernaturalStrength(EntityUid entity, VampireComponent component, VampireSupernaturalStrengthEvent ev) { if (!TryGetPowerDefinition(ev.DefinitionName, out var def)) return; var vampire = new Entity(entity, component); if (!IsAbilityUsable(vampire, def)) return; SupernaturalStrength(vampire); ev.Handled = true; } private void OnVampireCloakOfDarkness(EntityUid entity, VampireComponent component, VampireCloakOfDarknessEvent ev) { if (!TryGetPowerDefinition(ev.DefinitionName, out var def)) return; var vampire = new Entity(entity, component); if (!IsAbilityUsable(vampire, def)) return; var actionEntity = GetPowerEntity(vampire.Comp, def.ID); if (actionEntity == null) return; var toggled = CloakOfDarkness(vampire, def.Upkeep, 0.75f, -0.5f); _action.SetToggled(actionEntity, toggled); ev.Handled = true; } private void OnVampireBloodScale(EntityUid entity, VampireComponent component, VampireBloodScaleEvent ev) { if (!TryGetPowerDefinition(ev.DefinitionName, out var def)) return; var vampire = new Entity(entity, component); if (!IsAbilityUsable(vampire, def)) return; ToggleBloodScale(entity, vampire); var scale = EnsureComp(vampire); scale.Upkeep = 4f; ev.Handled = true; } private void ToggleBloodScale(EntityUid uid, VampireComponent comp) { var scale = EnsureComp(uid); if (!comp.BloodScaleActive) { _speed.ChangeBaseSpeed(uid, 4.5f, 6f, 20f); _popup.PopupEntity(Loc.GetString("blood-scale-start"), uid, uid); comp.BloodScaleActive = true; scale.IsActive = true; } else { _speed.ChangeBaseSpeed(uid, 2.5f, 4.5f, 20f); _popup.PopupEntity(Loc.GetString("blood-scale-end"), uid, uid); comp.BloodScaleActive = false; scale.IsActive = false; } } private void OnVampireThermalVision(EntityUid entity, VampireComponent component, VampireThermalVisionEvent ev) { if (!TryGetPowerDefinition(ev.DefinitionName, out var def)) return; if (!IsAbilityUsable((entity, component), def)) return; // Toggle thermal vision component if (HasComp(entity)) { RemComp(entity); _popup.PopupEntity(Loc.GetString("vampire-thermal-vision-off"), entity, entity); } else { EnsureComp(entity); _popup.PopupEntity(Loc.GetString("vampire-thermal-vision-on"), entity, entity); } } private void OnInteractHandEvent(EntityUid uid, VampireComponent component, BeforeInteractHandEvent args) { if (!HasComp(args.Target)) return; if (args.Target == uid) return; if (!TryGetPowerDefinition(VampireComponent.DrinkBloodPrototype, out var def)) return; var vampire = new Entity(uid, component); args.Handled = TryDrink(vampire, args.Target, def.DoAfterDelay!.Value); } #endregion private bool TryGetPowerDefinition(string name, [NotNullWhen(true)] out VampirePowerProtype? definition) => _powerCache.TryGetValue(name, out definition); private bool IsAbilityUsable(Entity vampire, VampirePowerProtype def) { if (!IsPowerUnlocked(vampire, def.ID)) return false; //Block if we are cuffed if (!def.UsableWhileCuffed && TryComp(vampire, out var cuffable) && !cuffable.CanStillInteract) { _popup.PopupEntity(Loc.GetString("vampire-cuffed"), vampire, vampire, PopupType.MediumCaution); return false; } //Block if we are stunned if (!def.UsableWhileStunned && HasComp(vampire)) { _popup.PopupEntity(Loc.GetString("vampire-stunned"), vampire, vampire, PopupType.MediumCaution); return false; } //Block if we are muzzled - so far only one item does this? if (!def.UsableWhileMuffled && TryComp(vampire, out var accent) && accent.Accent.Equals("mumble")) { _popup.PopupEntity(Loc.GetString("vampire-muffled"), vampire, vampire, PopupType.MediumCaution); return false; } //Block if we dont have enough essence if (def.ActivationCost > 0 && !SubtractBloodEssence(vampire, def.ActivationCost)) { _popup.PopupClient(Loc.GetString("vampire-not-enough-blood"), vampire, vampire, PopupType.MediumCaution); return false; } //Check if we are near an anchored prayable entity - ie the chapel if (IsNearPrayable(vampire)) { //Warning about holy power return false; } return true; } #region Activeable Gargantua Powers private void UnnaturalStrength(Entity vampire) { var damage = new DamageSpecifier(); damage.DamageDict.Add("Slash", 10); var meleeComp = EnsureComp(vampire); if (HasComp(vampire)) { meleeComp.Damage = meleeComp.Damage - damage; RemComp(vampire); } else { var strength = EnsureComp(vampire); strength.Upkeep = 1f; strength.Power = "UnholyStrength"; _popup.PopupEntity(Loc.GetString("vampire-unnaturalstrength", ("user", vampire)), vampire, vampire, Shared.Popups.PopupType.SmallCaution); meleeComp.Damage += damage; } } private void SupernaturalStrength(Entity vampire) { var damage = new DamageSpecifier(); damage.DamageDict.Add("Slash", 15); var meleeComp = EnsureComp(vampire); if (HasComp(vampire)) { meleeComp.Damage = meleeComp.Damage - damage; RemComp(vampire); RemComp(vampire); } else { var pryComp = EnsureComp(vampire); pryComp.Force = true; pryComp.PryPowered = true; _popup.PopupEntity(Loc.GetString("vampire-supernaturalstrength", ("user", vampire)), vampire, vampire, Shared.Popups.PopupType.SmallCaution); meleeComp.Damage += damage; } } #endregion #region Other Powers private void Screech(Entity vampire, TimeSpan? duration, DamageSpecifier? damage = null) { foreach (var entity in _entityLookup.GetEntitiesInRange(vampire, 3, LookupFlags.Approximate | LookupFlags.Dynamic | LookupFlags.Static | LookupFlags.Sundries)) { if (HasComp(entity)) continue; if (HasComp(entity)) continue; if (HasComp(entity)) { _stun.TryAddParalyzeDuration(entity, duration ?? TimeSpan.FromSeconds(3)); _chat.TryEmoteWithoutChat(entity, _prototypeManager.Index(VampireComponent.ScreamEmoteProto), true); } if (damage != null) _damageableSystem.TryChangeDamage(entity, damage); } } private void Glare(Entity vampire, EntityUid? target, TimeSpan? duration, DamageSpecifier? damage = null) { if (!target.HasValue) return; if (HasComp(target)) return; if (HasComp(target)) return; if (HasComp(target)) { _stun.TryAddParalyzeDuration(vampire, duration ?? TimeSpan.FromSeconds(3)); _chat.TryEmoteWithoutChat(vampire.Owner, _prototypeManager.Index(VampireComponent.ScreamEmoteProto), true); if (damage != null) _damageableSystem.TryChangeDamage(vampire.Owner, damage); return; } _stun.TryAddParalyzeDuration(target.Value, duration ?? TimeSpan.FromSeconds(3)); } private void PolymorphSelf(Entity vampire, string? polymorphTarget) { if (string.IsNullOrEmpty(polymorphTarget)) return; var prototypeId = polymorphTarget switch { "MobMouse" => "VampireMouse", "mobBatVampire" => "VampireBat", _ => null }; if (prototypeId == null) { Logger.Warning($"Unknown polymorph target: {polymorphTarget}. Polymorph operation aborted."); return; } if (!_prototypeManager.TryIndex(prototypeId, out var prototype)) { Logger.Warning($"Unknown prototype: {prototypeId}. Polymorph operation aborted."); return; } _polymorph.PolymorphEntity(vampire, prototype); } private void BloodSteal(Entity vampire) { var transform = Transform(vampire.Owner); var targets = new HashSet(); foreach (var entity in _entityLookup.GetEntitiesInRange(transform.Coordinates, 3, LookupFlags.Approximate | LookupFlags.Dynamic)) { if (entity == vampire.Owner) continue; if (!HasComp(entity)) continue; if (_rotting.IsRotten(entity)) continue; if (HasComp(entity)) continue; if (!TryComp(entity, out var bloodstream) || bloodstream.BloodSolution == null) continue; var victimBloodRemaining = bloodstream.BloodSolution.Value.Comp.Solution.Volume; if (victimBloodRemaining <= 0) continue; var volumeToConsume = (FixedPoint2) Math.Min((float) victimBloodRemaining.Value, 20); //HARDCODE, 20u of blood per person per use targets.Add(entity); //Transfer 80% to the vampire var bloodSolution = _solution.SplitSolution(bloodstream.BloodSolution.Value, volumeToConsume * 0.80); //And spill 20% on the floor _blood.TryModifyBloodLevel(entity, -(volumeToConsume * 0.2)); //Dont check this time, if we are full - just continue anyway TryIngestBlood(vampire, bloodSolution); AddBloodEssence(vampire, volumeToConsume * 0.80); _beam.TryCreateBeam(vampire, entity, "VampireLightning"); _popup.PopupEntity(Loc.GetString("vampire-bloodsteal-other"), entity, entity, Shared.Popups.PopupType.LargeCaution); } var bloodEssence = _vampire.GetBloodEssence(vampire); //Update abilities, add new unlocks UpdateAbilities(vampire, vampire.Comp, "ActionVampireBloodSteal", "BloodSteal" , bloodEssence >= FixedPoint2.New(200) && vampire.Comp.CurrentMutation == VampireMutationsType.Hemomancer); } private bool CloakOfDarkness(Entity vampire, float upkeep, float passiveVisibilityRate, float movementVisibilityRate) { if (HasComp(vampire)) { RemComp(vampire); RemComp(vampire); RemComp(vampire); _popup.PopupEntity(Loc.GetString("vampire-cloak-disable"), vampire, vampire); return false; } else { EnsureComp(vampire); var stealthMovement = EnsureComp(vampire); stealthMovement.PassiveVisibilityRate = passiveVisibilityRate; stealthMovement.MovementVisibilityRate = movementVisibilityRate; var vampireStealth = EnsureComp(vampire); vampireStealth.Upkeep = upkeep; _popup.PopupEntity(Loc.GetString("vampire-cloak-enable"), vampire, vampire); return true; } } #endregion #region Hypnotise private bool TryHypnotise(Entity vampire, EntityUid? target, TimeSpan? duration, TimeSpan? delay) { if (target == null) return false; var attempt = new FlashAttemptEvent(target.Value, vampire.Owner, vampire.Owner); RaiseLocalEvent(target.Value, ref attempt, true); if (attempt.Cancelled) return false; var doAfterEventArgs = new DoAfterArgs(EntityManager, vampire, delay ?? TimeSpan.FromSeconds(5), new VampireHypnotiseDoAfterEvent() { Duration = duration }, eventTarget: vampire, target: target, used: target) { BreakOnMove = true, BreakOnDamage = true, MovementThreshold = 0.01f, DistanceThreshold = 1.0f, NeedHand = false, }; if (_doAfter.TryStartDoAfter(doAfterEventArgs)) { _popup.PopupEntity(Loc.GetString("vampire-hypnotise-other", ("user", vampire.Owner), ("target", target.Value)), target.Value, Shared.Popups.PopupType.SmallCaution); } else { return false; } return true; } private void HypnotiseDoAfter(Entity vampire, ref VampireHypnotiseDoAfterEvent args) { if (!args.Target.HasValue) return; if (args.Cancelled) return; _statusEffects.TryAddStatusEffectDuration(args.Target.Value, SleepingSystem.StatusEffectForcedSleeping, args.Duration ?? TimeSpan.FromSeconds(30)); } #endregion #region Deaths Embrace /// /// When the vampire dies, attempt to activate the Deaths Embrace power /// private void OnVampireStateChanged(EntityUid uid, VampireDeathsEmbraceComponent component, MobStateChangedEvent args) { if (args.OldMobState != MobState.Dead && args.NewMobState == MobState.Dead) { //Home still exists? TryMoveToCoffin((uid, component)); } } /// /// When the vampire is inserted into a container (ie locker, crate etc) check for a coffin, and bind their home to it /// private void OnInsertedIntoContainer(EntityUid uid, VampireDeathsEmbraceComponent component, EntGotInsertedIntoContainerMessage args) { if (HasComp(args.Container.Owner)) { component.HomeCoffin = args.Container.Owner; var vhComp = EnsureComp(args.Entity); vhComp.Healing = component.CoffinHealing; _popup.PopupEntity(Loc.GetString("vampire-deathsembrace-bind"), uid, uid); _admin.Add(LogType.Damaged, LogImpact.Low, $"{ToPrettyString(uid):user} bound a new coffin"); } } /// /// When leaving a container, remove the healing component /// private void OnRemovedFromContainer(EntityUid uid, VampireDeathsEmbraceComponent component, EntGotRemovedFromContainerMessage args) { RemComp(args.Entity); } /// /// Attempt to move the vampire to their bound coffin /// private bool TryMoveToCoffin(Entity vampire) { if (!vampire.Comp.HomeCoffin.HasValue) return false; //Someone smashed your crib bro' if (!Exists(vampire.Comp.HomeCoffin.Value) || LifeStage(vampire.Comp.HomeCoffin.Value) >= EntityLifeStage.Terminating) { vampire.Comp.HomeCoffin = null; return false; } //Your crib.. is not a crib, how? if (!TryComp(vampire.Comp.HomeCoffin, out var coffinEntityStorage)) { vampire.Comp.HomeCoffin = null; return false; } //I guess its full? if (!_entityStorage.CanInsert(vampire, vampire.Comp.HomeCoffin.Value, coffinEntityStorage)) return false; Spawn("Smoke", Transform(vampire).Coordinates); _entityStorage.CloseStorage(vampire.Comp.HomeCoffin.Value, coffinEntityStorage); if (_entityStorage.Insert(vampire, vampire.Comp.HomeCoffin.Value, coffinEntityStorage)) { _admin.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(vampire):user} was moved to their home coffin"); return true; } return false; } /// /// Heal the vampire while they are in the coffin /// Revive them if they are dead and below a ~150ish damage /// private void DoCoffinHeal(EntityUid vampire, VampireHealingComponent healing) { if (healing.Healing == null) return; _damageableSystem.TryChangeDamage(vampire, healing.Healing, true, origin: vampire); //If they are dead and we are below the death threshold - revive if (!TryComp(vampire, out var mobStateComponent)) return; if (!_mobState.IsDead(vampire, mobStateComponent)) return; if (!_mobThreshold.TryGetThresholdForState(vampire, MobState.Dead, out var threshold)) return; if (!TryComp(vampire, out var damageableComponent)) return; //Should be around 150 total damage ish if (damageableComponent.TotalDamage < threshold * 0.75) { _mobState.ChangeMobState(vampire, MobState.Critical, mobStateComponent, vampire); } } #endregion #region Blood Drinking /// /// Toggle if fangs are extended /// private bool ToggleFangs(Entity vampire) { if (HasComp(vampire)) { RemComp(vampire); var popupText = Loc.GetString("vampire-fangs-retracted"); _admin.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(vampire):user} retracted their fangs"); _popup.PopupEntity(popupText, vampire.Owner, vampire.Owner); return false; } else { EnsureComp(vampire); var popupText = Loc.GetString("vampire-fangs-extended"); _admin.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(vampire):user} extended their fangs"); _popup.PopupEntity(popupText, vampire.Owner, vampire.Owner); return true; } } /// /// Check and start drinking blood from a humanoid /// private bool TryDrink(Entity vampire, EntityUid target, TimeSpan doAfterDelay) { //Do a precheck if (!HasComp(vampire)) return false; if (!HasComp(vampire)) return false; if (!_interaction.InRangeUnobstructed(vampire.Owner, target, popup: true)) return false; if (_rotting.IsRotten(target)) { _popup.PopupEntity(Loc.GetString("vampire-blooddrink-rotted"), vampire, vampire, PopupType.SmallCaution); return false; } var doAfterEventArgs = new DoAfterArgs(EntityManager, vampire, doAfterDelay, new VampireDrinkBloodDoAfterEvent() { Volume = vampire.Comp.MouthVolume }, eventTarget: vampire, target: target, used: target) { BreakOnMove = true, BreakOnDamage = true, MovementThreshold = 0.01f, DistanceThreshold = 1.0f, NeedHand = false, Hidden = true }; _doAfter.TryStartDoAfter(doAfterEventArgs); return true; } private void DrinkDoAfter(Entity entity, ref VampireDrinkBloodDoAfterEvent args) { if (args.Cancelled) return; if (!HasComp(entity)) return; if (_rotting.IsRotten(args.Target!.Value)) { _popup.PopupEntity(Loc.GetString("vampire-blooddrink-rotted"), args.User, args.User, PopupType.SmallCaution); return; } if (!TryComp(args.Target, out var targetBloodstream) || targetBloodstream == null || targetBloodstream.BloodSolution == null) return; //Ensure there is enough blood to drain var victimBloodRemaining = targetBloodstream.BloodSolution.Value.Comp.Solution.Volume; if (victimBloodRemaining <= 0) { _popup.PopupEntity(Loc.GetString("vampire-blooddrink-empty"), entity.Owner, entity.Owner, PopupType.SmallCaution); return; } var volumeToConsume = (FixedPoint2) Math.Min((float) victimBloodRemaining.Value, args.Volume); var volumeToDrain = (FixedPoint2) Math.Min((float) victimBloodRemaining.Value, args.Volume * 8); if (_mind.TryGetMind(entity, out var mindId, out var mind)) if (_mind.TryGetObjectiveComp(mindId, out var objective, mind)) objective.BloodDranked = entity.Comp.TotalBloodDrank; //Slurp _audio.PlayPvs(entity.Comp.BloodDrainSound, entity.Owner, AudioParams.Default.WithVolume(-3f)); //Spill an extra 5% on the floor _blood.TryModifyBloodLevel(args.Target.Value, -(volumeToDrain * 0.05)); //Thou shall not feed upon the blood of the holy //TODO: Replace with raised event? if (HasComp(args.Target)) { _damageableSystem.TryChangeDamage(entity.Owner, VampireComponent.HolyDamage, true); _popup.PopupEntity(Loc.GetString("vampire-ingest-holyblood"), entity, entity, PopupType.LargeCaution); _admin.Add(LogType.Damaged, LogImpact.Low, $"{ToPrettyString(entity):user} attempted to drink {volumeToConsume}u of {ToPrettyString(args.Target):target}'s holy blood"); return; } //Check for zombie else { //Pull out some of the blood var bloodSolution = _solution.SplitSolution(targetBloodstream.BloodSolution.Value, volumeToConsume); if (!TryIngestBlood(entity, bloodSolution)) { //Undo, put the blood back _solution.AddSolution(targetBloodstream.BloodSolution.Value, bloodSolution); return; } _admin.Add(LogType.Damaged, LogImpact.Low, $"{ToPrettyString(entity):user} drank {volumeToConsume}u of {ToPrettyString(args.Target):target}'s blood"); AddBloodEssence(entity, volumeToConsume * 0.95); args.Repeat = true; } } /// /// Attempt to insert the solution into the first stomach that has space available /// private bool TryIngestBlood(Entity vampire, Solution ingestedSolution, bool force = false) { //Get all stomaches if (TryComp(vampire.Owner, out var body) && _body.TryGetBodyOrganEntityComps((vampire.Owner, body), out var stomachs)) { //Pick the first one that has space available var firstStomach = stomachs.FirstOrNull(stomach => _stomach.CanTransferSolution(stomach.Owner, ingestedSolution, stomach.Comp1)); if (firstStomach == null) { //We are full _popup.PopupEntity(Loc.GetString("vampire-full-stomach"), vampire.Owner, vampire.Owner, PopupType.SmallCaution); return false; } //Fill the stomach with that delicious blood return _stomach.TryTransferSolution(firstStomach.Value.Owner, ingestedSolution, firstStomach.Value.Comp1); } //No stomach return false; } #endregion private bool IsPowerUnlocked(VampireComponent vampire, string name) { return vampire.UnlockedPowers.ContainsKey(name); } /*private bool IsPowerActive(VampireComponent vampire, VampirePowerProtype def) => IsPowerActive(vampire, def.ID); private bool IsPowerActive(VampireComponent vampire, string name) { return vampire.ActivePowers.Contains(name); } private bool SetPowerActive(VampireComponent vampire, string name, bool active) { if (active) { return vampire.ActivePowers.Add(name); } else { return vampire.ActivePowers.Remove(name); } }*/ /// /// Gets the Action EntityUid for a specific power /// private EntityUid? GetPowerEntity(VampireComponent vampire, string name) { if (!vampire.UnlockedPowers.TryGetValue(name, out var ability)) return null; return GetEntity(ability); } /// /// Cache all power prototypes in a dictionary by keyed by ID /// /// private FrozenDictionary BuildPowerCache() { var protos = _prototypeManager.EnumeratePrototypes(); return protos.ToFrozenDictionary(x => x.ID); } /// /// Cache all passive prototypes in a dictionary by keyed by listing id /// /// private FrozenDictionary BuildPassiveCache() { var protos = _prototypeManager.EnumeratePrototypes(); return protos.ToFrozenDictionary(x => x.CatalogEntry); } }