Ghosts do booo (spooky) (#3363)

* Light now use visualizer

* Added ghost actions

* Add hotkey input for ghosts

* no message

* Testing blinking animation

* Better animation

* Better customization

* No abuse

* Reversed sln

* No fun for ghosts

* No fun for ghosts x2

* Cooldown for lights

* Moved to component deps

* This tollist is unnecessary

* Enums to byte

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>

* Some lights can ignore ghosts now

Co-authored-by: Alex Evgrashin <evgrashin.adl@gmail.com>
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
This commit is contained in:
Alex Evgrashin
2021-02-26 04:31:06 +03:00
committed by GitHub
parent d177f0fd07
commit 0fa219365e
11 changed files with 360 additions and 35 deletions

View File

@@ -0,0 +1,153 @@
#nullable enable
using System;
using Content.Shared.GameObjects.Components.Power.ApcNetComponents.PowerReceiverUsers;
using JetBrains.Annotations;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
using Robust.Shared.Animations;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Random;
using Robust.Shared.Serialization;
using YamlDotNet.RepresentationModel;
namespace Content.Client.GameObjects.Components.Power.ApcNetComponents.PowerReceiverUsers
{
[UsedImplicitly]
public class PoweredLightVisualizer : AppearanceVisualizer
{
private float _minBlinkingTime;
private float _maxBlinkingTime;
private string? _blinkingSound;
private bool _wasBlinking;
private Action<string>? _blinkingCallback;
public override void LoadData(YamlMappingNode node)
{
base.LoadData(node);
var serializer = YamlObjectSerializer.NewReader(node);
serializer.DataField(ref _minBlinkingTime, "minBlinkingTime", 0.5f);
serializer.DataField(ref _maxBlinkingTime, "maxBlinkingTime", 2.0f);
serializer.DataField(ref _blinkingSound, "blinkingSound", null);
}
public override void OnChangeData(AppearanceComponent component)
{
base.OnChangeData(component);
if (!component.Owner.TryGetComponent(out ISpriteComponent? sprite)) return;
if (!component.Owner.TryGetComponent(out PointLightComponent? light)) return;
if (!component.TryGetData(PoweredLightVisuals.BulbState, out PoweredLightState state)) return;
switch (state)
{
case PoweredLightState.Empty:
sprite.LayerSetState(PoweredLightLayers.Base, "empty");
ToggleBlinkingAnimation(component, false);
light.Enabled = false;
break;
case PoweredLightState.Off:
sprite.LayerSetState(PoweredLightLayers.Base, "off");
ToggleBlinkingAnimation(component, false);
light.Enabled = false;
break;
case PoweredLightState.On:
if (component.TryGetData(PoweredLightVisuals.BulbColor, out Color color))
light.Color = color;
if (component.TryGetData(PoweredLightVisuals.Blinking, out bool isBlinking))
ToggleBlinkingAnimation(component, isBlinking);
if (!isBlinking)
{
sprite.LayerSetState(PoweredLightLayers.Base, "on");
light.Enabled = true;
}
break;
case PoweredLightState.Broken:
sprite.LayerSetState(PoweredLightLayers.Base, "broken");
ToggleBlinkingAnimation(component, false);
light.Enabled = false;
break;
case PoweredLightState.Burned:
sprite.LayerSetState(PoweredLightLayers.Base, "burn");
ToggleBlinkingAnimation(component, false);
light.Enabled = false;
break;
}
}
private void ToggleBlinkingAnimation(AppearanceComponent component, bool isBlinking)
{
if (isBlinking == _wasBlinking)
return;
_wasBlinking = isBlinking;
component.Owner.EnsureComponent(out AnimationPlayerComponent animationPlayer);
if (isBlinking)
{
_blinkingCallback = (animName) => animationPlayer.Play(BlinkingAnimation(), "blinking");
animationPlayer.AnimationCompleted += _blinkingCallback;
animationPlayer.Play(BlinkingAnimation(), "blinking");
}
else if (animationPlayer.HasRunningAnimation("blinking"))
{
if (_blinkingCallback != null)
animationPlayer.AnimationCompleted -= _blinkingCallback;
animationPlayer.Stop("blinking");
}
}
private Animation BlinkingAnimation()
{
var random = IoCManager.Resolve<IRobustRandom>();
var randomTime = random.NextFloat() *
(_maxBlinkingTime - _minBlinkingTime) + _minBlinkingTime;
var blinkingAnim = new Animation()
{
Length = TimeSpan.FromSeconds(randomTime),
AnimationTracks =
{
new AnimationTrackComponentProperty
{
ComponentType = typeof(PointLightComponent),
InterpolationMode = AnimationInterpolationMode.Nearest,
Property = nameof(PointLightComponent.Enabled),
KeyFrames =
{
new AnimationTrackProperty.KeyFrame(false, 0),
new AnimationTrackProperty.KeyFrame(true, 1)
}
},
new AnimationTrackSpriteFlick()
{
LayerKey = PoweredLightLayers.Base,
KeyFrames =
{
new AnimationTrackSpriteFlick.KeyFrame("off", 0),
new AnimationTrackSpriteFlick.KeyFrame("on", 0.5f)
}
}
}
};
if (_blinkingSound != null)
{
blinkingAnim.AnimationTracks.Add(new AnimationTrackPlaySound()
{
KeyFrames =
{
new AnimationTrackPlaySound.KeyFrame(_blinkingSound, 0.5f)
}
});
}
return blinkingAnim;
}
}
}

View File

@@ -46,26 +46,28 @@ namespace Content.Client.Input
human.AddFunction(ContentKeyFunctions.Arcade1);
human.AddFunction(ContentKeyFunctions.Arcade2);
human.AddFunction(ContentKeyFunctions.Arcade3);
human.AddFunction(ContentKeyFunctions.OpenActionsMenu);
human.AddFunction(ContentKeyFunctions.Hotbar0);
human.AddFunction(ContentKeyFunctions.Hotbar1);
human.AddFunction(ContentKeyFunctions.Hotbar2);
human.AddFunction(ContentKeyFunctions.Hotbar3);
human.AddFunction(ContentKeyFunctions.Hotbar4);
human.AddFunction(ContentKeyFunctions.Hotbar5);
human.AddFunction(ContentKeyFunctions.Hotbar6);
human.AddFunction(ContentKeyFunctions.Hotbar7);
human.AddFunction(ContentKeyFunctions.Hotbar8);
human.AddFunction(ContentKeyFunctions.Hotbar9);
human.AddFunction(ContentKeyFunctions.Loadout1);
human.AddFunction(ContentKeyFunctions.Loadout2);
human.AddFunction(ContentKeyFunctions.Loadout3);
human.AddFunction(ContentKeyFunctions.Loadout4);
human.AddFunction(ContentKeyFunctions.Loadout5);
human.AddFunction(ContentKeyFunctions.Loadout6);
human.AddFunction(ContentKeyFunctions.Loadout7);
human.AddFunction(ContentKeyFunctions.Loadout8);
human.AddFunction(ContentKeyFunctions.Loadout9);
// actions should be common (for ghosts, mobs, etc)
common.AddFunction(ContentKeyFunctions.OpenActionsMenu);
common.AddFunction(ContentKeyFunctions.Hotbar0);
common.AddFunction(ContentKeyFunctions.Hotbar1);
common.AddFunction(ContentKeyFunctions.Hotbar2);
common.AddFunction(ContentKeyFunctions.Hotbar3);
common.AddFunction(ContentKeyFunctions.Hotbar4);
common.AddFunction(ContentKeyFunctions.Hotbar5);
common.AddFunction(ContentKeyFunctions.Hotbar6);
common.AddFunction(ContentKeyFunctions.Hotbar7);
common.AddFunction(ContentKeyFunctions.Hotbar8);
common.AddFunction(ContentKeyFunctions.Hotbar9);
common.AddFunction(ContentKeyFunctions.Loadout1);
common.AddFunction(ContentKeyFunctions.Loadout2);
common.AddFunction(ContentKeyFunctions.Loadout3);
common.AddFunction(ContentKeyFunctions.Loadout4);
common.AddFunction(ContentKeyFunctions.Loadout5);
common.AddFunction(ContentKeyFunctions.Loadout6);
common.AddFunction(ContentKeyFunctions.Loadout7);
common.AddFunction(ContentKeyFunctions.Loadout8);
common.AddFunction(ContentKeyFunctions.Loadout9);
var ghost = contexts.New("ghost", "common");
ghost.AddFunction(EngineKeyFunctions.MoveUp);

View File

@@ -0,0 +1,54 @@
#nullable enable
using System.Linq;
using Content.Server.GameObjects.Components.Observer;
using Content.Shared.Actions;
using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.Utility;
using JetBrains.Annotations;
using Robust.Shared.Serialization;
namespace Content.Server.Actions
{
/// <summary>
/// Blink lights and scare livings
/// </summary>
[UsedImplicitly]
public class GhostBoo : IInstantAction
{
private float _radius;
private float _cooldown;
private int _maxTargets;
void IExposeData.ExposeData(ObjectSerializer serializer)
{
serializer.DataField(ref _radius, "radius", 3);
serializer.DataField(ref _cooldown, "cooldown", 120);
serializer.DataField(ref _maxTargets, "maxTargets", 3);
}
public void DoInstantAction(InstantActionEventArgs args)
{
if (!args.Performer.TryGetComponent<SharedActionsComponent>(out var actions)) return;
// find all IGhostBooAffected nearby and do boo on them
var entityMan = args.Performer.EntityManager;
var ents = entityMan.GetEntitiesInRange(args.Performer, _radius, false);
var booCounter = 0;
foreach (var ent in ents)
{
var boos = ent.GetAllComponents<IGhostBooAffected>().ToList();
foreach (var boo in boos)
{
if (boo.AffectedByGhostBoo(args))
booCounter++;
}
if (booCounter >= _maxTargets)
break;
}
actions.Cooldown(args.ActionType, Cooldowns.SecondsFromNow(_cooldown));
}
}
}

View File

@@ -0,0 +1,19 @@
#nullable enable
using Content.Shared.Actions;
namespace Content.Server.GameObjects.Components.Observer
{
/// <summary>
/// Allow ghost to interact with object by boo action
/// </summary>
public interface IGhostBooAffected
{
/// <summary>
/// Invokes when ghost used boo action near entity.
/// Use it to blink lights or make something spooky.
/// </summary>
/// <param name="args">Boo action details</param>
/// <returns>Returns true if object was affected</returns>
bool AffectedByGhostBoo(InstantActionEventArgs args);
}
}

View File

@@ -6,8 +6,11 @@ using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.GameObjects.Components.MachineLinking;
using Content.Server.GameObjects.Components.MachineLinking.Signals;
using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.Components.Observer;
using Content.Shared.Actions;
using Content.Shared.Damage;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Power.ApcNetComponents.PowerReceiverUsers;
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Server.GameObjects;
@@ -15,6 +18,7 @@ using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Serialization;
using Robust.Shared.Timing;
using Robust.Shared.ViewVariables;
@@ -25,17 +29,27 @@ namespace Content.Server.GameObjects.Components.Power.ApcNetComponents.PowerRece
/// Component that represents a wall light. It has a light bulb that can be replaced when broken.
/// </summary>
[RegisterComponent]
public class PoweredLightComponent : Component, IInteractHand, IInteractUsing, IMapInit, ISignalReceiver<bool>, ISignalReceiver<ToggleSignal>
public class PoweredLightComponent : Component, IInteractHand, IInteractUsing, IMapInit, ISignalReceiver<bool>, ISignalReceiver<ToggleSignal>, IGhostBooAffected
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
public override string Name => "PoweredLight";
private static readonly TimeSpan _thunkDelay = TimeSpan.FromSeconds(2);
// time to blink light when ghost made boo nearby
private static readonly TimeSpan ghostBlinkingTime = TimeSpan.FromSeconds(10);
private static readonly TimeSpan ghostBlinkingCooldown = TimeSpan.FromSeconds(60);
[ComponentDependency]
private readonly AppearanceComponent? _appearance;
private TimeSpan _lastThunk;
private TimeSpan? _lastGhostBlink;
private bool _hasLampOnSpawn;
[ViewVariables] private bool _on;
[ViewVariables] private bool _isBlinking;
[ViewVariables] private bool _ignoreGhostsBoo;
private LightBulbType BulbType = LightBulbType.Tube;
[ViewVariables] private ContainerSlot _lightBulbContainer = default!;
@@ -145,6 +159,7 @@ namespace Content.Server.GameObjects.Components.Power.ApcNetComponents.PowerRece
serializer.DataField(ref BulbType, "bulb", LightBulbType.Tube);
serializer.DataField(ref _on, "on", true);
serializer.DataField(ref _hasLampOnSpawn, "hasLampOnSpawn", true);
serializer.DataField(ref _ignoreGhostsBoo, "ignoreGhostsBoo", false);
}
/// <summary>
@@ -163,13 +178,11 @@ namespace Content.Server.GameObjects.Components.Power.ApcNetComponents.PowerRece
public void UpdateLight()
{
var powerReceiver = Owner.GetComponent<PowerReceiverComponent>();
var sprite = Owner.GetComponent<SpriteComponent>();
var light = Owner.GetComponent<PointLightComponent>();
if (LightBulb == null) // No light bulb.
{
powerReceiver.Load = 0;
sprite.LayerSetState(0, "empty");
light.Enabled = false;
_appearance?.SetData(PoweredLightVisuals.BulbState, PoweredLightState.Empty);
return;
}
@@ -179,9 +192,8 @@ namespace Content.Server.GameObjects.Components.Power.ApcNetComponents.PowerRece
if (powerReceiver.Powered && _on)
{
powerReceiver.Load = LightBulb.PowerUse;
sprite.LayerSetState(0, "on");
light.Enabled = true;
light.Color = LightBulb.Color;
_appearance?.SetData(PoweredLightVisuals.BulbState, PoweredLightState.On);
_appearance?.SetData(PoweredLightVisuals.BulbColor, LightBulb.Color);
var time = _gameTiming.CurTime;
if (time > _lastThunk + _thunkDelay)
{
@@ -191,17 +203,14 @@ namespace Content.Server.GameObjects.Components.Power.ApcNetComponents.PowerRece
}
else
{
sprite.LayerSetState(0, "off");
light.Enabled = false;
_appearance?.SetData(PoweredLightVisuals.BulbState, PoweredLightState.Off);
}
break;
case LightBulbState.Broken:
sprite.LayerSetState(0, "broken");
light.Enabled = false;
_appearance?.SetData(PoweredLightVisuals.BulbState, PoweredLightState.Broken);
break;
case LightBulbState.Burned:
sprite.LayerSetState(0, "burned");
light.Enabled = false;
_appearance?.SetData(PoweredLightVisuals.BulbState, PoweredLightState.Burned);
break;
}
}
@@ -268,5 +277,36 @@ namespace Content.Server.GameObjects.Components.Power.ApcNetComponents.PowerRece
_on = !_on;
UpdateLight();
}
public void ToggleBlinkingLight(bool isNowBlinking)
{
if (_isBlinking == isNowBlinking)
return;
_isBlinking = isNowBlinking;
_appearance?.SetData(PoweredLightVisuals.Blinking, _isBlinking);
}
public bool AffectedByGhostBoo(InstantActionEventArgs args)
{
if (_ignoreGhostsBoo)
return false;
// check cooldown first to prevent abuse
var time = _gameTiming.CurTime;
if (_lastGhostBlink != null)
{
if (time <= _lastGhostBlink + ghostBlinkingCooldown)
return false;
}
_lastGhostBlink = time;
ToggleBlinkingLight(true);
Owner.SpawnTimer(ghostBlinkingTime, () => {
ToggleBlinkingLight(false);
});
return true;
}
}
}

