2024-03-27 21:23:18 +07:00
|
|
|
using System.Linq;
|
2024-01-31 14:01:35 +00:00
|
|
|
using Content.Server.Antag;
|
|
|
|
|
using Content.Server.GameTicking;
|
|
|
|
|
using Content.Server.GameTicking.Rules;
|
|
|
|
|
using Content.Server.GameTicking.Rules.Components;
|
|
|
|
|
using Content.Server.Mind;
|
|
|
|
|
using Content.Server.Objectives;
|
|
|
|
|
using Content.Server.Roles;
|
2024-03-27 19:30:19 +07:00
|
|
|
using Content.Shared._White.Mood;
|
2024-01-31 14:01:35 +00:00
|
|
|
using Content.Shared.Changeling;
|
|
|
|
|
using Content.Shared.GameTicking;
|
2024-03-27 19:30:19 +07:00
|
|
|
using Content.Shared.NPC.Systems;
|
2024-01-31 14:01:35 +00:00
|
|
|
using Content.Shared.Objectives.Components;
|
|
|
|
|
using Content.Shared.Roles;
|
2024-03-27 19:30:19 +07:00
|
|
|
using Robust.Server.Player;
|
2024-01-31 14:01:35 +00:00
|
|
|
using Robust.Shared.Random;
|
|
|
|
|
using Robust.Shared.Timing;
|
|
|
|
|
|
|
|
|
|
namespace Content.Server.Changeling;
|
|
|
|
|
|
|
|
|
|
public sealed class ChangelingRuleSystem : GameRuleSystem<ChangelingRuleComponent>
|
|
|
|
|
{
|
|
|
|
|
[Dependency] private readonly AntagSelectionSystem _antagSelection = default!;
|
|
|
|
|
[Dependency] private readonly IRobustRandom _random = default!;
|
|
|
|
|
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
|
|
|
|
[Dependency] private readonly NpcFactionSystem _npcFaction = default!;
|
|
|
|
|
[Dependency] private readonly MindSystem _mindSystem = default!;
|
|
|
|
|
[Dependency] private readonly SharedRoleSystem _roleSystem = default!;
|
|
|
|
|
[Dependency] private readonly ObjectivesSystem _objectives = default!;
|
|
|
|
|
[Dependency] private readonly ChangelingNameGenerator _nameGenerator = default!;
|
2024-03-27 19:30:19 +07:00
|
|
|
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
2024-01-31 14:01:35 +00:00
|
|
|
|
2024-02-16 01:07:59 +07:00
|
|
|
private const int PlayersPerChangeling = 15;
|
|
|
|
|
private const int MaxChangelings = 4;
|
2024-01-31 14:01:35 +00:00
|
|
|
|
|
|
|
|
private const float ChangelingStartDelay = 3f * 60;
|
|
|
|
|
private const float ChangelingStartDelayVariance = 3f * 60;
|
|
|
|
|
|
|
|
|
|
private const int ChangelingMaxDifficulty = 5;
|
|
|
|
|
private const int ChangelingMaxPicks = 20;
|
|
|
|
|
|
|
|
|
|
public override void Initialize()
|
|
|
|
|
{
|
|
|
|
|
base.Initialize();
|
|
|
|
|
|
|
|
|
|
SubscribeLocalEvent<RoundStartAttemptEvent>(OnStartAttempt);
|
|
|
|
|
SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(OnPlayersSpawned);
|
|
|
|
|
SubscribeLocalEvent<PlayerSpawnCompleteEvent>(HandleLatejoin);
|
|
|
|
|
SubscribeLocalEvent<RoundRestartCleanupEvent>(ClearUsedNames);
|
|
|
|
|
|
|
|
|
|
SubscribeLocalEvent<ChangelingRuleComponent, ObjectivesTextGetInfoEvent>(OnObjectivesTextGetInfo);
|
2024-03-22 17:23:33 +09:00
|
|
|
|
|
|
|
|
SubscribeLocalEvent<ChangelingRoleComponent, GetBriefingEvent>(OnGetBriefing);
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-22 00:07:27 +09:00
|
|
|
protected override void Added(EntityUid uid, ChangelingRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)
|
|
|
|
|
{
|
|
|
|
|
base.Added(uid, component, gameRule, args);
|
|
|
|
|
|
|
|
|
|
gameRule.MinPlayers = PlayersPerChangeling;
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-22 17:23:33 +09:00
|
|
|
private void OnGetBriefing(Entity<ChangelingRoleComponent> ent, ref GetBriefingEvent args)
|
|
|
|
|
{
|
|
|
|
|
args.Append(Loc.GetString("changeling-role-briefing-short"));
|
2024-01-31 14:01:35 +00:00
|
|
|
}
|
|
|
|
|
|
2024-02-16 01:07:59 +07:00
|
|
|
protected override void ActiveTick(
|
|
|
|
|
EntityUid uid,
|
|
|
|
|
ChangelingRuleComponent component,
|
|
|
|
|
GameRuleComponent gameRule,
|
|
|
|
|
float frameTime)
|
2024-01-31 14:01:35 +00:00
|
|
|
{
|
|
|
|
|
base.ActiveTick(uid, component, gameRule, frameTime);
|
|
|
|
|
|
2024-03-27 19:30:19 +07:00
|
|
|
if (component.SelectionStatus < ChangelingRuleComponent.SelectionState.Started &&
|
|
|
|
|
component.AnnounceAt < _gameTiming.CurTime)
|
|
|
|
|
{
|
2024-01-31 14:01:35 +00:00
|
|
|
DoChangelingStart(component);
|
2024-03-27 19:30:19 +07:00
|
|
|
component.SelectionStatus = ChangelingRuleComponent.SelectionState.Started;
|
|
|
|
|
}
|
2024-01-31 14:01:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnStartAttempt(RoundStartAttemptEvent ev)
|
|
|
|
|
{
|
2024-03-27 19:30:19 +07:00
|
|
|
TryRoundStartAttempt(ev, Loc.GetString("changeling-title"));
|
|
|
|
|
}
|
2024-01-31 14:01:35 +00:00
|
|
|
|
2024-03-27 19:30:19 +07:00
|
|
|
private void OnPlayersSpawned(RulePlayerJobsAssignedEvent ev)
|
|
|
|
|
{
|
|
|
|
|
var query = QueryActiveRules();
|
|
|
|
|
while (query.MoveNext(out _, out var changeling, out _))
|
|
|
|
|
{
|
|
|
|
|
var delay = TimeSpan.FromSeconds(ChangelingStartDelay +
|
|
|
|
|
_random.NextFloat(0f, ChangelingStartDelayVariance));
|
2024-02-16 01:07:59 +07:00
|
|
|
|
2024-03-27 19:30:19 +07:00
|
|
|
changeling.AnnounceAt = _gameTiming.CurTime + delay;
|
2024-01-31 14:01:35 +00:00
|
|
|
|
2024-03-27 19:30:19 +07:00
|
|
|
changeling.SelectionStatus = ChangelingRuleComponent.SelectionState.ReadyToStart;
|
2024-01-31 14:01:35 +00:00
|
|
|
}
|
|
|
|
|
}
|
2024-02-16 01:07:59 +07:00
|
|
|
|
2024-03-27 19:30:19 +07:00
|
|
|
private void HandleLatejoin(PlayerSpawnCompleteEvent ev)
|
2024-01-31 14:01:35 +00:00
|
|
|
{
|
2024-03-27 19:30:19 +07:00
|
|
|
var query = QueryActiveRules();
|
|
|
|
|
while (query.MoveNext(out _, out var changeling, out _))
|
2024-01-31 14:01:35 +00:00
|
|
|
{
|
2024-03-27 19:30:19 +07:00
|
|
|
if (changeling.TotalChangelings >= MaxChangelings)
|
|
|
|
|
continue;
|
2024-01-31 14:01:35 +00:00
|
|
|
|
2024-03-27 19:30:19 +07:00
|
|
|
if (!ev.LateJoin)
|
|
|
|
|
continue;
|
2024-02-16 01:07:59 +07:00
|
|
|
|
2024-03-27 19:30:19 +07:00
|
|
|
if (!_antagSelection.IsPlayerEligible(ev.Player, changeling.ChangelingPrototypeId))
|
|
|
|
|
continue;
|
2024-02-16 01:07:59 +07:00
|
|
|
|
2024-03-27 19:30:19 +07:00
|
|
|
// Before the announcement is made, late-joiners are considered the same as players who readied.
|
|
|
|
|
if (changeling.SelectionStatus < ChangelingRuleComponent.SelectionState.Started)
|
|
|
|
|
continue;
|
2024-01-31 14:01:35 +00:00
|
|
|
|
2024-03-27 19:30:19 +07:00
|
|
|
var target = PlayersPerChangeling * changeling.TotalChangelings + 1;
|
|
|
|
|
var chance = 1f / PlayersPerChangeling;
|
|
|
|
|
|
|
|
|
|
if (ev.JoinOrder < target)
|
|
|
|
|
{
|
|
|
|
|
chance /= (target - ev.JoinOrder);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
chance *= ev.JoinOrder + 1 - target;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (chance > 1)
|
|
|
|
|
chance = 1;
|
|
|
|
|
|
|
|
|
|
if (_random.Prob(chance))
|
|
|
|
|
{
|
|
|
|
|
MakeChangeling(ev.Mob, changeling);
|
|
|
|
|
}
|
2024-01-31 14:01:35 +00:00
|
|
|
}
|
2024-03-27 19:30:19 +07:00
|
|
|
}
|
2024-01-31 14:01:35 +00:00
|
|
|
|
2024-03-27 19:30:19 +07:00
|
|
|
private void ClearUsedNames(RoundRestartCleanupEvent ev)
|
|
|
|
|
{
|
|
|
|
|
_nameGenerator.ClearUsed();
|
2024-01-31 14:01:35 +00:00
|
|
|
}
|
|
|
|
|
|
2024-03-27 19:30:19 +07:00
|
|
|
private void OnObjectivesTextGetInfo(
|
|
|
|
|
EntityUid uid,
|
|
|
|
|
ChangelingRuleComponent comp,
|
|
|
|
|
ref ObjectivesTextGetInfoEvent args)
|
2024-01-31 14:01:35 +00:00
|
|
|
{
|
2024-03-27 19:30:19 +07:00
|
|
|
args.Minds = comp.ChangelingMinds;
|
|
|
|
|
args.AgentName = Loc.GetString("changeling-round-end-agent-name");
|
|
|
|
|
}
|
2024-02-16 01:07:59 +07:00
|
|
|
|
2024-03-27 19:30:19 +07:00
|
|
|
private void DoChangelingStart(ChangelingRuleComponent component)
|
|
|
|
|
{
|
|
|
|
|
var eligiblePlayers =
|
|
|
|
|
_antagSelection.GetEligiblePlayers(_playerManager.Sessions, component.ChangelingPrototypeId);
|
2024-01-31 14:01:35 +00:00
|
|
|
|
2024-03-27 19:30:19 +07:00
|
|
|
if (eligiblePlayers.Count == 0)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
2024-01-31 14:01:35 +00:00
|
|
|
|
2024-03-27 19:30:19 +07:00
|
|
|
var changelingsToSelect =
|
|
|
|
|
_antagSelection.CalculateAntagCount(_playerManager.PlayerCount, PlayersPerChangeling, MaxChangelings);
|
2024-01-31 14:01:35 +00:00
|
|
|
|
2024-03-27 19:30:19 +07:00
|
|
|
var selectedChangelings = _antagSelection.ChooseAntags(changelingsToSelect, eligiblePlayers);
|
2024-01-31 14:01:35 +00:00
|
|
|
|
2024-03-27 19:30:19 +07:00
|
|
|
foreach (var changeling in selectedChangelings)
|
|
|
|
|
{
|
|
|
|
|
MakeChangeling(changeling, component);
|
2024-01-31 14:01:35 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-27 21:23:18 +07:00
|
|
|
public void AdminMakeChangeling(EntityUid entity)
|
|
|
|
|
{
|
|
|
|
|
var changelingRule = EntityQuery<ChangelingRuleComponent>().FirstOrDefault();
|
|
|
|
|
if (changelingRule == null)
|
|
|
|
|
{
|
|
|
|
|
GameTicker.StartGameRule("Changeling", out var ruleEntity);
|
|
|
|
|
changelingRule = Comp<ChangelingRuleComponent>(ruleEntity);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (HasComp<ChangelingRuleComponent>(entity))
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
MakeChangeling(entity, changelingRule);
|
|
|
|
|
}
|
2024-04-22 00:07:27 +09:00
|
|
|
|
2024-03-27 19:30:19 +07:00
|
|
|
public bool MakeChangeling(EntityUid changeling, ChangelingRuleComponent rule, bool giveObjectives = true)
|
2024-01-31 14:01:35 +00:00
|
|
|
{
|
|
|
|
|
if (!_mindSystem.TryGetMind(changeling, out var mindId, out var mind))
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (HasComp<ChangelingRoleComponent>(mindId))
|
|
|
|
|
{
|
2024-03-27 19:30:19 +07:00
|
|
|
Log.Error($"Player {mind.CharacterName} is already a changeling.");
|
2024-01-31 14:01:35 +00:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-22 00:07:27 +09:00
|
|
|
var briefing = Loc.GetString("changeling-role-greeting");
|
2024-03-27 19:30:19 +07:00
|
|
|
_antagSelection.SendBriefing(changeling, briefing, null, rule.GreetSoundNotification);
|
|
|
|
|
|
|
|
|
|
rule.ChangelingMinds.Add(mindId);
|
2024-01-31 14:01:35 +00:00
|
|
|
|
|
|
|
|
_roleSystem.MindAddRole(mindId, new ChangelingRoleComponent
|
|
|
|
|
{
|
2024-03-27 19:30:19 +07:00
|
|
|
PrototypeId = rule.ChangelingPrototypeId
|
2024-01-31 14:01:35 +00:00
|
|
|
}, mind);
|
|
|
|
|
|
|
|
|
|
// Change the faction
|
2024-03-27 19:30:19 +07:00
|
|
|
_npcFaction.RemoveFaction(changeling, "NanoTrasen", false);
|
2024-06-08 13:03:20 +00:00
|
|
|
_npcFaction.AddFaction(changeling, "Changeling");
|
2024-01-31 14:01:35 +00:00
|
|
|
|
2024-03-27 19:30:19 +07:00
|
|
|
EnsureComp<ChangelingComponent>(changeling, out var readyChangeling);
|
2024-01-31 14:01:35 +00:00
|
|
|
|
|
|
|
|
readyChangeling.HiveName = _nameGenerator.GetName();
|
2024-03-27 19:30:19 +07:00
|
|
|
Dirty(changeling, readyChangeling);
|
|
|
|
|
|
2024-04-22 00:07:27 +09:00
|
|
|
RaiseLocalEvent(changeling, new MoodEffectEvent("TraitorFocused"));
|
2024-01-31 14:01:35 +00:00
|
|
|
|
2024-02-16 01:07:59 +07:00
|
|
|
if (!giveObjectives)
|
|
|
|
|
return true;
|
2024-01-31 14:01:35 +00:00
|
|
|
|
2024-02-16 01:07:59 +07:00
|
|
|
var difficulty = 0f;
|
|
|
|
|
for (var pick = 0; pick < ChangelingMaxPicks && ChangelingMaxDifficulty > difficulty; pick++)
|
2024-01-31 14:01:35 +00:00
|
|
|
{
|
2024-02-16 01:07:59 +07:00
|
|
|
var objective = _objectives.GetRandomObjective(mindId, mind, "ChangelingObjectiveGroups");
|
|
|
|
|
if (objective == null)
|
|
|
|
|
continue;
|
2024-01-31 14:01:35 +00:00
|
|
|
|
2024-02-16 01:07:59 +07:00
|
|
|
_mindSystem.AddObjective(mindId, mind, objective.Value);
|
2024-03-27 19:30:19 +07:00
|
|
|
var adding = Comp<ObjectiveComponent>(objective.Value).Difficulty;
|
|
|
|
|
difficulty += adding;
|
|
|
|
|
Log.Debug($"Added objective {ToPrettyString(objective):objective} with {adding} difficulty");
|
2024-01-31 14:01:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2024-04-22 00:07:27 +09:00
|
|
|
}
|