mirror of
https://github.com/WWhiteDreamProject/wwdpublic.git
synced 2026-04-18 14:07:53 +03:00
Add guidebook protodata tag: embed prototype values in guidebook text (#27570)
* First clumsy implementation of guidebook protodata * Support for Properties, format strings * Better null * Rename file * Documentation and some cleanup * Handle errors better * Added note about client-side components * A couple of examples * DataFields * Use attributes like a sane person * Outdated doc * string.Empty * No IComponent? * No casting * Use EntityManager.ComponentFactory * Use FrozenDictionary * Cache tagged component fields * Iterate components and check if they're tagged (cherry picked from commit 320135347ffc01f078a89b10322d7952e3af92e4)
This commit is contained in:
45
Content.Client/Guidebook/GuidebookDataSystem.cs
Normal file
45
Content.Client/Guidebook/GuidebookDataSystem.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using Content.Shared.Guidebook;
|
||||
|
||||
namespace Content.Client.Guidebook;
|
||||
|
||||
/// <summary>
|
||||
/// Client system for storing and retrieving values extracted from entity prototypes
|
||||
/// for display in the guidebook (<see cref="RichText.ProtodataTag"/>).
|
||||
/// Requests data from the server on <see cref="Initialize"/>.
|
||||
/// Can also be pushed new data when the server reloads prototypes.
|
||||
/// </summary>
|
||||
public sealed class GuidebookDataSystem : EntitySystem
|
||||
{
|
||||
private GuidebookData? _data;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeNetworkEvent<UpdateGuidebookDataEvent>(OnServerUpdated);
|
||||
|
||||
// Request data from the server
|
||||
RaiseNetworkEvent(new RequestGuidebookDataEvent());
|
||||
}
|
||||
|
||||
private void OnServerUpdated(UpdateGuidebookDataEvent args)
|
||||
{
|
||||
// Got new data from the server, either in response to our request, or because prototypes reloaded on the server
|
||||
_data = args.Data;
|
||||
_data.Freeze();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to retrieve a value using the given identifiers.
|
||||
/// See <see cref="GuidebookData.TryGetValue"/> for more information.
|
||||
/// </summary>
|
||||
public bool TryGetValue(string prototype, string component, string field, out object? value)
|
||||
{
|
||||
if (_data == null)
|
||||
{
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
return _data.TryGetValue(prototype, component, field, out value);
|
||||
}
|
||||
}
|
||||
49
Content.Client/Guidebook/Richtext/ProtodataTag.cs
Normal file
49
Content.Client/Guidebook/Richtext/ProtodataTag.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using System.Globalization;
|
||||
using Robust.Client.UserInterface.RichText;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Guidebook.RichText;
|
||||
|
||||
/// <summary>
|
||||
/// RichText tag that can display values extracted from entity prototypes.
|
||||
/// In order to be accessed by this tag, the desired field/property must
|
||||
/// be tagged with <see cref="Shared.Guidebook.GuidebookDataAttribute"/>.
|
||||
/// </summary>
|
||||
public sealed class ProtodataTag : IMarkupTag
|
||||
{
|
||||
[Dependency] private readonly ILogManager _logMan = default!;
|
||||
[Dependency] private readonly IEntityManager _entMan = default!;
|
||||
|
||||
public string Name => "protodata";
|
||||
private ISawmill Log => _log ??= _logMan.GetSawmill("protodata_tag");
|
||||
private ISawmill? _log;
|
||||
|
||||
public string TextBefore(MarkupNode node)
|
||||
{
|
||||
// Do nothing with an empty tag
|
||||
if (!node.Value.TryGetString(out var prototype))
|
||||
return string.Empty;
|
||||
|
||||
if (!node.Attributes.TryGetValue("comp", out var component))
|
||||
return string.Empty;
|
||||
if (!node.Attributes.TryGetValue("member", out var member))
|
||||
return string.Empty;
|
||||
node.Attributes.TryGetValue("format", out var format);
|
||||
|
||||
var guidebookData = _entMan.System<GuidebookDataSystem>();
|
||||
|
||||
// Try to get the value
|
||||
if (!guidebookData.TryGetValue(prototype, component.StringValue!, member.StringValue!, out var value))
|
||||
{
|
||||
Log.Error($"Failed to find protodata for {component}.{member} in {prototype}");
|
||||
return "???";
|
||||
}
|
||||
|
||||
// If we have a format string and a formattable value, format it as requested
|
||||
if (!string.IsNullOrEmpty(format.StringValue) && value is IFormattable formattable)
|
||||
return formattable.ToString(format.StringValue, CultureInfo.CurrentCulture);
|
||||
|
||||
// No format string given, so just use default ToString
|
||||
return value?.ToString() ?? "NULL";
|
||||
}
|
||||
}
|
||||
111
Content.Server/Guidebook/GuidebookDataSystem.cs
Normal file
111
Content.Server/Guidebook/GuidebookDataSystem.cs
Normal file
@@ -0,0 +1,111 @@
|
||||
using System.Reflection;
|
||||
using Content.Shared.Guidebook;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Guidebook;
|
||||
|
||||
/// <summary>
|
||||
/// Server system for identifying component fields/properties to extract values from entity prototypes.
|
||||
/// Extracted data is sent to clients when they connect or when prototypes are reloaded.
|
||||
/// </summary>
|
||||
public sealed class GuidebookDataSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _protoMan = default!;
|
||||
|
||||
private readonly Dictionary<string, List<MemberInfo>> _tagged = [];
|
||||
private GuidebookData _cachedData = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeNetworkEvent<RequestGuidebookDataEvent>(OnRequestRules);
|
||||
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypesReloaded);
|
||||
|
||||
// Build initial cache
|
||||
GatherData(ref _cachedData);
|
||||
}
|
||||
|
||||
private void OnRequestRules(RequestGuidebookDataEvent ev, EntitySessionEventArgs args)
|
||||
{
|
||||
// Send cached data to requesting client
|
||||
var sendEv = new UpdateGuidebookDataEvent(_cachedData);
|
||||
RaiseNetworkEvent(sendEv, args.SenderSession);
|
||||
}
|
||||
|
||||
private void OnPrototypesReloaded(PrototypesReloadedEventArgs args)
|
||||
{
|
||||
// We only care about entity prototypes
|
||||
if (!args.WasModified<EntityPrototype>())
|
||||
return;
|
||||
|
||||
// The entity prototypes changed! Clear our cache and regather data
|
||||
RebuildDataCache();
|
||||
|
||||
// Send new data to all clients
|
||||
var ev = new UpdateGuidebookDataEvent(_cachedData);
|
||||
RaiseNetworkEvent(ev);
|
||||
}
|
||||
|
||||
private void GatherData(ref GuidebookData cache)
|
||||
{
|
||||
// Just for debug metrics
|
||||
var memberCount = 0;
|
||||
var prototypeCount = 0;
|
||||
|
||||
if (_tagged.Count == 0)
|
||||
{
|
||||
// Scan component registrations to find members tagged for extraction
|
||||
foreach (var registration in EntityManager.ComponentFactory.GetAllRegistrations())
|
||||
{
|
||||
foreach (var member in registration.Type.GetMembers())
|
||||
{
|
||||
if (member.HasCustomAttribute<GuidebookDataAttribute>())
|
||||
{
|
||||
// Note this component-member pair for later
|
||||
_tagged.GetOrNew(registration.Name).Add(member);
|
||||
memberCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scan entity prototypes for the component-member pairs we noted
|
||||
var entityPrototypes = _protoMan.EnumeratePrototypes<EntityPrototype>();
|
||||
foreach (var prototype in entityPrototypes)
|
||||
{
|
||||
foreach (var (component, entry) in prototype.Components)
|
||||
{
|
||||
if (!_tagged.TryGetValue(component, out var members))
|
||||
continue;
|
||||
|
||||
prototypeCount++;
|
||||
|
||||
foreach (var member in members)
|
||||
{
|
||||
// It's dumb that we can't just do member.GetValue, but we can't, so
|
||||
var value = member switch
|
||||
{
|
||||
FieldInfo field => field.GetValue(entry.Component),
|
||||
PropertyInfo property => property.GetValue(entry.Component),
|
||||
_ => throw new NotImplementedException("Unsupported member type")
|
||||
};
|
||||
// Add it into the data cache
|
||||
cache.AddData(prototype.ID, component, member.Name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log.Debug($"Collected {cache.Count} Guidebook Protodata value(s) - {prototypeCount} matched prototype(s), {_tagged.Count} component(s), {memberCount} member(s)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the cached data, then regathers it.
|
||||
/// </summary>
|
||||
private void RebuildDataCache()
|
||||
{
|
||||
_cachedData.Clear();
|
||||
GatherData(ref _cachedData);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.Guidebook;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
@@ -50,5 +52,15 @@ namespace Content.Shared.Explosion.Components
|
||||
/// Whether or not to show the user a popup when starting the timer.
|
||||
/// </summary>
|
||||
[DataField] public bool DoPopup = true;
|
||||
|
||||
#region GuidebookData
|
||||
|
||||
[GuidebookData]
|
||||
public float? ShortestDelayOption => DelayOptions?.Min();
|
||||
|
||||
[GuidebookData]
|
||||
public float? LongestDelayOption => DelayOptions?.Max();
|
||||
|
||||
#endregion GuidebookData
|
||||
}
|
||||
}
|
||||
|
||||
25
Content.Shared/Guidebook/Events.cs
Normal file
25
Content.Shared/Guidebook/Events.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Guidebook;
|
||||
|
||||
/// <summary>
|
||||
/// Raised by the client on GuidebookDataSystem Initialize to request a
|
||||
/// full set of guidebook data from the server.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class RequestGuidebookDataEvent : EntityEventArgs { }
|
||||
|
||||
/// <summary>
|
||||
/// Raised by the server at a specific client in response to <see cref="RequestGuidebookDataEvent"/>.
|
||||
/// Also raised by the server at ALL clients when prototype data is hot-reloaded.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class UpdateGuidebookDataEvent : EntityEventArgs
|
||||
{
|
||||
public GuidebookData Data;
|
||||
|
||||
public UpdateGuidebookDataEvent(GuidebookData data)
|
||||
{
|
||||
Data = data;
|
||||
}
|
||||
}
|
||||
99
Content.Shared/Guidebook/GuidebookData.cs
Normal file
99
Content.Shared/Guidebook/GuidebookData.cs
Normal file
@@ -0,0 +1,99 @@
|
||||
using System.Collections.Frozen;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Shared.Guidebook;
|
||||
|
||||
/// <summary>
|
||||
/// Used by GuidebookDataSystem to hold data extracted from prototype values,
|
||||
/// both for storage and for network transmission.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
[DataDefinition]
|
||||
public sealed partial class GuidebookData
|
||||
{
|
||||
/// <summary>
|
||||
/// Total number of data values stored.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int Count { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The data extracted by the system.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Structured as PrototypeName, ComponentName, FieldName, Value
|
||||
/// </remarks>
|
||||
[DataField]
|
||||
public Dictionary<string, Dictionary<string, Dictionary<string, object?>>> Data = [];
|
||||
|
||||
/// <summary>
|
||||
/// The data extracted by the system, converted to a FrozenDictionary for faster lookup.
|
||||
/// </summary>
|
||||
public FrozenDictionary<string, FrozenDictionary<string, FrozenDictionary<string, object?>>> FrozenData;
|
||||
|
||||
/// <summary>
|
||||
/// Has the data been converted to a FrozenDictionary for faster lookup?
|
||||
/// This should only be done on clients, as FrozenDictionary isn't serializable.
|
||||
/// </summary>
|
||||
public bool IsFrozen;
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new value using the given identifiers.
|
||||
/// </summary>
|
||||
public void AddData(string prototype, string component, string field, object? value)
|
||||
{
|
||||
if (IsFrozen)
|
||||
throw new InvalidOperationException("Attempted to add data to GuidebookData while it is frozen!");
|
||||
Data.GetOrNew(prototype).GetOrNew(component).Add(field, value);
|
||||
Count++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to retrieve a value using the given identifiers.
|
||||
/// </summary>
|
||||
/// <returns>true if the value was retrieved, otherwise false</returns>
|
||||
public bool TryGetValue(string prototype, string component, string field, out object? value)
|
||||
{
|
||||
if (!IsFrozen)
|
||||
throw new InvalidOperationException("Freeze the GuidebookData before calling TryGetValue!");
|
||||
|
||||
// Look in frozen dictionary
|
||||
if (FrozenData.TryGetValue(prototype, out var p)
|
||||
&& p.TryGetValue(component, out var c)
|
||||
&& c.TryGetValue(field, out value))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes all data.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
Data.Clear();
|
||||
Count = 0;
|
||||
IsFrozen = false;
|
||||
}
|
||||
|
||||
public void Freeze()
|
||||
{
|
||||
var protos = new Dictionary<string, FrozenDictionary<string, FrozenDictionary<string, object?>>>();
|
||||
foreach (var (protoId, protoData) in Data)
|
||||
{
|
||||
var comps = new Dictionary<string, FrozenDictionary<string, object?>>();
|
||||
foreach (var (compId, compData) in protoData)
|
||||
{
|
||||
comps.Add(compId, FrozenDictionary.ToFrozenDictionary(compData));
|
||||
}
|
||||
protos.Add(protoId, FrozenDictionary.ToFrozenDictionary(comps));
|
||||
}
|
||||
FrozenData = FrozenDictionary.ToFrozenDictionary(protos);
|
||||
Data.Clear();
|
||||
IsFrozen = true;
|
||||
}
|
||||
}
|
||||
12
Content.Shared/Guidebook/GuidebookDataAttribute.cs
Normal file
12
Content.Shared/Guidebook/GuidebookDataAttribute.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace Content.Shared.Guidebook;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that GuidebookDataSystem should include this field/property when
|
||||
/// scanning entity prototypes for values to extract.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Note that this will not work for client-only components, because the data extraction
|
||||
/// is done on the server (it uses reflection, which is blocked by the sandbox on clients).
|
||||
/// </remarks>
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
|
||||
public sealed class GuidebookDataAttribute : Attribute { }
|
||||
@@ -1,4 +1,5 @@
|
||||
using Robust.Shared.GameStates;
|
||||
using Content.Shared.Guidebook;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Power.Generator;
|
||||
|
||||
@@ -17,19 +18,20 @@ public sealed partial class FuelGeneratorComponent : Component
|
||||
/// <summary>
|
||||
/// Is the generator currently running?
|
||||
/// </summary>
|
||||
[DataField("on"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool On;
|
||||
|
||||
/// <summary>
|
||||
/// The generator's target power.
|
||||
/// </summary>
|
||||
[DataField("targetPower"), ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public float TargetPower = 15_000.0f;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum target power.
|
||||
/// </summary>
|
||||
[DataField("maxTargetPower"), ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
[GuidebookData]
|
||||
public float MaxTargetPower = 30_000.0f;
|
||||
|
||||
/// <summary>
|
||||
@@ -38,24 +40,24 @@ public sealed partial class FuelGeneratorComponent : Component
|
||||
/// <remarks>
|
||||
/// Setting this to any value above 0 means that the generator can't idle without consuming some amount of fuel.
|
||||
/// </remarks>
|
||||
[DataField("minTargetPower"), ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public float MinTargetPower = 1_000;
|
||||
|
||||
/// <summary>
|
||||
/// The "optimal" power at which the generator is considered to be at 100% efficiency.
|
||||
/// </summary>
|
||||
[DataField("optimalPower"), ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public float OptimalPower = 15_000.0f;
|
||||
|
||||
/// <summary>
|
||||
/// The rate at which one unit of fuel should be consumed.
|
||||
/// </summary>
|
||||
[DataField("optimalBurnRate"), ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public float OptimalBurnRate = 1 / 60.0f; // Once every 60 seconds.
|
||||
|
||||
/// <summary>
|
||||
/// A constant used to calculate fuel efficiency in relation to target power output and optimal power output
|
||||
/// </summary>
|
||||
[DataField("fuelEfficiencyConstant")]
|
||||
[DataField]
|
||||
public float FuelEfficiencyConstant = 1.3f;
|
||||
}
|
||||
|
||||
@@ -1,90 +1,90 @@
|
||||
<Document>
|
||||
# Обезвреживание Крупных Бомб
|
||||
# Обезвреживание Крупных Бомб
|
||||
|
||||
Ну что, ты нашёл большую бомбу, и она уже пикает? Эти штуки детонируют не сразу, но оставляют после себя здоровенную дыру в корпусе станции. Просто читай дальше — и, возможно, не взорвёшься.
|
||||
Ну что, ты нашёл большую бомбу, и она уже пикает? Эти штуки детонируют не сразу, но оставляют после себя здоровенную дыру в корпусе станции. Просто читай дальше — и, возможно, не взорвёшься.
|
||||
|
||||
## Снаряжение
|
||||
## Снаряжение
|
||||
|
||||
Для обезвреживания тебе понадобятся два основных инструмента, а мультитул поможет безопасно опознавать провода:
|
||||
<Box>
|
||||
<GuideEntityEmbed Entity="Wirecutter"/>
|
||||
<GuideEntityEmbed Entity="Screwdriver"/>
|
||||
<GuideEntityEmbed Entity="Multitool"/>
|
||||
</Box>
|
||||
Для обезвреживания тебе понадобятся два основных инструмента, а мультитул поможет безопасно опознавать провода:
|
||||
<Box>
|
||||
<GuideEntityEmbed Entity="Wirecutter"/>
|
||||
<GuideEntityEmbed Entity="Screwdriver"/>
|
||||
<GuideEntityEmbed Entity="Multitool"/>
|
||||
</Box>
|
||||
|
||||
Для защиты от превращения в фарш пригодится [color=yellow]костюм сапёра[/color] или любое другое защитное снаряжение:
|
||||
<Box>
|
||||
<GuideEntityEmbed Entity="ClothingHeadHelmetBombSuit"/>
|
||||
<GuideEntityEmbed Entity="ClothingOuterSuitBomb"/>
|
||||
<GuideEntityEmbed Entity="ClothingOuterHardsuitRd"/>
|
||||
<GuideEntityEmbed Entity="ClothingOuterHardsuitAtmos"/>
|
||||
</Box>
|
||||
Для защиты от превращения в фарш пригодится [color=yellow]костюм сапёра[/color] или любое другое защитное снаряжение:
|
||||
<Box>
|
||||
<GuideEntityEmbed Entity="ClothingHeadHelmetBombSuit"/>
|
||||
<GuideEntityEmbed Entity="ClothingOuterSuitBomb"/>
|
||||
<GuideEntityEmbed Entity="ClothingOuterHardsuitRd"/>
|
||||
<GuideEntityEmbed Entity="ClothingOuterHardsuitAtmos"/>
|
||||
</Box>
|
||||
|
||||
## Типы Бомб
|
||||
## Типы Бомб
|
||||
|
||||
Чаще всего тебе встретятся два вида:
|
||||
Чаще всего тебе встретятся два вида:
|
||||
|
||||
<Box>
|
||||
<GuideEntityEmbed Entity="SyndicateBomb"/>
|
||||
<GuideEntityEmbed Entity="TrainingBomb"/>
|
||||
</Box>
|
||||
<Box>
|
||||
<GuideEntityEmbed Entity="SyndicateBomb"/>
|
||||
<GuideEntityEmbed Entity="TrainingBomb"/>
|
||||
</Box>
|
||||
|
||||
Учебная бомба наносит лишь лёгкие повреждения и вряд ли убьёт. А вот синдикатовская устроит шоу с фейерверками, если не облачён в скафандр.
|
||||
Учебная бомба наносит лишь лёгкие повреждения и вряд ли убьёт. А вот синдикатовская устроит шоу с фейерверками, если не облачён в скафандр.
|
||||
|
||||
## Активация
|
||||
## Активация
|
||||
|
||||
Чтобы активировать бомбу, [color=yellow]кликни правой кнопкой[/color] и выбери [color=yellow]Начать отсчёт[/color], либо [color=yellow]альт-кликни[/color] по ней. После этого она начнёт пикать.
|
||||
Чтобы активировать бомбу, [color=yellow]кликни правой кнопкой[/color] и выбери [color=yellow]Начать отсчёт[/color], либо [color=yellow]альт-кликни[/color] по ней. После этого она начнёт пикать.
|
||||
|
||||
## Время
|
||||
## Время
|
||||
|
||||
Бомба тикает от 90 до 300 секунд. Время можно узнать, осмотрев бомбу, если только не перерезан провод Proceed. Как только отсчёт дойдёт до нуля — бабах.
|
||||
Бомба тикает от [protodata="SyndicateBomb" comp="OnUseTimerTrigger" member="ShortestDelayOption"/] до [protodata="SyndicateBomb" comp="OnUseTimerTrigger" member="LongestDelayOption"/] секунд. Время можно узнать, осмотрев бомбу, если только не перерезан провод Proceed. Как только отсчёт дойдёт до нуля — бабах.
|
||||
|
||||
## Болты
|
||||
## Болты
|
||||
|
||||
После активации бомба сама прикручивается к полу. Найди провод BOLT и перережь его, чтобы открутить её. После этого можно выкинуть бомбу в космос.
|
||||
После активации бомба сама прикручивается к полу. Найди провод BOLT и перережь его, чтобы открутить её. После этого можно выкинуть бомбу в космос.
|
||||
|
||||
## Провода
|
||||
## Провода
|
||||
|
||||
Открывай крышку отвёрткой и готовься к разгадыванию проводной головоломки. В стандартной синдикатовской бомбе около [color=yellow]10 проводов[/color]:
|
||||
- 3 — бесполезные (dummy),
|
||||
- [color=red]3 — вызовут взрыв[/color],
|
||||
- остальные выполняют полезные функции.
|
||||
Открывай крышку отвёрткой и готовься к разгадыванию проводной головоломки. В стандартной синдикатовской бомбе около [color=yellow]10 проводов[/color]:
|
||||
- 3 — бесполезные (dummy),
|
||||
- [color=red]3 — вызовут взрыв[/color],
|
||||
- остальные выполняют полезные функции.
|
||||
|
||||
Ты можешь:
|
||||
- [color=yellow]Пульсировать[/color] мультитулом — безопасно, полезно.
|
||||
- [color=red]Резать[/color] кусачками — может взорваться. Осторожно!
|
||||
- [color=green]Соединять[/color] обратно — иногда полезно.
|
||||
Ты можешь:
|
||||
- [color=yellow]Пульсировать[/color] мультитулом — безопасно, полезно.
|
||||
- [color=red]Резать[/color] кусачками — может взорваться. Осторожно!
|
||||
- [color=green]Соединять[/color] обратно — иногда полезно.
|
||||
|
||||
## Типы Проводов
|
||||
## Типы Проводов
|
||||
|
||||
[color=#a4885c]Провод Активации (LIVE)[/color]
|
||||
- [color=yellow]Пульс[/color]: Отложит взрыв на 30 сек.
|
||||
- [color=red]Резка[/color]: Обезвредит бомбу, если та активна. Если нет — активирует таймер.
|
||||
- [color=green]Соединение[/color]: Бесполезно.
|
||||
[color=#a4885c]Провод Активации (LIVE)[/color]
|
||||
- [color=yellow]Пульс[/color]: Отложит взрыв на 30 сек.
|
||||
- [color=red]Резка[/color]: Обезвредит бомбу, если та активна. Если нет — активирует таймер.
|
||||
- [color=green]Соединение[/color]: Бесполезно.
|
||||
|
||||
[color=#a4885c]Провод Отсчёта (PRCD)[/color]
|
||||
- [color=yellow]Пульс[/color]: Ускорит таймер на 15 сек.
|
||||
- [color=red]Резка[/color]: Скрывает таймер на осмотре.
|
||||
- [color=green]Соединение[/color]: Бесполезно.
|
||||
[color=#a4885c]Провод Отсчёта (PRCD)[/color]
|
||||
- [color=yellow]Пульс[/color]: Ускорит таймер на 15 сек.
|
||||
- [color=red]Резка[/color]: Скрывает таймер на осмотре.
|
||||
- [color=green]Соединение[/color]: Бесполезно.
|
||||
|
||||
[color=#a4885c]Провод Задержки (DLAY)[/color]
|
||||
- [color=yellow]Пульс[/color]: Замедляет бомбу на 30 сек.
|
||||
- [color=red]Резка[/color]: Ничего.
|
||||
- [color=green]Соединение[/color]: Ничего.
|
||||
[color=#a4885c]Провод Задержки (DLAY)[/color]
|
||||
- [color=yellow]Пульс[/color]: Замедляет бомбу на 30 сек.
|
||||
- [color=red]Резка[/color]: Ничего.
|
||||
- [color=green]Соединение[/color]: Ничего.
|
||||
|
||||
[color=#a4885c]Провод БУМ (BOOM)[/color]
|
||||
- [color=yellow]Пульс[/color]: [color=red]БАБАХ, если бомба активна![/color]
|
||||
- [color=red]Резка[/color]: [color=red]БАБАХ, если активна![/color] Иначе — отключает бомбу.
|
||||
- [color=green]Соединение[/color]: Повторно активирует бомбу.
|
||||
[color=#a4885c]Провод БУМ (BOOM)[/color]
|
||||
- [color=yellow]Пульс[/color]: [color=red]БАБАХ, если бомба активна![/color]
|
||||
- [color=red]Резка[/color]: [color=red]БАБАХ, если активна![/color] Иначе — отключает бомбу.
|
||||
- [color=green]Соединение[/color]: Повторно активирует бомбу.
|
||||
|
||||
[color=#a4885c]Провод Болтов (BOLT)[/color]
|
||||
- [color=yellow]Пульс[/color]: Визуально прокручивает болты.
|
||||
- [color=red]Резка[/color]: Открепляет бомбу от пола.
|
||||
- [color=green]Соединение[/color]: Прикручивает снова.
|
||||
[color=#a4885c]Провод Болтов (BOLT)[/color]
|
||||
- [color=yellow]Пульс[/color]: Визуально прокручивает болты.
|
||||
- [color=red]Резка[/color]: Открепляет бомбу от пола.
|
||||
- [color=green]Соединение[/color]: Прикручивает снова.
|
||||
|
||||
[color=#a4885c]Фальшивый Провод (Dummy)[/color]
|
||||
- Делай с ним что хочешь — бесполезен. Отличный кандидат для тестов.
|
||||
[color=#a4885c]Фальшивый Провод (Dummy)[/color]
|
||||
- Делай с ним что хочешь — бесполезен. Отличный кандидат для тестов.
|
||||
|
||||
Теперь ты знаешь всё, чтобы не закончить смену в виде кровавого облака.
|
||||
Теперь ты знаешь всё, чтобы не закончить смену в виде кровавого облака.
|
||||
</Document>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user