diff --git a/Content.Client/CardboardBox/CardboardBoxSystem.cs b/Content.Client/CardboardBox/CardboardBoxSystem.cs new file mode 100644 index 0000000000..1b67c8be75 --- /dev/null +++ b/Content.Client/CardboardBox/CardboardBoxSystem.cs @@ -0,0 +1,60 @@ +using Content.Shared.CardboardBox; +using Content.Shared.CardboardBox.Components; +using Content.Shared.Examine; +using Content.Shared.Movement.Components; +using Robust.Client.GameObjects; + +namespace Content.Client.CardboardBox; + +public sealed class CardboardBoxSystem : SharedCardboardBoxSystem +{ + [Dependency] private readonly EntityLookupSystem _entityLookup = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeNetworkEvent(OnBoxEffect); + } + + private void OnBoxEffect(PlayBoxEffectMessage msg) + { + if (!TryComp(msg.Source, out var box)) + return; + + var xformQuery = GetEntityQuery(); + + if (!xformQuery.TryGetComponent(msg.Source, out var xform)) + return; + + var sourcePos = xform.MapPosition; + + //Any mob that can move should be surprised? + //God mind rework needs to come faster so it can just check for mind + //TODO: Replace with Mind Query when mind rework is in. + var mobMoverEntities = new HashSet(); + + //Filter out entities in range to see that they're a mob and add them to the mobMoverEntities hash for faster lookup + foreach (var moverComp in _entityLookup.GetComponentsInRange(xform.Coordinates, box.Distance)) + { + if (moverComp.Owner == msg.Mover) + continue; + + mobMoverEntities.Add(moverComp.Owner); + } + + //Play the effect for the mobs as long as they can see the box and are in range. + foreach (var mob in mobMoverEntities) + { + if (!xformQuery.TryGetComponent(mob, out var moverTransform) || !ExamineSystemShared.InRangeUnOccluded(sourcePos, moverTransform.MapPosition, box.Distance, null)) + continue; + + var ent = Spawn(box.Effect, moverTransform.MapPosition); + + if (!xformQuery.TryGetComponent(ent, out var entTransform) || !TryComp(ent, out var sprite)) + continue; + + sprite.Offset = new Vector2(0, 1); + entTransform.AttachParent(mob); + } + } +} diff --git a/Content.Client/Stealth/StealthSystem.cs b/Content.Client/Stealth/StealthSystem.cs new file mode 100644 index 0000000000..0e116f07a0 --- /dev/null +++ b/Content.Client/Stealth/StealthSystem.cs @@ -0,0 +1,74 @@ +using Content.Client.Interactable.Components; +using Content.Shared.Stealth; +using Content.Shared.Stealth.Components; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Shared.Prototypes; + +namespace Content.Client.Stealth; + +public sealed class StealthSystem : SharedStealthSystem +{ + [Dependency] private readonly IPrototypeManager _protoMan = default!; + + private ShaderInstance _shader = default!; + + public override void Initialize() + { + base.Initialize(); + + _shader = _protoMan.Index("Stealth").InstanceUnique(); + SubscribeLocalEvent(OnRemove); + SubscribeLocalEvent(OnShaderRender); + } + + protected override void OnInit(EntityUid uid, StealthComponent component, ComponentInit args) + { + base.OnInit(uid, component, args); + if (!TryComp(uid, out SpriteComponent? sprite)) + return; + + sprite.PostShader = _shader; + sprite.GetScreenTexture = true; + sprite.RaiseShaderEvent = true; + + if (TryComp(uid, out InteractionOutlineComponent? outline)) + { + RemComp(uid, outline); + component.HadOutline = true; + } + } + + private void OnRemove(EntityUid uid, StealthComponent component, ComponentRemove args) + { + if (!TryComp(uid, out SpriteComponent? sprite)) + return; + + sprite.PostShader = null; + sprite.GetScreenTexture = false; + sprite.RaiseShaderEvent = false; + sprite.Color = Color.White; + + if (component.HadOutline) + AddComp(uid); + } + + private void OnShaderRender(EntityUid uid, StealthComponent component, BeforePostShaderRenderEvent args) + { + // Distortion effect uses screen coordinates. If a player moves, the entities appear to move on screen. this + // makes the distortion very noticeable. + + // So we need to use relative screen coordinates. The reference frame we use is the parent's position on screen. + // this ensures that if the Stealth is not moving relative to the parent, its relative screen position remains + // unchanged. + var parentXform = Transform(Transform(uid).ParentUid); + var reference = args.Viewport.WorldToLocal(parentXform.WorldPosition); + var visibility = GetVisibility(uid, component); + _shader.SetParameter("reference", reference); + _shader.SetParameter("visibility", visibility); + + visibility = MathF.Max(0, visibility); + args.Sprite.Color = new Color(visibility, visibility, 1, 1); + } +} + diff --git a/Content.Server/CardboardBox/CardboardBoxSystem.cs b/Content.Server/CardboardBox/CardboardBoxSystem.cs new file mode 100644 index 0000000000..6b30bf0013 --- /dev/null +++ b/Content.Server/CardboardBox/CardboardBoxSystem.cs @@ -0,0 +1,61 @@ +using System.Linq; +using Content.Shared.CardboardBox.Components; +using Content.Server.Storage.Components; +using Content.Shared.CardboardBox; +using Content.Shared.Movement.Components; +using Content.Shared.Movement.Systems; +using Robust.Shared.Player; +using Robust.Shared.Timing; + +namespace Content.Server.CardboardBox; + +public sealed class CardboardBoxSystem : SharedCardboardBoxSystem +{ + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedMoverController _mover = default!; + [Dependency] private readonly IGameTiming _timing = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnBeforeStorageClosed); + SubscribeLocalEvent(AfterStorageOpen); + } + + private void OnBeforeStorageClosed(EntityUid uid, CardboardBoxComponent component, StorageBeforeCloseEvent args) + { + var mobMover = args.Contents.Where(HasComp).ToList(); + + //Grab the first mob to set as the mover and to prevent other mobs from entering. + foreach (var mover in mobMover) + { + //Set the movement relay for the box as the first mob + if (component.Mover == null && args.Contents.Contains(mover)) + { + var relay = EnsureComp(mover); + _mover.SetRelay(mover, uid, relay); + component.Mover = mover; + } + + if (mover != component.Mover) + args.Contents.Remove(mover); + } + } + + private void AfterStorageOpen(EntityUid uid, CardboardBoxComponent component, StorageAfterOpenEvent args) + { + //Remove the mover after the box is opened and play the effect if it hasn't been played yet. + if (component.Mover != null) + { + RemComp(component.Mover.Value); + if (_timing.CurTime > component.EffectCooldown) + { + RaiseNetworkEvent(new PlayBoxEffectMessage(component.Owner, component.Mover.Value), Filter.PvsExcept(component.Owner)); + _audio.PlayPvs(component.EffectSound, component.Owner); + component.EffectCooldown = _timing.CurTime + CardboardBoxComponent.MaxEffectCooldown; + } + } + + component.Mover = null; + } +} diff --git a/Content.Server/Stealth/StealthSystem.cs b/Content.Server/Stealth/StealthSystem.cs new file mode 100644 index 0000000000..b64eb4b9af --- /dev/null +++ b/Content.Server/Stealth/StealthSystem.cs @@ -0,0 +1,8 @@ +using Content.Shared.Stealth; + +namespace Content.Server.Stealth; + +public sealed class StealthSystem : SharedStealthSystem +{ + +} diff --git a/Content.Server/Storage/Components/EntityStorageComponent.cs b/Content.Server/Storage/Components/EntityStorageComponent.cs index 66f98b7838..a15911e584 100644 --- a/Content.Server/Storage/Components/EntityStorageComponent.cs +++ b/Content.Server/Storage/Components/EntityStorageComponent.cs @@ -37,6 +37,15 @@ public sealed class EntityStorageComponent : Component, IGasMixtureHolder [DataField("isCollidableWhenOpen")] public bool IsCollidableWhenOpen; + /// + /// If true, it opens the storage when the entity inside of it moves + /// If false, it prevents the storage from opening when the entity inside of it moves. + /// This is for objects that you want the player to move while inside, like large cardboard boxes, without opening the storage. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("openOnMove")] + public bool OpenOnMove = true; + //The offset for where items are emptied/vacuumed for the EntityStorage. [DataField("enteringOffset")] public Vector2 EnteringOffset = new(0, 0); diff --git a/Content.Server/Storage/EntitySystems/StorageSystem.cs b/Content.Server/Storage/EntitySystems/StorageSystem.cs index 9b135f177a..64a29aaf29 100644 --- a/Content.Server/Storage/EntitySystems/StorageSystem.cs +++ b/Content.Server/Storage/EntitySystems/StorageSystem.cs @@ -149,7 +149,10 @@ namespace Content.Server.Storage.EntitySystems return; component.LastInternalOpenAttempt = _gameTiming.CurTime; - _entityStorage.TryOpenStorage(args.Entity, component.Owner); + if (component.OpenOnMove) + { + _entityStorage.TryOpenStorage(args.Entity, component.Owner); + } } diff --git a/Content.Shared/CardboardBox/Components/SharedCardboardBoxComponent.cs b/Content.Shared/CardboardBox/Components/SharedCardboardBoxComponent.cs new file mode 100644 index 0000000000..7fdccaaa9e --- /dev/null +++ b/Content.Shared/CardboardBox/Components/SharedCardboardBoxComponent.cs @@ -0,0 +1,68 @@ +using Robust.Shared.Audio; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Shared.CardboardBox.Components; +/// +/// Allows a user to control an EntityStorage entity while inside of it. +/// Used for big cardboard box entities. +/// +[RegisterComponent, NetworkedComponent] +public sealed class CardboardBoxComponent : Component +{ + /// + /// The person in control of this box + /// + [ViewVariables] + [DataField("mover")] + public EntityUid? Mover; + + /// + /// The entity used for the box opening effect + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("effect")] + public string Effect = "Exclamation"; + + /// + /// Sound played upon effect creation + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("effectSound")] + public SoundSpecifier? EffectSound; + + /// + /// How far should the box opening effect go? + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("distance")] + public float Distance = 6f; + + /// + /// Current time + max effect cooldown to check to see if effect can play again + /// Prevents effect spam + /// + [DataField("effectCooldown", customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan EffectCooldown = TimeSpan.FromSeconds(1f); + + /// + /// How much time should pass + current time until the effect plays again + /// Prevents effect spam + /// + [DataField("maxEffectCooldown", customTypeSerializer: typeof(TimeOffsetSerializer))] + public static readonly TimeSpan MaxEffectCooldown = TimeSpan.FromSeconds(5f); +} + +[Serializable, NetSerializable] +public sealed class PlayBoxEffectMessage : EntityEventArgs +{ + public EntityUid Source; + public EntityUid Mover; + + public PlayBoxEffectMessage(EntityUid source, EntityUid mover) + { + Source = source; + Mover = mover; + } +} diff --git a/Content.Shared/CardboardBox/SharedCardboardBoxSystem.cs b/Content.Shared/CardboardBox/SharedCardboardBoxSystem.cs new file mode 100644 index 0000000000..856bb94ae0 --- /dev/null +++ b/Content.Shared/CardboardBox/SharedCardboardBoxSystem.cs @@ -0,0 +1,6 @@ +namespace Content.Shared.CardboardBox; + +public abstract class SharedCardboardBoxSystem : EntitySystem +{ + +} diff --git a/Content.Shared/Stealth/Components/StealthComponent.cs b/Content.Shared/Stealth/Components/StealthComponent.cs new file mode 100644 index 0000000000..b41f05c9ae --- /dev/null +++ b/Content.Shared/Stealth/Components/StealthComponent.cs @@ -0,0 +1,60 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Shared.Stealth.Components; +/// +/// Add this component to an entity that you want to be cloaked. +/// It overlays a shader on the entity to give them an invisibility cloaked effect +/// It also turns the entity invisible +/// +[RegisterComponent, NetworkedComponent] +[Access(typeof(SharedStealthSystem))] +public sealed class StealthComponent : Component +{ + /// + /// Whether or not the entity previously had an interaction outline prior to cloaking. + /// + [DataField("hadOutline")] + public bool HadOutline; + + /// + /// Last set level of visibility. Ranges from 1 (fully visible) and -1 (fully hidden). To get the actual current + /// visibility, use + /// + [DataField("lastVisibility")] + [Access(typeof(SharedStealthSystem), Other = AccessPermissions.None)] + public float LastVisibility; + + /// + /// Time at which was set. Null implies the entity is currently paused and not + /// accumulating any visibility change. + /// + [DataField("lastUpdate", customTypeSerializer:typeof(TimeOffsetSerializer))] + public TimeSpan? LastUpdated; + + /// + /// Rate that effects how fast an entity's visibility passively changes. + /// + [DataField("passiveVisibilityRate")] + public readonly float PassiveVisibilityRate = -0.15f; + + /// + /// Rate for movement induced visibility changes. Scales with distance moved. + /// + [DataField("movementVisibilityRate")] + public readonly float MovementVisibilityRate = 0.2f; +} + +[Serializable, NetSerializable] +public sealed class StealthComponentState : ComponentState +{ + public float Visibility; + public TimeSpan? LastUpdated; + + public StealthComponentState(float stealthLevel, TimeSpan? lastUpdated) + { + Visibility = stealthLevel; + LastUpdated = lastUpdated; + } +} diff --git a/Content.Shared/Stealth/SharedStealthSystem.cs b/Content.Shared/Stealth/SharedStealthSystem.cs new file mode 100644 index 0000000000..582bd7b57d --- /dev/null +++ b/Content.Shared/Stealth/SharedStealthSystem.cs @@ -0,0 +1,122 @@ +using Content.Shared.Stealth.Components; +using Robust.Shared.GameStates; +using Robust.Shared.Timing; + +namespace Content.Shared.Stealth; + +public abstract class SharedStealthSystem : EntitySystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnStealthGetState); + SubscribeLocalEvent(OnStealthHandleState); + SubscribeLocalEvent(OnMove); + SubscribeLocalEvent(OnPaused); + SubscribeLocalEvent(OnInit); + } + + private void OnPaused(EntityUid uid, StealthComponent component, EntityPausedEvent args) + { + if (args.Paused) + { + component.LastVisibility = GetVisibility(uid, component); + component.LastUpdated = null; + } + else + { + component.LastUpdated = _timing.CurTime; + } + + Dirty(component); + } + + protected virtual void OnInit(EntityUid uid, StealthComponent component, ComponentInit args) + { + if (component.LastUpdated != null || Paused(uid)) + return; + + component.LastUpdated = _timing.CurTime; + } + + private void OnStealthGetState(EntityUid uid, StealthComponent component, ref ComponentGetState args) + { + args.State = new StealthComponentState(component.LastVisibility, component.LastUpdated); + } + + private void OnStealthHandleState(EntityUid uid, StealthComponent component, ref ComponentHandleState args) + { + if (args.Current is not StealthComponentState cast) + return; + + component.LastVisibility = cast.Visibility; + component.LastUpdated = cast.LastUpdated; + } + + private void OnMove(EntityUid uid, StealthComponent component, ref MoveEvent args) + { + if (args.FromStateHandling) + return; + + if (args.NewPosition.EntityId != args.OldPosition.EntityId) + return; + + var delta = component.MovementVisibilityRate * (args.NewPosition.Position - args.OldPosition.Position).Length; + ModifyVisibility(uid, delta, component); + } + + /// + /// Modifies the visibility based on the delta provided. + /// + /// The delta to be used in visibility calculation. + public void ModifyVisibility(EntityUid uid, float delta, StealthComponent? component = null) + { + if (delta == 0 || !Resolve(uid, ref component)) + return; + + if (component.LastUpdated != null) + { + component.LastVisibility = GetVisibility(uid, component); + component.LastUpdated = _timing.CurTime; + } + + component.LastVisibility = Math.Clamp(component.LastVisibility + delta, -1f, 1f); + Dirty(component); + } + + /// + /// Sets the visibility directly with no modifications + /// + /// The value to set the visibility to. -1 is fully invisible, 1 is fully visible + public void SetVisibility(EntityUid uid, float value, StealthComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + + component.LastVisibility = value; + if (component.LastUpdated != null) + component.LastUpdated = _timing.CurTime; + + Dirty(component); + } + + /// + /// Gets the current visibility from the + /// Use this instead of getting LastVisibility from the component directly. + /// + /// Returns a calculation that accounts for any stealth change that happened since last update, otherwise returns based on if it can resolve the component. + public float GetVisibility(EntityUid uid, StealthComponent? component = null) + { + if (!Resolve(uid, ref component)) + return 1; + + if (component.LastUpdated == null) + return component.LastVisibility; + + var deltaTime = _timing.CurTime - component.LastUpdated.Value; + return Math.Clamp(component.LastVisibility + (float) deltaTime.TotalSeconds * component.PassiveVisibilityRate, -1f, 1f); + } +} diff --git a/Resources/Audio/Effects/box_deploy.ogg b/Resources/Audio/Effects/box_deploy.ogg new file mode 100644 index 0000000000..3fbe895950 Binary files /dev/null and b/Resources/Audio/Effects/box_deploy.ogg differ diff --git a/Resources/Audio/Effects/chime.ogg b/Resources/Audio/Effects/chime.ogg new file mode 100644 index 0000000000..20548c73cd Binary files /dev/null and b/Resources/Audio/Effects/chime.ogg differ diff --git a/Resources/Audio/Effects/licenses.txt b/Resources/Audio/Effects/licenses.txt index 9630104bbc..94c3613c63 100644 --- a/Resources/Audio/Effects/licenses.txt +++ b/Resources/Audio/Effects/licenses.txt @@ -40,4 +40,16 @@ The following sounds are taken from TGstation github (licensed under CC by 3.0): demon_dies.ogg: taken at https://github.com/tgstation/tgstation/commit/d4f678a1772007ff8d7eddd21cf7218c8e07bfc0 -pop.ogg licensed under CC0 1.0 by mirrorcult \ No newline at end of file +pop.ogg licensed under CC0 1.0 by mirrorcult + +box_deploy.ogg and chime.ogg taken from Citadel Station at commit: https://github.com/Citadel-Station-13/Citadel-Station-13/commit/b604390f334343be80045d955705cf48ee056c61 + +- files: ["box_deploy.ogg"] + license: "CC-BY-NC-SA-3.0" + copyright: "box_deploy.ogg taken from Citadel Station." + source: "https://github.com/Citadel-Station-13/Citadel-Station-13/commit/b604390f334343be80045d955705cf48ee056c61" + +- files: ["chime.ogg"] + license: "CC-BY-NC-SA-3.0" + copyright: "chime.ogg taken from Citadel Station." + source: "https://github.com/Citadel-Station-13/Citadel-Station-13/commit/b604390f334343be80045d955705cf48ee056c61" \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Structures/Storage/Closets/big_boxes.yml b/Resources/Prototypes/Entities/Structures/Storage/Closets/big_boxes.yml new file mode 100644 index 0000000000..0cc5ef792a --- /dev/null +++ b/Resources/Prototypes/Entities/Structures/Storage/Closets/big_boxes.yml @@ -0,0 +1,115 @@ +- type: entity + id: BaseBigBox + name: cardboard box + description: Huh? Just a box... + components: + - type: Transform + noRot: true + - type: Clickable + - type: Physics + bodyType: KinematicController + - type: Fixtures + fixtures: + - shape: + !type:PhysShapeAabb + bounds: "-0.4,-0.4,0.4,0.4" + mask: + - MobMask + layer: + - SlipLayer + hard: true + - type: Pullable + - type: CardboardBox + effectSound: /Audio/Effects/chime.ogg + - type: InputMover + - type: EntityStorage + isCollidableWhenOpen: false + openOnMove: false + airtight: false + capacity: 4 #4 Entities seems like a nice comfy fit for a cardboard box. + - type: ContainerContainer + containers: + entity_storage: !type:Container + - type: Sprite + noRot: true + netsync: false + sprite: Structures/Storage/closet.rsi + layers: + - state: cardboard + - state: cardboard_open + map: ["enum.StorageVisualLayers.Door"] + - type: Appearance + visuals: + - type: StorageVisualizer + state: cardboard + state_open: cardboard_open + - type: Tag + tags: + - DoorBumpOpener + +- type: entity + id: StealthBox + parent: BaseBigBox + name: cardboard box #it's still just a box + description: Kept ya waiting, huh? + components: + - type: Sprite + noRot: true + netsync: false + sprite: Structures/Storage/closet.rsi + layers: + - state: agentbox + - state: cardboard_open + map: ["enum.StorageVisualLayers.Door"] + - type: Appearance + visuals: + - type: StorageVisualizer + state: agentbox + state_open: cardboard_open + - type: Stealth + passiveVisibilityRate: -0.24 + movementVisibilityRate: 0.10 + +#For admin spawning only +- type: entity + id: GhostBox + parent: StealthBox + name: ghost box + description: Beware! + components: + - type: Fixtures + fixtures: + - shape: + !type:PhysShapeAabb + bounds: "-0.4,-0.4,0.4,0.4" + mask: + - MobMask + layer: + - SlipLayer + hard: false #It's a ghostly box, it can go through walls + - type: EntityStorage + isCollidableWhenOpen: false + openOnMove: false + airtight: true #whoever's inside should probably breath + +#Exclamation effect for box opening +- type: entity + id: Exclamation + name: exclamation + noSpawn: true + save: false + components: + - type: Transform + noRot: true + - type: Sprite + sprite: Structures/Storage/closet.rsi + drawdepth: Effects + noRot: true + netsync: false + layers: + - state: "cardboard_special" + - type: TimedDespawn + lifetime: 1 + - type: Tag + tags: + - HideContextMenu diff --git a/Resources/Prototypes/Shaders/shaders.yml b/Resources/Prototypes/Shaders/shaders.yml index 2f87278a93..74d715ee25 100644 --- a/Resources/Prototypes/Shaders/shaders.yml +++ b/Resources/Prototypes/Shaders/shaders.yml @@ -70,3 +70,9 @@ id: Dim kind: source path: "/Textures/Shaders/dim.swsl" + +# cloaking distortion effect +- type: shader + id: Stealth + kind: source + path: "/Textures/Shaders/stealth.swsl" diff --git a/Resources/Textures/Shaders/stealth.swsl b/Resources/Textures/Shaders/stealth.swsl new file mode 100644 index 0000000000..fc85dcf5d8 --- /dev/null +++ b/Resources/Textures/Shaders/stealth.swsl @@ -0,0 +1,37 @@ +light_mode unshaded; + +uniform sampler2D SCREEN_TEXTURE; +uniform highp float visibility; // number between -1 and 1 +uniform mediump vec2 reference; + +const mediump float time_scale = 0.25; +const mediump float distance_scale = 0.125; + +void fragment() { + + vec4 sprite = zTexture(UV); + + if (sprite.a == 0.0) { + discard; + } + + // get distortion magnitude. hand crafted from a random jumble of trig functions + vec2 coords = (FRAGCOORD.xy + reference) * distance_scale; + float w = sin(TIME + (coords.x + coords.y + 2.0*sin(TIME*time_scale) * sin(TIME*time_scale + coords.x - coords.y)) ); + + // visualize distortion via: + // COLOR = vec4(w,w,w,1); + + w *= (3 + visibility * 2); + + vec4 background = zTextureSpec(SCREEN_TEXTURE, ( FRAGCOORD.xy + vec2(w) ) * SCREEN_PIXEL_SIZE ); + + float alpha; + if (visibility>0) + alpha = sprite.a * visibility; + else + alpha = 0; + + COLOR.xyz = mix(background.xyz, sprite.xyz, alpha); + COLOR.a = 1.0; +}