Added Kill Tome (Death Note). (#39011)

Co-authored-by: ScarKy0 <scarky0@onet.eu>
This commit is contained in:
xsainteer
2025-07-26 04:00:58 +06:00
committed by GitHub
parent 2be968ccb1
commit d0c104e4b0
8 changed files with 328 additions and 1 deletions

View File

@@ -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;
/// <summary>
/// Paper with that component is KillTome.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class KillTomeComponent : Component
{
/// <summary>
/// if delay is not specified, it will use this default value
/// </summary>
[DataField, AutoNetworkedField]
public TimeSpan DefaultKillDelay = TimeSpan.FromSeconds(40);
/// <summary>
/// Damage specifier that will be used to kill the target.
/// </summary>
[DataField, AutoNetworkedField]
public DamageSpecifier Damage = new()
{
DamageDict = new Dictionary<string, FixedPoint2>
{
{ "Blunt", 200 }
}
};
/// <summary>
/// to keep a track of already killed people so they won't be killed again
/// </summary>
[DataField]
public HashSet<EntityUid> KilledEntities = [];
}

View File

@@ -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;
/// <summary>
/// This handles KillTome functionality.
/// </summary>
/// 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!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<KillTomeComponent, PaperAfterWriteEvent>(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<KillTomeTargetComponent>();
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<KillTomeTargetComponent>(uid);
}
}
private void OnPaperAfterWriteInteract(Entity<KillTomeComponent> ent, ref PaperAfterWriteEvent args)
{
// if the entity is not a paper, we don't do anything
if (!TryComp<PaperComponent>(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<KillTomeTargetComponent>(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<MobStateComponent>(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<HumanoidAppearanceComponent>();
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);
}
}

View File

@@ -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;
/// <summary>
/// Entity with this component is a Kill Tome target.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause]
public sealed partial class KillTomeTargetComponent : Component
{
///<summary>
/// Damage that will be dealt to the target.
/// </summary>
[DataField, AutoNetworkedField]
public DamageSpecifier Damage = new()
{
DamageDict = new Dictionary<string, FixedPoint2>
{
{ "Blunt", 200 }
}
};
/// <summary>
/// The time when the target is killed.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField]
[AutoPausedField]
public TimeSpan KillTime = TimeSpan.Zero;
/// <summary>
/// Indicates this target has been killed by the killtome.
/// </summary>
[DataField, AutoNetworkedField]
public bool Dead;
// Disallows cheat clients from seeing who is about to die to the killtome.
public override bool SendOnlyToOwner => true;
}

View File

@@ -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<RandomPaperContentComponent> ent, ref MapInitEvent args)
@@ -319,6 +323,14 @@ public record struct PaperWriteEvent(EntityUid User, EntityUid Paper);
/// <summary>
/// Cancellable event for attempting to write on a piece of paper.
/// </summary>
/// <param name="paper">The paper that the writing will take place on.</param>
/// <param name="Paper">The paper that the writing will take place on.</param>
[ByRefEvent]
public record struct PaperWriteAttemptEvent(EntityUid Paper, string? FailReason = null, bool Cancelled = false);
/// <summary>
/// Event raised on paper after it was written on by someone.
/// </summary>
/// <param name="Actor">Entity that wrote something on the paper.</param>
[ByRefEvent]
public readonly record struct PaperAfterWriteEvent(EntityUid Actor);

View File

@@ -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!

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 B

View File

@@ -0,0 +1,14 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "alexmactep",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "icon"
}
]
}