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.Arcade1);
human.AddFunction(ContentKeyFunctions.Arcade2); human.AddFunction(ContentKeyFunctions.Arcade2);
human.AddFunction(ContentKeyFunctions.Arcade3); human.AddFunction(ContentKeyFunctions.Arcade3);
human.AddFunction(ContentKeyFunctions.OpenActionsMenu);
human.AddFunction(ContentKeyFunctions.Hotbar0); // actions should be common (for ghosts, mobs, etc)
human.AddFunction(ContentKeyFunctions.Hotbar1); common.AddFunction(ContentKeyFunctions.OpenActionsMenu);
human.AddFunction(ContentKeyFunctions.Hotbar2); common.AddFunction(ContentKeyFunctions.Hotbar0);
human.AddFunction(ContentKeyFunctions.Hotbar3); common.AddFunction(ContentKeyFunctions.Hotbar1);
human.AddFunction(ContentKeyFunctions.Hotbar4); common.AddFunction(ContentKeyFunctions.Hotbar2);
human.AddFunction(ContentKeyFunctions.Hotbar5); common.AddFunction(ContentKeyFunctions.Hotbar3);
human.AddFunction(ContentKeyFunctions.Hotbar6); common.AddFunction(ContentKeyFunctions.Hotbar4);
human.AddFunction(ContentKeyFunctions.Hotbar7); common.AddFunction(ContentKeyFunctions.Hotbar5);
human.AddFunction(ContentKeyFunctions.Hotbar8); common.AddFunction(ContentKeyFunctions.Hotbar6);
human.AddFunction(ContentKeyFunctions.Hotbar9); common.AddFunction(ContentKeyFunctions.Hotbar7);
human.AddFunction(ContentKeyFunctions.Loadout1); common.AddFunction(ContentKeyFunctions.Hotbar8);
human.AddFunction(ContentKeyFunctions.Loadout2); common.AddFunction(ContentKeyFunctions.Hotbar9);
human.AddFunction(ContentKeyFunctions.Loadout3); common.AddFunction(ContentKeyFunctions.Loadout1);
human.AddFunction(ContentKeyFunctions.Loadout4); common.AddFunction(ContentKeyFunctions.Loadout2);
human.AddFunction(ContentKeyFunctions.Loadout5); common.AddFunction(ContentKeyFunctions.Loadout3);
human.AddFunction(ContentKeyFunctions.Loadout6); common.AddFunction(ContentKeyFunctions.Loadout4);
human.AddFunction(ContentKeyFunctions.Loadout7); common.AddFunction(ContentKeyFunctions.Loadout5);
human.AddFunction(ContentKeyFunctions.Loadout8); common.AddFunction(ContentKeyFunctions.Loadout6);
human.AddFunction(ContentKeyFunctions.Loadout9); common.AddFunction(ContentKeyFunctions.Loadout7);
common.AddFunction(ContentKeyFunctions.Loadout8);
common.AddFunction(ContentKeyFunctions.Loadout9);
var ghost = contexts.New("ghost", "common"); var ghost = contexts.New("ghost", "common");
ghost.AddFunction(EngineKeyFunctions.MoveUp); 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;
using Content.Server.GameObjects.Components.MachineLinking.Signals; using Content.Server.GameObjects.Components.MachineLinking.Signals;
using Content.Server.GameObjects.Components.Mobs; using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.Components.Observer;
using Content.Shared.Actions;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.GameObjects.Components.Damage; using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Power.ApcNetComponents.PowerReceiverUsers;
using Content.Shared.Interfaces; using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components; using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
@@ -15,6 +18,7 @@ using Robust.Shared.Audio;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Localization; using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
using Robust.Shared.Timing; using Robust.Shared.Timing;
using Robust.Shared.ViewVariables; 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. /// Component that represents a wall light. It has a light bulb that can be replaced when broken.
/// </summary> /// </summary>
[RegisterComponent] [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!; [Dependency] private readonly IGameTiming _gameTiming = default!;
public override string Name => "PoweredLight"; public override string Name => "PoweredLight";
private static readonly TimeSpan _thunkDelay = TimeSpan.FromSeconds(2); 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 _lastThunk;
private TimeSpan? _lastGhostBlink;
private bool _hasLampOnSpawn; private bool _hasLampOnSpawn;
[ViewVariables] private bool _on; [ViewVariables] private bool _on;
[ViewVariables] private bool _isBlinking;
[ViewVariables] private bool _ignoreGhostsBoo;
private LightBulbType BulbType = LightBulbType.Tube; private LightBulbType BulbType = LightBulbType.Tube;
[ViewVariables] private ContainerSlot _lightBulbContainer = default!; [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 BulbType, "bulb", LightBulbType.Tube);
serializer.DataField(ref _on, "on", true); serializer.DataField(ref _on, "on", true);
serializer.DataField(ref _hasLampOnSpawn, "hasLampOnSpawn", true); serializer.DataField(ref _hasLampOnSpawn, "hasLampOnSpawn", true);
serializer.DataField(ref _ignoreGhostsBoo, "ignoreGhostsBoo", false);
} }
/// <summary> /// <summary>
@@ -163,13 +178,11 @@ namespace Content.Server.GameObjects.Components.Power.ApcNetComponents.PowerRece
public void UpdateLight() public void UpdateLight()
{ {
var powerReceiver = Owner.GetComponent<PowerReceiverComponent>(); var powerReceiver = Owner.GetComponent<PowerReceiverComponent>();
var sprite = Owner.GetComponent<SpriteComponent>();
var light = Owner.GetComponent<PointLightComponent>();
if (LightBulb == null) // No light bulb. if (LightBulb == null) // No light bulb.
{ {
powerReceiver.Load = 0; powerReceiver.Load = 0;
sprite.LayerSetState(0, "empty"); _appearance?.SetData(PoweredLightVisuals.BulbState, PoweredLightState.Empty);
light.Enabled = false;
return; return;
} }
@@ -179,9 +192,8 @@ namespace Content.Server.GameObjects.Components.Power.ApcNetComponents.PowerRece
if (powerReceiver.Powered && _on) if (powerReceiver.Powered && _on)
{ {
powerReceiver.Load = LightBulb.PowerUse; powerReceiver.Load = LightBulb.PowerUse;
sprite.LayerSetState(0, "on"); _appearance?.SetData(PoweredLightVisuals.BulbState, PoweredLightState.On);
light.Enabled = true; _appearance?.SetData(PoweredLightVisuals.BulbColor, LightBulb.Color);
light.Color = LightBulb.Color;
var time = _gameTiming.CurTime; var time = _gameTiming.CurTime;
if (time > _lastThunk + _thunkDelay) if (time > _lastThunk + _thunkDelay)
{ {
@@ -191,17 +203,14 @@ namespace Content.Server.GameObjects.Components.Power.ApcNetComponents.PowerRece
} }
else else
{ {
sprite.LayerSetState(0, "off"); _appearance?.SetData(PoweredLightVisuals.BulbState, PoweredLightState.Off);
light.Enabled = false;
} }
break; break;
case LightBulbState.Broken: case LightBulbState.Broken:
sprite.LayerSetState(0, "broken"); _appearance?.SetData(PoweredLightVisuals.BulbState, PoweredLightState.Broken);
light.Enabled = false;
break; break;
case LightBulbState.Burned: case LightBulbState.Burned:
sprite.LayerSetState(0, "burned"); _appearance?.SetData(PoweredLightVisuals.BulbState, PoweredLightState.Burned);
light.Enabled = false;
break; break;
} }
} }
@@ -268,5 +277,36 @@ namespace Content.Server.GameObjects.Components.Power.ApcNetComponents.PowerRece
_on = !_on; _on = !_on;
UpdateLight(); 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, Error,
HumanScream, HumanScream,
Disarm, Disarm,
GhostBoo,
DebugInstant, DebugInstant,
DebugToggle, DebugToggle,
DebugTargetPoint, 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 repeat: true
behavior: !type:DisarmAction { } 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 - type: action
actionType: DebugInstant actionType: DebugInstant
icon: Interface/Alerts/Human/human1.png icon: Interface/Alerts/Human/human1.png

View File

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

View File

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

View File

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