mirror of
https://github.com/DeltaV-Station/Delta-v.git
synced 2026-09-29 15:47:26 +00:00
tips / tutorial system (#5199)
* somewhat functional yes * some cleanup * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * review * missed one * why not params… * review 2 --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
pre-commit-ci[bot]
parent
885d6e8872
commit
e0fd0cb2cd
@@ -1,21 +1,18 @@
|
||||
<tabs:DeltaTab xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:tabs="clr-namespace:Content.Client._DV.Options.UI.Tabs"
|
||||
xmlns:xNamespace="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:optionsUI="clr-namespace:Content.Client.Options.UI"
|
||||
xmlns:s="clr-namespace:Content.Client.Stylesheets"
|
||||
xmlns:ui="clr-namespace:Content.Client.Options.UI"
|
||||
xmlns:dvui="clr-namespace:Content.Client._DV.Options.UI">
|
||||
xmlns:ui="clr-namespace:Content.Client.Options.UI">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<BoxContainer Orientation="Vertical" Margin="8 8 8 8" VerticalExpand="True">
|
||||
<Label Text="{Loc 'ui-options-general-forknotice'}"
|
||||
FontColorOverride="{xNamespace:Static s:StyleNano.ConcerningOrangeFore}"
|
||||
StyleClasses="LabelKeyText"/>
|
||||
<Label Text="{Loc 'ui-options-general-accessibility'}"
|
||||
FontColorOverride="{xNamespace:Static s:StyleNano.NanoGold}"
|
||||
StyleClasses="LabelKeyText"/>
|
||||
<CheckBox Name="DisableFiltersCheckBox" Text="{Loc 'ui-options-no-filters'}" />
|
||||
<CheckBox Name="DisableGlimmerEffectCheckBox" Text="{Loc 'ui-options-disable-glimmer-effect'}" />
|
||||
<Label Text="{Loc 'ui-options-delta-tips'}"
|
||||
StyleClasses="LabelKeyText"/>
|
||||
<CheckBox Name="DisableTipsCheckBox" Text="{Loc 'ui-options-delta-disable-tips'}" />
|
||||
<Button Name="ResetTipsButton" Text="{Loc 'ui-options-delta-reset-tips'}" MaxWidth="200" Margin="0 4 0 0" />
|
||||
</BoxContainer>
|
||||
<ui:OptionsTabControlRow Name="Control" Access="Public" />
|
||||
</BoxContainer>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Content.Shared._DV.CCVars;
|
||||
using Content.Client.Options.UI;
|
||||
using Content.Shared._DV.Tips;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
@@ -9,6 +9,8 @@ namespace Content.Client._DV.Options.UI.Tabs;
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class DeltaTab : Control
|
||||
{
|
||||
[Dependency] private readonly IEntityNetworkManager _entNet = default!;
|
||||
|
||||
public DeltaTab()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
@@ -16,9 +18,15 @@ public sealed partial class DeltaTab : Control
|
||||
|
||||
Control.AddOptionCheckBox(DCCVars.NoVisionFilters, DisableFiltersCheckBox);
|
||||
Control.AddOptionCheckBox(DCCVars.DisableGlimmerShader, DisableGlimmerEffectCheckBox);
|
||||
//Control.AddOptionCheckBox(DCCVars.ChatAutoFillHighlights, AutoFillHighlightsCheckBox); // Migrating to the new system
|
||||
//Control.AddOptionColorSlider(DCCVars.ChatHighlightsColor, HighlightsColorSlider); // Migrating to the new system
|
||||
Control.AddOptionCheckBox(DCCVars.DisableTips, DisableTipsCheckBox);
|
||||
|
||||
ResetTipsButton.OnPressed += _ => OnResetTipsPressed();
|
||||
|
||||
Control.Initialize();
|
||||
}
|
||||
|
||||
private void OnResetTipsPressed()
|
||||
{
|
||||
_entNet.SendSystemNetworkMessage(new ResetAllSeenTipsRequest());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
using Content.Client._DV.Tips.UI;
|
||||
using Content.Shared._DV.CCVars;
|
||||
using Content.Shared._DV.Tips;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client._DV.Tips;
|
||||
|
||||
/// <summary>
|
||||
/// Client-side system that receives tip events from the server and displays the tip popup UI.
|
||||
/// </summary>
|
||||
public sealed class TipSystem : SharedTipSystem
|
||||
{
|
||||
[Dependency] private readonly IUserInterfaceManager _ui = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Queue of tips waiting to be shown. Only one tip is displayed at a time.
|
||||
/// </summary>
|
||||
private readonly Queue<ShowTipEvent> _tipQueue = new();
|
||||
|
||||
/// <summary>
|
||||
/// Currently displayed tip popup, if any.
|
||||
/// </summary>
|
||||
private TipPopup? _currentPopup;
|
||||
|
||||
/// <summary>
|
||||
/// The tip ID of the currently displayed popup, used for dismiss events.
|
||||
/// </summary>
|
||||
private ProtoId<TipPrototype>? _currentTipId;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeNetworkEvent<ShowTipEvent>(OnShowTip);
|
||||
}
|
||||
|
||||
private void OnShowTip(ShowTipEvent ev)
|
||||
{
|
||||
// Return if tips are disabled globally
|
||||
if (_cfg.GetCVar(DCCVars.DisableTipsGlobal))
|
||||
return;
|
||||
|
||||
// Return if tips are disabled locally and IgnoreCvar is the default value
|
||||
if (_cfg.GetCVar(DCCVars.DisableTips) && !ev.IgnoreCvar)
|
||||
return;
|
||||
|
||||
_tipQueue.Enqueue(ev);
|
||||
TryShowNextTip();
|
||||
}
|
||||
|
||||
private void TryShowNextTip()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
if (_currentPopup != null)
|
||||
return;
|
||||
|
||||
if (_tipQueue.Count == 0)
|
||||
return;
|
||||
|
||||
var tipEvent = _tipQueue.Dequeue();
|
||||
|
||||
if (!Prototype.TryIndex(tipEvent.TipId, out var tipProto))
|
||||
{
|
||||
Log.Warning($"Received tip event for unknown prototype: {tipEvent.TipId}");
|
||||
continue;
|
||||
}
|
||||
|
||||
ShowTipPopup(tipProto, tipEvent);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowTipPopup(TipPrototype tip, ShowTipEvent ev)
|
||||
{
|
||||
_currentPopup = new TipPopup(tip);
|
||||
_currentPopup.OnClose += OnPopupClosed;
|
||||
_currentTipId = tip.ID;
|
||||
|
||||
_ui.RootControl.AddChild(_currentPopup);
|
||||
|
||||
// Play the sound
|
||||
_audio.PlayGlobal(ev.Sound, Filter.Local(), false);
|
||||
}
|
||||
|
||||
private void OnPopupClosed(bool dontShowAgain)
|
||||
{
|
||||
if (_currentPopup != null && _currentTipId != null)
|
||||
{
|
||||
// Send dismiss event to server
|
||||
RaiseNetworkEvent(new TipDismissedEvent(_currentTipId.Value, dontShowAgain));
|
||||
|
||||
_currentPopup.OnClose -= OnPopupClosed;
|
||||
_currentPopup?.Close();
|
||||
_currentPopup = null;
|
||||
_currentTipId = null;
|
||||
}
|
||||
|
||||
TryShowNextTip();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the current tip popup if one is open.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public void CloseCurrentTip()
|
||||
{
|
||||
_currentPopup?.Close();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all queued tips without showing them.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public void ClearTipQueue()
|
||||
{
|
||||
_tipQueue.Clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<Control xmlns="https://spacestation14.io"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Top"
|
||||
Margin="80 100 0 0">
|
||||
<PanelContainer Name="BackgroundPanel"
|
||||
MinWidth="320"
|
||||
MaxWidth="420">
|
||||
<BoxContainer Orientation="Vertical"
|
||||
Margin="15">
|
||||
<!-- Title at the top, centered -->
|
||||
<Label Name="TitleLabel"
|
||||
StyleClasses="LabelBig"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0 0 0 10" />
|
||||
|
||||
<!-- Description in a flexible vertical container -->
|
||||
<PanelContainer StyleClasses="BackgroundDark"
|
||||
Margin="0 0 0 12">
|
||||
<BoxContainer Orientation="Vertical"
|
||||
Margin="10">
|
||||
<RichTextLabel Name="DescriptionLabel"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalExpand="True" />
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
|
||||
<!-- Bottom controls -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
HorizontalAlignment="Center">
|
||||
<CheckBox Name="DontShowAgainCheckBox"
|
||||
Text="{Loc 'tip-popup-dont-show-again'}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0 0 0 8" />
|
||||
<Button Name="OkayButton"
|
||||
Text="{Loc 'tip-popup-okay-button'}"
|
||||
HorizontalAlignment="Center"
|
||||
MinWidth="100" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</Control>
|
||||
@@ -0,0 +1,98 @@
|
||||
using Content.Client.Guidebook.Richtext;
|
||||
using Content.Shared._DV.Tips;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.RichText;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client._DV.Tips.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Popup control that displays a tip to the player.
|
||||
/// Appears in the top-left corner with a semi-transparent background.
|
||||
/// </summary>
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class TipPopup : Control
|
||||
{
|
||||
/// <summary>
|
||||
/// Event raised when the popup is closed. The bool parameter indicates
|
||||
/// whether the "Don't show again" checkbox was checked.
|
||||
/// </summary>
|
||||
public event Action<bool>? OnClose;
|
||||
|
||||
private readonly TipPrototype _tip;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the "Don't show again" checkbox is checked.
|
||||
/// </summary>
|
||||
public bool DontShowAgain => DontShowAgainCheckBox.Pressed;
|
||||
|
||||
public TipPopup(TipPrototype tip)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
_tip = tip;
|
||||
|
||||
InitializeUI();
|
||||
InitializeEvents();
|
||||
}
|
||||
|
||||
private void InitializeUI()
|
||||
{
|
||||
TitleLabel.Text = Loc.GetString(_tip.Title);
|
||||
|
||||
var description = Loc.GetString(_tip.Description);
|
||||
DescriptionLabel.SetMessage(FormattedMessage.FromMarkupPermissive(description),
|
||||
tagsAllowed:
|
||||
[
|
||||
typeof(ItalicTag),
|
||||
typeof(ColorTag),
|
||||
typeof(BoldTag),
|
||||
typeof(BoldItalicTag),
|
||||
typeof(KeyBindTag),
|
||||
]);
|
||||
|
||||
ApplyTransparentStyle();
|
||||
}
|
||||
|
||||
private void ApplyTransparentStyle()
|
||||
{
|
||||
var styleBox = new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = Color.FromHex("#1a1a1a").WithAlpha(0.85f),
|
||||
BorderColor = Color.FromHex("#3a3a3a"),
|
||||
BorderThickness = new Thickness(1),
|
||||
ContentMarginLeftOverride = 0,
|
||||
ContentMarginRightOverride = 0,
|
||||
ContentMarginTopOverride = 0,
|
||||
ContentMarginBottomOverride = 0,
|
||||
};
|
||||
|
||||
BackgroundPanel.PanelOverride = styleBox;
|
||||
}
|
||||
|
||||
private void InitializeEvents()
|
||||
{
|
||||
OkayButton.OnPressed += OnOkayPressed;
|
||||
}
|
||||
|
||||
private void OnOkayPressed(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
if (Parent == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var dontShowAgain = DontShowAgain;
|
||||
Parent.RemoveChild(this);
|
||||
OnClose?.Invoke(dontShowAgain);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// File to store DeltaV-specific database models
|
||||
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Content.Server.Database;
|
||||
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static class DVModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Stores tip dismissal preferences for a player, tracking which tips they've marked
|
||||
/// as "Don't show again".
|
||||
/// </summary>
|
||||
[Table("dv_seen_tips")]
|
||||
[Index(nameof(PlayerUserId), nameof(TipProtoId), IsUnique = true)]
|
||||
public class SeenTip
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The player's user ID (GUID). References the Player table.
|
||||
/// </summary>
|
||||
public Guid PlayerUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The prototype ID of the tip that was dismissed.
|
||||
/// </summary>
|
||||
[MaxLength(64)]
|
||||
public string TipProtoId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// When the tip was dismissed.
|
||||
/// </summary>
|
||||
public DateTime DismissedAt { get; set; }
|
||||
}
|
||||
}
|
||||
+2273
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Content.Server.Database.Migrations.Postgres
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class DVSeenTips : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "dv_seen_tips",
|
||||
columns: table => new
|
||||
{
|
||||
dv_seen_tips_id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
player_user_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
tip_proto_id = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
dismissed_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_dv_seen_tips", x => x.dv_seen_tips_id);
|
||||
table.ForeignKey(
|
||||
name: "FK_dv_seen_tips_player_player_id",
|
||||
column: x => x.player_user_id,
|
||||
principalTable: "player",
|
||||
principalColumn: "user_id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_dv_seen_tips_player_user_id_tip_proto_id",
|
||||
table: "dv_seen_tips",
|
||||
columns: new[] { "player_user_id", "tip_proto_id" },
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "dv_seen_tips");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -711,6 +711,39 @@ namespace Content.Server.Database.Migrations.Postgres
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.DVModel+SeenTip", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("dv_seen_tips_id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("DismissedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("dismissed_at");
|
||||
|
||||
b.Property<Guid>("PlayerUserId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("player_user_id");
|
||||
|
||||
b.Property<string>("TipProtoId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)")
|
||||
.HasColumnName("tip_proto_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("PK_dv_seen_tips");
|
||||
|
||||
b.HasIndex("PlayerUserId", "TipProtoId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("IX_dv_seen_tips_player_user_id_tip_proto_id");
|
||||
|
||||
b.ToTable("dv_seen_tips", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.IPIntelCache", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -1801,6 +1834,17 @@ namespace Content.Server.Database.Migrations.Postgres
|
||||
b.Navigation("Server");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.DVModel+SeenTip", b =>
|
||||
{
|
||||
b.HasOne("Content.Server.Database.Player", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("PlayerUserId")
|
||||
.HasPrincipalKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("FK_dv_seen_tips_player_player_id");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.Job", b =>
|
||||
{
|
||||
b.HasOne("Content.Server.Database.Profile", "Profile")
|
||||
|
||||
+2191
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Content.Server.Database.Migrations.Sqlite
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class DVSeenTips : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "dv_seen_tips",
|
||||
columns: table => new
|
||||
{
|
||||
dv_seen_tips_id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
player_user_id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
tip_proto_id = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
dismissed_at = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_dv_seen_tips", x => x.dv_seen_tips_id);
|
||||
table.ForeignKey(
|
||||
name: "FK_dv_seen_tips_player_player_id",
|
||||
column: x => x.player_user_id,
|
||||
principalTable: "player",
|
||||
principalColumn: "user_id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_dv_seen_tips_player_user_id_tip_proto_id",
|
||||
table: "dv_seen_tips",
|
||||
columns: new[] { "player_user_id", "tip_proto_id" },
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "dv_seen_tips");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -671,6 +671,37 @@ namespace Content.Server.Database.Migrations.Sqlite
|
||||
b.ToTable("connection_log", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.DVModel+SeenTip", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("dv_seen_tips_id");
|
||||
|
||||
b.Property<DateTime>("DismissedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("dismissed_at");
|
||||
|
||||
b.Property<Guid>("PlayerUserId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("player_user_id");
|
||||
|
||||
b.Property<string>("TipProtoId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("tip_proto_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("PK_dv_seen_tips");
|
||||
|
||||
b.HasIndex("PlayerUserId", "TipProtoId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("IX_dv_seen_tips_player_user_id_tip_proto_id");
|
||||
|
||||
b.ToTable("dv_seen_tips", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.IPIntelCache", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -1721,6 +1752,17 @@ namespace Content.Server.Database.Migrations.Sqlite
|
||||
b.Navigation("Server");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.DVModel+SeenTip", b =>
|
||||
{
|
||||
b.HasOne("Content.Server.Database.Player", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("PlayerUserId")
|
||||
.HasPrincipalKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("FK_dv_seen_tips_player_player_id");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.Job", b =>
|
||||
{
|
||||
b.HasOne("Content.Server.Database.Profile", "Profile")
|
||||
|
||||
@@ -46,6 +46,7 @@ namespace Content.Server.Database
|
||||
public DbSet<RoleWhitelist> RoleWhitelists { get; set; } = null!;
|
||||
public DbSet<BanTemplate> BanTemplate { get; set; } = null!;
|
||||
public DbSet<IPIntelCache> IPIntelCache { get; set; } = null!;
|
||||
public DbSet<DVModel.SeenTip> DVSeenTips { get; set; } = null!; // DeltaV - Tips
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
@@ -71,6 +72,15 @@ namespace Content.Server.Database
|
||||
.IsRequired();
|
||||
// End CD - CD Character Data
|
||||
|
||||
// DeltaV - Tips
|
||||
modelBuilder.Entity<DVModel.SeenTip>()
|
||||
.HasOne<Player>()
|
||||
.WithMany()
|
||||
.HasForeignKey(s => s.PlayerUserId)
|
||||
.HasPrincipalKey(p => p.UserId)
|
||||
.IsRequired();
|
||||
// End DeltaV
|
||||
|
||||
modelBuilder.Entity<Antag>()
|
||||
.HasIndex(p => new {HumanoidProfileId = p.ProfileId, p.AntagName})
|
||||
.IsUnique();
|
||||
|
||||
@@ -24,6 +24,7 @@ using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
using Content.Server._CD.Records; // CD - Character Records
|
||||
using Content.Shared._CD.Records; // CD - Character Records
|
||||
using Content.Shared._DV.Tips; // DV - Tips
|
||||
|
||||
namespace Content.Server.Database
|
||||
{
|
||||
@@ -1849,6 +1850,82 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
|
||||
|
||||
#endregion
|
||||
|
||||
#region DV - Seen Tips
|
||||
|
||||
public async Task<HashSet<string>> GetSeenTips(Guid player, CancellationToken cancel = default)
|
||||
{
|
||||
await using var db = await GetDb(cancel);
|
||||
|
||||
var seenTips = await db.DbContext.DVSeenTips
|
||||
.Where(s => s.PlayerUserId == player)
|
||||
.Select(s => s.TipProtoId)
|
||||
.ToListAsync(cancel);
|
||||
|
||||
return seenTips.ToHashSet();
|
||||
}
|
||||
|
||||
public async Task<bool> HasSeenTip(Guid player, ProtoId<TipPrototype> tip)
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
|
||||
return await db.DbContext.DVSeenTips
|
||||
.Where(s => s.PlayerUserId == player)
|
||||
.Where(s => s.TipProtoId == tip.Id)
|
||||
.AnyAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> MarkTipSeen(Guid player, ProtoId<TipPrototype> tip)
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
|
||||
var exists = await db.DbContext.DVSeenTips
|
||||
.Where(s => s.PlayerUserId == player)
|
||||
.Where(s => s.TipProtoId == tip.Id)
|
||||
.AnyAsync();
|
||||
|
||||
if (exists)
|
||||
return false;
|
||||
|
||||
var seenTip = new DVModel.SeenTip
|
||||
{
|
||||
PlayerUserId = player,
|
||||
TipProtoId = tip.Id,
|
||||
DismissedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
db.DbContext.DVSeenTips.Add(seenTip);
|
||||
await db.DbContext.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> ResetSeenTip(Guid player, ProtoId<TipPrototype> tip)
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
|
||||
var entry = await db.DbContext.DVSeenTips
|
||||
.Where(s => s.PlayerUserId == player)
|
||||
.Where(s => s.TipProtoId == tip.Id)
|
||||
.SingleOrDefaultAsync();
|
||||
|
||||
if (entry == null)
|
||||
return false;
|
||||
|
||||
db.DbContext.DVSeenTips.Remove(entry);
|
||||
await db.DbContext.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<int> ResetAllSeenTips(Guid player)
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
|
||||
return await db.DbContext.DVSeenTips
|
||||
.Where(s => s.PlayerUserId == player)
|
||||
.ExecuteDeleteAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public abstract Task SendNotification(DatabaseNotification notification);
|
||||
|
||||
// SQLite returns DateTime as Kind=Unspecified, Npgsql actually knows for sure it's Kind=Utc.
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Shared._DV.Tips; // DeltaV - Tips
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Construction.Prototypes;
|
||||
@@ -343,6 +344,16 @@ namespace Content.Server.Database
|
||||
|
||||
#endregion
|
||||
|
||||
#region DeltaV - Seen Tips
|
||||
|
||||
Task<HashSet<string>> GetSeenTips(Guid player, CancellationToken cancel = default);
|
||||
Task<bool> HasSeenTip(Guid player, ProtoId<TipPrototype> tip);
|
||||
Task<bool> MarkTipSeen(Guid player, ProtoId<TipPrototype> tip);
|
||||
Task<bool> ResetSeenTip(Guid player, ProtoId<TipPrototype> tip);
|
||||
Task<int> ResetAllSeenTips(Guid player);
|
||||
|
||||
#endregion
|
||||
|
||||
#region DB Notifications
|
||||
|
||||
void SubscribeToNotifications(Action<DatabaseNotification> handler);
|
||||
@@ -1052,6 +1063,40 @@ namespace Content.Server.Database
|
||||
return RunDbCommand(() => _db.CleanIPIntelCache(range));
|
||||
}
|
||||
|
||||
#region DeltaV - Seen Tips
|
||||
|
||||
public Task<HashSet<string>> GetSeenTips(Guid player, CancellationToken cancel = default)
|
||||
{
|
||||
DbReadOpsMetric.Inc();
|
||||
return RunDbCommand(() => _db.GetSeenTips(player, cancel));
|
||||
}
|
||||
|
||||
public Task<bool> HasSeenTip(Guid player, ProtoId<TipPrototype> tip)
|
||||
{
|
||||
DbReadOpsMetric.Inc();
|
||||
return RunDbCommand(() => _db.HasSeenTip(player, tip));
|
||||
}
|
||||
|
||||
public Task<bool> MarkTipSeen(Guid player, ProtoId<TipPrototype> tip)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return RunDbCommand(() => _db.MarkTipSeen(player, tip));
|
||||
}
|
||||
|
||||
public Task<bool> ResetSeenTip(Guid player, ProtoId<TipPrototype> tip)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return RunDbCommand(() => _db.ResetSeenTip(player, tip));
|
||||
}
|
||||
|
||||
public Task<int> ResetAllSeenTips(Guid player)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return RunDbCommand(() => _db.ResetAllSeenTips(player));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public void SubscribeToNotifications(Action<DatabaseNotification> handler)
|
||||
{
|
||||
lock (_notificationHandlers)
|
||||
|
||||
@@ -18,6 +18,7 @@ using Robust.Shared.Random;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Content.Server.Codewords;
|
||||
using Content.Shared._DV.Tips;
|
||||
|
||||
namespace Content.Server.GameTicking.Rules;
|
||||
|
||||
@@ -35,6 +36,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
|
||||
[Dependency] private readonly SharedRoleSystem _roleSystem = default!;
|
||||
[Dependency] private readonly UplinkSystem _uplink = default!;
|
||||
[Dependency] private readonly CodewordSystem _codewordSystem = default!;
|
||||
[Dependency] private readonly SharedTipSystem _tips = default!; // DeltaV - Tips
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -143,6 +145,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
|
||||
_npcFaction.AddFaction(traitor, component.SyndicateFaction);
|
||||
|
||||
Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - Finished");
|
||||
_tips.ShowTip(traitor, new ProtoId<TipPrototype>("TraitorTip")); // DeltaV - Tips
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Server.Administration;
|
||||
using Content.Shared._DV.Tips;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Server._DV.Tips;
|
||||
|
||||
[AdminCommand(AdminFlags.Fun)]
|
||||
public sealed class ShowTipCommand : LocalizedEntityCommands
|
||||
{
|
||||
public override string Command => "showtip";
|
||||
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
[Dependency] private readonly ISharedPlayerManager _player = default!;
|
||||
[Dependency] private readonly SharedTipSystem _tip = default!;
|
||||
|
||||
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
return args.Length switch
|
||||
{
|
||||
1 =>
|
||||
CompletionResult.FromHintOptions(
|
||||
CompletionHelper.SessionNames(players: _player),
|
||||
Loc.GetString("shell-argument-username-hint")),
|
||||
2 =>
|
||||
CompletionResult.FromHintOptions(
|
||||
CompletionHelper.PrototypeIDs<TipPrototype>(proto: _prototype),
|
||||
Loc.GetString("cmd-showtip-hint-tip")),
|
||||
_ => CompletionResult.Empty
|
||||
};
|
||||
}
|
||||
|
||||
public override async void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length is 0 or > 2)
|
||||
{
|
||||
shell.WriteError(LocalizationManager.GetString("shell-wrong-arguments-number"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_player.TryGetSessionByUsername(args[0], out var player))
|
||||
{
|
||||
shell.WriteError(LocalizationManager.GetString("shell-target-player-does-not-exist"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_prototype.TryIndex<TipPrototype>(args[1], out var proto))
|
||||
{
|
||||
shell.WriteError(Loc.GetString($"shell-argument-must-be-prototype",
|
||||
("index", args[1]),
|
||||
("prototype", nameof(TipPrototype))));
|
||||
return;
|
||||
}
|
||||
|
||||
_tip.ShowTip(player, proto, true);
|
||||
|
||||
shell.WriteLine(Loc.GetString("shell-command-success"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
using Content.Server.Database;
|
||||
using Content.Server.Mind;
|
||||
using Content.Server.Players.PlayTimeTracking;
|
||||
using Content.Server.Roles.Jobs;
|
||||
using Content.Server.Tips;
|
||||
using Content.Shared._DV.Tips;
|
||||
using Content.Shared._DV.Tips.Conditions;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.Roles.Components;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server._DV.Tips;
|
||||
|
||||
/// <summary>
|
||||
/// Server-side system that handles showing tips to players after they spawn alongside validation.
|
||||
/// Not to be confused with <see cref="TipsSystem"/>.
|
||||
/// </summary>
|
||||
public sealed class TipSystem : SharedTipSystem
|
||||
{
|
||||
[Dependency] private readonly IComponentFactory _component = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly IServerDbManager _db = default!;
|
||||
[Dependency] private readonly JobSystem _job = default!;
|
||||
[Dependency] private readonly PlayTimeTrackingManager _playtime = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Tracks scheduled tips for each player session.
|
||||
/// </summary>
|
||||
private readonly Dictionary<ICommonSession, List<ScheduledTip>> _scheduledTips = new();
|
||||
|
||||
/// <summary>
|
||||
/// Cache of seen tips per player to avoid repeated DB queries.
|
||||
/// </summary>
|
||||
private readonly Dictionary<NetUserId, HashSet<string>> _seenTipsCache = new();
|
||||
|
||||
private readonly List<ICommonSession> _toRemove = new();
|
||||
|
||||
private sealed class ScheduledTip
|
||||
{
|
||||
public ProtoId<TipPrototype> TipId;
|
||||
public TimeSpan ShowTime;
|
||||
public EntityUid PlayerEntity;
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnPlayerSpawnComplete);
|
||||
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestart);
|
||||
SubscribeNetworkEvent<TipDismissedEvent>(OnTipDismissed);
|
||||
SubscribeNetworkEvent<ResetAllSeenTipsRequest>(OnResetAllSeenTipsRequest);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
var currentTime = _timing.CurTime;
|
||||
_toRemove.Clear();
|
||||
|
||||
foreach (var (session, tips) in _scheduledTips)
|
||||
{
|
||||
if (session.Status != SessionStatus.InGame)
|
||||
{
|
||||
_toRemove.Add(session);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var i = tips.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var scheduled = tips[i];
|
||||
|
||||
if (currentTime < scheduled.ShowTime)
|
||||
continue;
|
||||
|
||||
// Re-check conditions at show time in case state changed
|
||||
if (Prototype.TryIndex(scheduled.TipId, out var tipProto) &&
|
||||
CheckConditions(scheduled.PlayerEntity, session, tipProto))
|
||||
{
|
||||
ShowTip(session, tipProto);
|
||||
}
|
||||
|
||||
tips.RemoveAt(i);
|
||||
}
|
||||
|
||||
if (tips.Count == 0)
|
||||
_toRemove.Add(session);
|
||||
}
|
||||
|
||||
foreach (var session in _toRemove)
|
||||
{
|
||||
_scheduledTips.Remove(session);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPlayerSpawnComplete(PlayerSpawnCompleteEvent ev)
|
||||
{
|
||||
if (ev.Player is not { } session)
|
||||
return;
|
||||
|
||||
ScheduleTipsForPlayer(session, ev.Mob);
|
||||
}
|
||||
|
||||
private void OnRoundRestart(RoundRestartCleanupEvent ev)
|
||||
{
|
||||
_scheduledTips.Clear();
|
||||
_seenTipsCache.Clear();
|
||||
}
|
||||
|
||||
private void OnTipDismissed(TipDismissedEvent ev, EntitySessionEventArgs args)
|
||||
{
|
||||
if (args.SenderSession is not { } session)
|
||||
return;
|
||||
|
||||
// Only persist if the player checked "Don't show again"
|
||||
if (!ev.DontShowAgain)
|
||||
return;
|
||||
|
||||
MarkTipSeen(session.UserId, ev.TipId);
|
||||
}
|
||||
|
||||
private void OnResetAllSeenTipsRequest(ResetAllSeenTipsRequest ev, EntitySessionEventArgs args)
|
||||
{
|
||||
if (args.SenderSession is not { } session)
|
||||
return;
|
||||
|
||||
ResetAllSeenTips(session.UserId);
|
||||
Log.Info($"Player {session.Name} reset all seen tips.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks a tip as seen for a player, updating both cache and database.
|
||||
/// </summary>
|
||||
public async void MarkTipSeen(NetUserId player, ProtoId<TipPrototype> tipId)
|
||||
{
|
||||
// Update cache immediately
|
||||
if (!_seenTipsCache.TryGetValue(player, out var seenTips))
|
||||
{
|
||||
seenTips = new HashSet<string>();
|
||||
_seenTipsCache[player] = seenTips;
|
||||
}
|
||||
|
||||
seenTips.Add(tipId.Id);
|
||||
|
||||
// Persist to database
|
||||
await _db.MarkTipSeen(player, tipId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets a seen tip for a player, allowing it to be shown again.
|
||||
/// </summary>
|
||||
public async void ResetSeenTip(NetUserId player, ProtoId<TipPrototype> tipId)
|
||||
{
|
||||
// Update cache
|
||||
if (_seenTipsCache.TryGetValue(player, out var seenTips))
|
||||
{
|
||||
seenTips.Remove(tipId.Id);
|
||||
}
|
||||
|
||||
// Update database
|
||||
await _db.ResetSeenTip(player, tipId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets all seen tips for a player.
|
||||
/// </summary>
|
||||
public async void ResetAllSeenTips(NetUserId player)
|
||||
{
|
||||
// Clear cache
|
||||
_seenTipsCache.Remove(player);
|
||||
|
||||
// Clear database
|
||||
await _db.ResetAllSeenTips(player);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a tip has been seen by a player.
|
||||
/// </summary>
|
||||
public bool HasSeenTip(NetUserId player, ProtoId<TipPrototype> tipId)
|
||||
{
|
||||
if (!_seenTipsCache.TryGetValue(player, out var seenTips))
|
||||
return false;
|
||||
|
||||
return seenTips.Contains(tipId.Id);
|
||||
}
|
||||
|
||||
private async void ScheduleTipsForPlayer(ICommonSession session, EntityUid playerEntity)
|
||||
{
|
||||
// Load seen tips from database if not cached
|
||||
if (!_seenTipsCache.ContainsKey(session.UserId))
|
||||
{
|
||||
var seenTips = await _db.GetSeenTips(session.UserId);
|
||||
_seenTipsCache[session.UserId] = seenTips;
|
||||
}
|
||||
|
||||
var currentTime = _timing.CurTime;
|
||||
var tips = new List<ScheduledTip>();
|
||||
|
||||
foreach (var tipProto in Prototype.EnumeratePrototypes<TipPrototype>())
|
||||
{
|
||||
// Skip tips the player has already dismissed with "Don't show again"
|
||||
if (HasSeenTip(session.UserId, tipProto.ID))
|
||||
continue;
|
||||
|
||||
// Check conditions at schedule time
|
||||
if (!CheckConditions(playerEntity, session, tipProto))
|
||||
continue;
|
||||
|
||||
tips.Add(new ScheduledTip
|
||||
{
|
||||
TipId = tipProto.ID,
|
||||
ShowTime = currentTime + tipProto.Delay,
|
||||
PlayerEntity = playerEntity
|
||||
});
|
||||
}
|
||||
|
||||
if (tips.Count == 0)
|
||||
return;
|
||||
|
||||
// Sort by priority then show time
|
||||
tips.Sort((a, b) =>
|
||||
{
|
||||
var protoA = Prototype.Index(a.TipId);
|
||||
var protoB = Prototype.Index(b.TipId);
|
||||
var priorityCompare = protoA.Priority.CompareTo(protoB.Priority);
|
||||
return priorityCompare != 0 ? priorityCompare : a.ShowTime.CompareTo(b.ShowTime);
|
||||
});
|
||||
|
||||
_scheduledTips.Remove(session);
|
||||
_scheduledTips[session] = tips;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if all conditions for a tip are met.
|
||||
/// </summary>
|
||||
private bool CheckConditions(EntityUid player, ICommonSession session, TipPrototype tip)
|
||||
{
|
||||
foreach (var condition in tip.Conditions)
|
||||
{
|
||||
var result = EvaluateCondition(player, session, condition);
|
||||
// Apply inversion
|
||||
result ^= condition.Invert;
|
||||
|
||||
if (!result)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates a single condition based on its type.
|
||||
/// </summary>
|
||||
private bool EvaluateCondition(EntityUid player, ICommonSession session, TipCondition condition)
|
||||
{
|
||||
return condition switch
|
||||
{
|
||||
HasCompCondition hasComp => EvaluateHasComp(player, hasComp),
|
||||
HasJobCondition hasJob => EvaluateHasJob(player, hasJob),
|
||||
HasRoleTypeCondition hasRoleType => EvaluateHasRoleType(player, hasRoleType),
|
||||
MinPlaytimeCondition minTracker => EvaluateMinPlaytime(session, minTracker),
|
||||
MaxPlaytimeCondition maxTracker => EvaluateMaxPlaytime(session, maxTracker),
|
||||
_ => true // Unknown condition types pass by default
|
||||
};
|
||||
}
|
||||
|
||||
private bool EvaluateHasComp(EntityUid player, HasCompCondition condition)
|
||||
{
|
||||
if (!_component.TryGetRegistration(condition.Comp, out var registration))
|
||||
{
|
||||
Log.Warning($"Tip condition references unknown component: {condition.Comp}");
|
||||
return false;
|
||||
}
|
||||
|
||||
return HasComp(player, registration.Type);
|
||||
}
|
||||
|
||||
private bool EvaluateHasJob(EntityUid player, HasJobCondition condition)
|
||||
{
|
||||
if (!_job.MindTryGetJobId(Mind.GetMind(player), out var jobId))
|
||||
return false;
|
||||
|
||||
return jobId == condition.Job;
|
||||
}
|
||||
|
||||
private bool EvaluateHasRoleType(EntityUid player, HasRoleTypeCondition condition)
|
||||
{
|
||||
if (!Mind.TryGetMind(player, out _, out var mind))
|
||||
return false;
|
||||
|
||||
foreach (var role in mind.MindRoleContainer.ContainedEntities)
|
||||
{
|
||||
if (!TryComp<MindRoleComponent>(role, out var mindRole))
|
||||
continue;
|
||||
|
||||
if (mindRole.RoleType == condition.RoleType)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool EvaluateMinPlaytime(ICommonSession session, MinPlaytimeCondition condition)
|
||||
{
|
||||
if (!_playtime.TryGetTrackerTime(session, condition.Tracker, out var time))
|
||||
return false;
|
||||
|
||||
return time.Value >= condition.Time;
|
||||
}
|
||||
|
||||
private bool EvaluateMaxPlaytime(ICommonSession session, MaxPlaytimeCondition condition)
|
||||
{
|
||||
if (!_playtime.TryGetTrackerTime(session, condition.Tracker, out var time))
|
||||
return true; // No time tracked = 0 minutes, which is less than any positive max
|
||||
|
||||
return time.Value < condition.Time;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels all scheduled tips for a player.
|
||||
/// </summary>
|
||||
public void CancelTipsForPlayer(ICommonSession session)
|
||||
{
|
||||
_scheduledTips.Remove(session);
|
||||
}
|
||||
}
|
||||
@@ -100,6 +100,18 @@ public sealed partial class DCCVars
|
||||
public static readonly CVarDef<bool> DisableGlimmerShader =
|
||||
CVarDef.Create("accessibility.disable_glimmer_shader", false, CVar.CLIENTONLY | CVar.ARCHIVE);
|
||||
|
||||
/// <summary>
|
||||
/// Disables all tips for a player.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> DisableTips =
|
||||
CVarDef.Create("game.disable_tips", false, CVar.CLIENTONLY | CVar.ARCHIVE);
|
||||
|
||||
/// <summary>
|
||||
/// Disables all tips for ALL PLAYERS.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> DisableTipsGlobal =
|
||||
CVarDef.Create("game.disable_tips_global", false, CVar.ARCHIVE | CVar.SERVER | CVar.REPLICATED);
|
||||
|
||||
/// <summary>
|
||||
/// Whether the Shipyard is enabled.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
|
||||
namespace Content.Shared._DV.Tips.Conditions;
|
||||
|
||||
/// <summary>
|
||||
/// Condition that checks if the player entity has a specific component.
|
||||
/// </summary>
|
||||
public sealed partial class HasCompCondition : TipCondition
|
||||
{
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The component name to check for.
|
||||
/// </summary>
|
||||
[DataField(required: true, customTypeSerializer: typeof(ComponentNameSerializer))]
|
||||
public string Comp = string.Empty;
|
||||
|
||||
protected override bool EvaluateImplementation(TipConditionContext ctx)
|
||||
{
|
||||
_sawmill = ctx.LogMan.GetSawmill("HasCompCondition");
|
||||
if (!ctx.CompFactory.TryGetRegistration(Comp, out var registration))
|
||||
{
|
||||
_sawmill.Warning("tip", $"Tip condition references unknown component: {Comp}");
|
||||
return false;
|
||||
}
|
||||
|
||||
return ctx.EntMan.HasComponent(ctx.Player, registration.Type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared._DV.Tips.Conditions;
|
||||
|
||||
/// <summary>
|
||||
/// Condition that checks if the player has a specific job.
|
||||
/// </summary>
|
||||
public sealed partial class HasJobCondition : TipCondition
|
||||
{
|
||||
/// <summary>
|
||||
/// The job prototype ID to check for.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public ProtoId<JobPrototype> Job;
|
||||
|
||||
protected override bool EvaluateImplementation(TipConditionContext ctx)
|
||||
{
|
||||
// This needs to be checked server-side with JobSystem
|
||||
// Return true here as a placeholder - actual check is in server TipSystem
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Content.Shared.Mind;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared._DV.Tips.Conditions;
|
||||
|
||||
/// <summary>
|
||||
/// Condition that checks if the player's mind has a specific role type.
|
||||
/// Used for checking antag status (e.g., "SoloAntagonist", "TeamAntagonist", "FreeAgent").
|
||||
/// </summary>
|
||||
public sealed partial class HasRoleTypeCondition : TipCondition
|
||||
{
|
||||
/// <summary>
|
||||
/// The role type prototype to check for.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public ProtoId<RoleTypePrototype> RoleType;
|
||||
|
||||
protected override bool EvaluateImplementation(TipConditionContext ctx)
|
||||
{
|
||||
// This needs to be checked server-side
|
||||
// Return true here as a placeholder - actual check is in server TipSystem
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using Content.Shared.Players.PlayTimeTracking;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared._DV.Tips.Conditions;
|
||||
|
||||
/// <summary>
|
||||
/// Condition that checks if the player has LESS than a specified amount of playtime
|
||||
/// on a specific playtime tracker.
|
||||
/// Useful for showing tips to players new to a specific role.
|
||||
/// </summary>
|
||||
public sealed partial class MaxPlaytimeCondition : TipCondition
|
||||
{
|
||||
/// <summary>
|
||||
/// The playtime tracker prototype ID to check.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public ProtoId<PlayTimeTrackerPrototype> Tracker;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum playtime allowed on this tracker.
|
||||
/// Player must have less than this to pass.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public TimeSpan Time;
|
||||
|
||||
protected override bool EvaluateImplementation(TipConditionContext ctx)
|
||||
{
|
||||
// This needs to be checked server-side with PlayTimeTrackingManager
|
||||
// Return true here as a placeholder - actual check is in server TipSystem
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using Content.Shared.Players.PlayTimeTracking;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared._DV.Tips.Conditions;
|
||||
|
||||
/// <summary>
|
||||
/// Condition that checks if the player has MORE than a specified amount of playtime
|
||||
/// on a specific playtime tracker.
|
||||
/// </summary>
|
||||
public sealed partial class MinPlaytimeCondition : TipCondition
|
||||
{
|
||||
/// <summary>
|
||||
/// The playtime tracker prototype ID to check.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public ProtoId<PlayTimeTrackerPrototype> Tracker;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum playtime required on this tracker.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public TimeSpan Time;
|
||||
|
||||
protected override bool EvaluateImplementation(TipConditionContext ctx)
|
||||
{
|
||||
// This needs to be checked server-side with PlayTimeTrackingManager
|
||||
// Return true here as a placeholder - actual check is in server TipSystem
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared._DV.Tips.Conditions;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for tip conditions. Implementations check if a tip should be shown to a player.
|
||||
/// </summary>
|
||||
[ImplicitDataDefinitionForInheritors, UsedImplicitly(ImplicitUseTargetFlags.WithInheritors)]
|
||||
public abstract partial class TipCondition
|
||||
{
|
||||
/// <summary>
|
||||
/// If true, inverts the result of the condition.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool Invert;
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the condition, applying inversion if configured.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public bool Evaluate(TipConditionContext ctx)
|
||||
{
|
||||
var result = EvaluateImplementation(ctx);
|
||||
return result ^ Invert;
|
||||
}
|
||||
|
||||
protected abstract bool EvaluateImplementation(TipConditionContext ctx);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Context passed to tip conditions for evaluation.
|
||||
/// Contains references to the player and relevant systems.
|
||||
/// </summary>
|
||||
public sealed class TipConditionContext
|
||||
{
|
||||
public required EntityUid Player { get; init; }
|
||||
public required ICommonSession Session { get; init; }
|
||||
public required IEntityManager EntMan { get; init; }
|
||||
public required IPrototypeManager Proto { get; init; }
|
||||
public required IComponentFactory CompFactory { get; init; }
|
||||
public required ILogManager LogMan { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using Content.Shared.Mind;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared._DV.Tips;
|
||||
|
||||
/// <summary>
|
||||
/// Shared tip system that provides the public API for showing tips to players.
|
||||
/// </summary>
|
||||
public abstract class SharedTipSystem : EntitySystem
|
||||
{
|
||||
[Dependency] protected new readonly IPrototypeManager Prototype = default!;
|
||||
[Dependency] private readonly ISharedPlayerManager _player = default!;
|
||||
[Dependency] protected readonly SharedMindSystem Mind = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Shows a tip to a player.
|
||||
/// </summary>
|
||||
/// <param name="uid">EntityUid of the player to show the tip to.</param>
|
||||
/// <param name="tipId"><see cref="TipPrototype"/> to use, wrapped in a <see cref="ProtoId{T}"/></param>
|
||||
/// <param name="ignoreCvar">Whether to ignore the DisableTips CCvar on the client.</param>
|
||||
/// <param name="sound">Optional SoundSpecifier, will play the prototype's default sound if null.</param>
|
||||
[PublicAPI]
|
||||
public void ShowTip(EntityUid uid, ProtoId<TipPrototype> tipId, bool ignoreCvar = false, SoundSpecifier? sound = null)
|
||||
{
|
||||
if (!Prototype.TryIndex(tipId, out var tipProto))
|
||||
{
|
||||
Log.Warning($"Attempted to show non-existent tip: {tipId}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Mind.TryGetMind(uid, out _, out var mind) || !_player.TryGetSessionById(mind.UserId, out var session))
|
||||
return;
|
||||
|
||||
ShowTip(session, tipProto, ignoreCvar, sound);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows a tip to a player.
|
||||
/// </summary>
|
||||
/// <param name="session"><see cref="ICommonSession"/> of the player to show the tip to.</param>
|
||||
/// <param name="tipId"><see cref="TipPrototype"/> to use, wrapped in a <see cref="ProtoId{T}"/></param>
|
||||
/// <param name="ignoreCvar">Whether to ignore the DisableTips CCvar on the client.</param>
|
||||
/// <param name="sound">Optional SoundSpecifier, will play the prototype's default sound if null.</param>
|
||||
[PublicAPI]
|
||||
public void ShowTip(ICommonSession session, ProtoId<TipPrototype> tipId, bool ignoreCvar = false, SoundSpecifier? sound = null)
|
||||
{
|
||||
if (!Prototype.TryIndex(tipId, out var tipProto))
|
||||
{
|
||||
Log.Warning($"Attempted to show non-existent tip: {tipId}");
|
||||
return;
|
||||
}
|
||||
|
||||
ShowTip(session, tipProto, ignoreCvar, sound);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows a tip to a player.
|
||||
/// </summary>
|
||||
/// <param name="session"><see cref="ICommonSession"/> of the player to show the tip to.</param>
|
||||
/// <param name="tip"><see cref="TipPrototype"/> to use.</param>
|
||||
/// <param name="ignoreCvar">Whether to ignore the DisableTips CCvar on the client.</param>
|
||||
/// <param name="sound">Optional SoundSpecifier, will play the prototype's default sound if null.</param>
|
||||
[PublicAPI]
|
||||
public void ShowTip(ICommonSession session, TipPrototype tip, bool ignoreCvar = false, SoundSpecifier? sound = null)
|
||||
{
|
||||
RaiseNetworkEvent(new ShowTipEvent(tip.ID, sound ?? tip.Sound, ignoreCvar), session);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared._DV.Tips;
|
||||
|
||||
/// <summary>
|
||||
/// Server-to-client message to display a tip popup.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class ShowTipEvent(ProtoId<TipPrototype> tipId, SoundSpecifier? sound, bool ignoreCvar = false) : EntityEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// The prototype ID of the tip to show.
|
||||
/// </summary>
|
||||
public ProtoId<TipPrototype> TipId = tipId;
|
||||
|
||||
/// <summary>
|
||||
/// The sound to play when showing the tip.
|
||||
/// Sent separately since SoundSpecifier may not serialize well across network.
|
||||
/// </summary>
|
||||
public SoundSpecifier? Sound = sound;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to ignore the DisableTips CCvar on the client
|
||||
/// </summary>
|
||||
public bool IgnoreCvar = ignoreCvar;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Event sent from client to server when a tip popup is dismissed.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class TipDismissedEvent(ProtoId<TipPrototype> tipId, bool dontShowAgain) : EntityEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// The prototype ID of the dismissed tip.
|
||||
/// </summary>
|
||||
public ProtoId<TipPrototype> TipId = tipId;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the player checked "Don't show again".
|
||||
/// </summary>
|
||||
public bool DontShowAgain = dontShowAgain;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event sent from client to server to request resetting all seen tips.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class ResetAllSeenTipsRequest : EntityEventArgs;
|
||||
@@ -0,0 +1,53 @@
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared._DV.Tips;
|
||||
|
||||
/// <summary>
|
||||
/// Prototype for dynamic tips shown to players after spawning.
|
||||
/// Tips can have conditions and delays before appearing.
|
||||
/// </summary>
|
||||
[Prototype]
|
||||
public sealed partial class TipPrototype : IPrototype
|
||||
{
|
||||
[IdDataField]
|
||||
public string ID { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Localization key for the tip title.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public LocId Title;
|
||||
|
||||
/// <summary>
|
||||
/// Localization key for the tip description/body text.
|
||||
/// Supports rich text formatting.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public LocId Description;
|
||||
|
||||
/// <summary>
|
||||
/// Delay after player spawns before showing this tip.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan Delay = TimeSpan.FromSeconds(5);
|
||||
|
||||
/// <summary>
|
||||
/// Priority for tip ordering. Lower values show first when multiple tips are queued.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int Priority;
|
||||
|
||||
/// <summary>
|
||||
/// Sound to play when the tip is shown.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public SoundSpecifier? Sound = new SoundPathSpecifier("/Audio/Effects/voteding.ogg");
|
||||
|
||||
/// <summary>
|
||||
/// Conditions that must ALL be met for this tip to be shown.
|
||||
/// If empty, the tip is always shown.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public List<Conditions.TipCondition> Conditions = new();
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Content.Shared._DV.Tips;
|
||||
using Content.Shared.Trigger.Components.Effects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared._DV.Trigger.Components.Effects;
|
||||
|
||||
/// <summary>
|
||||
/// Shows a tip to the triggering player when triggered.
|
||||
/// Note: this requires the trigger to be on the user to actually show the tip.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
|
||||
public sealed partial class ShowTipOnTriggerComponent : BaseXOnTriggerComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// The tip prototype to show when triggered.
|
||||
/// </summary>
|
||||
[DataField(required: true), AutoNetworkedField]
|
||||
public ProtoId<TipPrototype> Tip;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using Content.Shared._DV.Tips;
|
||||
using Content.Shared._DV.Trigger.Components.Effects;
|
||||
using Content.Shared.Trigger;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Shared._DV.Trigger.Systems;
|
||||
|
||||
public sealed class ShowTipOnTriggerSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedTipSystem _tips = default!;
|
||||
[Dependency] private readonly ISharedPlayerManager _player = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<ShowTipOnTriggerComponent, TriggerEvent>(OnTrigger);
|
||||
}
|
||||
|
||||
private void OnTrigger(Entity<ShowTipOnTriggerComponent> ent, ref TriggerEvent args)
|
||||
{
|
||||
if (args.Key != null && !ent.Comp.KeysIn.Contains(args.Key))
|
||||
return;
|
||||
|
||||
var target = ent.Comp.TargetUser ? args.User : ent.Owner;
|
||||
|
||||
if (target == null)
|
||||
return;
|
||||
|
||||
if (!_player.TryGetSessionByEntity(target.Value, out var session))
|
||||
return;
|
||||
|
||||
_tips.ShowTip(session, ent.Comp.Tip);
|
||||
args.Handled = true;
|
||||
}
|
||||
}
|
||||
@@ -13,3 +13,8 @@ ui-options-function-nano-chat-navigate-down-unread = Navigate down to next unrea
|
||||
|
||||
## DeltaV Curator Chat
|
||||
ui-options-function-open-c-help = Open curator help
|
||||
|
||||
## DeltaV Tips
|
||||
ui-options-delta-tips = Tips
|
||||
ui-options-delta-disable-tips = Disable tips
|
||||
ui-options-delta-reset-tips = Reset all seen tips
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
cmd-showtip-desc = Shows a tip to a specific player.
|
||||
cmd-showtip-help = Usage: showhint <username> <prototype>
|
||||
cmd-showtip-hint-tip = The tip prototype to use.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Tip popup UI strings
|
||||
tip-popup-okay-button = Okay
|
||||
tip-popup-dont-show-again = Don't show this again
|
||||
|
||||
# Example tip content
|
||||
tip-welcome-title = Welcome to the Station!
|
||||
tip-welcome-description = Welcome aboard! If you're new, consider asking other crew members for help. Press [color=yellow][keybind="OpenGuidebook"][/color] to open the guidebook at any time.
|
||||
|
||||
tip-chat-title = Communication
|
||||
tip-chat-description = Use [color=yellow][keybind="FocusChatInputWindow"][/color] for local chat, [color=cyan][keybind="FocusRadioWindow"][/color] for radio, and [color=lime]:[/color] prefix for department channels.
|
||||
|
||||
tip-newplayer-title = New Player Guide
|
||||
tip-newplayer-description = It looks like you're new here! Some helpful tips:
|
||||
• Press [color=yellow][keybind="OpenGuidebook"][/color] to open the guidebook
|
||||
• Use [color=yellow][keybind="ActivateItemInHand"][/color] to activate items in hand
|
||||
• Press [color=yellow][keybind="Drop"][/color] to drop items
|
||||
|
||||
tip-signlanguage-title = Sign Language
|
||||
tip-signlanguage-description = You know sign language! Press [color=yellow]J[/color] to open the sign language radial menu and communicate silently with others who understand.
|
||||
|
||||
tip-security-title = Security Officer
|
||||
tip-security-description = As a Security Officer, your job is to [color=yellow]protect and serve[/color] the crew. Remember to always follow Space Law. Your radio prefix is [color=red]:s[/color] for the security channel.
|
||||
|
||||
tip-cadet-title = Security Cadet
|
||||
tip-cadet-description = As a new Security Cadet, stick close to your fellow officers and learn the ropes. Don't be afraid to ask questions! Make sure to read up on the [color=yellow]SOP[/color] and [color=yellow]Space Law[/color].
|
||||
|
||||
tip-medical-title = Medical Inter
|
||||
# TODO: If/when we get ProtoData tag that's generic to all prototypes rather than just EntityPrototype, this should be using that for RadioChannelPrototype
|
||||
tip-medical-description = As a Medical Doctor, check the [color=cyan]health analyzer[/color] for patient status. Your radio prefix is [color=lightblue]:m[/color] for the medical channel.
|
||||
|
||||
tip-traitor-title = Syndicate Agent
|
||||
tip-traitor-description = You are a Syndicate agent. Check your [color=red]uplink[/color] in your PDA (or wherever you were told) to purchase equipment. Complete your objectives and don't get caught!
|
||||
|
||||
tip-veteran-title = Thank You!
|
||||
tip-veteran-description = Thank you for playing! Your dedication to the station is appreciated. Consider helping new players learn the ropes.
|
||||
@@ -0,0 +1,99 @@
|
||||
# Basic tip shown to everyone
|
||||
- type: tip
|
||||
id: WelcomeTip
|
||||
title: tip-welcome-title
|
||||
description: tip-welcome-description
|
||||
delay: 5
|
||||
priority: 0
|
||||
|
||||
# Tip with custom sound
|
||||
- type: tip
|
||||
id: ChatTip
|
||||
title: tip-chat-title
|
||||
description: tip-chat-description
|
||||
delay: 30
|
||||
priority: 10
|
||||
# sound: /Audio/Effects/adminhelp.ogg # not actually bwoinking people because heart attack but you can do that
|
||||
|
||||
# Tip for new players (less than 60 minutes total playtime)
|
||||
- type: tip
|
||||
id: NewPlayerTip
|
||||
title: tip-newplayer-title
|
||||
description: tip-newplayer-description
|
||||
delay: 10
|
||||
priority: 1
|
||||
conditions:
|
||||
- !type:MaxPlaytimeCondition
|
||||
tracker: Overall
|
||||
time: 1h
|
||||
|
||||
# Tip for players who understand sign language # TODO placeholder
|
||||
#- type: tip
|
||||
# id: SignLanguageTip
|
||||
# title: tip-signlanguage-title
|
||||
# description: tip-signlanguage-description
|
||||
# delay: 15
|
||||
# priority: 5
|
||||
# conditions:
|
||||
# - !type:HasCompCondition
|
||||
# comp: UnderstandsSignLanguageComponent
|
||||
|
||||
# Security job tip
|
||||
- type: tip
|
||||
id: SecurityTip
|
||||
title: tip-security-title
|
||||
description: tip-security-description
|
||||
delay: 10
|
||||
priority: 5
|
||||
conditions:
|
||||
- !type:HasJobCondition
|
||||
job: SecurityOfficer
|
||||
|
||||
# Security cadet tip for players new to security (both conditions must pass)
|
||||
# OR conditions are not supported as of right now
|
||||
- type: tip
|
||||
id: SecurityCadetTip
|
||||
title: tip-cadet-title
|
||||
description: tip-cadet-description
|
||||
delay: 8
|
||||
priority: 3
|
||||
conditions:
|
||||
- !type:HasJobCondition
|
||||
job: SecurityCadet
|
||||
- !type:MaxPlaytimeCondition
|
||||
tracker: JobSecurityCadet
|
||||
time: 2h
|
||||
|
||||
# Medical Intern job tip
|
||||
- type: tip
|
||||
id: MedicalTip
|
||||
title: tip-medical-title
|
||||
description: tip-medical-description
|
||||
delay: 10
|
||||
priority: 5
|
||||
conditions:
|
||||
- !type:HasJobCondition
|
||||
job: MedicalIntern
|
||||
|
||||
# Tip for traitors, shown when becoming a traitor
|
||||
- type: tip
|
||||
id: TraitorTip
|
||||
title: tip-traitor-title
|
||||
description: tip-traitor-description
|
||||
delay: 20
|
||||
priority: 2
|
||||
conditions:
|
||||
- !type:HasRoleTypeCondition
|
||||
roleType: SoloAntagonist
|
||||
|
||||
# Tip for experienced players
|
||||
#- type: tip
|
||||
# id: VeteranTip
|
||||
# title: tip-veteran-title
|
||||
# description: tip-veteran-description
|
||||
# delay: 60
|
||||
# priority: 100
|
||||
# conditions:
|
||||
# - !type:MinPlaytimeCondition
|
||||
# tracker: Overall
|
||||
# time: 10h
|
||||
Reference in New Issue
Block a user