Files
RickGrymes 443dc7e11c Add terminals, holotapes, radiant quests & traders
Introduce terminal/holotape features, hacking jobs and UI plus extensive radiant quest content and faction/trader support. Adds many new C# systems (holotape storage/comp, terminal hacking, job drivers, quest nodes/parts, gensteps, settlement spawn control, pocket map components) and XML defs (quest scripts for CaravanRescue/ScavengerSupplyRun, mapgen prefab, faction/trader defs for currency exchange, site and research defs). Updates: JobDefs to include holotape/hack jobs, PawnKind apparel season handling and faction requirement metadata, ThingDefs and map/site defs, language word lists, map icons and textures, and various patches (workgiver, mapgen, currency). Removes obsolete VNPE compatibility patch, legacy pawn-rescue-animal script and a few unused defs/assets.
2026-05-31 10:34:25 -05:00

261 lines
8.3 KiB
C#

namespace FCP.Core.Buildings;
public class CompTerminalHacking : ThingComp
{
private static readonly Dictionary<int, List<string>> wordPools = new Dictionary<int, List<string>>();
private bool isLocked = true;
private int lockoutTicksRemaining;
private string correctPassword;
private List<string> displayedWords;
private int attemptsRemaining;
public CompProperties_TerminalHacking Props => (CompProperties_TerminalHacking)props;
public bool IsLocked => isLocked;
public bool IsLockedOut => lockoutTicksRemaining > 0;
public int LockoutHoursRemaining => lockoutTicksRemaining / 2500;
public int AttemptsRemaining => attemptsRemaining;
public string CorrectPassword => correctPassword;
public List<string> DisplayedWords => displayedWords;
public override void PostSpawnSetup(bool respawningAfterLoad)
{
base.PostSpawnSetup(respawningAfterLoad);
if (!respawningAfterLoad && displayedWords == null)
{
GeneratePasswords();
attemptsRemaining = Props.maxAttempts;
}
}
public override void CompTick()
{
base.CompTick();
if (parent.IsHashIntervalTick(60) && lockoutTicksRemaining > 0)
{
lockoutTicksRemaining--;
}
}
public void GeneratePasswords()
{
displayedWords = new List<string>(Props.wordCount);
var usedWords = new HashSet<string>();
correctPassword = GenerateRandomWord(Props.wordLength, usedWords);
displayedWords.Add(correctPassword);
usedWords.Add(correctPassword);
int[] desiredLikeness = new int[Props.wordCount - 1];
for (int i = 0; i < desiredLikeness.Length; i++)
{
desiredLikeness[i] = Rand.Range(0, Props.wordLength);
}
int attempts = 0;
int maxAttempts = Props.wordCount * 50;
while (displayedWords.Count < Props.wordCount && attempts < maxAttempts)
{
attempts++;
string candidate = GenerateRandomWord(Props.wordLength, usedWords);
if (usedWords.Contains(candidate))
continue;
int likeness = CalculateLikeness(candidate, correctPassword);
int targetIndex = displayedWords.Count - 1;
if (targetIndex < desiredLikeness.Length &&
(likeness == desiredLikeness[targetIndex] || attempts > maxAttempts / 2))
{
displayedWords.Add(candidate);
usedWords.Add(candidate);
}
}
while (displayedWords.Count < Props.wordCount)
{
string word = GenerateRandomWord(Props.wordLength, usedWords);
if (!usedWords.Contains(word))
{
displayedWords.Add(word);
usedWords.Add(word);
}
}
displayedWords.Shuffle();
}
private static List<string> GetWordPool(int length)
{
if (wordPools.TryGetValue(length, out List<string> pool))
return pool;
if (Translator.TryGetTranslatedStringsForFile($"Words/HackingMinigame/words_{length}", out List<string> strings))
{
pool = strings
.Select(word => word.Trim().ToUpperInvariant())
.Where(word => word.Length == length)
.Distinct()
.ToList();
}
else
{
pool = [];
FCPLog.Error($"Failed to get a word pool for length {length} at Words/HackingMinigame/words_{length}");
}
wordPools[length] = pool;
return pool;
}
private string GenerateRandomWord(int length, HashSet<string> usedWords)
{
List<string> pool = GetWordPool(length);
if (pool.Count > 0)
{
var unusedWords = pool.Where(word => !usedWords.Contains(word)).ToList();
if (unusedWords.Count > 0)
return unusedWords.RandomElement();
}
return GenerateRandomLetters(length, usedWords);
}
private string GenerateRandomLetters(int length, HashSet<string> usedWords)
{
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
string word;
do
{
char[] result = new char[length];
for (int i = 0; i < length; i++)
{
result[i] = chars[Rand.Range(0, chars.Length)];
}
word = new string(result);
}
while (usedWords.Contains(word));
return word;
}
private int CalculateLikeness(string word1, string word2)
{
int matches = 0;
int minLength = word1.Length < word2.Length ? word1.Length : word2.Length;
for (int i = 0; i < minLength; i++)
{
if (word1[i] == word2[i])
matches++;
}
return matches;
}
public int CheckLikeness(string guess)
{
int matches = 0;
int minLength = guess.Length < correctPassword.Length ? guess.Length : correctPassword.Length;
for (int i = 0; i < minLength; i++)
{
if (guess[i] == correctPassword[i])
matches++;
}
return matches;
}
public bool TryHack(string guess)
{
if (guess == correctPassword)
{
isLocked = false;
GiveReward();
return true;
}
attemptsRemaining--;
if (attemptsRemaining <= 0)
{
ApplyLockout();
}
return false;
}
public void RemoveDud(string word)
{
int index = displayedWords.IndexOf(word);
if (index >= 0 && word != correctPassword)
{
displayedWords[index] = new string('.', word.Length);
}
}
public void RestoreAttempt()
{
attemptsRemaining = Props.maxAttempts;
}
private void ApplyLockout()
{
lockoutTicksRemaining = Props.lockoutDurationHours * 2500;
GeneratePasswords();
attemptsRemaining = Props.maxAttempts;
}
private void GiveReward()
{
var terminal = parent as Building_Terminal;
IntVec3 dropPos = terminal?.InteractionCell ?? parent.Position;
var tracker = Holotapes.HolotapeTracker.Instance;
var undiscovered = tracker?.GetUndiscoveredHolotapes();
if (undiscovered != null && undiscovered.Count > 0)
{
var holotapeDef = undiscovered.RandomElement();
var thingDef = DefDatabase<ThingDef>.GetNamedSilentFail(holotapeDef.defName);
if (thingDef != null)
{
Thing holotape = ThingMaker.MakeThing(thingDef);
tracker.MarkAsDiscovered(holotapeDef);
var storage = terminal?.GetComp<CompHolotapeStorage>();
if (storage != null)
{
storage.TryStoreHolotape(holotape);
Messages.Message("FCP_TerminalHackSuccess_Holotape".Translate(holotape.Label),
terminal, MessageTypeDefOf.PositiveEvent);
}
else
{
GenPlace.TryPlaceThing(holotape, dropPos, parent.Map, ThingPlaceMode.Near);
Messages.Message("FCP_TerminalHackSuccess".Translate(holotape.Label),
holotape, MessageTypeDefOf.PositiveEvent);
}
return;
}
}
if (Props.rewardPool == null || Props.rewardPool.Count == 0)
return;
ThingDef rewardDef = Props.rewardPool.RandomElement();
Thing reward = ThingMaker.MakeThing(rewardDef);
GenPlace.TryPlaceThing(reward, dropPos, parent.Map, ThingPlaceMode.Near);
Messages.Message("FCP_TerminalHackSuccess".Translate(reward.Label), reward, MessageTypeDefOf.PositiveEvent);
}
public override void PostExposeData()
{
base.PostExposeData();
Scribe_Values.Look(ref isLocked, "isLocked", true);
Scribe_Values.Look(ref lockoutTicksRemaining, "lockoutTicksRemaining", 0);
Scribe_Values.Look(ref correctPassword, "correctPassword");
Scribe_Values.Look(ref attemptsRemaining, "attemptsRemaining", Props.maxAttempts);
Scribe_Collections.Look(ref displayedWords, "displayedWords", LookMode.Value);
}
}