diff --git a/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs b/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs index 9ae8c5ff837eb..6e3b354a48ae8 100644 --- a/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs @@ -8,6 +8,7 @@ using Content.Server.Station.Systems; using Content.Shared.GameTicking; using Content.Shared.GameTicking.Components; +using Content.Shared.Mobs; using Content.Shared.Points; using Content.Shared.Storage; using Robust.Server.GameObjects; @@ -57,7 +58,9 @@ private void OnBeforeSpawn(PlayerBeforeSpawnEvent ev) _mind.TransferTo(newMind, mob); _outfitSystem.SetOutfit(mob, dm.Gear); - EnsureComp(mob); + // We need to ensure any spawned player has the kill tracker, and that Critical is used for the DeathMatch gamemode. + var killTracker = EnsureComp(mob); + killTracker.KillState = MobState.Critical; _respawn.AddToTracker(ev.Player.UserId, (uid, tracker)); _point.EnsurePlayer(ev.Player.UserId, uid, point); @@ -69,12 +72,16 @@ private void OnBeforeSpawn(PlayerBeforeSpawnEvent ev) private void OnSpawnComplete(PlayerSpawnCompleteEvent ev) { - EnsureComp(ev.Mob); var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out _, out var tracker, out var rule)) { if (!GameTicker.IsGameRuleActive(uid, rule)) continue; + + // We need to ensure any spawned player has the kill tracker, and that Critical is used for the DeathMatch gamemode. + var killTracker = EnsureComp(ev.Mob); + killTracker.KillState = MobState.Critical; + _respawn.AddToTracker((ev.Mob, null), (uid, tracker)); } } diff --git a/Content.Server/KillTracking/KillTrackerComponent.cs b/Content.Server/KillTracking/KillTrackerComponent.cs index aad6671df94ca..0693ad465d846 100644 --- a/Content.Server/KillTracking/KillTrackerComponent.cs +++ b/Content.Server/KillTracking/KillTrackerComponent.cs @@ -7,19 +7,19 @@ namespace Content.Server.KillTracking; /// /// This is used for entities that track player damage sources and killers. /// -[RegisterComponent, Access(typeof(KillTrackingSystem))] +[RegisterComponent] public sealed partial class KillTrackerComponent : Component { /// /// The mobstate that registers as a "kill" /// - [DataField("killState")] + [DataField] public MobState KillState = MobState.Critical; /// /// A dictionary of sources and how much damage they've done to this entity over time. /// - [DataField("lifetimeDamage")] + [DataField] public Dictionary LifetimeDamage = new(); } diff --git a/Content.Server/Objectives/Components/KillLimitConditionComponent.cs b/Content.Server/Objectives/Components/KillLimitConditionComponent.cs new file mode 100644 index 0000000000000..b349495f98b33 --- /dev/null +++ b/Content.Server/Objectives/Components/KillLimitConditionComponent.cs @@ -0,0 +1,52 @@ +using Content.Server.Objectives.Systems; + +namespace Content.Server.Objectives.Components; + +/// +/// Requires that a player doesn't kill more than a set number of characters, or it fails. +/// +[RegisterComponent, Access(typeof(KillLimitConditionSystem))] +public sealed partial class KillLimitConditionComponent : Component +{ + /// + /// The number of kills that are permissible for this condition; set upon the objective being assigned. + /// + [DataField] + public HashSet KillList = new(); + + /// + /// The number of kills that are permissible for this condition; set upon the objective being assigned. + /// + [DataField] + public int PermissibleKillCount; + + /// + /// The minimum roll for permissible kills for this objective. + /// + [DataField] + public int MinKillCount = 5; + + /// + /// The maximum roll for permissible kills for this objective. + /// + [DataField] + public int MaxKillCount = 5; + + /// + /// If true, an entity that gets revived will be removed from the kill limit tracker. + /// + [DataField] + public bool AllowReviving = true; + + /// + /// The title of the objective. Takes the kill limit as input as "limit". + /// + [DataField] + public LocId ObjectiveTitle = "objective-condition-kill-limit-title"; + + /// + /// The title of the objective. Takes the kill limit as input as "limit", and kill count as "value". + /// + [DataField] + public LocId ObjectiveDescription = "objective-condition-kill-limit-description"; +} diff --git a/Content.Server/Objectives/Systems/KillLimitConditionSystem.cs b/Content.Server/Objectives/Systems/KillLimitConditionSystem.cs new file mode 100644 index 0000000000000..40a87fe381a2b --- /dev/null +++ b/Content.Server/Objectives/Systems/KillLimitConditionSystem.cs @@ -0,0 +1,72 @@ +using Content.Server.KillTracking; +using Content.Server.Objectives.Components; +using Content.Shared.Mind; +using Content.Shared.Mobs; +using Content.Shared.Objectives.Components; +using Robust.Shared.Random; + +namespace Content.Server.Objectives.Systems; + +public sealed class KillLimitConditionSystem : EntitySystem +{ + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly SharedMindSystem _mind = default!; + [Dependency] private readonly MetaDataSystem _metaData = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnAssigned); + SubscribeLocalEvent(OnAfterAssign); + SubscribeLocalEvent(OnGetProgress); + SubscribeLocalEvent(OnMobStateChanged); + SubscribeLocalEvent(OnKillReported); + } + + private void OnAssigned(Entity condition, ref ObjectiveAssignedEvent args) + { + condition.Comp.PermissibleKillCount = _random.Next(condition.Comp.MinKillCount, condition.Comp.MaxKillCount); + } + private void OnAfterAssign(Entity condition, ref ObjectiveAfterAssignEvent args) + { + string title; + title = Loc.GetString(condition.Comp.ObjectiveTitle, ("limit", condition.Comp.PermissibleKillCount)); + + _metaData.SetEntityName(condition.Owner, title, args.Meta); + } + + private void OnGetProgress(Entity condition, ref ObjectiveGetProgressEvent args) + { + args.Progress = condition.Comp.PermissibleKillCount >= condition.Comp.KillList.Count ? 1f : 0f; + + string description; + description = Loc.GetString(condition.Comp.ObjectiveDescription, ("limit", condition.Comp.PermissibleKillCount), ("value", condition.Comp.KillList.Count)); + _metaData.SetEntityDescription(condition.Owner, description); + } + + /// + /// Tracks revival of a possible target. + /// + private void OnMobStateChanged(MobStateChangedEvent ev) + { + if (ev.NewMobState == MobState.Dead) + return; + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var comp)) + { + if (comp.AllowReviving) + comp.KillList.Remove(ev.Target); + } + } + + private void OnKillReported(ref KillReportedEvent ev) + { + if (ev.Primary is KillPlayerSource killer) + { + if (_mind.TryGetMind(killer.PlayerId, out var mind) && _mind.TryGetObjectiveComp(mind.Value.Owner, out var condition, mind.Value.Comp)) + condition.KillList.Add(ev.Entity); + } + } +} diff --git a/Content.Server/Zombies/ZombieSystem.Transform.cs b/Content.Server/Zombies/ZombieSystem.Transform.cs index c44c2e7aa00bd..8894daca80b1d 100644 --- a/Content.Server/Zombies/ZombieSystem.Transform.cs +++ b/Content.Server/Zombies/ZombieSystem.Transform.cs @@ -7,6 +7,7 @@ using Content.Server.Ghost.Roles.Components; using Content.Server.Humanoid; using Content.Server.Inventory; +using Content.Server.KillTracking; using Content.Server.Mind; using Content.Server.NPC; using Content.Server.NPC.HTN; @@ -143,6 +144,7 @@ public void ZombifyEntity(EntityUid target, MobStateComponent? mobState = null) RemComp(target); RemComp(target); RemComp(target); + RemComp(target); //A dead person is already dead - maybe worth reapplying if we want to track zombie slaying kills //funny voice var accentType = "zombie"; diff --git a/Resources/Locale/en-US/objectives/conditions/kill-limit.ftl b/Resources/Locale/en-US/objectives/conditions/kill-limit.ftl new file mode 100644 index 0000000000000..56f1f26a3370c --- /dev/null +++ b/Resources/Locale/en-US/objectives/conditions/kill-limit.ftl @@ -0,0 +1,2 @@ +objective-condition-kill-limit-title = Do not permanently kill more than {$limit} people. +objective-condition-kill-limit-description = Ninjas act with honor. You are responsible for {$value} deaths. diff --git a/Resources/Prototypes/Entities/Mobs/Species/base.yml b/Resources/Prototypes/Entities/Mobs/Species/base.yml index f33cee8b209c1..99a8dee2359b0 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/base.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/base.yml @@ -204,6 +204,8 @@ - type: MobPrice price: 1500 # Kidnapping a living person and selling them for cred is a good move. deathPenalty: 0.01 # However they really ought to be living and intact, otherwise they're worth 100x less. + - type: KillTracker + killState: Dead - type: Tag tags: - CanPilot diff --git a/Resources/Prototypes/GameRules/events.yml b/Resources/Prototypes/GameRules/events.yml index 8ff3960703364..9765bad94fbff 100644 --- a/Resources/Prototypes/GameRules/events.yml +++ b/Resources/Prototypes/GameRules/events.yml @@ -235,6 +235,7 @@ - TerrorObjective - MassArrestObjective - NinjaSurviveObjective + - NinjaKillLimitObjective - type: AntagSelection agentName: ninja-round-end-agent-name definitions: @@ -262,6 +263,9 @@ - NamesNinjaTitle - NamesNinja nameFormat: name-format-ninja + - type: GibOnRoundEnd + preventGibbingObjectives: + - NinjaKillLimitObjective mindRoles: - MindRoleNinja - type: DynamicRuleCost diff --git a/Resources/Prototypes/Objectives/ninja.yml b/Resources/Prototypes/Objectives/ninja.yml index f015dd8f72364..9f55bdc67faf2 100644 --- a/Resources/Prototypes/Objectives/ninja.yml +++ b/Resources/Prototypes/Objectives/ninja.yml @@ -88,3 +88,15 @@ icon: sprite: Objects/Weapons/Melee/stunbaton.rsi state: stunbaton_on + +- type: entity + parent: [BaseNinjaObjective] + id: NinjaKillLimitObjective + components: + - type: Objective + icon: + sprite: Objects/Weapons/Melee/energykatana.rsi + state: icon + - type: KillLimitCondition + minKillCount: 3 + maxKillCount: 3