Files
wwdpublic/Content.Shared/Customization/Systems/CharacterRequirements.Job.cs
Timfa 4618b94807 CharacterRequirements on ExtendDescriptions (#1862)
<!--
This is a semi-strict format, you can add/remove sections as needed but
the order/format should be kept the same
Remove these comments before submitting
-->

# Description

<!--
Explain this PR in as much detail as applicable

Some example prompts to consider:
How might this affect the game? The codebase?
What might be some alternatives to this?
How/Who does this benefit/hurt [the game/codebase]?
-->

This PR does not have any effects on the game from a player-perspective.
It does, however, allow us to add CharacterRequirements to
ExtendDescriptions, which allows us to add contextual information to
items that only show up if characters know about them, for example. It
has an optional field that can also show text if your character does
_not_ meet requirements.

---

# TODO

<!--
A list of everything you have to do before this PR is "complete"
You probably won't have to complete everything before merging but it's
good to leave future references
-->

- [x] Add a bunch of CharacterRequirements to new and existing
ExtendDescriptions for contraband or other neat info

---

<!--
This is default collapsed, readers click to expand it and see all your
media
The PR media section can get very large at times, so this is a good way
to keep it clean
The title is written using HTML tags
The title must be within the <summary> tags or you won't see it
-->

<details><summary><h1>Media</h1></summary>
<p>
Example of how to add a requirement:

![image](https://github.com/user-attachments/assets/18c105f0-550b-410a-b0be-15e5c8f8a73f)

https://github.com/user-attachments/assets/67ad6ecd-1886-4f71-85c0-fdd035a9f5c9

![image](https://github.com/user-attachments/assets/8195b744-ec6b-4b69-bc9e-a86443847239)

</p>
</details>

---

# Changelog

<!--
You can add an author after the `🆑` to change the name that appears
in the changelog (ex: `🆑 Death`)
Leaving it blank will default to your GitHub display name
This includes all available types for the changelog
-->

🆑
- tweak: Tweaked Extended Descriptions to be able to require
CharacterRequirements before being shown to the player. Currently not
actually implemented anywhere except for the emag and some posters.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Summary by CodeRabbit

- **New Features**
- Enhanced character creation and role-validation systems now
incorporate playtime tracking and additional criteria, providing a more
tailored experience.
- In-game items—such as hacking devices, weapons, and posters—feature
extended, lore-rich descriptions that adjust based on character
attributes.
- New localized texts enrich the narrative by offering clear feedback
when character requirements are or aren’t met.
- New character requirements related to antagonists and mindshields have
been introduced, enhancing gameplay dynamics.
- A new method for validating character requirements has been added,
improving the accuracy of checks during character creation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: VMSolidus <evilexecutive@gmail.com>
Co-authored-by: VMSolidus <evilexecutive@gmail.com>
(cherry picked from commit 0640f1f54619a95a4360a79b870654b2c4a1e433)
2025-03-08 14:43:38 +03:00

334 lines
10 KiB
C#

using System.Linq;
using Content.Shared.CCVar;
using Content.Shared.Mind;
using Content.Shared.Players.PlayTimeTracking;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Content.Shared.Roles.Jobs;
using JetBrains.Annotations;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Content.Shared.Customization.Systems;
/// <summary>
/// Requires the selected job to be one of the specified jobs
/// </summary>
[UsedImplicitly]
[Serializable, NetSerializable]
public sealed partial class CharacterJobRequirement : CharacterRequirement
{
[DataField(required: true)]
public List<ProtoId<JobPrototype>> Jobs;
public override bool IsValid(JobPrototype job,
HumanoidCharacterProfile profile,
Dictionary<string, TimeSpan> playTimes,
bool whitelisted,
IPrototype prototype,
IEntityManager entityManager,
IPrototypeManager prototypeManager,
IConfigurationManager configManager,
out string? reason,
int depth = 0,
MindComponent? mind = null)
{
var jobs = new List<string>();
var depts = prototypeManager.EnumeratePrototypes<DepartmentPrototype>();
// Get the job names and department colors
foreach (var j in Jobs)
{
var jobProto = prototypeManager.Index(j);
var color = Color.LightBlue;
foreach (var dept in depts.ToList().OrderBy(d => Loc.GetString($"department-{d.ID}")))
{
if (!dept.Roles.Contains(j))
continue;
color = dept.Color;
break;
}
jobs.Add($"[color={color.ToHex()}]{Loc.GetString(jobProto.Name)}[/color]");
}
// Join the job names
var jobsString = Loc.GetString("character-job-requirement",
("inverted", Inverted), ("jobs", string.Join(", ", jobs)));
reason = jobsString;
return Jobs.Contains(job.ID);
}
}
/// <summary>
/// Requires the selected job to be in one of the specified departments
/// </summary>
[UsedImplicitly]
[Serializable, NetSerializable]
public sealed partial class CharacterDepartmentRequirement : CharacterRequirement
{
[DataField(required: true)]
public List<ProtoId<DepartmentPrototype>> Departments;
public override bool IsValid(JobPrototype job,
HumanoidCharacterProfile profile,
Dictionary<string, TimeSpan> playTimes,
bool whitelisted,
IPrototype prototype,
IEntityManager entityManager,
IPrototypeManager prototypeManager,
IConfigurationManager configManager,
out string? reason,
int depth = 0,
MindComponent? mind = null)
{
var departments = new List<string>();
// Get the department names and colors
foreach (var d in Departments)
{
var deptProto = prototypeManager.Index(d);
var color = deptProto.Color;
departments.Add($"[color={color.ToHex()}]{Loc.GetString($"department-{deptProto.ID}")}[/color]");
}
// Join the department names
var departmentsString = Loc.GetString("character-department-requirement",
("inverted", Inverted), ("departments", string.Join(", ", departments)));
reason = departmentsString;
return Departments.Any(d => prototypeManager.Index(d).Roles.Contains(job.ID));
}
}
/// <summary>
/// Requires the playtime for a department to be within a certain range
/// </summary>
[UsedImplicitly]
[Serializable, NetSerializable]
public sealed partial class CharacterDepartmentTimeRequirement : CharacterRequirement
{
[DataField]
public TimeSpan Min = TimeSpan.MinValue;
[DataField]
public TimeSpan Max = TimeSpan.MaxValue;
[DataField(required: true)]
public ProtoId<DepartmentPrototype> Department;
public override bool IsValid(JobPrototype job,
HumanoidCharacterProfile profile,
Dictionary<string, TimeSpan> playTimes,
bool whitelisted,
IPrototype prototype,
IEntityManager entityManager,
IPrototypeManager prototypeManager,
IConfigurationManager configManager,
out string? reason,
int depth = 0,
MindComponent? mind = null)
{
// Disable the requirement if the role timers are disabled
if (!configManager.GetCVar(CCVars.GameRoleTimers))
{
reason = null;
return !Inverted;
}
var department = prototypeManager.Index(Department);
// Combine all of this department's job playtimes
var playtime = TimeSpan.Zero;
foreach (var other in department.Roles)
{
var proto = prototypeManager.Index<JobPrototype>(other).PlayTimeTracker;
playTimes.TryGetValue(proto, out var otherTime);
playtime += otherTime;
}
if (playtime > Max)
{
// Show the reason if invalid
reason = Inverted
? null
: Loc.GetString("character-timer-department-too-high",
("time", playtime.TotalMinutes - Max.TotalMinutes),
("department", Loc.GetString($"department-{department.ID}")),
("departmentColor", department.Color));
return false;
}
if (playtime < Min)
{
// Show the reason if invalid
reason = Inverted
? null
: Loc.GetString("character-timer-department-insufficient",
("time", Min.TotalMinutes - playtime.TotalMinutes),
("department", Loc.GetString($"department-{department.ID}")),
("departmentColor", department.Color));
return false;
}
reason = null;
return true;
}
}
/// <summary>
/// Requires the player to have a certain amount of overall job time
/// </summary>
[UsedImplicitly]
[Serializable, NetSerializable]
public sealed partial class CharacterOverallTimeRequirement : CharacterRequirement
{
[DataField]
public TimeSpan Min = TimeSpan.MinValue;
[DataField]
public TimeSpan Max = TimeSpan.MaxValue;
public override bool IsValid(JobPrototype job,
HumanoidCharacterProfile profile,
Dictionary<string, TimeSpan> playTimes,
bool whitelisted,
IPrototype prototype,
IEntityManager entityManager,
IPrototypeManager prototypeManager,
IConfigurationManager configManager,
out string? reason,
int depth = 0,
MindComponent? mind = null)
{
// Disable the requirement if the role timers are disabled
if (!configManager.GetCVar(CCVars.GameRoleTimers))
{
reason = null;
return !Inverted;
}
// Get the overall time
var overallTime = playTimes.GetValueOrDefault(PlayTimeTrackingShared.TrackerOverall);
if (overallTime > Max)
{
// Show the reason if invalid
reason = Inverted
? null
: Loc.GetString("character-timer-overall-too-high",
("time", overallTime.TotalMinutes - Max.TotalMinutes));
return false;
}
if (overallTime < Min)
{
// Show the reason if invalid
reason = Inverted
? null
: Loc.GetString("character-timer-overall-insufficient",
("time", Min.TotalMinutes - overallTime.TotalMinutes));
return false;
}
reason = null;
return true;
}
}
/// <summary>
/// Requires the playtime for a tracker to be within a certain range
/// </summary>
[UsedImplicitly]
[Serializable, NetSerializable]
public sealed partial class CharacterPlaytimeRequirement : CharacterRequirement
{
[DataField]
public TimeSpan Min = TimeSpan.MinValue;
[DataField]
public TimeSpan Max = TimeSpan.MaxValue;
[DataField(required: true)]
public ProtoId<PlayTimeTrackerPrototype> Tracker;
public override bool IsValid(JobPrototype job,
HumanoidCharacterProfile profile,
Dictionary<string, TimeSpan> playTimes,
bool whitelisted,
IPrototype prototype,
IEntityManager entityManager,
IPrototypeManager prototypeManager,
IConfigurationManager configManager,
out string? reason,
int depth = 0,
MindComponent? mind = null)
{
// Disable the requirement if the role timers are disabled
if (!configManager.GetCVar(CCVars.GameRoleTimers))
{
reason = null;
return !Inverted;
}
// Get SharedJobSystem
if (!entityManager.EntitySysManager.TryGetEntitySystem(out SharedJobSystem? jobSystem))
{
DebugTools.Assert("CharacterRequirements: SharedJobSystem not found");
reason = null;
return false;
}
// Get the JobPrototype of the Tracker
var trackerJob = jobSystem.GetJobPrototype(Tracker);
var jobStr = prototypeManager.Index<JobPrototype>(trackerJob).LocalizedName;
// Get the primary department of the Tracker
if (!jobSystem.TryGetPrimaryDepartment(trackerJob, out var department) &&
!jobSystem.TryGetDepartment(trackerJob, out department))
{
DebugTools.Assert($"CharacterRequirements: Department not found for job {trackerJob}");
reason = null;
return false;
}
// Get the time for the tracker
var time = playTimes.GetValueOrDefault(Tracker);
reason = null;
if (time > Max)
{
// Show the reason if invalid
reason = Inverted
? null
: Loc.GetString("character-timer-role-too-high",
("time", time.TotalMinutes - Max.TotalMinutes),
("job", jobStr),
("departmentColor", department.Color));
return false;
}
if (time < Min)
{
// Show the reason if invalid
reason = Inverted
? null
: Loc.GetString("character-timer-role-insufficient",
("time", Min.TotalMinutes - time.TotalMinutes),
("job", jobStr),
("departmentColor", department.Color));
return false;
}
return true;
}
}