Merge d3a27692ff into c3c6a6abd9
This commit is contained in:
commit
cb6fb50c38
|
|
@ -0,0 +1,39 @@
|
|||
using Content.Shared._DV.Body.Components;
|
||||
using Content.Shared.Clothing.Components;
|
||||
using Content.Shared.Clothing.EntitySystems;
|
||||
using Robust.Client.GameObjects;
|
||||
|
||||
namespace Content.Client._DV.Body;
|
||||
|
||||
/// <summary>
|
||||
/// Colors feathers.
|
||||
/// </summary>
|
||||
public sealed class FeatherVisualizer : VisualizerSystem<FeatherComponent>
|
||||
{
|
||||
[Dependency] private readonly ClothingSystem _clothing = default!;
|
||||
|
||||
protected override void OnAppearanceChange(EntityUid uid, FeatherComponent component, ref AppearanceChangeEvent args)
|
||||
{
|
||||
if (AppearanceSystem.TryGetData<Color>(uid, FeatherVisuals.FeatherColor, out var featherColor, args.Component))
|
||||
{
|
||||
SpriteSystem.LayerSetColor(uid, FeatherVisualLayers.Feather, featherColor);
|
||||
|
||||
if (TryComp<ClothingComponent>(uid, out var clothing))
|
||||
{
|
||||
foreach (var slotPair in clothing.ClothingVisuals)
|
||||
{
|
||||
_clothing.SetLayerColor(clothing, slotPair.Key, "feather", featherColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (AppearanceSystem.TryGetData<Color>(uid, FeatherVisuals.BloodColor, out var bloodColor, args.Component))
|
||||
SpriteSystem.LayerSetColor(uid, FeatherVisualLayers.Blood, bloodColor);
|
||||
}
|
||||
}
|
||||
|
||||
public enum FeatherVisualLayers : byte
|
||||
{
|
||||
Feather,
|
||||
Blood,
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ using System.Linq;
|
|||
using Content.Shared.Dataset;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Shared.Random.Helpers
|
||||
{
|
||||
|
|
@ -210,5 +211,34 @@ namespace Content.Shared.Random.Helpers
|
|||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
// TODO: REPLACE ALL OF THIS WITH PREDICTED RANDOM WHEN ENGINE PR IS MERGED
|
||||
/// <summary>
|
||||
/// Creates an instance of System.Random that will be the same for both the server and client.
|
||||
/// This allows for the client and server to roll the same results when determining things randomly, preventing mispredictions.
|
||||
/// We generate a unique seed by getting 2-3 unique but predictable integers into a Hashcode.
|
||||
/// </summary>
|
||||
/// <param name="timing">An instance if IGameTiming.
|
||||
/// We use the integer value of the current tick to ensure a different seed every tick.</param>
|
||||
/// <param name="netEnt">The relevant net entity to our seed.
|
||||
/// This allows different entities to have different seeds and therefore different results on the same game-tick.</param>
|
||||
/// <param name="netEnt2">An optional relevant net entity to our seed.
|
||||
/// Typically used if we have an entity checking random potentially multiple times per tick, to ensure we get a unique seed each time.
|
||||
/// This entity should not be the same entity as <see cref="netEnt"/>.</param>
|
||||
public static System.Random PredictedRandom(IGameTiming timing, NetEntity netEnt, NetEntity? netEnt2 = null)
|
||||
{
|
||||
var seed = HashCodeCombine((int)timing.CurTick.Value, netEnt.Id, netEnt2?.Id ?? 0);
|
||||
return new System.Random(seed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks a probability against a <see cref="PredictedRandom"/> instance.
|
||||
/// Returns true if the amount rolled is below the probability.
|
||||
/// </summary>
|
||||
public static bool PredictedProb(IGameTiming timing, float probability, NetEntity netEnt1, NetEntity? netEnt2 = null)
|
||||
{
|
||||
var rand = PredictedRandom(timing, netEnt1, netEnt2);
|
||||
return rand.Prob(probability);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared._DV.Body.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Marks this entity as a feather, that probably has a custom color
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed partial class FeatherComponent : Component
|
||||
{
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public enum FeatherVisuals : byte
|
||||
{
|
||||
FeatherColor,
|
||||
BloodColor,
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
using Content.Shared.Chat.Prototypes;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
|
||||
namespace Content.Shared._DV.Body.Components;
|
||||
|
||||
/// <summary>
|
||||
/// This is used for Avali feather preening functionality.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
|
||||
public sealed partial class PreenableComponent : Component
|
||||
{
|
||||
[DataField]
|
||||
public EntProtoId FeatherPrototype;
|
||||
|
||||
[DataField]
|
||||
public HashSet<ProtoId<DamageGroupPrototype>>? ValidDamageGroups = new()
|
||||
{
|
||||
"Brute",
|
||||
};
|
||||
|
||||
[DataField]
|
||||
public LocId SelfPreeningMessage = "preening-popup-self";
|
||||
|
||||
[DataField]
|
||||
public LocId GettingPreenedMessage = "preening-popup-self-recipient";
|
||||
|
||||
[DataField]
|
||||
public LocId PreeningOtherMessage = "preening-popup-other";
|
||||
|
||||
[DataField]
|
||||
public LocId FeatherBloodiedNameString = "feather-bloody-name-modifier";
|
||||
|
||||
[DataField]
|
||||
public LocId FeatherBloodiedDescString = "feather-bloody-desc";
|
||||
|
||||
[DataField]
|
||||
public LocId PreeningVerbString = "preening-action-verb";
|
||||
|
||||
[DataField]
|
||||
public LocId DroppedFeatherString = "preening-feather-dropped-injured";
|
||||
|
||||
[DataField]
|
||||
public ProtoId<EmotePrototype> ScreamEmote = "Scream";
|
||||
|
||||
/// <summary>
|
||||
/// The minimum amount of damage that must be taken from one attack to have a chance to shed a feather.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public FixedPoint2 ShedDamageThreshold = 9;
|
||||
|
||||
/// <summary>
|
||||
/// The chance for a feather to be shed on hit, per point of damage taken.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float ShedScalingChance = 0.0125f;
|
||||
|
||||
[DataField]
|
||||
public DamageModifierSet? VulnerabilityModifier;
|
||||
|
||||
[DataField, AutoNetworkedField]
|
||||
public int MaximumFeathers = 3;
|
||||
|
||||
[DataField, AutoNetworkedField]
|
||||
public int CurrentFeathers;
|
||||
|
||||
[DataField]
|
||||
public TimeSpan ReplenishDelay = TimeSpan.FromSeconds(150);
|
||||
|
||||
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField]
|
||||
public TimeSpan? ReplenishTime;
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
using Content.Shared.DoAfter;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared._DV.Body.Events;
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed partial class PreeningEvent : SimpleDoAfterEvent;
|
||||
|
|
@ -0,0 +1,229 @@
|
|||
using Content.Shared._DV.Body.Components;
|
||||
using Content.Shared._DV.Body.Events;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Forensics.Systems;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Random.Helpers;
|
||||
using Content.Shared.StatusEffect;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Shared._DV.Body.Systems;
|
||||
|
||||
public sealed class PreenableSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly SharedHandsSystem _hands = default!;
|
||||
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly SharedForensicsSystem _forensics = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metaData = default!;
|
||||
[Dependency] private readonly StatusEffectsSystem _statusEffects = default!;
|
||||
[Dependency] private readonly SharedChatSystem _chat = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<PreenableComponent, GetVerbsEvent<Verb>>(AddVerb);
|
||||
SubscribeLocalEvent<PreenableComponent, PreeningEvent>(OnPreened);
|
||||
SubscribeLocalEvent<PreenableComponent, DamageChangedEvent>(OnDamaged);
|
||||
SubscribeLocalEvent<PreenableComponent, DamageModifyEvent>(OnDamageModify);
|
||||
SubscribeLocalEvent<PreenableComponent, ComponentInit>(OnCompInit);
|
||||
}
|
||||
|
||||
private void OnCompInit(Entity<PreenableComponent> ent, ref ComponentInit args)
|
||||
{
|
||||
ent.Comp.CurrentFeathers = ent.Comp.MaximumFeathers;
|
||||
Dirty(ent);
|
||||
}
|
||||
|
||||
private void AddVerb(Entity<PreenableComponent> ent, ref GetVerbsEvent<Verb> args)
|
||||
{
|
||||
if (!args.CanInteract)
|
||||
return;
|
||||
|
||||
// can't preen with no feathers
|
||||
if (ent.Comp.CurrentFeathers <= 0)
|
||||
return;
|
||||
|
||||
var user = args.User;
|
||||
|
||||
Verb verb = new()
|
||||
{
|
||||
Act = () => AttemptDoAfter(ent, user),
|
||||
Text = Loc.GetString(ent.Comp.PreeningVerbString),
|
||||
};
|
||||
args.Verbs.Add(verb);
|
||||
}
|
||||
|
||||
private void AttemptDoAfter(Entity<PreenableComponent> ent, EntityUid userUid)
|
||||
{
|
||||
var doArgs = new DoAfterArgs(EntityManager, userUid, 5f, new PreeningEvent(), ent, ent)
|
||||
{
|
||||
BreakOnMove = true,
|
||||
BreakOnDamage = true,
|
||||
};
|
||||
|
||||
if (userUid == ent.Owner)
|
||||
{
|
||||
_popup.PopupClient(Loc.GetString(ent.Comp.SelfPreeningMessage), ent, ent);
|
||||
}
|
||||
else
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString(ent.Comp.GettingPreenedMessage, ("preener", Identity.Entity(userUid, EntityManager))), ent, ent, PopupType.Medium);
|
||||
_popup.PopupClient(Loc.GetString(ent.Comp.PreeningOtherMessage, ("preenee", Identity.Entity(ent, EntityManager))), userUid, userUid);
|
||||
}
|
||||
|
||||
_doAfter.TryStartDoAfter(doArgs);
|
||||
}
|
||||
|
||||
private void OnPreened(Entity<PreenableComponent> ent, ref PreeningEvent args)
|
||||
{
|
||||
if (args.Cancelled || args.Handled)
|
||||
return;
|
||||
|
||||
if (ent.Comp.CurrentFeathers <= 0)
|
||||
return;
|
||||
|
||||
var feather = SpawnFeather(ent, false);
|
||||
|
||||
_hands.TryPickupAnyHand(args.User, feather);
|
||||
}
|
||||
|
||||
private void OnDamaged(Entity<PreenableComponent> ent, ref DamageChangedEvent args)
|
||||
{
|
||||
if (args.DamageDelta == null || ent.Comp.ValidDamageGroups == null)
|
||||
return;
|
||||
|
||||
if (ent.Comp.CurrentFeathers <= 0)
|
||||
return;
|
||||
|
||||
var totalApplicableDamage = FixedPoint2.Zero;
|
||||
foreach (var (group, value) in args.DamageDelta.GetDamagePerGroup(_prototype))
|
||||
{
|
||||
if (!ent.Comp.ValidDamageGroups.Contains(group))
|
||||
continue;
|
||||
|
||||
totalApplicableDamage += value;
|
||||
}
|
||||
|
||||
if (totalApplicableDamage <= ent.Comp.ShedDamageThreshold)
|
||||
return;
|
||||
|
||||
// predicted randomness is a truly evil thing
|
||||
var rand = SharedRandomExtensions.PredictedRandom(_timing, GetNetEntity(ent));
|
||||
var randomDouble = rand.NextDouble();
|
||||
|
||||
var triggerChance = totalApplicableDamage * ent.Comp.ShedScalingChance;
|
||||
|
||||
if (randomDouble >= triggerChance)
|
||||
return;
|
||||
|
||||
var feather = SpawnFeather(ent, true);
|
||||
|
||||
// apply a random impulse so it's flying off the body. similar code to GibbingSystem
|
||||
var scatterVector = rand.NextAngle().ToVec() * (rand.NextFloat(10, 40));
|
||||
_physics.ApplyLinearImpulse(feather, scatterVector);
|
||||
_physics.ApplyAngularImpulse(feather, rand.NextFloat(-30, 30));
|
||||
|
||||
// update name/desc for increased validness
|
||||
var meta = MetaData(feather);
|
||||
_metaData.SetEntityName(feather, Loc.GetString(ent.Comp.FeatherBloodiedNameString, ("item", Name(feather))), meta);
|
||||
_metaData.SetEntityDescription(feather, Loc.GetString(ent.Comp.FeatherBloodiedDescString), meta);
|
||||
Dirty(feather, meta);
|
||||
|
||||
// yeeeowch!
|
||||
_popup.PopupClient(Loc.GetString(ent.Comp.DroppedFeatherString), ent, ent, PopupType.MediumCaution);
|
||||
_chat.TryEmoteWithoutChat(ent, ent.Comp.ScreamEmote);
|
||||
|
||||
// old StatusEffects is obsolete, however Adrenaline has not been moved over to the new system yet
|
||||
_statusEffects.TryAddStatusEffect(ent, "Adrenaline", TimeSpan.FromSeconds(3), true);
|
||||
}
|
||||
|
||||
private void OnDamageModify(Entity<PreenableComponent> ent, ref DamageModifyEvent args)
|
||||
{
|
||||
if (ent.Comp.VulnerabilityModifier == null)
|
||||
return;
|
||||
|
||||
// zero vulnerability at max feathers, full vulnerability at 0 feathers
|
||||
var vulnerabilityModifier = 1f - (ent.Comp.CurrentFeathers / (float)ent.Comp.MaximumFeathers);
|
||||
|
||||
var damageSpecifier = new DamageModifierSet
|
||||
{
|
||||
Coefficients = new Dictionary<string, float>(ent.Comp.VulnerabilityModifier.Coefficients),
|
||||
};
|
||||
|
||||
foreach (var key in damageSpecifier.Coefficients.Keys)
|
||||
{
|
||||
damageSpecifier.Coefficients[key] = 1f + ((damageSpecifier.Coefficients[key] - 1f) * vulnerabilityModifier);
|
||||
}
|
||||
|
||||
args.Damage = DamageSpecifier.ApplyModifierSet(args.Damage, damageSpecifier);
|
||||
}
|
||||
|
||||
private EntityUid SpawnFeather(Entity<PreenableComponent> ent, bool bloody)
|
||||
{
|
||||
var feather = PredictedSpawnAtPosition(ent.Comp.FeatherPrototype.Id, Transform(ent).Coordinates);
|
||||
|
||||
if (TryComp<HumanoidAppearanceComponent>(ent, out var appearance))
|
||||
{
|
||||
_appearance.SetData(feather, FeatherVisuals.FeatherColor, appearance.SkinColor);
|
||||
}
|
||||
|
||||
// best be careful, no cleaning this
|
||||
_forensics.TransferDna(feather, ent, false);
|
||||
|
||||
ent.Comp.CurrentFeathers -= 1;
|
||||
ent.Comp.ReplenishTime = _timing.CurTime + ent.Comp.ReplenishDelay;
|
||||
Dirty(ent);
|
||||
|
||||
if (!bloody || !TryComp<BloodstreamComponent>(ent, out var bloodstream) || bloodstream.BloodSolution == null)
|
||||
return feather;
|
||||
|
||||
var solution = bloodstream.BloodSolution.Value.Comp.Solution;
|
||||
_appearance.SetData(feather, FeatherVisuals.BloodColor, solution.GetColor(_prototype));
|
||||
|
||||
return feather;
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
base.Update(deltaTime);
|
||||
|
||||
var preenableQuery = EntityQueryEnumerator<PreenableComponent>();
|
||||
|
||||
while (preenableQuery.MoveNext(out var uid, out var preenable))
|
||||
{
|
||||
if (preenable.ReplenishTime == null || !(preenable.ReplenishTime <= _timing.CurTime))
|
||||
continue;
|
||||
|
||||
if (preenable.CurrentFeathers >= preenable.MaximumFeathers)
|
||||
continue;
|
||||
|
||||
preenable.CurrentFeathers += 1;
|
||||
|
||||
if (preenable.CurrentFeathers >= preenable.MaximumFeathers)
|
||||
{
|
||||
preenable.ReplenishTime = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
preenable.ReplenishTime = _timing.CurTime + preenable.ReplenishDelay;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
preening-action-verb = Preen
|
||||
|
||||
preening-popup-self = You begin to preen your feathers...
|
||||
preening-popup-other = You begin to preen { CAPITALIZE(THE($preenee)) }'s feathers...
|
||||
preening-popup-self-recipient = { CAPITALIZE(THE($preener)) } begins to preen your feathers!
|
||||
|
||||
preening-feather-dropped-injured = You feel a feather get torn off!
|
||||
|
||||
feather-bloody-name-modifier = bloody {$item}
|
||||
feather-bloody-desc = DEFINITELY not ethically sourced.
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
- type: entity
|
||||
parent: BaseItem
|
||||
id: AvaliFeather
|
||||
name: feather
|
||||
description: Probably not ethically sourced.
|
||||
components:
|
||||
- type: Appearance
|
||||
- type: Sprite
|
||||
sprite: _DV/Objects/Misc/feather.rsi
|
||||
layers:
|
||||
- state: feather
|
||||
map: [ "enum.FeatherVisualLayers.Feather" ]
|
||||
- state: featherblood
|
||||
color: "#FFFFFF00"
|
||||
map: [ "enum.FeatherVisualLayers.Blood"]
|
||||
- type: Flammable
|
||||
fireSpread: true
|
||||
alwaysCombustible: true
|
||||
damage:
|
||||
types:
|
||||
Heat: 1
|
||||
- type: FireVisuals
|
||||
sprite: Effects/fire.rsi
|
||||
normalState: fire
|
||||
- type: Damageable # a lot of this is sensible boilerplate from the Paper prototype
|
||||
damageModifierSet: Wood
|
||||
- type: Destructible
|
||||
thresholds:
|
||||
- trigger:
|
||||
!type:DamageTrigger
|
||||
damage: 15
|
||||
behaviors:
|
||||
- !type:DoActsBehavior
|
||||
acts: [ "Destruction" ]
|
||||
- type: Item
|
||||
size: Tiny
|
||||
- type: Clothing
|
||||
sprite: _DV/Objects/Misc/feather.rsi
|
||||
clothingVisuals:
|
||||
ears:
|
||||
- state: equipped-EARS
|
||||
map: [ "feather" ]
|
||||
head:
|
||||
- state: equipped-HELMET
|
||||
map: [ "feather" ]
|
||||
slots:
|
||||
- ears
|
||||
- head
|
||||
- type: Tag
|
||||
tags:
|
||||
- Write # Like a quill!
|
||||
- Pen
|
||||
- Trash
|
||||
- type: Feather
|
||||
|
|
@ -115,6 +115,8 @@
|
|||
279: 0.95
|
||||
265: 0.9
|
||||
240: 0.85
|
||||
- type: Preenable
|
||||
featherPrototype: AvaliFeather
|
||||
# End DeltaV additions
|
||||
|
||||
- type: entity
|
||||
|
|
|
|||
|
|
@ -29,4 +29,10 @@
|
|||
- As a lightweight species, they tend to be a bit frail, taking [color=#ffa500]30% more blunt damage[/color], as well as [color=#ffa500]10% more slash and pierce[/color].
|
||||
- For the same reason, they lack muscle, [color=#ffa500]dealing 35% less blunt damage[/color] and [color=#ffa500]15% less slash and stamina damage[/color].
|
||||
|
||||
## Feathers
|
||||
<Box>
|
||||
<GuideEntityEmbed Entity="AvaliFeather" Caption=""/>
|
||||
</Box>
|
||||
Being injured has a chance to cause an Avali to drop a feather, granting them a [color=#1e90ff]short burst of adrenaline[/color]. They can lose up to three feathers at once, which will very slowly regrow over time. Feathers can also be [color=#1e90ff]manually preened[/color] via the context menu.
|
||||
|
||||
</Document>
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 222 B |
Binary file not shown.
|
After Width: | Height: | Size: 222 B |
Binary file not shown.
|
After Width: | Height: | Size: 255 B |
Binary file not shown.
|
After Width: | Height: | Size: 211 B |
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"version": 1,
|
||||
"license": "CC-BY-4.0",
|
||||
"copyright": "Item sprite modified from https://opengameart.org/content/animated-feather-pickup. featherblood and equip sprites by kotobdev (github).",
|
||||
"size": {
|
||||
"x": 32,
|
||||
"y": 32
|
||||
},
|
||||
"states": [
|
||||
{
|
||||
"name": "feather"
|
||||
},
|
||||
{
|
||||
"name": "featherblood"
|
||||
},
|
||||
{
|
||||
"name": "equipped-HELMET",
|
||||
"directions": 4
|
||||
},
|
||||
{
|
||||
"name": "equipped-EARS",
|
||||
"directions": 4
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Reference in New Issue