View File

@@ -8,6 +8,7 @@
Error,
HumanScream,
Disarm,
GhostBoo,
DebugInstant,
DebugToggle,
DebugTargetPoint,

View File

@@ -0,0 +1,29 @@
#nullable enable
using Robust.Shared.Serialization;
using System;
namespace Content.Shared.GameObjects.Components.Power.ApcNetComponents.PowerReceiverUsers
{
[Serializable, NetSerializable]
public enum PoweredLightVisuals : byte
{
BulbState,
BulbColor,
Blinking
}
[Serializable, NetSerializable]
public enum PoweredLightState : byte
{
Empty,
On,
Off,
Broken,
Burned
}
public enum PoweredLightLayers : byte
{
Base
}
}

View File

@@ -33,6 +33,19 @@
repeat: true
behavior: !type:DisarmAction { }
- type: action
actionType: GhostBoo
icon: Interface/Actions/scream.png
name: "Boo"
description: "Scare your crew members because of boredom!"
filters:
- ghost
behaviorType: Instant
behavior: !type:GhostBoo
radius: 3
cooldown: 120
maxTargets: 3
- type: action
actionType: DebugInstant
icon: Interface/Alerts/Human/human1.png

View File

@@ -18,7 +18,8 @@
- type: EmergencyLight
- type: Sprite
sprite: Constructible/Lighting/emergency_light.rsi
state: emergency_light_off
layers:
- state: emergency_light_off
placement:
snap:
- Wallmount

View File

@@ -16,6 +16,9 @@
- type: LoopingSound
- type: Sprite
sprite: Constructible/Lighting/light_tube.rsi
layers:
- state: on
map: ["enum.PoweredLightLayers.Base"]
state: on
- type: PointLight
radius: 8
@@ -57,6 +60,10 @@
- type: PoweredLight
bulb: Tube
- type: PowerReceiver
- type: Appearance
visuals:
- type: PoweredLightVisualizer
blinkingSound: "/Audio/Machines/light_tube_on.ogg"
- type: entity
id: PoweredlightEmpty
@@ -122,6 +129,9 @@
- type: PoweredLight
bulb: Bulb
- type: PowerReceiver
- type: Appearance
visuals:
- type: PoweredLightVisualizer
- type: entity
id: PoweredSmallLightEmpty

View File

@@ -41,3 +41,6 @@
baseSprintSpeed: 14
baseWalkSpeed: 7
- type: MovementIgnoreGravity
- type: Actions
innateActions:
- GhostBoo