Weather tweaks (#14271)

This commit is contained in:
metalgearsloth
2023-03-07 12:28:50 +11:00
committed by GitHub
parent 84052bb27d
commit 46b27a68ff
7 changed files with 247 additions and 170 deletions

View File

@@ -46,7 +46,7 @@ public sealed class WeatherOverlay : Overlay
return false; return false;
if (!_entManager.TryGetComponent<WeatherComponent>(_mapManager.GetMapEntityId(args.MapId), out var weather) || if (!_entManager.TryGetComponent<WeatherComponent>(_mapManager.GetMapEntityId(args.MapId), out var weather) ||
weather.Weather == null) weather.Weather.Count == 0)
{ {
return false; return false;
} }
@@ -58,15 +58,19 @@ public sealed class WeatherOverlay : Overlay
{ {
var mapUid = _mapManager.GetMapEntityId(args.MapId); var mapUid = _mapManager.GetMapEntityId(args.MapId);
if (!_entManager.TryGetComponent<WeatherComponent>(mapUid, out var weather) || if (!_entManager.TryGetComponent<WeatherComponent>(mapUid, out var comp))
weather.Weather == null ||
!_protoManager.TryIndex<WeatherPrototype>(weather.Weather, out var weatherProto))
{ {
return; return;
} }
var alpha = _weather.GetPercent(weather, mapUid, weatherProto); foreach (var (proto, weather) in comp.Weather)
DrawWorld(args, weatherProto, alpha); {
if (!_protoManager.TryIndex<WeatherPrototype>(proto, out var weatherProto))
continue;
var alpha = _weather.GetPercent(weather, mapUid);
DrawWorld(args, weatherProto, alpha);
}
} }
private void DrawWorld(in OverlayDrawArgs args, WeatherPrototype weatherProto, float alpha) private void DrawWorld(in OverlayDrawArgs args, WeatherPrototype weatherProto, float alpha)

View File

@@ -17,14 +17,9 @@ public sealed class WeatherSystem : SharedWeatherSystem
[Dependency] private readonly IOverlayManager _overlayManager = default!; [Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly AudioSystem _audio = default!; [Dependency] private readonly AudioSystem _audio = default!;
[Dependency] private readonly MetaDataSystem _metadata = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!; [Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly SharedTransformSystem _transform = default!;
// Consistency isn't really important, just want to avoid sharp changes and there's no way to lerp on engine nicely atm.
private float _lastAlpha;
private float _lastOcclusion;
private const float OcclusionLerpRate = 4f; private const float OcclusionLerpRate = 4f;
private const float AlphaLerpRate = 4f; private const float AlphaLerpRate = 4f;
@@ -41,9 +36,9 @@ public sealed class WeatherSystem : SharedWeatherSystem
_overlayManager.RemoveOverlay<WeatherOverlay>(); _overlayManager.RemoveOverlay<WeatherOverlay>();
} }
protected override void Run(EntityUid uid, WeatherComponent component, WeatherPrototype weather, WeatherState state, float frameTime) protected override void Run(EntityUid uid, WeatherData weather, WeatherPrototype weatherProto, float frameTime)
{ {
base.Run(uid, component, weather, state, frameTime); base.Run(uid, weather, weatherProto, frameTime);
var ent = _playerManager.LocalPlayer?.ControlledEntity; var ent = _playerManager.LocalPlayer?.ControlledEntity;
@@ -56,21 +51,21 @@ public sealed class WeatherSystem : SharedWeatherSystem
// Maybe have the viewports manage this? // Maybe have the viewports manage this?
if (mapUid == null || entXform.MapUid != mapUid) if (mapUid == null || entXform.MapUid != mapUid)
{ {
_lastOcclusion = 0f; weather.LastOcclusion = 0f;
_lastAlpha = 0f; weather.LastAlpha = 0f;
component.Stream?.Stop(); weather.Stream?.Stop();
component.Stream = null; weather.Stream = null;
return; return;
} }
if (!Timing.IsFirstTimePredicted || weather.Sound == null) if (!Timing.IsFirstTimePredicted || weatherProto.Sound == null)
return; return;
component.Stream ??= _audio.PlayGlobal(weather.Sound, Filter.Local(), true); weather.Stream ??= _audio.PlayGlobal(weatherProto.Sound, Filter.Local(), true);
var volumeMod = MathF.Pow(10, weather.Sound.Params.Volume / 10f); var volumeMod = MathF.Pow(10, weatherProto.Sound.Params.Volume / 10f);
var stream = (AudioSystem.PlayingStream) component.Stream!; var stream = (AudioSystem.PlayingStream) weather.Stream!;
var alpha = GetPercent(component, mapUid.Value, weather); var alpha = weather.LastAlpha;
alpha = MathF.Pow(alpha, 2f) * volumeMod; alpha = MathF.Pow(alpha, 2f) * volumeMod;
// TODO: Lerp this occlusion. // TODO: Lerp this occlusion.
var occlusion = 0f; var occlusion = 0f;
@@ -138,81 +133,75 @@ public sealed class WeatherSystem : SharedWeatherSystem
} }
} }
if (MathHelper.CloseTo(_lastOcclusion, occlusion, 0.01f)) if (MathHelper.CloseTo(weather.LastOcclusion, occlusion, 0.01f))
_lastOcclusion = occlusion; weather.LastOcclusion = occlusion;
else else
_lastOcclusion += (occlusion - _lastOcclusion) * OcclusionLerpRate * frameTime; weather.LastOcclusion += (occlusion - weather.LastOcclusion) * OcclusionLerpRate * frameTime;
if (MathHelper.CloseTo(_lastAlpha, alpha, 0.01f)) if (MathHelper.CloseTo(weather.LastAlpha, alpha, 0.01f))
_lastAlpha = alpha; weather.LastAlpha = alpha;
else else
_lastAlpha += (alpha - _lastAlpha) * AlphaLerpRate * frameTime; weather.LastAlpha += (alpha - weather.LastAlpha) * AlphaLerpRate * frameTime;
// Full volume if not on grid // Full volume if not on grid
stream.Source.SetVolumeDirect(_lastAlpha); stream.Source.SetVolumeDirect(weather.LastAlpha);
stream.Source.SetOcclusion(_lastOcclusion); stream.Source.SetOcclusion(weather.LastOcclusion);
} }
public float GetPercent(WeatherComponent component, EntityUid mapUid, WeatherPrototype weatherProto) protected override void EndWeather(EntityUid uid, WeatherComponent component, string proto)
{ {
var pauseTime = _metadata.GetPauseTime(mapUid); base.EndWeather(uid, component, proto);
var elapsed = Timing.CurTime - (component.StartTime + pauseTime);
var duration = component.Duration;
var remaining = duration - elapsed;
float alpha;
if (elapsed < weatherProto.StartupTime) if (!component.Weather.TryGetValue(proto, out var weather))
{ return;
alpha = (float) (elapsed / weatherProto.StartupTime);
}
else if (remaining < weatherProto.ShutdownTime)
{
alpha = (float) (remaining / weatherProto.ShutdownTime);
}
else
{
alpha = 1f;
}
return alpha; weather.LastAlpha = 0f;
weather.LastOcclusion = 0f;
} }
protected override bool SetState(EntityUid uid, WeatherComponent component, WeatherState state, WeatherPrototype prototype) protected override bool SetState(WeatherState state, WeatherComponent comp, WeatherData weather, WeatherPrototype weatherProto)
{ {
if (!base.SetState(uid, component, state, prototype)) if (!base.SetState(state, comp, weather, weatherProto))
return false; return false;
if (!Timing.IsFirstTimePredicted) if (!Timing.IsFirstTimePredicted)
return true; return true;
// TODO: Fades // TODO: Fades (properly)
component.Stream?.Stop(); weather.Stream?.Stop();
component.Stream = null; weather.Stream = null;
component.Stream = _audio.PlayGlobal(prototype.Sound, Filter.Local(), true); weather.Stream = _audio.PlayGlobal(weatherProto.Sound, Filter.Local(), true);
return true; return true;
} }
protected override void EndWeather(WeatherComponent component)
{
_lastOcclusion = 0f;
_lastAlpha = 0f;
base.EndWeather(component);
}
private void OnWeatherHandleState(EntityUid uid, WeatherComponent component, ref ComponentHandleState args) private void OnWeatherHandleState(EntityUid uid, WeatherComponent component, ref ComponentHandleState args)
{ {
if (args.Current is not WeatherComponentState state) if (args.Current is not WeatherComponentState state)
return; return;
if (component.Weather != state.Weather || !component.EndTime.Equals(state.EndTime) || !component.StartTime.Equals(state.StartTime)) foreach (var (proto, weather) in component.Weather)
{ {
EndWeather(component); // End existing one
if (!state.Weather.TryGetValue(proto, out var stateData))
{
EndWeather(uid, component, proto);
continue;
}
if (state.Weather != null) // Data update?
StartWeather(component, ProtoMan.Index<WeatherPrototype>(state.Weather)); weather.StartTime = stateData.StartTime;
weather.EndTime = stateData.EndTime;
weather.State = stateData.State;
} }
component.EndTime = state.EndTime; foreach (var (proto, weather) in state.Weather)
component.StartTime = state.StartTime; {
if (component.Weather.ContainsKey(proto))
continue;
// New weather
StartWeather(component, ProtoMan.Index<WeatherPrototype>(proto), weather.EndTime);
weather.LastAlpha = 0f;
}
} }
} }

