diff --git a/Content.Shared/KillTome/KillTomeComponent.cs b/Content.Shared/KillTome/KillTomeComponent.cs
new file mode 100644
index 0000000000..266ff1a8f8
--- /dev/null
+++ b/Content.Shared/KillTome/KillTomeComponent.cs
@@ -0,0 +1,38 @@
+using Content.Shared.Damage;
+using Content.Shared.Damage.Prototypes;
+using Content.Shared.FixedPoint;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.KillTome;
+
+///
+/// Paper with that component is KillTome.
+///
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class KillTomeComponent : Component
+{
+ ///
+ /// if delay is not specified, it will use this default value
+ ///
+ [DataField, AutoNetworkedField]
+ public TimeSpan DefaultKillDelay = TimeSpan.FromSeconds(40);
+
+ ///
+ /// Damage specifier that will be used to kill the target.
+ ///
+ [DataField, AutoNetworkedField]
+ public DamageSpecifier Damage = new()
+ {
+ DamageDict = new Dictionary
+ {
+ { "Blunt", 200 }
+ }
+ };
+
+ ///
+ /// to keep a track of already killed people so they won't be killed again
+ ///
+ [DataField]
+ public HashSet KilledEntities = [];
+}
diff --git a/Content.Shared/KillTome/KillTomeSystem.cs b/Content.Shared/KillTome/KillTomeSystem.cs
new file mode 100644
index 0000000000..bd49c483d9
--- /dev/null
+++ b/Content.Shared/KillTome/KillTomeSystem.cs
@@ -0,0 +1,187 @@
+using System.Diagnostics.CodeAnalysis;
+using Content.Shared.Administration.Logs;
+using Content.Shared.Damage;
+using Content.Shared.Database;
+using Content.Shared.Humanoid;
+using Content.Shared.Mobs;
+using Content.Shared.Mobs.Components;
+using Content.Shared.NameModifier.EntitySystems;
+using Content.Shared.Paper;
+using Content.Shared.Popups;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.KillTome;
+
+///
+/// This handles KillTome functionality.
+///
+
+/// Kill Tome Rules:
+// 1. The humanoid whose name is written in this note shall die.
+// 2. If the name is shared by multiple humanoids, a random humanoid with that name will die.
+// 3. Each name shall be written on a new line.
+// 4. Names must be written in the format: "Name, Delay (in seconds)" (e.g., John Doe, 40).
+// 5. A humanoid can be killed by the same Kill Tome only once.
+public sealed class KillTomeSystem : EntitySystem
+{
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+ [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
+ [Dependency] private readonly DamageableSystem _damageSystem = default!;
+ [Dependency] private readonly ISharedAdminLogManager _adminLogs = default!;
+ [Dependency] private readonly NameModifierSystem _nameModifierSystem = default!;
+
+ ///
+ public override void Initialize()
+ {
+ SubscribeLocalEvent(OnPaperAfterWriteInteract);
+ }
+
+ public override void Update(float frameTime)
+ {
+ // Getting all the entities that are targeted by Kill Tome and checking if their kill time has passed.
+ // If it has, we kill them and remove the KillTomeTargetComponent.
+ var query = EntityQueryEnumerator();
+
+ while (query.MoveNext(out var uid, out var targetComp))
+ {
+ if (_gameTiming.CurTime < targetComp.KillTime)
+ continue;
+
+ // The component doesn't get removed fast enough and the update loop will run through it a few more times.
+ // This check is here to ensure it will not spam popups or kill you several times over.
+ if (targetComp.Dead)
+ continue;
+
+ Kill(uid, targetComp);
+
+ _popupSystem.PopupPredicted(Loc.GetString("killtome-death"),
+ Loc.GetString("killtome-death-others", ("target", uid)),
+ uid,
+ uid,
+ PopupType.LargeCaution);
+
+ targetComp.Dead = true;
+
+ RemCompDeferred(uid);
+ }
+ }
+
+ private void OnPaperAfterWriteInteract(Entity ent, ref PaperAfterWriteEvent args)
+ {
+ // if the entity is not a paper, we don't do anything
+ if (!TryComp(ent.Owner, out var paper))
+ return;
+
+ var content = paper.Content;
+
+ var lines = content.Split('\n', StringSplitOptions.RemoveEmptyEntries);
+
+ var showPopup = false;
+
+ foreach (var line in lines)
+ {
+ if (string.IsNullOrEmpty(line))
+ continue;
+
+ var parts = line.Split(',', 2, StringSplitOptions.RemoveEmptyEntries);
+
+ var name = parts[0].Trim();
+
+ var delay = ent.Comp.DefaultKillDelay;
+
+ if (parts.Length == 2 && Parse.TryInt32(parts[1].Trim(), out var parsedDelay) && parsedDelay > 0)
+ delay = TimeSpan.FromSeconds(parsedDelay);
+
+ if (!CheckIfEligible(name, ent.Comp, out var uid))
+ {
+ continue;
+ }
+
+ // Compiler will complain if we don't check for null here.
+ if (uid is not { } realUid)
+ continue;
+
+ showPopup = true;
+
+ EnsureComp(realUid, out var targetComp);
+
+ targetComp.KillTime = _gameTiming.CurTime + delay;
+ targetComp.Damage = ent.Comp.Damage;
+
+ Dirty(realUid, targetComp);
+
+ ent.Comp.KilledEntities.Add(realUid);
+
+ Dirty(ent);
+
+ _adminLogs.Add(LogType.Chat,
+ LogImpact.High,
+ $"{ToPrettyString(args.Actor)} has written {ToPrettyString(uid)}'s name in Kill Tome.");
+ }
+
+ // If we have written at least one eligible name, we show the popup (So the player knows death note worked).
+ if (showPopup)
+ _popupSystem.PopupEntity(Loc.GetString("killtome-kill-success"), ent.Owner, args.Actor, PopupType.Large);
+ }
+
+ // A person to be killed by KillTome must:
+ // 1. be with the name
+ // 2. have HumanoidAppearanceComponent (so it targets only humanoids, obv)
+ // 3. not be already dead
+ // 4. not be already killed by Kill Tome
+
+ // If all these conditions are met, we return true and the entityUid of the person to kill.
+ private bool CheckIfEligible(string name, KillTomeComponent comp, [NotNullWhen(true)] out EntityUid? entityUid)
+ {
+ if (!TryFindEntityByName(name, out var uid) ||
+ !TryComp(uid, out var mob))
+ {
+ entityUid = null;
+ return false;
+ }
+
+ if (uid is not { } realUid)
+ {
+ entityUid = null;
+ return false;
+ }
+
+ if (comp.KilledEntities.Contains(realUid))
+ {
+ entityUid = null;
+ return false;
+ }
+
+ if (mob.CurrentState == MobState.Dead)
+ {
+ entityUid = null;
+ return false;
+ }
+
+ entityUid = uid;
+ return true;
+ }
+
+ private bool TryFindEntityByName(string name, [NotNullWhen(true)] out EntityUid? entityUid)
+ {
+ var query = EntityQueryEnumerator();
+
+ while (query.MoveNext(out var uid, out _))
+ {
+ if (!_nameModifierSystem.GetBaseName(uid).Equals(name, StringComparison.OrdinalIgnoreCase))
+ continue;
+
+ entityUid = uid;
+ return true;
+ }
+
+ entityUid = null;
+ return false;
+ }
+
+ private void Kill(EntityUid uid, KillTomeTargetComponent comp)
+ {
+ _damageSystem.TryChangeDamage(uid, comp.Damage, true);
+ }
+}
diff --git a/Content.Shared/KillTome/KillTomeTargetComponent.cs b/Content.Shared/KillTome/KillTomeTargetComponent.cs
new file mode 100644
index 0000000000..14a573b75f
--- /dev/null
+++ b/Content.Shared/KillTome/KillTomeTargetComponent.cs
@@ -0,0 +1,41 @@
+using Content.Shared.Damage;
+using Content.Shared.FixedPoint;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+
+namespace Content.Shared.KillTome;
+
+///
+/// Entity with this component is a Kill Tome target.
+///
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause]
+public sealed partial class KillTomeTargetComponent : Component
+{
+ ///
+ /// Damage that will be dealt to the target.
+ ///
+ [DataField, AutoNetworkedField]
+ public DamageSpecifier Damage = new()
+ {
+ DamageDict = new Dictionary
+ {
+ { "Blunt", 200 }
+ }
+ };
+
+ ///
+ /// The time when the target is killed.
+ ///
+ [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField]
+ [AutoPausedField]
+ public TimeSpan KillTime = TimeSpan.Zero;
+
+ ///
+ /// Indicates this target has been killed by the killtome.
+ ///
+ [DataField, AutoNetworkedField]
+ public bool Dead;
+
+ // Disallows cheat clients from seeing who is about to die to the killtome.
+ public override bool SendOnlyToOwner => true;
+}
diff --git a/Content.Shared/Paper/PaperSystem.cs b/Content.Shared/Paper/PaperSystem.cs
index 75496d93b4..6a181e4ae9 100644
--- a/Content.Shared/Paper/PaperSystem.cs
+++ b/Content.Shared/Paper/PaperSystem.cs
@@ -187,6 +187,7 @@ public sealed class PaperSystem : EntitySystem
{
var ev = new PaperWriteAttemptEvent(entity.Owner);
RaiseLocalEvent(args.Actor, ref ev);
+
if (ev.Cancelled)
return;
@@ -211,6 +212,9 @@ public sealed class PaperSystem : EntitySystem
entity.Comp.Mode = PaperAction.Read;
UpdateUserInterface(entity);
+
+ var writeAfterEv = new PaperAfterWriteEvent(args.Actor);
+ RaiseLocalEvent(entity.Owner, ref writeAfterEv);
}
private void OnRandomPaperContentMapInit(Entity ent, ref MapInitEvent args)
@@ -319,6 +323,14 @@ public record struct PaperWriteEvent(EntityUid User, EntityUid Paper);
///
/// Cancellable event for attempting to write on a piece of paper.
///
-/// The paper that the writing will take place on.
+/// The paper that the writing will take place on.
[ByRefEvent]
public record struct PaperWriteAttemptEvent(EntityUid Paper, string? FailReason = null, bool Cancelled = false);
+
+///
+/// Event raised on paper after it was written on by someone.
+///
+/// Entity that wrote something on the paper.
+[ByRefEvent]
+public readonly record struct PaperAfterWriteEvent(EntityUid Actor);
+
diff --git a/Resources/Locale/en-US/killtome.ftl b/Resources/Locale/en-US/killtome.ftl
new file mode 100644
index 0000000000..2a45db41ff
--- /dev/null
+++ b/Resources/Locale/en-US/killtome.ftl
@@ -0,0 +1,11 @@
+killtome-rules =
+ Kill Tome Rules:
+ 1. The humanoid whose name is written in this note shall die.
+ 2. If the name is shared by multiple humanoids, a random humanoid with that name will die.
+ 3. Each name shall be written on a new line.
+ 4. Names must be written in the format: "Name, Delay (in seconds)" (e.g., John Doe, 40).
+ 5. A humanoid can be killed by the same Kill Tome only once.
+
+killtome-kill-success = The name is written. The countdown begins.
+killtome-death = You feel sudden pain in your chest!
+killtome-death-others = {CAPITALIZE($target)} grabs onto {POSS-ADJ($target)} chest and falls to the ground!
diff --git a/Resources/Prototypes/Entities/Objects/Misc/killtome.yml b/Resources/Prototypes/Entities/Objects/Misc/killtome.yml
new file mode 100644
index 0000000000..41339a9281
--- /dev/null
+++ b/Resources/Prototypes/Entities/Objects/Misc/killtome.yml
@@ -0,0 +1,24 @@
+- type: entity
+ name: black tome
+ parent: BasePaper
+ id: KillTome
+ suffix: KillTome, Admeme # To stay true to the lore, please never make this accessible outside of divine intervention (admeme).
+ description: A worn black tome. It smells like old paper.
+ components:
+ - type: Sprite
+ sprite: Objects/Misc/killtome.rsi
+ state: icon
+ - type: KillTome
+ defaultKillDelay: 40
+ damage:
+ types:
+ Blunt: 200
+ - type: Paper
+ content: killtome-rules
+ - type: ActivatableUI
+ key: enum.PaperUiKey.Key
+ requiresComplex: false
+ - type: UserInterface
+ interfaces:
+ enum.PaperUiKey.Key:
+ type: PaperBoundUserInterface
diff --git a/Resources/Textures/Objects/Misc/killtome.rsi/icon.png b/Resources/Textures/Objects/Misc/killtome.rsi/icon.png
new file mode 100644
index 0000000000..b3f5427ebe
Binary files /dev/null and b/Resources/Textures/Objects/Misc/killtome.rsi/icon.png differ
diff --git a/Resources/Textures/Objects/Misc/killtome.rsi/meta.json b/Resources/Textures/Objects/Misc/killtome.rsi/meta.json
new file mode 100644
index 0000000000..b418b2056f
--- /dev/null
+++ b/Resources/Textures/Objects/Misc/killtome.rsi/meta.json
@@ -0,0 +1,14 @@
+{
+ "version": 1,
+ "license": "CC-BY-SA-3.0",
+ "copyright": "alexmactep",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "icon"
+ }
+ ]
+}