View File

@@ -26,19 +26,15 @@ public sealed class WeatherSystem : SharedWeatherSystem
private void OnWeatherGetState(EntityUid uid, WeatherComponent component, ref ComponentGetState args) private void OnWeatherGetState(EntityUid uid, WeatherComponent component, ref ComponentGetState args)
{ {
args.State = new WeatherComponentState() args.State = new WeatherComponentState(component.Weather);
{
Weather = component.Weather,
EndTime = component.EndTime,
StartTime = component.StartTime,
};
} }
[AdminCommand(AdminFlags.Fun)] [AdminCommand(AdminFlags.Fun)]
private void WeatherTwo(IConsoleShell shell, string argstr, string[] args) private void WeatherTwo(IConsoleShell shell, string argstr, string[] args)
{ {
if (args.Length != 2) if (args.Length < 2)
{ {
shell.WriteError($"A");
return; return;
} }
@@ -54,13 +50,36 @@ public sealed class WeatherSystem : SharedWeatherSystem
return; return;
} }
TimeSpan? endTime = null;
if (args.Length == 3)
{
if (int.TryParse(args[2], out var durationInt))
{
var curTime = Timing.CurTime;
var maxTime = TimeSpan.MaxValue;
// If it's already running then just fade out with how much time we're into the weather.
if (TryComp<WeatherComponent>(MapManager.GetMapEntityId(mapId), out var weatherComp) &&
weatherComp.Weather.TryGetValue(args[1], out var existing))
{
maxTime = curTime - existing.StartTime;
}
endTime = curTime + TimeSpan.FromSeconds(durationInt);
if (endTime > maxTime)
endTime = maxTime;
}
}
if (args[1].Equals("null")) if (args[1].Equals("null"))
{ {
SetWeather(mapId, null); SetWeather(mapId, null, endTime);
} }
else if (ProtoMan.TryIndex<WeatherPrototype>(args[1], out var weatherProto)) else if (ProtoMan.TryIndex<WeatherPrototype>(args[1], out var weatherProto))
{ {
SetWeather(mapId, weatherProto); SetWeather(mapId, weatherProto, endTime);
} }
else else
{ {

View File

@@ -3,10 +3,8 @@ using Robust.Shared.Map;
using Robust.Shared.Map.Components; using Robust.Shared.Map.Components;
using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Components;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
using Robust.Shared.Timing; using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Shared.Weather; namespace Content.Shared.Weather;
@@ -15,8 +13,8 @@ public abstract class SharedWeatherSystem : EntitySystem
[Dependency] protected readonly IGameTiming Timing = default!; [Dependency] protected readonly IGameTiming Timing = default!;
[Dependency] protected readonly IMapManager MapManager = default!; [Dependency] protected readonly IMapManager MapManager = default!;
[Dependency] protected readonly IPrototypeManager ProtoMan = default!; [Dependency] protected readonly IPrototypeManager ProtoMan = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefManager = default!; [Dependency] private readonly ITileDefinitionManager _tileDefManager = default!;
[Dependency] private readonly MetaDataSystem _metadata = default!;
protected ISawmill Sawmill = default!; protected ISawmill Sawmill = default!;
@@ -29,7 +27,13 @@ public abstract class SharedWeatherSystem : EntitySystem
private void OnWeatherUnpaused(EntityUid uid, WeatherComponent component, ref EntityUnpausedEvent args) private void OnWeatherUnpaused(EntityUid uid, WeatherComponent component, ref EntityUnpausedEvent args)
{ {
component.EndTime += args.PausedTime; foreach (var weather in component.Weather.Values)
{
weather.StartTime += args.PausedTime;
if (weather.EndTime != null)
weather.EndTime = weather.EndTime.Value + args.PausedTime;
}
} }
public bool CanWeatherAffect(MapGridComponent grid, TileRef tileRef, EntityQuery<PhysicsComponent> bodyQuery) public bool CanWeatherAffect(MapGridComponent grid, TileRef tileRef, EntityQuery<PhysicsComponent> bodyQuery)
@@ -56,6 +60,31 @@ public abstract class SharedWeatherSystem : EntitySystem
} }
public float GetPercent(WeatherData component, EntityUid mapUid)
{
var pauseTime = _metadata.GetPauseTime(mapUid);
var elapsed = Timing.CurTime - (component.StartTime + pauseTime);
var duration = component.Duration;
var remaining = duration - elapsed;
float alpha;
if (remaining < WeatherComponent.ShutdownTime)
{
alpha = (float) (remaining / WeatherComponent.ShutdownTime);
}
else if (elapsed < WeatherComponent.StartupTime)
{
alpha = (float) (elapsed / WeatherComponent.StartupTime);
}
else
{
alpha = 1f;
}
return alpha;
}
public override void Update(float frameTime) public override void Update(float frameTime)
{ {
base.Update(frameTime); base.Update(frameTime);
@@ -65,103 +94,141 @@ public abstract class SharedWeatherSystem : EntitySystem
var curTime = Timing.CurTime; var curTime = Timing.CurTime;
foreach (var (comp, metadata) in EntityQuery<WeatherComponent, MetaDataComponent>()) foreach (var comp in EntityQuery<WeatherComponent>())
{ {
if (comp.Weather == null) if (comp.Weather.Count == 0)
continue; continue;
var uid = comp.Owner; var uid = comp.Owner;
var endTime = comp.EndTime;
// Ended foreach (var (proto, weather) in comp.Weather)
if (endTime < curTime)
{ {
EndWeather(comp); var endTime = weather.EndTime;
continue;
}
// Admin messed up or the likes. // Ended
if (!ProtoMan.TryIndex<WeatherPrototype>(comp.Weather, out var weatherProto)) if (endTime != null && endTime < curTime)
{
Sawmill.Error($"Unable to find weather prototype for {comp.Weather}, ending!");
EndWeather(comp);
continue;
}
var remainingTime = endTime - curTime;
// Shutting down
if (remainingTime < weatherProto.ShutdownTime)
{
SetState(uid, comp, WeatherState.Ending, weatherProto);
}
// Starting up
else
{
var startTime = comp.StartTime;
var elapsed = Timing.CurTime - startTime;
if (elapsed < weatherProto.StartupTime)
{ {
SetState(uid, comp, WeatherState.Starting, weatherProto); EndWeather(uid, comp, proto);
continue;
} }
}
// Run whatever code we need. var remainingTime = endTime - curTime;
Run(uid, comp, weatherProto, comp.State, frameTime);
// Admin messed up or the likes.
if (!ProtoMan.TryIndex<WeatherPrototype>(proto, out var weatherProto))
{
Sawmill.Error($"Unable to find weather prototype for {comp.Weather}, ending!");
EndWeather(uid, comp, proto);
continue;
}
// Shutting down
if (endTime != null && remainingTime < WeatherComponent.ShutdownTime)
{
SetState(WeatherState.Ending, comp, weather, weatherProto);
}
// Starting up
else
{
var startTime = weather.StartTime;
var elapsed = Timing.CurTime - startTime;
if (elapsed < WeatherComponent.StartupTime)
{
SetState(WeatherState.Starting, comp, weather, weatherProto);
}
}
// Run whatever code we need.
Run(uid, weather, weatherProto, frameTime);
}
} }
} }
public void SetWeather(MapId mapId, WeatherPrototype? weather) /// <summary>
/// Shuts down all existing weather and starts the new one if applicable.
/// </summary>
public void SetWeather(MapId mapId, WeatherPrototype? proto, TimeSpan? endTime)
{ {
var weatherComp = EnsureComp<WeatherComponent>(MapManager.GetMapEntityId(mapId)); var weatherComp = EnsureComp<WeatherComponent>(MapManager.GetMapEntityId(mapId));
EndWeather(weatherComp);
if (weather != null) foreach (var (eProto, weather) in weatherComp.Weather)
StartWeather(weatherComp, weather); {
// Reset cooldown if it's an existing one.
if (eProto == proto?.ID)
{
weather.EndTime = endTime;
if (weather.State == WeatherState.Ending)
weather.State = WeatherState.Running;
Dirty(weatherComp);
continue;
}
// Speedrun
var end = Timing.CurTime + WeatherComponent.ShutdownTime;
if (weather.EndTime == null || weather.EndTime > end)
{
weather.EndTime = end;
Dirty(weatherComp);
}
}
if (proto != null)
StartWeather(weatherComp, proto, endTime);
} }
/// <summary> /// <summary>
/// Run every tick when the weather is running. /// Run every tick when the weather is running.
/// </summary> /// </summary>
protected virtual void Run(EntityUid uid, WeatherComponent component, WeatherPrototype weather, WeatherState state, float frameTime) {} protected virtual void Run(EntityUid uid, WeatherData weather, WeatherPrototype weatherProto, float frameTime) {}
protected void StartWeather(WeatherComponent component, WeatherPrototype weather) protected void StartWeather(WeatherComponent component, WeatherPrototype weather, TimeSpan? endTime)
{ {
component.Weather = weather.ID; if (component.Weather.ContainsKey(weather.ID))
// TODO: ENGINE PR return;
var duration = _random.NextDouble(weather.DurationMinimum.TotalSeconds, weather.DurationMaximum.TotalSeconds);
component.EndTime = Timing.CurTime + TimeSpan.FromSeconds(duration); var data = new WeatherData()
component.StartTime = Timing.CurTime; {
DebugTools.Assert(component.State == WeatherState.Invalid); StartTime = Timing.CurTime,
EndTime = endTime,
};
component.Weather.Add(weather.ID, data);
Dirty(component); Dirty(component);
} }
protected virtual void EndWeather(WeatherComponent component) protected virtual void EndWeather(EntityUid uid, WeatherComponent component, string proto)
{ {
component.Stream?.Stop(); if (!component.Weather.TryGetValue(proto, out var data))
component.Stream = null; return;
component.Weather = null;
component.StartTime = TimeSpan.Zero; data.Stream?.Stop();
component.EndTime = TimeSpan.Zero; data.Stream = null;
component.State = WeatherState.Invalid; component.Weather.Remove(proto);
Dirty(component); Dirty(component);
} }
protected virtual bool SetState(EntityUid uid, WeatherComponent component, WeatherState state, WeatherPrototype prototype) protected virtual bool SetState(WeatherState state, WeatherComponent component, WeatherData weather, WeatherPrototype weatherProto)
{ {
if (component.State.Equals(state)) if (weather.State.Equals(state))
return false; return false;
component.State = state; weather.State = state;
Dirty(component);
return true; return true;
} }
[Serializable, NetSerializable] [Serializable, NetSerializable]
protected sealed class WeatherComponentState : ComponentState protected sealed class WeatherComponentState : ComponentState
{ {
public string? Weather; public Dictionary<string, WeatherData> Weather;
public TimeSpan StartTime;
public TimeSpan EndTime; public WeatherComponentState(Dictionary<string, WeatherData> weather)
{
Weather = weather;
}
} }
} }

View File

@@ -1,5 +1,8 @@
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
namespace Content.Shared.Weather; namespace Content.Shared.Weather;
@@ -7,31 +10,45 @@ namespace Content.Shared.Weather;
public sealed class WeatherComponent : Component public sealed class WeatherComponent : Component
{ {
/// <summary> /// <summary>
/// Currently running weather. /// Currently running weathers
/// </summary> /// </summary>
[ViewVariables, DataField("weather")] [ViewVariables, DataField("weather", customTypeSerializer:typeof(PrototypeIdDictionarySerializer<WeatherData, WeatherPrototype>))]
public string? Weather; public Dictionary<string, WeatherData> Weather = new();
// now public static readonly TimeSpan StartupTime = TimeSpan.FromSeconds(15);
public static readonly TimeSpan ShutdownTime = TimeSpan.FromSeconds(15);
}
[DataDefinition, Serializable, NetSerializable]
public sealed class WeatherData
{
// Client audio stream.
[NonSerialized]
public IPlayingAudioStream? Stream; public IPlayingAudioStream? Stream;
/// <summary> /// <summary>
/// When the weather started. /// When the weather started if relevant.
/// </summary> /// </summary>
[ViewVariables, DataField("startTime")] [ViewVariables, DataField("startTime", customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan StartTime = TimeSpan.Zero; public TimeSpan StartTime = TimeSpan.Zero;
/// <summary> /// <summary>
/// When the applied weather will end. /// When the applied weather will end.
/// </summary> /// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("endTime")] [ViewVariables(VVAccess.ReadWrite), DataField("endTime", customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan EndTime = TimeSpan.Zero; public TimeSpan? EndTime;
[ViewVariables] [ViewVariables]
public TimeSpan Duration => EndTime - StartTime; public TimeSpan Duration => EndTime == null ? TimeSpan.MaxValue : EndTime.Value - StartTime;
[ViewVariables] [DataField("state")]
public WeatherState State = WeatherState.Invalid; public WeatherState State = WeatherState.Invalid;
[ViewVariables, NonSerialized]
public float LastAlpha;
[ViewVariables, NonSerialized]
public float LastOcclusion;
} }
public enum WeatherState : byte public enum WeatherState : byte

View File

@@ -1,7 +1,5 @@
using Content.Shared.Maps;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
using Robust.Shared.Utility; using Robust.Shared.Utility;
namespace Content.Shared.Weather; namespace Content.Shared.Weather;
@@ -11,24 +9,6 @@ public sealed class WeatherPrototype : IPrototype
{ {
[IdDataField] public string ID { get; } = default!; [IdDataField] public string ID { get; } = default!;
/// <summary>
/// Minimum duration for the weather.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public TimeSpan DurationMinimum = TimeSpan.FromSeconds(120);
/// <summary>
/// Maximum duration for the weather.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public TimeSpan DurationMaximum = TimeSpan.FromSeconds(300);
[ViewVariables(VVAccess.ReadWrite), DataField("startupTime")]
public TimeSpan StartupTime = TimeSpan.FromSeconds(30);
[ViewVariables(VVAccess.ReadWrite), DataField("endTime")]
public TimeSpan ShutdownTime = TimeSpan.FromSeconds(30);
[ViewVariables(VVAccess.ReadWrite), DataField("sprite", required: true)] [ViewVariables(VVAccess.ReadWrite), DataField("sprite", required: true)]
public SpriteSpecifier Sprite = default!; public SpriteSpecifier Sprite = default!;

View File

@@ -18,6 +18,7 @@
- Space - Space
isSubfloor: true isSubfloor: true
canWirecutter: true canWirecutter: true
weather: true
footstepSounds: footstepSounds:
collection: FootstepPlating collection: FootstepPlating
friction: 0.5 friction: 0.5