diff --git a/1.6/Assemblies/FactionLoadout.dll b/1.6/Assemblies/FactionLoadout.dll index 4b6fd84..4e79f5d 100644 Binary files a/1.6/Assemblies/FactionLoadout.dll and b/1.6/Assemblies/FactionLoadout.dll differ diff --git a/1.6/Source/Dialog_XenotypeEdit.cs b/1.6/Source/Dialog_XenotypeEdit.cs index 879f30a..2f657ef 100644 --- a/1.6/Source/Dialog_XenotypeEdit.cs +++ b/1.6/Source/Dialog_XenotypeEdit.cs @@ -38,11 +38,11 @@ public override void DoWindowContents(Rect inRect) if (_edit.xenotypeChances.NullOrEmpty()) { _edit.xenotypeChances = _edit.Faction?.Def?.xenotypeSet?.xenotypeChances?.ToDictionary(x => x.xenotype.defName, x => x.chance) ?? new Dictionary(); - if (!_edit.xenotypeChances.ContainsKey(FactionEditUI.BaselinerDefName)) - _edit.xenotypeChances.Add(FactionEditUI.BaselinerDefName, _edit.Faction?.Def?.xenotypeSet?.BaselinerChance ?? 1f); + if (!_edit.xenotypeChances.ContainsKey(FactionEdit.BaselinerDefName)) + _edit.xenotypeChances.Add(FactionEdit.BaselinerDefName, _edit.Faction?.Def?.xenotypeSet?.BaselinerChance ?? 1f); } - _edit.xenotypeChances[FactionEditUI.BaselinerDefName] = Math.Max(0f, 1f - _edit.xenotypeChances.Sum(x => x.Key == FactionEditUI.BaselinerDefName ? 0 : x.Value)); + _edit.xenotypeChances[FactionEdit.BaselinerDefName] = Math.Max(0f, 1f - _edit.xenotypeChances.Sum(x => x.Key == FactionEdit.BaselinerDefName ? 0 : x.Value)); // Reserve space for add buttons at bottom. const float addButtonsHeight = 70f; diff --git a/1.6/Source/FactionEdit.cs b/1.6/Source/FactionEdit.cs index e40339a..361486c 100644 --- a/1.6/Source/FactionEdit.cs +++ b/1.6/Source/FactionEdit.cs @@ -13,6 +13,9 @@ namespace FactionLoadout; [HotSwappable] public class FactionEdit : IExposable { + /// Xenotype defName used to represent the "Baseliner" chance in xenotype edits. + public const string BaselinerDefName = "Baseliner"; + private static readonly Dictionary originalFactionDefs = new(); private static Dictionary<(FactionDef, PawnKindDef), PawnKindDef> factionSpecificPawnKindReplacements = new(); public bool Active = true; diff --git a/1.6/Source/FactionEditUI.cs b/1.6/Source/FactionEditUI.cs deleted file mode 100644 index 6e34f6d..0000000 --- a/1.6/Source/FactionEditUI.cs +++ /dev/null @@ -1,616 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using FactionLoadout.Modules; -using FactionLoadout.Patches; -using FactionLoadout.UISupport; -using FactionLoadout.Util; -using RimWorld; -using RimWorld.Planet; -using UnityEngine; -using Verse; - -namespace FactionLoadout; - -[HotSwappable] -public class FactionEditUI : Window -{ - public static string BaselinerDefName = "Baseliner"; - - public readonly FactionEdit Current; - - private readonly List bin = []; - private FactionDef clonedFac; - private ThingFilterUI.UIState filterState = new(); - private int framesSinceF; - private readonly List pawns = []; - private readonly HashSet tempKinds = []; - private bool _ThingIDPatch = false; - private bool _previewFailed = false; - private Vector2 overridesScrollPos; - private float overridesContentHeight = 10000f; // measured after first frame; init large so nothing clips - - public FactionEditUI(FactionEdit fac) - { - Current = fac; - draggable = true; - resizeable = true; - doCloseX = true; - closeOnCancel = true; - closeOnClickedOutside = false; - } - - public static void OpenEditor(FactionEdit fac) - { - if (fac == null) - return; - - Find.WindowStack.Add(new FactionEditUI(fac)); - } - - public override void PostOpen() - { - base.PostOpen(); - Rect copy = windowRect; - copy.y = 110; - copy.x -= copy.width * 0.5f + 15; - copy.height = 800; - windowRect = copy; - } - - public override void PostClose() - { - base.PostClose(); - - DestroyPawns(); - clonedFac = null; - - Find.WindowStack.WindowOfType()?.Close(); - } - - private void DestroyPawns() - { - foreach (Pawn pawn in pawns) - { - if (pawn == null) - continue; - - if (Find.WorldPawns?.Contains(pawn) == true) - { - Find.WorldPawns.RemoveAndDiscardPawnViaGC(pawn); - } - else if (!pawn.Discarded) - { - Find.WorldPawns?.PassToWorld(pawn, PawnDiscardDecideMode.Discard); - } - } - - pawns.Clear(); - } - - public override void DoWindowContents(Rect inRect) - { - framesSinceF++; - - if (Current == null || Current.DeletedOrClosed) - { - Close(); - return; - } - - Listing_Standard ui = new(); - ui.Begin(inRect); - - // --- Header (always visible) --- - Rect r = ui.GetRect(50); - Widgets.Label(r, $"Faction: {Current.Faction.Def?.LabelCap ?? "None".Translate()}"); - if (Current.Faction.IsMissing) - { - ui.Label($"{"FactionLoadout_FactionMissingEditWarning".Translate()}"); - } - - if (Current.Faction.DefName == Preset.SpecialCreepjoinerFactionDefName) - ui.Label($"{"FactionLoadout_FactionEdit_ExperimentalCreepjoiner".Translate()}"); - if (Current.Faction.DefName == Preset.SpecialWildManFactionDefName) - ui.Label($"{"FactionLoadout_FactionEdit_ExperimentalWildMan".Translate()}"); - if (Current.Faction.DefName == Preset.SpecialFactionlessPawnsFactionDefName) - ui.Label($"{"FactionLoadout_Special_FactionlessWarning".Translate()}"); - - // Disabled for now - // DrawMaterialFilter(ui); - - if (!Current.Faction.IsMissing) - { - DrawFactionClipboardToolbar(ui); - } - - // --- Scrollable overrides --- - // Outer container fills available space (no content dependency = no feedback loop). - // Inner rect uses previous frame's measurement + buffer so content is never clipped. - const float minFooterHeight = 200f; - float scrollOutH = Mathf.Max(60f, inRect.height - ui.CurHeight - minFooterHeight); - Rect scrollOutRect = ui.GetRect(scrollOutH); - float innerH = Mathf.Max(overridesContentHeight + 100f, scrollOutH); - Rect scrollViewRect = new(0f, 0f, scrollOutRect.width - 16f, innerH); - - Widgets.BeginScrollView(scrollOutRect, ref overridesScrollPos, scrollViewRect); - Listing_Standard inner = new(); - inner.Begin(scrollViewRect); - - if ( - inner.ButtonTextLabeled( - "FactionLoadout_Faction_Techlevel".Translate(), - Current.TechLevel?.ToStringHuman() ?? "FactionLoadout_NotOverriden_WithDefault".Translate((Current.Faction?.Def?.techLevel ?? TechLevel.Undefined).ToStringHuman()) - ) - ) - { - IEnumerable enums = Enum.GetValues(typeof(TechLevel)).Cast().Append(null); - FloatMenuUtility.MakeMenu( - enums, - e => e?.ToStringHuman() ?? "FactionLoadout_NotOverriden_WithDefault".Translate((Current.Faction?.Def?.techLevel ?? TechLevel.Undefined).ToStringHuman()), - e => - () => - { - Current.TechLevel = e; - } - ); - } - - if ( - ModsConfig.BiotechActive - && Current.Faction is { IsMissing: false } - && Current.Faction?.Def != Preset.SpecialWildManFaction - && Current.Faction?.Def != Preset.SpecialFactionlessPawnsFaction - ) - { - if (!Current.OverrideFactionXenotypes) - { - Current.xenotypeChances.Clear(); - Current.xenotypeChancesByDef.Clear(); - } - - inner.GapLine(); - string xenoState = Current.OverrideFactionXenotypes - ? "FactionLoadout_Xenotype_ActiveCount".Translate(Current.xenotypeChances.Count) - : "FactionLoadout_Xenotype_Off".Translate(); - if (inner.ButtonTextLabeled("FactionLoadout_EditXenoSpawnRates".Translate(), xenoState)) - Find.WindowStack.Add(new Dialog_XenotypeEdit(Current)); - } - - // --- Spawn Groups section --- - if (Current.Faction is not { IsMissing: true }) - { - inner.GapLine(); - - // Summary row: "Spawn Groups: N groups [Edit Spawn Groups]" - Rect groupsRow = inner.GetRect(28f); - float editBtnW = 160f; - Rect editGroupsBtn = new(groupsRow.xMax - editBtnW, groupsRow.y, editBtnW, 24f); - - Text.Anchor = TextAnchor.MiddleLeft; - string groupsSummary; - int groupCount; - if (Current.PawnGroupMakerEdits != null) - { - groupCount = Current.PawnGroupMakerEdits.Count; - groupsSummary = "FactionLoadout_SpawnGroups_SummaryModified".Translate(groupCount, "FactionLoadout_GroupEditor_NewTag".Translate().ToString().ToLower()); - } - else - { - groupCount = Current?.Faction?.Def?.pawnGroupMakers?.Count ?? 0; - groupsSummary = "FactionLoadout_SpawnGroups_Summary".Translate(groupCount); - } - - Rect summaryLabelRect = new(groupsRow.x, groupsRow.y, groupsRow.width - editBtnW - 4f, groupsRow.height); - GUI.color = Color.grey; - Widgets.Label(summaryLabelRect, "FactionLoadout_SpawnGroups_Label".Translate() + " " + groupsSummary); - GUI.color = Color.white; - Text.Anchor = TextAnchor.UpperLeft; - - if (Widgets.ButtonText(editGroupsBtn, "FactionLoadout_SpawnGroups_EditButton".Translate())) - GroupEditorUI.OpenEditor(Current); - - // Orphan warning (conditional) — lists which pawnkinds are missing from groups - HashSet orphaned = Current?.GetOrphanedKinds() ?? []; - if (orphaned.Count > 0) - { - string names = orphaned.Select(k => k.LabelCap.ToString()).OrderBy(n => n).ToCommaList(); - string warnText = "FactionLoadout_SpawnGroups_OrphanWarning".Translate(names); - float warnH = Text.CalcHeight(warnText, inner.ColumnWidth); - Rect warnRow = inner.GetRect(warnH); - GUI.color = new Color(1f, 0.6f, 0.1f); - Widgets.Label(warnRow, warnText); - GUI.color = Color.white; - } - } - - // Give each active module a chance to add faction-level UI (e.g. a button row). - foreach (ITotalControlModule module in ModuleRegistry.Modules) - { - if (!module.IsActive) - continue; - try - { - module.AddFactionUI(Current, inner); - } - catch (Exception e) - { - ModCore.Error($"Error drawing faction UI for module '{module.ModuleName}'", e); - } - } - - inner.GapLine(); - inner.Label($"{"FactionLoadout_FactionEdit_LoadoutOverrides".Translate()}"); - inner.Gap(); - - HashSet orphanedKinds = Current?.GetOrphanedKinds() ?? []; - foreach (PawnKindEdit edit in Current.KindEdits) - { - Rect rect = inner.GetRect(30); - string delText = "Delete".Translate(); - float delW = Mathf.Max(38, Text.CalcSize(delText).x + 10); - GUI.color = Color.red; - if (Widgets.ButtonText(new Rect(rect.x, rect.y, delW, 24), delText)) - { - bin.Add(edit); - edit.DeletedOrClosed = true; - } - - GUI.color = Color.white; - rect.x += delW + 4; - string editText = "FactionLoadout_Edit".Translate().CapitalizeFirst(); - float editW = Mathf.Max(50, Text.CalcSize(editText).x + 10); - if (Widgets.ButtonText(new Rect(rect.x, rect.y, editW, 24), editText)) - Find.WindowStack.Add(new PawnKindEditUI(edit)); - - rect.x += editW + 4; - if (Widgets.ButtonImageFitted(new Rect(rect.x, rect.y, 24, 24), TexButton.Copy)) - PawnKindClipboard.Copy(edit); - TooltipHandler.TipRegion(new Rect(rect.x, rect.y, 24, 24), "FactionLoadout_Clipboard_CopyTooltip".Translate()); - - rect.x += 28; - if (PawnKindClipboard.HasData) - { - if (Widgets.ButtonImageFitted(new Rect(rect.x, rect.y, 24, 24), TexButton.Paste)) - PawnKindClipboard.PasteAll(edit); - TooltipHandler.TipRegion(new Rect(rect.x, rect.y, 24, 24), "FactionLoadout_Clipboard_PasteAllTooltip".Translate(PawnKindClipboard.GetDescription())); - } - else - { - GUI.color = Color.gray; - Widgets.DrawTextureFitted(new Rect(rect.x, rect.y, 24, 24), TexButton.Paste, 1f); - GUI.color = Color.white; - TooltipHandler.TipRegion(new Rect(rect.x, rect.y, 24, 24), "FactionLoadout_Clipboard_Empty".Translate()); - } - - rect.x += 28; - - // Orphan warning icon - bool isOrphaned = !edit.IsGlobal && edit.Def != null && orphanedKinds.Contains(edit.Def); - if (isOrphaned) - { - GUI.color = Color.yellow; - Widgets.Label(new Rect(rect.x, rect.y, 20f, 24f), "⚠"); - GUI.color = Color.white; - TooltipHandler.TipRegion(new Rect(rect.x, rect.y, 20f, 24f), "FactionLoadout_SpawnGroups_OrphanKindTooltip".Translate()); - rect.x += 22f; - } - - Widgets.Label(rect, $"{(edit.IsGlobal ? $"{"FactionLoadout_GlobalLabel".Translate()}" : edit.Def.LabelCap.ToString())}"); - } - - foreach (PawnKindEdit item in bin) - Current.KindEdits.Remove(item); - bin.Clear(); - - if (!Current.Faction.IsMissing && inner.ButtonText("Add".Translate().CapitalizeFirst() + "...")) - { - IEnumerable MakeKinds() - { - tempKinds.Clear(); - if (!Current.HasGlobalEditor()) - tempKinds.Add(null); - - // Collect all pawnkinds from group edits (if active) or the - // live faction def. When PawnGroupMakerEdits is null, - // GetAllKindDefsForUI delegates to GetAllPawnKinds which already - // includes basicMemberKind and fixedLeaderKinds. - foreach (PawnKindDef kind in Current.GetAllKindDefsForUI()) - { - if (!Current.HasEditFor(kind)) - tempKinds.Add(kind); - } - - // basicMemberKind and fixedLeaderKinds are NOT included in the - // PawnGroupMaker-based path of GetAllKindDefsForUI (those fields - // are not part of any group maker entry). When PawnGroupMakerEdits - // is null the GetAllPawnKinds path already covers them, so only - // add them here when group edits are active to avoid duplicates. - if (Current.PawnGroupMakerEdits != null) - { - if (Current.Faction.Def?.basicMemberKind != null && !Current.HasEditFor(Current.Faction.Def.basicMemberKind)) - tempKinds.Add(Current.Faction.Def.basicMemberKind); - if (Current.Faction.Def?.fixedLeaderKinds != null) - { - foreach (PawnKindDef item in Current.Faction.Def.fixedLeaderKinds) - { - if (!Current.HasEditFor(item)) - tempKinds.Add(item); - } - } - } - - foreach (PawnKindDef item in tempKinds) - yield return item; - - if (tempKinds.Count(k => k != null) == 0) - { - if (Current.Faction.Def == FactionDefOf.Ancients || Current.Faction.Def == FactionDefOf.AncientsHostile) - { - yield return PawnKindDefOf.AncientSoldier; - yield return PawnKindDefOf.Slave; - } - } - - tempKinds.Clear(); - } - - List kinds = MakeKinds().ToList(); - List items = CustomFloatMenu.MakeItems( - kinds, - k => - k != null - ? new MenuItemText(k, $"{k.LabelCap} ({k.defName})", tooltip: k.description) - : new MenuItemText(null, $"{"FactionLoadout_GlobalLabel".Translate()}") - ); - CustomFloatMenu.Open( - items, - raw => - { - PawnKindDef k = raw.GetPayload(); - if (k != null) - { - Current.KindEdits.Add(new PawnKindEdit(k)); - } - else - { - PawnKindDef kind = kinds.FirstOrDefault(pawnKindDef => pawnKindDef != null); - ModCore.Log($"Using {kind} as global base."); - if (kind != null) - Current.KindEdits.Insert(0, new PawnKindEdit(kind) { IsGlobal = true }); - } - } - ); - } - - overridesContentHeight = inner.CurHeight; - inner.End(); - Widgets.EndScrollView(); - - // --- Footer (always visible) --- - ui.GapLine(26); - - if (Prefs.DevMode && clonedFac != null && ui.ButtonText("FactionLoadout_FactionEdit_DebugClonedKinds".Translate())) - foreach (PawnKindDef kind in clonedFac.GetKindDefs()) - { - ModCore.Log($"Kind: {kind.label} ({kind.defName})"); - ModCore.Log($" - Apparel Money: {kind.apparelMoney}"); - if (kind.apparelRequired == null) - continue; - ModCore.Log(" - Apparel required:"); - foreach (ThingDef item in kind.apparelRequired) - ModCore.Log($" * {item?.LabelCap ?? ""}"); - } - - bool isInGame = Verse.Current.Game != null; - - if (!isInGame) - { - ui.Label($"{"FactionLoadout_FactionEdit_PreviewError".Translate()}"); - } - else - { - ui.CheckboxLabeled("FactionLoadout_FactionEdit_ThingIDPatch".Translate(), ref _ThingIDPatch, "FactionLoadout_FactionEdit_ThingIDPatchTooltip".Translate()); - ui.Gap(20); - Rect total = ui.GetRect(inRect.height - ui.CurHeight - 32); - int count = pawns.Count; - - if (count != 0) - { - const float labelH = 26f; - float maxIconH = Mathf.Max(total.height - labelH - 10f, 50f); - float w = Mathf.Min(total.width / count, maxIconH); - for (int i = 0; i < count; i++) - { - Rect pawnArea = new(total.x + i * w, total.y, w, w); - Pawn pawn = pawns[i]; - - if (pawn != null) - Widgets.ThingIcon(pawnArea, pawn); - else - Widgets.DrawTextureFitted(pawnArea, Widgets.CheckboxOffTex, 1f); - - Widgets.DrawHighlightIfMouseover(pawnArea); - TooltipHandler.TipRegion(pawnArea, pawn?.KindLabel?.CapitalizeFirst() ?? ""); - if (Mouse.IsOver(pawnArea) && pawn != null) - { - Pawn p = pawns[i]; - Rect window = windowRect; - window.y += 510; - window.x -= 465 - 40; - window.height = 550; - window.width = 410; - Find.WindowStack.ImmediateWindow( - 90812358, - window, - WindowLayer.Super, - () => - { - var list = - typeof(Selector).GetField("selected", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(Find.Selector) as List - ?? new List(); - list.Clear(); - list.Add(p); - typeof(ITab_Pawn_Gear).GetMethod("FillTab", BindingFlags.Instance | BindingFlags.NonPublic)?.Invoke(new ITab_Pawn_Gear(), []); - list.Clear(); - } - ); - } - - pawnArea.height = 200; - pawnArea.y += w + 10; - if (pawnArea.width >= 50) - Widgets.Label(pawnArea, pawns[i]?.KindLabel.CapitalizeFirst() ?? ""); - } - } - } - - GUI.enabled = isInGame; - bool f = Input.GetKeyDown(KeyCode.F); - if ((ui.ButtonText("FactionLoadout_FactionEdit_RegeneratePreviews".Translate()) || (pawns.Count == 0 && !_previewFailed) || (f && framesSinceF > 20)) && isInGame) - { - if (f) - framesSinceF = 0; - _previewFailed = false; - - FactionDef toClone = FactionEdit.TryGetOriginal(Current.Faction.Def.defName) ?? Current.Faction.Def; - clonedFac = CloningUtility.Clone(toClone); - clonedFac.defName = Current.Faction.Def.defName; - clonedFac.humanlikeFaction = Current.Faction.Def.humanlikeFaction; - clonedFac.fixedName = $"TEMP FACTION CLONE ({clonedFac.defName})"; - - Current.Apply(clonedFac, false); - DestroyPawns(); - - Faction faction = new() - { - def = clonedFac, - loadID = -1, - colorFromSpectrum = Rand.Range(0f, 1f), - hidden = true, - ideos = Find.FactionManager?.FirstFactionOfDef(Current.Faction.Def)?.ideos, - Name = clonedFac.fixedName, - relations = Find - .FactionManager.AllFactionsVisible.Select(otherFaction => new FactionRelation - { - other = otherFaction, - baseGoodwill = 0, - kind = FactionRelationKind.Neutral, - }) - .ToList(), - temporary = true, - deactivated = true, - }; - - ThingIDPatch.Active = _ThingIDPatch; - IdeoUtilityPatch.Active = true; - FactionUtilityPawnGenPatch.Active = true; - - foreach (PawnKindDef item in FactionEdit.GetAllPawnKinds(clonedFac)) - try - { - Pawn pawn = PawnGenerator.GeneratePawn( - new PawnGenerationRequest(item, faction) - { - ForceGenerateNewPawn = true, - AllowDowned = false, - AllowDead = false, - CanGeneratePawnRelations = false, - RelationWithExtraPawnChanceFactor = 0, - ColonistRelationChanceFactor = 0, - ForceNoIdeo = true, - ForbidAnyTitle = true, - } - ); - pawns.Add(pawn); - } - catch (Exception e) - { - ModCore.Error($"Failed to generate pawn of type '{item.LabelCap}':", e); - pawns.Add(null); - } - - Find.FactionManager.Remove(faction); - - ThingIDPatch.Active = false; - FactionLeaderPatch.Active = false; - FactionUtilityPawnGenPatch.Active = false; - IdeoUtilityPatch.Active = false; - } - - GUI.enabled = true; - ui.End(); - } - - private void DrawFactionClipboardToolbar(Listing_Standard ui) - { - Rect toolbar = ui.GetRect(28f); - float x = toolbar.x; - float y = toolbar.y; - const float btnSize = 24f; - const float gap = 4f; - - if (Widgets.ButtonImageFitted(new Rect(x, y, btnSize, btnSize), TexButton.Copy)) - FactionEditClipboard.Copy(Current); - TooltipHandler.TipRegion(new Rect(x, y, btnSize, btnSize), "FactionLoadout_FactionClipboard_CopyTooltip".Translate()); - - x += btnSize + gap; - if (FactionEditClipboard.HasData) - { - if (Widgets.ButtonImageFitted(new Rect(x, y, btnSize, btnSize), TexButton.Paste)) - FactionEditClipboard.PasteAll(Current); - TooltipHandler.TipRegion(new Rect(x, y, btnSize, btnSize), "FactionLoadout_FactionClipboard_PasteTooltip".Translate(FactionEditClipboard.GetDescription())); - } - else - { - GUI.color = Color.gray; - Widgets.DrawTextureFitted(new Rect(x, y, btnSize, btnSize), TexButton.Paste, 1f); - GUI.color = Color.white; - TooltipHandler.TipRegion(new Rect(x, y, btnSize, btnSize), "FactionLoadout_Clipboard_Empty".Translate()); - } - } - - private void DrawMaterialFilter(Listing_Standard ui) - { - Rect matRect = ui.GetRect(28); - matRect.width = 300; - if ( - Widgets.ButtonText( - matRect, - $"{"FactionLoadout_FactionEdit_CustomMaterials".Translate()}{(Current.ApparelStuffFilter == null ? $"{"No".Translate()}" : $"{"Yes".Translate()}")}" - ) - ) - { - filterState = new ThingFilterUI.UIState(); - - if (Current.ApparelStuffFilter != null) - { - Current.ApparelStuffFilter = null; - } - else - { - Current.ApparelStuffFilter = new ThingFilter(); - if (Current.Faction.Def.apparelStuffFilter != null) - Current.ApparelStuffFilter.CopyAllowancesFrom(Current.Faction.Def.apparelStuffFilter); - } - } - - if (Current.ApparelStuffFilter == null) - return; - Rect filter = ui.GetRect(240); - ThingFilterUI.DoThingFilterConfigWindow( - filter, - filterState, - Current.ApparelStuffFilter, - forceHideHitPointsConfig: true, - forceHiddenFilters: - [ - SpecialThingFilterDefOf.AllowDeadmansApparel, - SpecialThingFilterDefOf.AllowNonDeadmansApparel, - SpecialThingFilterDefOf.AllowFresh, - DefDatabase.GetNamed("AllowRotten"), - ] - ); - } -} diff --git a/1.6/Source/ModCore.cs b/1.6/Source/ModCore.cs index d3b5546..37dfcea 100644 --- a/1.6/Source/ModCore.cs +++ b/1.6/Source/ModCore.cs @@ -12,9 +12,7 @@ namespace FactionLoadout; public class ModCore : Mod { - public Dialog_FactionLoadout settingsDialog = null; public static MySettings Settings; - public Dialog_FactionLoadout SettingsDialog => settingsDialog ??= new Dialog_FactionLoadout(); public static void Debug(string msg) { @@ -54,7 +52,17 @@ public override string SettingsCategory() public override void DoSettingsWindowContents(Rect inRect) { - SettingsDialog.DoWindowContents(inRect); + Listing_Standard ui = new(); + ui.Begin(inRect); + ui.Label("FactionLoadout_Settings_LauncherBlurb".Translate()); + ui.Gap(); + if (ui.ButtonText("FactionLoadout_Open".Translate())) + { + Find.WindowStack.Add(new Dialog_TotalControl()); + Find.WindowStack.WindowOfType()?.Close(); + Find.WindowStack.WindowOfType()?.Close(); + } + ui.End(); } private void LoadLate() diff --git a/1.6/Source/Patches/OptionListingUtility_Patch.cs b/1.6/Source/Patches/OptionListingUtility_Patch.cs index 0c8358f..9d7ec1e 100644 --- a/1.6/Source/Patches/OptionListingUtility_Patch.cs +++ b/1.6/Source/Patches/OptionListingUtility_Patch.cs @@ -19,7 +19,7 @@ public static void DrawOptionListing_Patch(ref List optList) "FactionLoadout_SettingName".Translate(), delegate { - Find.WindowStack.Add(new Dialog_FactionLoadout()); + Find.WindowStack.Add(new Dialog_TotalControl()); }, Textures.TC_Link ) diff --git a/1.6/Source/PawnKindEditUI.cs b/1.6/Source/PawnKindEditUI.cs deleted file mode 100644 index f493ab0..0000000 --- a/1.6/Source/PawnKindEditUI.cs +++ /dev/null @@ -1,166 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using FactionLoadout.Modules; -using FactionLoadout.UISupport; -using FactionLoadout.UISupport.DrawSupport; -using FactionLoadout.Util; -using RimWorld; -using UnityEngine; -using Verse; - -namespace FactionLoadout; - -[HotSwappable] -public class PawnKindEditUI : Window -{ - public readonly PawnKindEdit Current; - - private readonly Dictionary tabHeights = new(); - private Vector2 globalScroll; - private int selectedTab; - private List tabs; - - public PawnKindDef DefaultKind - { - get - { - if (Current.DeletedOrClosed) - return Current.Def; - - FactionDef found = FactionEdit.TryGetOriginal(Current.ParentEdit.Faction.DefName); - if (found == null) - return Current.Def; - PawnKindDef found2 = found.GetKindDefs().FirstOrDefault(k => k.defName == Current.Def.defName); - return found2 ?? Current.Def; - } - } - - public PawnKindEditUI(PawnKindEdit toEdit) - { - draggable = true; - resizeable = true; - doCloseX = true; - Current = toEdit; - - DefCache.ScanDefs(); - } - - public override void PostOpen() - { - base.PostOpen(); - windowRect = new Rect(UI.screenWidth * 0.5f, 30, UI.screenWidth * 0.5f - 20, UI.screenHeight - 50); - } - - public override void DoWindowContents(Rect inRect) - { - if (Current == null || Current.DeletedOrClosed) - { - Close(); - return; - } - - Text.Font = GameFont.Small; - - if (tabs == null) - BuildTabs(); - - if ((tabs?.Count ?? 0) == 0) - { - Widgets.Label(inRect, "FactionLoadout_NoEditableProperties".Translate()); - return; - } - - Rect titleArea = inRect; - titleArea.height = 40; - string title = - $"Pawn Type: {(Current.IsGlobal ? "FactionLoadout_GlobalLabel".Translate().ToString() : Current.Def.LabelCap.ToString())}"; - Widgets.Label(titleArea, title); - - Rect tabRect = inRect; - float tabRows = (float)Math.Ceiling(tabs.Count / 5f); - tabRect.height = tabRows * 50 + 50; - tabRect.y += 50; - - for (int i = 0; i < tabs.Count; i++) - { - float row = (float)Math.Floor(i / 5f); - if (row > 0 && i % 5 == 0) - { - tabRect.ExpandedBy(0, 50f); - tabRect.yMin += 50; - } - - Rect button = tabRect; - button.height = 40; - button.width = 140; - button.x += 150 * (i - 5 * row); - - Tab tab = tabs[i]; - Color bg = selectedTab == i ? new Color32(49, 82, 133, 255) : new Color(0.2f, 0.2f, 0.2f, 1f); - if (Widgets.CustomButtonText(ref button, $"{tab.Name}", bg, Color.white, Color.white)) - selectedTab = i; - - if (selectedTab != i) - continue; - - float toolbarY = inRect.y + 100 + 50 * (tabRows - 1); - ClipboardToolbar.Draw( - new Rect(inRect.x, toolbarY, inRect.width, 28), - Current, - () => - { - if (selectedTab >= 0 && selectedTab < tabs.Count && tabs[selectedTab] is EditTab et) - et.ResetBuffers(); - } - ); - - Rect contentArea = inRect; - contentArea.yMin += 100 + 50 * (tabRows - 1) + 32; - float tabContentH = tabHeights.TryGetValue(tab, out float storedH) ? Mathf.Max(storedH, contentArea.height) : contentArea.height; - Widgets.BeginScrollView(contentArea, ref globalScroll, new Rect(0, 0, inRect.width - 24, tabContentH)); - - Listing_Standard ui = new() { ColumnWidth = inRect.width - 24 }; - ui.Begin(new Rect(0, 0, inRect.width - 24, 1000000)); - - tab.Draw(ui); - - tabHeights[tab] = ui.CurHeight; - ui.End(); - Widgets.EndScrollView(); - } - } - - private void BuildTabs() - { - PawnKindDef dk = DefaultKind; - tabs = [new GeneralTab(Current, dk)]; - - bool isAnimal = dk.RaceProps.Animal; - if (!isAnimal) - { - tabs.AddRange([ - new BackstoryTab(Current, dk), - new AppearanceTab(Current, dk), - new ApparelTab(Current, dk), - new WeaponTab(Current, dk), - new ImplantsTab(Current, dk), - new InventoryTab(Current, dk), - new RaidPointsTab(Current, dk), - new RaidLootTab(Current, dk), - ]); - if (VFEAncientsReflectionModule.ModLoaded.Value) - tabs.Add(new AncientsTab(Current, dk)); - if (VEPsycastsReflectionModule.ModLoaded.Value) - tabs.Add(new PsycastsTab(Current, dk)); - if (ModsConfig.BiotechActive) - tabs.Add(new XenotypeTab(Current, dk)); - - foreach (ITotalControlModule module in ModuleRegistry.Modules) - { - if (module.IsActive) - module.AddTabs(Current, dk, tabs); - } - } - } -} diff --git a/1.6/Source/PresetUI.cs b/1.6/Source/PresetUI.cs deleted file mode 100644 index ee58fcd..0000000 --- a/1.6/Source/PresetUI.cs +++ /dev/null @@ -1,224 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using FactionLoadout.UISupport; -using RimWorld; -using UnityEngine; -using Verse; - -namespace FactionLoadout; - -public class PresetUI : Window -{ - public static void OpenEditor(Preset pre) - { - if (pre == null) - return; - - Find.WindowStack.Add(new PresetUI(pre)); - } - - public readonly Preset Current; - - private Vector2 scroll; - - public PresetUI(Preset pre) - { - Current = pre; - draggable = true; - resizeable = true; - doCloseX = false; - closeOnCancel = false; - closeOnCancel = false; - closeOnClickedOutside = false; - } - - public override void PostOpen() - { - base.PostOpen(); - - windowRect = new Rect(20, 110, Mathf.Max(UI.screenWidth * 0.5f - 550, 450), 1000); - } - - public override void PostClose() - { - base.PostClose(); - Find.WindowStack.WindowOfType()?.Close(); - } - - public override void DoWindowContents(Rect inRect) - { - if (Current == null) - { - Close(); - return; - } - - Listing_Standard ui = new(); - ui.Begin(inRect); - - Rect rect = ui.GetRect(50); - Widgets.Label(rect, $"Preset: {Current.Name}"); - - Rect buttonsRect = ui.GetRect(32); - - // Save button - Rect button = buttonsRect; - button.x = Mathf.Lerp(button.x, button.xMax, 0f); - button.width *= 0.3f; - button = button.ExpandedBy(-2f, -5f); - GUI.color = Color.green; - string saveLabel = Current.IsPackaged ? "FactionLoadout_SaveToSourceFile".Translate().ToString() : "Save".Translate().ToString().ToUpper(); - if (Widgets.ButtonText(button, $"{saveLabel}")) - Current.Save(); - - // Save & exit - button = buttonsRect; - button.x = Mathf.Lerp(button.x, button.xMax, 1f / 3f); - button.width *= 0.3f; - button = button.ExpandedBy(-2f, -5f); - if (Widgets.ButtonText(button, $"{"FactionLoadout_Preset_SaveAndExit".Translate()}")) - { - Current.Save(); - Close(); - } - - // Exit button. - GUI.color = Color.Lerp(Color.white, Color.red, 0.65f); - button = buttonsRect; - button.x = Mathf.Lerp(button.x, button.xMax, 2f / 3f); - button.width *= 0.3f; - button = button.ExpandedBy(-2f, -5f); - if (Widgets.ButtonText(button, $"{"Close".Translate().ToString().ToUpper()}")) - Close(); - - GUI.color = Color.white; - ui.GapLine(); - - if (Current.IsPackaged) - { - Rect warningRect = ui.GetRect(44); - Widgets.DrawBoxSolid(warningRect, new Color(0.45f, 0.35f, 0.05f, 0.85f)); - warningRect = warningRect.ContractedBy(6f); - Widgets.Label(warningRect, "FactionLoadout_PackagedPresetWarning".Translate(Current.PackagedModName).ToString()); - } - - // Missing faction handling. - if (Current.HasMissingFactions()) - { - ui.Label($"{"FactionLoadout_Preset_MissingWarning".Translate()}"); - ui.Label($"{"FactionLoadout_Preset_MissingHeader".Translate()}"); - ui.GapLine(); - foreach (string str in Current.GetMissingFactionAndModNames()) - ui.Label($" - {str}"); - } - - Rect nameArea = ui.GetRect(28); - nameArea.width = 200; - Widgets.Label(nameArea, "FactionLoadout_Preset_EditName".Translate()); - nameArea.x += 80; - nameArea.height -= 5; - Current.Name = Widgets.TextField(nameArea, Current.Name); - - ui.Label($"{"FactionLoadout_Preset_EditCount".Translate(Current.factionChanges.Count)}"); - ui.Gap(); - - float factionListHeight = Mathf.Max(100f, inRect.height - ui.CurHeight - 60f); - Widgets.BeginScrollView(ui.GetRect(factionListHeight), ref scroll, new Rect(0, 0, inRect.width - 20, Current.factionChanges.Count * (28 * 2 + 10))); - - Listing_Standard oldUI = ui; - ui = new Listing_Standard(); - ui.Begin(new Rect(0, 0, inRect.width - 20, 99999)); - - for (int i = 0; i < Current.factionChanges.Count; i++) - { - FactionEdit item = Current.factionChanges[i]; - Rect area = ui.GetRect(28); - Widgets.Label(area, $"{item.Faction.LabelCap} ({item.Faction.DefName})"); - - area = ui.GetRect(28); - area.width = 80; - area.y -= 5; - GUI.color = Color.red; - string deleteLabel = $"[{"Delete".Translate()}]"; - area.width = Mathf.Max(80, Text.CalcSize(deleteLabel).x + 10); - if (Widgets.ButtonText(area, deleteLabel)) - { - item.DeletedOrClosed = true; - Current.factionChanges.RemoveAt(i); - i--; - continue; - } - - GUI.color = Color.white; - - area.x += area.width + 10; - if (item.Faction.IsMissing) - { - area.width = 120; - GUI.color = new Color(1f, 0.75f, 0.2f); - if (Widgets.ButtonText(area, "FactionLoadout_EditAnyway".Translate())) - FactionEditUI.OpenEditor(item); - GUI.color = Color.white; - area.x += 130; - area.width = inRect.width - 20 - area.x; - GUI.color = new Color(1f, 0.4f, 0.4f); - Widgets.Label(area, "FactionLoadout_FactionMissing".Translate()); - GUI.color = Color.white; - } - else - { - string editLabel = "FactionLoadout_Edit".Translate().CapitalizeFirst(); - area.width = Mathf.Max(80, Text.CalcSize(editLabel).x + 10); - if (Widgets.ButtonText(area, editLabel)) - FactionEditUI.OpenEditor(item); - area.x += area.width + 10; - Widgets.CheckboxLabeled(area, "Enabled".Translate(), ref item.Active, placeCheckboxNearText: true); - } - - ui.GapLine(10); - } - - ui.End(); - Widgets.EndScrollView(); - ui = oldUI; - - ui.Gap(); - if (ui.ButtonText("FactionLoadout_Preset_AddFactionEdit".Translate())) - { - List raw = DefDatabase.AllDefsListForReading.Where(f => !Current.HasEditFor(f)).ToList(); - if (!Current.HasEditFor(Preset.SpecialCreepjoinerFaction) && !raw.Any(f => f.defName == Preset.SpecialCreepjoinerFaction.defName)) - { - raw.Add(Preset.SpecialCreepjoinerFaction); - } - if (!Current.HasEditFor(Preset.SpecialWildManFaction) && !raw.Any(f => f.defName == Preset.SpecialWildManFaction.defName)) - { - raw.Add(Preset.SpecialWildManFaction); - } - if ( - Preset.FactionlessPawnKindsSet.Count > 0 - && !Current.HasEditFor(Preset.SpecialFactionlessPawnsFaction) - && !raw.Any(f => f.defName == Preset.SpecialFactionlessPawnsFaction.defName) - ) - { - raw.Add(Preset.SpecialFactionlessPawnsFaction); - } - List items = CustomFloatMenu.MakeItems( - raw, - f => new MenuItemText(f, $"{f.LabelCap} ({f.defName})", DefUtils.TryGetIcon(f, out Color c), c, f.description) - ); - - CustomFloatMenu.Open( - items, - menuItemBase => - { - FactionDef e = menuItemBase.GetPayload(); - - FactionEdit edit = new() { Faction = e }; - Current.factionChanges.Add(edit); - } - ); - } - - ui.End(); - } -} diff --git a/1.6/Source/TCEditContext.cs b/1.6/Source/TCEditContext.cs new file mode 100644 index 0000000..7933af8 --- /dev/null +++ b/1.6/Source/TCEditContext.cs @@ -0,0 +1,17 @@ +namespace FactionLoadout; + +/// +/// Ambient "currently being edited" context for the fullscreen Total Control UI. +/// +/// Some draw helpers (notably SpecificGearDrawer) historically resolved the +/// active faction by looking for a FactionEditUI window on the WindowStack. The +/// fullscreen shell renders the equivalent screens without that window present, so it +/// publishes the faction being edited here instead. The old windows fall back to this +/// too, so both UIs work during the transition. +/// +/// Set when a faction is opened in the shell; cleared when the shell closes. +/// +public static class TCEditContext +{ + public static FactionEdit CurrentFaction; +} diff --git a/1.6/Source/Tabs/XenotypeTab.cs b/1.6/Source/Tabs/XenotypeTab.cs index ad196f3..db977f0 100644 --- a/1.6/Source/Tabs/XenotypeTab.cs +++ b/1.6/Source/Tabs/XenotypeTab.cs @@ -27,8 +27,8 @@ protected override void DrawContents(Listing_Standard ui) if (Current.ForcedXenotypeChances.NullOrEmpty()) { Current.ForcedXenotypeChances = Current.Def?.xenotypeSet?.xenotypeChances?.ToDictionary(x => x.xenotype.defName, x => x.chance) ?? new Dictionary(); - if (!Current.ForcedXenotypeChances.ContainsKey(FactionEditUI.BaselinerDefName)) - Current.ForcedXenotypeChances.Add(FactionEditUI.BaselinerDefName, Current.Def?.xenotypeSet?.BaselinerChance ?? 1f); + if (!Current.ForcedXenotypeChances.ContainsKey(FactionEdit.BaselinerDefName)) + Current.ForcedXenotypeChances.Add(FactionEdit.BaselinerDefName, Current.Def?.xenotypeSet?.BaselinerChance ?? 1f); } foreach (string key in Current.ForcedXenotypeChances.Keys.ToList()) diff --git a/1.6/Source/UISupport/Dialog_FactionLoadout.cs b/1.6/Source/UISupport/Dialog_FactionLoadout.cs deleted file mode 100644 index 969b1b3..0000000 --- a/1.6/Source/UISupport/Dialog_FactionLoadout.cs +++ /dev/null @@ -1,185 +0,0 @@ -using System; -using System.Collections.Generic; -using RimWorld; -using UnityEngine; -using Verse; - -namespace FactionLoadout.UISupport; - -public class Dialog_FactionLoadout : Window -{ - public override Vector2 InitialSize => new Vector2(800f, 600f); - public Vector2 scrollPosition = Vector2.zero; - - public Dialog_FactionLoadout() - { - doCloseButton = true; - closeOnAccept = true; - closeOnCancel = true; - doCloseX = true; - forcePause = true; - absorbInputAroundWindow = true; - } - - public override void DoWindowContents(Rect inRect) - { - int presetHeight = (Preset.LoadedPresets.Count + 1) * 30; - int restHeight = 300; // Adjust this value as needed - - float scrollViewHeight = presetHeight + restHeight; - - Rect viewRect = new Rect(0, 0, inRect.width - 20, scrollViewHeight); - Rect viewPortRect = new Rect(0, 30, inRect.width, inRect.height - 70); - scrollPosition = GUI.BeginScrollView(viewPortRect, scrollPosition, viewRect); - Listing_Standard ui = new Listing_Standard(); - - try - { - ui.Begin(viewRect); - - ui.Label("FactionLoadout_Settings_FactionPresetDesc".Translate()); - ui.GapLine(); - - ui.CheckboxLabeled( - "FactionLoadout_Settings_VanillaRestrictions".Translate(), - ref MySettings.VanillaRestrictions, - "FactionLoadout_Settings_VanillaRestrictionsDesc".Translate() - ); - ui.GapLine(); - ui.CheckboxLabeled("FactionLoadout_Settings_Verbose".Translate(), ref MySettings.VerboseLogging, "FactionLoadout_Settings_VerboseDesc".Translate()); - ui.CheckboxLabeled( - "FactionLoadout_Settings_PatchKindInRequests".Translate(), - ref MySettings.PatchKindInRequests, - "FactionLoadout_Settings_PatchKindInRequestsDesc".Translate() - ); - ui.GapLine(); - ui.Label("FactionLoadout_Settings_FactionPresetDesc".Translate()); - ui.GapLine(); - - bool deleteMode = Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift); - Preset toDelete = null; - - foreach (Preset preset in Preset.LoadedPresets) - { - Rect area = ui.GetRect(30); - area.width = 80; - - bool active = MySettings.ActivePreset == preset.GUID; - - GUI.color = active ? Color.green : Color.red; - bool currentActive = active; - Widgets.CheckboxLabeled(area, "FactionLoadout_Active".Translate().CapitalizeFirst(), ref active, placeCheckboxNearText: true); - if (currentActive != active) - { - MySettings.ActivePreset = active ? preset.GUID : null; - ModCore.Settings.Write(); - } - - GUI.color = Color.white; - area.x += 90; - - if (preset.IsPackaged) - { - GUI.color = new Color(1f, 0.75f, 0.2f); - if (Widgets.ButtonText(area, "FactionLoadout_PackagedLabel".Translate().CapitalizeFirst())) - { - Preset capturedPreset = preset; - List options = - [ - new FloatMenuOption( - "FactionLoadout_CopyToMyPresets".Translate(), - () => - { - try - { - Preset copy = Preset.CreateCopy(capturedPreset); - Preset.AddNewPreset(copy); - copy.Save(); - PresetUI.OpenEditor(copy); - Find.WindowStack.WindowOfType()?.Close(); - Find.WindowStack.WindowOfType()?.Close(); - } - catch (Exception ex) - { - ModCore.Error("Failed to copy packaged preset.", ex); - } - } - ), - new FloatMenuOption( - "FactionLoadout_EditSourceFile".Translate(), - () => - { - PresetUI.OpenEditor(capturedPreset); - Find.WindowStack.WindowOfType()?.Close(); - Find.WindowStack.WindowOfType()?.Close(); - } - ), - ]; - Find.WindowStack.Add(new FloatMenu(options)); - } - GUI.color = Color.white; - - area.x += 90; - area.width = 9999; - Widgets.Label(area, $"{preset.Name} ({preset.PackagedModName})"); - } - else - { - GUI.color = deleteMode ? Color.red : Color.white; - if (Widgets.ButtonText(area, deleteMode ? "Delete".Translate().CapitalizeFirst() : "FactionLoadout_Edit".Translate().CapitalizeFirst())) - { - if (!deleteMode) - { - PresetUI.OpenEditor(preset); - Find.WindowStack.WindowOfType()?.Close(); - Find.WindowStack.WindowOfType()?.Close(); - } - else - { - toDelete = preset; - } - } - - GUI.color = Color.white; - - area.x += 90; - area.width = 9999; - Widgets.Label(area, preset.Name); - } - } - - if (toDelete != null) - Preset.DeletePreset(toDelete); - - if (Preset.LoadedPresets.EnumerableNullOrEmpty()) - ui.Label("FactionLoadout_NothingHere".Translate()); - - ui.GapLine(); - if (ui.ButtonText("FactionLoadout_CreateNewPreset".Translate())) - { - Preset preset = new(); - Preset.AddNewPreset(preset); - preset.Save(); - - MySettings.ActivePreset = preset.GUID; - - PresetUI.OpenEditor(preset); - - Find.WindowStack.WindowOfType()?.Close(); - Find.WindowStack.WindowOfType()?.Close(); - } - } - finally - { - ui.End(); - GUI.EndScrollView(); - } - } - - public override void PostClose() - { - base.PostClose(); - Find.WindowStack.WindowOfType()?.Close(); - ModCore.Settings.Write(); - } -} diff --git a/1.6/Source/UISupport/Dialog_TotalControl.cs b/1.6/Source/UISupport/Dialog_TotalControl.cs new file mode 100644 index 0000000..e5d1ffb --- /dev/null +++ b/1.6/Source/UISupport/Dialog_TotalControl.cs @@ -0,0 +1,66 @@ +using UnityEngine; +using Verse; + +namespace FactionLoadout.UISupport; + +/// +/// The single fullscreen, paused workspace that replaces the old window cascade +/// (Dialog_FactionLoadout -> PresetUI -> FactionEditUI -> PawnKindEditUI). All +/// navigation and screen drawing is delegated to . +/// +[HotSwappable] +public class Dialog_TotalControl : Window +{ + public readonly TotalControlController Controller; + + public Dialog_TotalControl(Preset preset = null) + { + forcePause = true; + doCloseX = true; + absorbInputAroundWindow = true; + closeOnAccept = false; // Enter is used by text fields; must not close the window. + closeOnCancel = true; // Esc: handled by OnCancelKeyPressed (step back), closes from Home. + closeOnClickedOutside = false; + draggable = false; + resizeable = false; + preventCameraMotion = true; + + Controller = new TotalControlController(this, preset); + } + + public override Vector2 InitialSize => new(UI.screenWidth * 0.96f, UI.screenHeight * 0.96f); + + public override float Margin => 12f; + + public override void PostOpen() + { + base.PostOpen(); + float w = UI.screenWidth * 0.96f; + float h = UI.screenHeight * 0.96f; + windowRect = new Rect((UI.screenWidth - w) / 2f, (UI.screenHeight - h) / 2f, w, h); + } + + public override void DoWindowContents(Rect inRect) + { + Controller.Draw(inRect); + } + + public override void OnCancelKeyPressed() + { + // Esc steps back one screen; only closes the window once at Home. + if (Controller.HandleEscape()) + { + Event.current.Use(); + return; + } + + base.OnCancelKeyPressed(); + } + + public override void PostClose() + { + base.PostClose(); + Controller.Dispose(); + ModCore.Settings.Write(); + } +} diff --git a/1.6/Source/UISupport/DrawSupport/PawnPreviewWidget.cs b/1.6/Source/UISupport/DrawSupport/PawnPreviewWidget.cs new file mode 100644 index 0000000..e7ba9d8 --- /dev/null +++ b/1.6/Source/UISupport/DrawSupport/PawnPreviewWidget.cs @@ -0,0 +1,112 @@ +using System; +using RimWorld; +using UnityEngine; +using Verse; + +namespace FactionLoadout.UISupport.DrawSupport; + +/// +/// Draws the big live pawn portrait plus its control strip (rotate, regenerate, and +/// headgear/clothes toggles). Pure drawing + view state; the expensive pawn lifecycle +/// lives in . +/// +public class PawnPreviewWidget +{ + private Rot4 rotation = Rot4.South; + private bool renderHeadgear = true; + private bool renderClothes = true; + + public void Draw(Rect inRect, PreviewPawnController controller) + { + Widgets.DrawMenuSection(inRect); + Rect content = inRect.ContractedBy(8f); + + const float stripH = 92f; + Rect portraitArea = new(content.x, content.y, content.width, content.height - stripH - 4f); + Rect strip = new(content.x, portraitArea.yMax + 4f, content.width, stripH); + + DrawPortrait(portraitArea, controller); + DrawControls(strip, controller); + } + + private void DrawPortrait(Rect area, PreviewPawnController controller) + { + if (!controller.InGame) + { + DrawCentered(area, $"{"FactionLoadout_FactionEdit_PreviewError".Translate()}"); + return; + } + if (controller.PreviewFailed) + { + DrawCentered(area, $"{"FactionLoadout_Preview_Failed".Translate()}"); + return; + } + if (controller.PreviewPawn == null) + { + DrawCentered(area, "FactionLoadout_Preview_Placeholder".Translate()); + return; + } + + // Centered portrait-aspect sub-rect so the pawn fills nicely without distortion. + const float aspect = 0.7f; // width / height + float w = Mathf.Min(area.width, area.height * aspect); + float h = w / aspect; + if (h > area.height) + { + h = area.height; + w = h * aspect; + } + Rect portrait = new(area.x + (area.width - w) / 2f, area.y + (area.height - h) / 2f, w, h); + + try + { + RenderTexture tex = PortraitsCache.Get( + controller.PreviewPawn, + new Vector2(portrait.width, portrait.height), + rotation, + default, + 1f, + supersample: true, + compensateForUIScale: true, + renderHeadgear, + renderClothes + ); + GUI.DrawTexture(portrait, tex); + } + catch (Exception e) + { + ModCore.Error("Failed to render preview portrait.", e); + controller.MarkFailed(); + } + } + + private void DrawControls(Rect strip, PreviewPawnController controller) + { + // Row 1: rotate ◀ / regenerate / rotate ▶ + Rect row1 = new(strip.x, strip.y, strip.width, 30f); + float rotW = 40f; + if (Widgets.ButtonText(new Rect(row1.x, row1.y, rotW, row1.height), "◀")) + rotation = rotation.Rotated(RotationDirection.Counterclockwise); + if (Widgets.ButtonText(new Rect(row1.xMax - rotW, row1.y, rotW, row1.height), "▶")) + rotation = rotation.Rotated(RotationDirection.Clockwise); + Rect regenRect = new(row1.x + rotW + 4f, row1.y, row1.width - 2 * (rotW + 4f), row1.height); + if (Widgets.ButtonText(regenRect, "FactionLoadout_Preview_Regenerate".Translate())) + controller.RequestRegenerate(); + + // Row 2: headgear / clothes toggles + Rect row2 = new(strip.x, row1.yMax + 6f, strip.width, 24f); + Rect half1 = new(row2.x, row2.y, row2.width / 2f - 4f, row2.height); + Rect half2 = new(row2.x + row2.width / 2f + 4f, row2.y, row2.width / 2f - 4f, row2.height); + Widgets.CheckboxLabeled(half1, "FactionLoadout_Preview_ShowHeadgear".Translate(), ref renderHeadgear); + Widgets.CheckboxLabeled(half2, "FactionLoadout_Preview_ShowClothes".Translate(), ref renderClothes); + } + + private static void DrawCentered(Rect area, string text) + { + Text.Anchor = TextAnchor.MiddleCenter; + GUI.color = new Color(1f, 1f, 1f, 0.7f); + Widgets.Label(area, text); + GUI.color = Color.white; + Text.Anchor = TextAnchor.UpperLeft; + } +} diff --git a/1.6/Source/UISupport/DrawSupport/SpecificGearDrawer.cs b/1.6/Source/UISupport/DrawSupport/SpecificGearDrawer.cs index 0a085bc..fb8cada 100644 --- a/1.6/Source/UISupport/DrawSupport/SpecificGearDrawer.cs +++ b/1.6/Source/UISupport/DrawSupport/SpecificGearDrawer.cs @@ -204,7 +204,7 @@ private static void DrawItemMaterial(Rect area, SpecRequirementEdit item) if (item.Material == null) { - FactionDef faction = Find.WindowStack.WindowOfType()?.Current?.Faction?.Def; + FactionDef faction = TCEditContext.CurrentFaction?.Faction?.Def; TechLevel techLevel = MySettings.VanillaRestrictions ? faction?.techLevel ?? TechLevel.Undefined : TechLevel.Undefined; item.Material = GenStuff.AllowedStuffsFor(item.Thing, techLevel).FirstOrDefault(); } @@ -212,7 +212,7 @@ private static void DrawItemMaterial(Rect area, SpecRequirementEdit item) Widgets.DrawHighlightIfMouseover(material); if (Widgets.ButtonInvisible(material)) { - FactionDef faction = Find.WindowStack.WindowOfType()?.Current?.Faction?.Def; + FactionDef faction = TCEditContext.CurrentFaction?.Faction?.Def; TechLevel techLevel = MySettings.VanillaRestrictions ? faction?.techLevel ?? TechLevel.Undefined : TechLevel.Undefined; IEnumerable defs = GenStuff.AllowedStuffsFor(item.Thing, techLevel); List stuffItems = CustomFloatMenu.MakeItems( diff --git a/1.6/Source/UISupport/PreviewPawnController.cs b/1.6/Source/UISupport/PreviewPawnController.cs new file mode 100644 index 0000000..c78d74d --- /dev/null +++ b/1.6/Source/UISupport/PreviewPawnController.cs @@ -0,0 +1,183 @@ +using System; +using System.Linq; +using FactionLoadout.Patches; +using FactionLoadout.Util; +using RimWorld; +using RimWorld.Planet; +using UnityEngine; +using Verse; + +namespace FactionLoadout.UISupport; + +/// +/// Owns the live preview pawn for the Pawn Edit screen. Generates the pawn through the +/// REAL generation pipeline (so the always-active apparel/weapon/hediff Harmony patches +/// apply the edits exactly as they would for a raid — "match real raids"), debounces +/// regeneration so dragging a slider doesn't regenerate every frame, and disposes the +/// throwaway pawn safely so it can never leak into the save. +/// +[HotSwappable] +public class PreviewPawnController +{ + public readonly FactionEdit ParentFaction; + public PawnKindDef TargetKind; + + public Pawn PreviewPawn { get; private set; } + public bool PreviewFailed { get; private set; } + + /// Mirrors the old FactionEditUI "Thing ID Patch" toggle (off by default). + public bool UseThingIDPatch; + + private bool dirty; + private int frameCounter; + private int dirtyAtFrame; + private const int DebounceFrames = 20; + + public bool InGame => Verse.Current.Game != null; + + public PreviewPawnController(FactionEdit parentFaction, PawnKindDef targetKind) + { + ParentFaction = parentFaction; + TargetKind = targetKind; + // Generate immediately on the first Tick. + dirty = true; + dirtyAtFrame = -DebounceFrames; + } + + /// An option changed; regenerate after a short idle (debounced). + public void NotifyEditChanged() + { + dirty = true; + dirtyAtFrame = frameCounter; + PreviewFailed = false; + } + + /// Force an immediate fresh roll (also re-rolls randomized pools/chances). + public void RequestRegenerate() + { + PreviewFailed = false; + Regenerate(); + } + + public void MarkFailed() => PreviewFailed = true; + + /// Call once per frame from the screen. + public void Tick() + { + frameCounter++; + if (!InGame) + return; + if (dirty && !PreviewFailed && frameCounter - dirtyAtFrame >= DebounceFrames) + Regenerate(); + } + + private void Regenerate() + { + dirty = false; + if (!InGame || TargetKind == null) + { + PreviewFailed = TargetKind == null; + return; + } + + DiscardPawn(PreviewPawn); + PreviewPawn = null; + + try + { + PreviewPawn = GeneratePreviewPawn(); + PreviewFailed = PreviewPawn == null; + if (PreviewPawn != null) + PortraitsCache.SetDirty(PreviewPawn); + } + catch (Exception e) + { + ModCore.Error($"Failed to generate preview pawn for '{TargetKind?.LabelCap}'.", e); + PreviewPawn = null; + PreviewFailed = true; + } + } + + private Pawn GeneratePreviewPawn() + { + FactionDef toClone = FactionEdit.TryGetOriginal(ParentFaction.Faction.Def.defName) ?? ParentFaction.Faction.Def; + FactionDef clonedFac = CloningUtility.Clone(toClone); + clonedFac.defName = ParentFaction.Faction.Def.defName; + clonedFac.humanlikeFaction = ParentFaction.Faction.Def.humanlikeFaction; + // The "TEMP FACTION CLONE" prefix is load-bearing — PawnKindEdit.GetEditsFor + // special-cases it so the edits resolve for this throwaway faction. + clonedFac.fixedName = $"TEMP FACTION CLONE ({clonedFac.defName})"; + + ParentFaction.Apply(clonedFac, false); + + Faction faction = new() + { + def = clonedFac, + loadID = -1, + colorFromSpectrum = Rand.Range(0f, 1f), + hidden = true, + ideos = Find.FactionManager?.FirstFactionOfDef(ParentFaction.Faction.Def)?.ideos, + Name = clonedFac.fixedName, + relations = Find + .FactionManager.AllFactionsVisible.Select(otherFaction => new FactionRelation + { + other = otherFaction, + baseGoodwill = 0, + kind = FactionRelationKind.Neutral, + }) + .ToList(), + temporary = true, + deactivated = true, + }; + + ThingIDPatch.Active = UseThingIDPatch; + IdeoUtilityPatch.Active = true; + FactionUtilityPawnGenPatch.Active = true; + + try + { + return PawnGenerator.GeneratePawn( + new PawnGenerationRequest(TargetKind, faction) + { + ForceGenerateNewPawn = true, + AllowDowned = false, + AllowDead = false, + CanGeneratePawnRelations = false, + RelationWithExtraPawnChanceFactor = 0, + ColonistRelationChanceFactor = 0, + ForceNoIdeo = true, + ForbidAnyTitle = true, + } + ); + } + finally + { + Find.FactionManager.Remove(faction); + ThingIDPatch.Active = false; + FactionLeaderPatch.Active = false; + FactionUtilityPawnGenPatch.Active = false; + IdeoUtilityPatch.Active = false; + } + } + + public void Dispose() + { + DiscardPawn(PreviewPawn); + PreviewPawn = null; + } + + /// + /// Save-contamination guard (same pattern as the old FactionEditUI.DestroyPawns): + /// never keep the throwaway pawn — discard it, stripping dangling references. + /// + private static void DiscardPawn(Pawn pawn) + { + if (pawn == null) + return; + + if (Find.WorldPawns?.Contains(pawn) == true) + Find.WorldPawns.RemoveAndDiscardPawnViaGC(pawn); + else if (!pawn.Discarded) + Find.WorldPawns?.PassToWorld(pawn, PawnDiscardDecideMode.Discard); + } +} diff --git a/1.6/Source/UISupport/Screens/FactionEditScreen.cs b/1.6/Source/UISupport/Screens/FactionEditScreen.cs new file mode 100644 index 0000000..c0beeb8 --- /dev/null +++ b/1.6/Source/UISupport/Screens/FactionEditScreen.cs @@ -0,0 +1,378 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FactionLoadout.Modules; +using FactionLoadout.Util; +using RimWorld; +using UnityEngine; +using Verse; + +namespace FactionLoadout.UISupport.Screens; + +/// +/// Edits one faction: a two-panel layout with the pawnkind-edit list on the left and +/// faction-level settings (tech level, xenotypes, spawn groups, module UI) on the right. +/// Extracted from the old FactionEditUI.DoWindowContents, minus the preview block +/// (the live preview lands on the Pawn Edit screen in Phase C). +/// +[HotSwappable] +public class FactionEditScreen +{ + public readonly TotalControlController Controller; + public readonly FactionEdit Current; + + private readonly List bin = []; + private readonly HashSet tempKinds = []; + + private Vector2 kindsScroll; + private Vector2 settingsScroll; + private float settingsContentHeight = 10000f; // measured after first frame; large so nothing clips + + public FactionEditScreen(TotalControlController controller, FactionEdit fac) + { + Controller = controller; + Current = fac; + } + + public void Draw(Rect inRect) + { + if (Current == null || Current.DeletedOrClosed) + return; + + // --- Header block (title + warnings + clipboard toolbar) --- + Listing_Standard head = new(); + head.Begin(new Rect(inRect.x, inRect.y, inRect.width, 240f)); + + Rect titleRect = head.GetRect(44f); + Widgets.Label(titleRect, $"{"FactionLoadout_TC_FactionHeader".Translate()}: {Current.Faction.Def?.LabelCap ?? "None".Translate()}"); + + if (Current.Faction.IsMissing) + head.Label($"{"FactionLoadout_FactionMissingEditWarning".Translate()}"); + if (Current.Faction.DefName == Preset.SpecialCreepjoinerFactionDefName) + head.Label($"{"FactionLoadout_FactionEdit_ExperimentalCreepjoiner".Translate()}"); + if (Current.Faction.DefName == Preset.SpecialWildManFactionDefName) + head.Label($"{"FactionLoadout_FactionEdit_ExperimentalWildMan".Translate()}"); + if (Current.Faction.DefName == Preset.SpecialFactionlessPawnsFactionDefName) + head.Label($"{"FactionLoadout_Special_FactionlessWarning".Translate()}"); + + if (!Current.Faction.IsMissing) + DrawFactionClipboardToolbar(head); + + float headH = head.CurHeight; + head.End(); + + // --- Two columns --- + float colsY = inRect.y + headH + 6f; + float colsH = inRect.yMax - colsY; + float leftW = inRect.width * 0.36f; + const float gap = 10f; + Rect leftRect = new(inRect.x, colsY, leftW, colsH); + Rect rightRect = new(inRect.x + leftW + gap, colsY, inRect.width - leftW - gap, colsH); + + DrawKindList(leftRect); + DrawFactionSettings(rightRect); + } + + // ----------------------------------------------------------- left: pawnkinds + + private void DrawKindList(Rect rect) + { + Widgets.DrawMenuSection(rect); + rect = rect.ContractedBy(6f); + + Rect titleR = new(rect.x, rect.y, rect.width, 24f); + Widgets.Label(titleR, $"{"FactionLoadout_FactionEdit_LoadoutOverrides".Translate()}"); + + // "Add..." button pinned at the bottom of the panel. + Rect addBtn = new(rect.x, rect.yMax - 30f, rect.width, 30f); + float listY = titleR.yMax + 4f; + Rect scrollOut = new(rect.x, listY, rect.width, addBtn.y - listY - 6f); + + const float rowH = 32f; + float contentH = Mathf.Max(Current.KindEdits.Count * rowH + 6f, scrollOut.height); + Rect view = new(0, 0, scrollOut.width - 16f, contentH); + + HashSet orphanedKinds = Current?.GetOrphanedKinds() ?? []; + + Widgets.BeginScrollView(scrollOut, ref kindsScroll, view); + for (int i = 0; i < Current.KindEdits.Count; i++) + { + PawnKindEdit edit = Current.KindEdits[i]; + Rect row = new(0, i * rowH, view.width, rowH - 2f); + if (i % 2 == 1) + Widgets.DrawHighlight(row); + DrawKindRow(row, edit, orphanedKinds); + } + Widgets.EndScrollView(); + + foreach (PawnKindEdit item in bin) + Current.KindEdits.Remove(item); + bin.Clear(); + + if (!Current.Faction.IsMissing && Widgets.ButtonText(addBtn, "Add".Translate().CapitalizeFirst() + "...")) + OpenAddKindMenu(); + } + + private void DrawKindRow(Rect row, PawnKindEdit edit, HashSet orphanedKinds) + { + float x = row.x + 2f; + float y = row.y + 2f; + + string delText = "Delete".Translate(); + float delW = Mathf.Max(38f, Text.CalcSize(delText).x + 10f); + GUI.color = Color.red; + if (Widgets.ButtonText(new Rect(x, y, delW, 24f), delText)) + { + bin.Add(edit); + edit.DeletedOrClosed = true; + } + GUI.color = Color.white; + x += delW + 4f; + + string editText = "FactionLoadout_Edit".Translate().CapitalizeFirst(); + float editW = Mathf.Max(50f, Text.CalcSize(editText).x + 10f); + if (Widgets.ButtonText(new Rect(x, y, editW, 24f), editText)) + Controller.OpenKind(edit); + x += editW + 4f; + + if (Widgets.ButtonImageFitted(new Rect(x, y, 24f, 24f), TexButton.Copy)) + PawnKindClipboard.Copy(edit); + TooltipHandler.TipRegion(new Rect(x, y, 24f, 24f), "FactionLoadout_Clipboard_CopyTooltip".Translate()); + x += 28f; + + if (PawnKindClipboard.HasData) + { + if (Widgets.ButtonImageFitted(new Rect(x, y, 24f, 24f), TexButton.Paste)) + PawnKindClipboard.PasteAll(edit); + TooltipHandler.TipRegion(new Rect(x, y, 24f, 24f), "FactionLoadout_Clipboard_PasteAllTooltip".Translate(PawnKindClipboard.GetDescription())); + } + else + { + GUI.color = Color.gray; + Widgets.DrawTextureFitted(new Rect(x, y, 24f, 24f), TexButton.Paste, 1f); + GUI.color = Color.white; + TooltipHandler.TipRegion(new Rect(x, y, 24f, 24f), "FactionLoadout_Clipboard_Empty".Translate()); + } + x += 28f; + + bool isOrphaned = !edit.IsGlobal && edit.Def != null && orphanedKinds.Contains(edit.Def); + if (isOrphaned) + { + GUI.color = Color.yellow; + Widgets.Label(new Rect(x, y, 20f, 24f), "⚠"); + GUI.color = Color.white; + TooltipHandler.TipRegion(new Rect(x, y, 20f, 24f), "FactionLoadout_SpawnGroups_OrphanKindTooltip".Translate()); + x += 22f; + } + + Rect labelRect = new(x, row.y, row.xMax - x, row.height); + Text.Anchor = TextAnchor.MiddleLeft; + Widgets.Label(labelRect, $"{(edit.IsGlobal ? $"{"FactionLoadout_GlobalLabel".Translate()}" : edit.Def.LabelCap.ToString())}"); + Text.Anchor = TextAnchor.UpperLeft; + } + + // ------------------------------------------------------ right: faction settings + + private void DrawFactionSettings(Rect rect) + { + Widgets.DrawMenuSection(rect); + rect = rect.ContractedBy(6f); + + float innerH = Mathf.Max(settingsContentHeight + 40f, rect.height); + Rect view = new(0, 0, rect.width - 16f, innerH); + + Widgets.BeginScrollView(rect, ref settingsScroll, view); + Listing_Standard ui = new() { ColumnWidth = view.width }; + ui.Begin(view); + + // Tech level. + if ( + ui.ButtonTextLabeled( + "FactionLoadout_Faction_Techlevel".Translate(), + Current.TechLevel?.ToStringHuman() ?? "FactionLoadout_NotOverriden_WithDefault".Translate((Current.Faction?.Def?.techLevel ?? TechLevel.Undefined).ToStringHuman()) + ) + ) + { + IEnumerable enums = Enum.GetValues(typeof(TechLevel)).Cast().Append(null); + FloatMenuUtility.MakeMenu( + enums, + e => e?.ToStringHuman() ?? "FactionLoadout_NotOverriden_WithDefault".Translate((Current.Faction?.Def?.techLevel ?? TechLevel.Undefined).ToStringHuman()), + e => () => Current.TechLevel = e + ); + } + + // Xenotype spawn rates (Biotech). + if ( + ModsConfig.BiotechActive + && Current.Faction is { IsMissing: false } + && Current.Faction?.Def != Preset.SpecialWildManFaction + && Current.Faction?.Def != Preset.SpecialFactionlessPawnsFaction + ) + { + if (!Current.OverrideFactionXenotypes) + { + Current.xenotypeChances.Clear(); + Current.xenotypeChancesByDef.Clear(); + } + + ui.GapLine(); + string xenoState = Current.OverrideFactionXenotypes ? "FactionLoadout_Xenotype_ActiveCount".Translate(Current.xenotypeChances.Count) : "FactionLoadout_Xenotype_Off".Translate(); + if (ui.ButtonTextLabeled("FactionLoadout_EditXenoSpawnRates".Translate(), xenoState)) + Find.WindowStack.Add(new Dialog_XenotypeEdit(Current)); + } + + // Spawn groups. + if (Current.Faction is not { IsMissing: true }) + { + ui.GapLine(); + + Rect groupsRow = ui.GetRect(28f); + const float editBtnW = 160f; + Rect editGroupsBtn = new(groupsRow.xMax - editBtnW, groupsRow.y, editBtnW, 24f); + + Text.Anchor = TextAnchor.MiddleLeft; + string groupsSummary; + if (Current.PawnGroupMakerEdits != null) + groupsSummary = "FactionLoadout_SpawnGroups_SummaryModified".Translate(Current.PawnGroupMakerEdits.Count, "FactionLoadout_GroupEditor_NewTag".Translate().ToString().ToLower()); + else + groupsSummary = "FactionLoadout_SpawnGroups_Summary".Translate(Current?.Faction?.Def?.pawnGroupMakers?.Count ?? 0); + + Rect summaryLabelRect = new(groupsRow.x, groupsRow.y, groupsRow.width - editBtnW - 4f, groupsRow.height); + GUI.color = Color.grey; + Widgets.Label(summaryLabelRect, "FactionLoadout_SpawnGroups_Label".Translate() + " " + groupsSummary); + GUI.color = Color.white; + Text.Anchor = TextAnchor.UpperLeft; + + if (Widgets.ButtonText(editGroupsBtn, "FactionLoadout_SpawnGroups_EditButton".Translate())) + GroupEditorUI.OpenEditor(Current); + + HashSet orphaned = Current?.GetOrphanedKinds() ?? []; + if (orphaned.Count > 0) + { + string names = orphaned.Select(k => k.LabelCap.ToString()).OrderBy(n => n).ToCommaList(); + string warnText = "FactionLoadout_SpawnGroups_OrphanWarning".Translate(names); + Rect warnRow = ui.GetRect(Text.CalcHeight(warnText, ui.ColumnWidth)); + GUI.color = new Color(1f, 0.6f, 0.1f); + Widgets.Label(warnRow, warnText); + GUI.color = Color.white; + } + } + + // Active modules contribute faction-level UI here (unchanged contract). + foreach (ITotalControlModule module in ModuleRegistry.Modules) + { + if (!module.IsActive) + continue; + try + { + module.AddFactionUI(Current, ui); + } + catch (Exception e) + { + ModCore.Error($"Error drawing faction UI for module '{module.ModuleName}'", e); + } + } + + settingsContentHeight = ui.CurHeight; + ui.End(); + Widgets.EndScrollView(); + } + + // ------------------------------------------------------------------- helpers + + private void DrawFactionClipboardToolbar(Listing_Standard ui) + { + Rect toolbar = ui.GetRect(28f); + float x = toolbar.x; + float y = toolbar.y; + const float btnSize = 24f; + const float gap = 4f; + + if (Widgets.ButtonImageFitted(new Rect(x, y, btnSize, btnSize), TexButton.Copy)) + FactionEditClipboard.Copy(Current); + TooltipHandler.TipRegion(new Rect(x, y, btnSize, btnSize), "FactionLoadout_FactionClipboard_CopyTooltip".Translate()); + + x += btnSize + gap; + if (FactionEditClipboard.HasData) + { + if (Widgets.ButtonImageFitted(new Rect(x, y, btnSize, btnSize), TexButton.Paste)) + FactionEditClipboard.PasteAll(Current); + TooltipHandler.TipRegion(new Rect(x, y, btnSize, btnSize), "FactionLoadout_FactionClipboard_PasteTooltip".Translate(FactionEditClipboard.GetDescription())); + } + else + { + GUI.color = Color.gray; + Widgets.DrawTextureFitted(new Rect(x, y, btnSize, btnSize), TexButton.Paste, 1f); + GUI.color = Color.white; + TooltipHandler.TipRegion(new Rect(x, y, btnSize, btnSize), "FactionLoadout_Clipboard_Empty".Translate()); + } + } + + private void OpenAddKindMenu() + { + IEnumerable MakeKinds() + { + tempKinds.Clear(); + if (!Current.HasGlobalEditor()) + tempKinds.Add(null); + + foreach (PawnKindDef kind in Current.GetAllKindDefsForUI()) + { + if (!Current.HasEditFor(kind)) + tempKinds.Add(kind); + } + + if (Current.PawnGroupMakerEdits != null) + { + if (Current.Faction.Def?.basicMemberKind != null && !Current.HasEditFor(Current.Faction.Def.basicMemberKind)) + tempKinds.Add(Current.Faction.Def.basicMemberKind); + if (Current.Faction.Def?.fixedLeaderKinds != null) + { + foreach (PawnKindDef item in Current.Faction.Def.fixedLeaderKinds) + { + if (!Current.HasEditFor(item)) + tempKinds.Add(item); + } + } + } + + foreach (PawnKindDef item in tempKinds) + yield return item; + + if (tempKinds.Count(k => k != null) == 0) + { + if (Current.Faction.Def == FactionDefOf.Ancients || Current.Faction.Def == FactionDefOf.AncientsHostile) + { + yield return PawnKindDefOf.AncientSoldier; + yield return PawnKindDefOf.Slave; + } + } + + tempKinds.Clear(); + } + + List kinds = MakeKinds().ToList(); + List items = CustomFloatMenu.MakeItems( + kinds, + k => k != null ? new MenuItemText(k, $"{k.LabelCap} ({k.defName})", tooltip: k.description) : new MenuItemText(null, $"{"FactionLoadout_GlobalLabel".Translate()}") + ); + CustomFloatMenu.Open( + items, + raw => + { + PawnKindDef k = raw.GetPayload(); + if (k != null) + { + Current.KindEdits.Add(new PawnKindEdit(k)); + } + else + { + PawnKindDef kind = kinds.FirstOrDefault(pawnKindDef => pawnKindDef != null); + ModCore.Log($"Using {kind} as global base."); + if (kind != null) + Current.KindEdits.Insert(0, new PawnKindEdit(kind) { IsGlobal = true }); + } + } + ); + } +} diff --git a/1.6/Source/UISupport/Screens/FactionViewScreen.cs b/1.6/Source/UISupport/Screens/FactionViewScreen.cs new file mode 100644 index 0000000..d61a194 --- /dev/null +++ b/1.6/Source/UISupport/Screens/FactionViewScreen.cs @@ -0,0 +1,274 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using RimWorld; +using UnityEngine; +using Verse; + +namespace FactionLoadout.UISupport.Screens; + +/// +/// Home screen: global settings + active-preset management + the selected preset's +/// faction-edit list. Merges the old Dialog_FactionLoadout (settings + preset +/// list) and PresetUI (per-preset faction list) into one in-shell screen. +/// +[HotSwappable] +public class FactionViewScreen +{ + public readonly TotalControlController Controller; + + private Vector2 factionScroll; + + public FactionViewScreen(TotalControlController controller) + { + Controller = controller; + } + + public void Draw(Rect inRect) + { + Listing_Standard ui = new(); + ui.Begin(inRect); + + // --- Global settings --- + ui.CheckboxLabeled( + "FactionLoadout_Settings_VanillaRestrictions".Translate(), + ref MySettings.VanillaRestrictions, + "FactionLoadout_Settings_VanillaRestrictionsDesc".Translate() + ); + ui.CheckboxLabeled("FactionLoadout_Settings_Verbose".Translate(), ref MySettings.VerboseLogging, "FactionLoadout_Settings_VerboseDesc".Translate()); + ui.CheckboxLabeled( + "FactionLoadout_Settings_PatchKindInRequests".Translate(), + ref MySettings.PatchKindInRequests, + "FactionLoadout_Settings_PatchKindInRequestsDesc".Translate() + ); + ui.GapLine(); + + Preset preset = Controller.SelectedPreset; + if (preset == null) + { + ui.Label("FactionLoadout_NothingHere".Translate()); + ui.Gap(); + if (ui.ButtonText("FactionLoadout_CreateNewPreset".Translate())) + CreateNewPreset(); + ui.End(); + return; + } + + DrawPresetHeader(ui, preset); + ui.GapLine(); + + ui.Label($"{"FactionLoadout_Preset_EditCount".Translate(preset.factionChanges.Count)}"); + ui.Gap(4); + + // --- Faction list (scrollable). Fixed row height -> compute content directly. --- + const float rowH = 34f; + float listH = Mathf.Max(120f, inRect.height - ui.CurHeight - 44f); + Rect scrollOut = ui.GetRect(listH); + float contentH = Mathf.Max(preset.factionChanges.Count * rowH + 8f, listH); + Rect view = new(0, 0, scrollOut.width - 16f, contentH); + + Widgets.BeginScrollView(scrollOut, ref factionScroll, view); + FactionEdit toDelete = null; + for (int i = 0; i < preset.factionChanges.Count; i++) + { + FactionEdit item = preset.factionChanges[i]; + Rect row = new(0, i * rowH, view.width, rowH - 2f); + if (i % 2 == 1) + Widgets.DrawHighlight(row); + Widgets.DrawHighlightIfMouseover(row); + if (DrawFactionRow(row, item)) + toDelete = item; + } + + Widgets.EndScrollView(); + + if (toDelete != null) + { + toDelete.DeletedOrClosed = true; + preset.factionChanges.Remove(toDelete); + } + + // --- Add faction edit --- + if (ui.ButtonText("FactionLoadout_Preset_AddFactionEdit".Translate())) + OpenAddFactionMenu(preset); + + ui.End(); + } + + /// Returns true if this faction was marked for deletion. + private bool DrawFactionRow(Rect row, FactionEdit item) + { + bool delete = false; + float rightX = row.xMax; + + // Delete (far right, red). + string delText = "Delete".Translate(); + float delW = Mathf.Max(80f, Text.CalcSize(delText).x + 16f); + Rect delBtn = new(rightX - delW, row.y + 3f, delW, row.height - 6f); + GUI.color = Color.red; + if (Widgets.ButtonText(delBtn, delText)) + delete = true; + GUI.color = Color.white; + rightX -= delW + 6f; + + if (item.Faction.IsMissing) + { + string editAnyway = "FactionLoadout_EditAnyway".Translate(); + float ew = Mathf.Max(120f, Text.CalcSize(editAnyway).x + 16f); + Rect editBtn = new(rightX - ew, row.y + 3f, ew, row.height - 6f); + GUI.color = new Color(1f, 0.75f, 0.2f); + if (Widgets.ButtonText(editBtn, editAnyway)) + Controller.OpenFaction(item); + GUI.color = new Color(1f, 0.4f, 0.4f); + Rect lbl = new(row.x, row.y, editBtn.x - row.x - 6f, row.height); + Text.Anchor = TextAnchor.MiddleLeft; + Widgets.Label(lbl, $"{item.Faction.LabelCap} ({item.Faction.DefName}) — {"FactionLoadout_FactionMissing".Translate()}"); + Text.Anchor = TextAnchor.UpperLeft; + GUI.color = Color.white; + return delete; + } + + // Edit. + string editText = "FactionLoadout_Edit".Translate().CapitalizeFirst(); + float editW = Mathf.Max(80f, Text.CalcSize(editText).x + 16f); + Rect edit = new(rightX - editW, row.y + 3f, editW, row.height - 6f); + if (Widgets.ButtonText(edit, editText)) + Controller.OpenFaction(item); + rightX -= editW + 6f; + + // Enabled checkbox. + Rect enabledRect = new(rightX - 110f, row.y, 110f, row.height); + Widgets.CheckboxLabeled(enabledRect, "Enabled".Translate(), ref item.Active, placeCheckboxNearText: true); + rightX -= 116f; + + // Label (fills remaining space on the left). + Rect nameRect = new(row.x + 4f, row.y, rightX - row.x - 4f, row.height); + Text.Anchor = TextAnchor.MiddleLeft; + Widgets.Label(nameRect, $"{item.Faction.LabelCap} ({item.Faction.DefName})"); + Text.Anchor = TextAnchor.UpperLeft; + + return delete; + } + + private void DrawPresetHeader(Listing_Standard ui, Preset preset) + { + // Name row. + Rect nameRow = ui.GetRect(30); + Rect nameLbl = new(nameRow.x, nameRow.y, 80f, nameRow.height); + Text.Anchor = TextAnchor.MiddleLeft; + Widgets.Label(nameLbl, "FactionLoadout_Preset_EditName".Translate()); + Text.Anchor = TextAnchor.UpperLeft; + Rect nameField = new(nameRow.x + 84f, nameRow.y + 2f, 280f, 26f); + preset.Name = Widgets.TextField(nameField, preset.Name); + + // Active toggle (only one preset may be active). + Rect activeRect = new(nameRow.x + 380f, nameRow.y, 140f, nameRow.height); + bool active = MySettings.ActivePreset == preset.GUID; + bool was = active; + GUI.color = active ? Color.green : Color.white; + Widgets.CheckboxLabeled(activeRect, "FactionLoadout_Active".Translate().CapitalizeFirst(), ref active, placeCheckboxNearText: true); + GUI.color = Color.white; + if (active != was) + { + MySettings.ActivePreset = active ? preset.GUID : null; + ModCore.Settings.Write(); + } + + // Action buttons. + Rect btnRow = ui.GetRect(30); + float bx = btnRow.x; + const float bw = 150f; + + string saveLabel = preset.IsPackaged ? "FactionLoadout_SaveToSourceFile".Translate() : "Save".Translate().ToString().ToUpper(); + GUI.color = Color.green; + if (Widgets.ButtonText(new Rect(bx, btnRow.y, bw, 28f), $"{saveLabel}")) + preset.Save(); + GUI.color = Color.white; + bx += bw + 6f; + + if (Widgets.ButtonText(new Rect(bx, btnRow.y, bw, 28f), "FactionLoadout_CopyToMyPresets".Translate())) + CopyPreset(preset); + bx += bw + 6f; + + if (!preset.IsPackaged) + { + GUI.color = Color.Lerp(Color.white, Color.red, 0.65f); + if (Widgets.ButtonText(new Rect(bx, btnRow.y, bw, 28f), $"{"Delete".Translate().ToString().ToUpper()}")) + { + Preset.DeletePreset(preset); + Controller.SelectPreset(Preset.LoadedPresets.FirstOrDefault()); + } + GUI.color = Color.white; + bx += bw + 6f; + } + + if (Widgets.ButtonText(new Rect(bx, btnRow.y, bw, 28f), "FactionLoadout_CreateNewPreset".Translate())) + CreateNewPreset(); + + // Packaged + missing-faction warnings. + if (preset.IsPackaged) + { + Rect warningRect = ui.GetRect(44); + Widgets.DrawBoxSolid(warningRect, new Color(0.45f, 0.35f, 0.05f, 0.85f)); + Widgets.Label(warningRect.ContractedBy(6f), "FactionLoadout_PackagedPresetWarning".Translate(preset.PackagedModName).ToString()); + } + + if (preset.HasMissingFactions()) + { + ui.Label($"{"FactionLoadout_Preset_MissingWarning".Translate()}"); + foreach (string str in preset.GetMissingFactionAndModNames()) + ui.Label($" - {str}"); + } + } + + private void CreateNewPreset() + { + Preset preset = new(); + Preset.AddNewPreset(preset); + preset.Save(); + MySettings.ActivePreset = preset.GUID; + ModCore.Settings.Write(); + Controller.SelectPreset(preset); + } + + private void CopyPreset(Preset src) + { + try + { + Preset copy = Preset.CreateCopy(src); + Preset.AddNewPreset(copy); + copy.Save(); + Controller.SelectPreset(copy); + } + catch (Exception ex) + { + ModCore.Error("Failed to copy preset.", ex); + } + } + + private void OpenAddFactionMenu(Preset preset) + { + List raw = DefDatabase.AllDefsListForReading.Where(f => !preset.HasEditFor(f)).ToList(); + if (!preset.HasEditFor(Preset.SpecialCreepjoinerFaction) && !raw.Any(f => f.defName == Preset.SpecialCreepjoinerFaction.defName)) + raw.Add(Preset.SpecialCreepjoinerFaction); + if (!preset.HasEditFor(Preset.SpecialWildManFaction) && !raw.Any(f => f.defName == Preset.SpecialWildManFaction.defName)) + raw.Add(Preset.SpecialWildManFaction); + if ( + Preset.FactionlessPawnKindsSet.Count > 0 + && !preset.HasEditFor(Preset.SpecialFactionlessPawnsFaction) + && !raw.Any(f => f.defName == Preset.SpecialFactionlessPawnsFaction.defName) + ) + raw.Add(Preset.SpecialFactionlessPawnsFaction); + + List items = CustomFloatMenu.MakeItems(raw, f => new MenuItemText(f, $"{f.LabelCap} ({f.defName})", DefUtils.TryGetIcon(f, out Color c), c, f.description)); + + CustomFloatMenu.Open( + items, + menuItemBase => + { + FactionDef e = menuItemBase.GetPayload(); + preset.factionChanges.Add(new FactionEdit { Faction = e }); + } + ); + } +} diff --git a/1.6/Source/UISupport/Screens/PawnEditScreen.cs b/1.6/Source/UISupport/Screens/PawnEditScreen.cs new file mode 100644 index 0000000..ff2d359 --- /dev/null +++ b/1.6/Source/UISupport/Screens/PawnEditScreen.cs @@ -0,0 +1,215 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FactionLoadout.Modules; +using FactionLoadout.UISupport.DrawSupport; +using FactionLoadout.Util; +using RimWorld; +using UnityEngine; +using Verse; + +namespace FactionLoadout.UISupport.Screens; + +/// +/// Edits one pawnkind in three columns: a big live preview (left), a vertical category +/// nav (middle), and the selected category's option content (right). Reuses the existing +/// machinery unchanged — the only structural changes vs. the old +/// PawnKindEditUI are the vertical nav (instead of cramped tab buttons) and a +/// per-category scroll position (fixes the old shared-scroll bug). +/// +/// The preview column is a placeholder in Phase B; Phase C plugs the live portrait in +/// at . +/// +[HotSwappable] +public class PawnEditScreen +{ + public readonly TotalControlController Controller; + public readonly FactionEdit ParentFaction; + public readonly PawnKindEdit Current; + public readonly PreviewPawnController Preview; + private readonly PawnPreviewWidget previewWidget = new(); + + private List tabs; + private int selectedCategory; + private readonly Dictionary categoryScroll = new(); + private readonly Dictionary tabHeights = new(); + private Vector2 navScroll; + + public PawnEditScreen(TotalControlController controller, FactionEdit parentFaction, PawnKindEdit kind) + { + Controller = controller; + ParentFaction = parentFaction; + Current = kind; + Preview = new PreviewPawnController(parentFaction, kind?.Def); + DefCache.ScanDefs(); + } + + public void Dispose() => Preview.Dispose(); + + /// The unmodified base def, used to show "default" values next to overrides. + public PawnKindDef DefaultKind + { + get + { + if (Current.DeletedOrClosed) + return Current.Def; + + FactionDef found = FactionEdit.TryGetOriginal(ParentFaction.Faction.DefName); + if (found == null) + return Current.Def; + PawnKindDef found2 = found.GetKindDefs().FirstOrDefault(k => k.defName == Current.Def.defName); + return found2 ?? Current.Def; + } + } + + public void Draw(Rect inRect) + { + if (Current == null || Current.DeletedOrClosed) + return; + + Preview.Tick(); + + Text.Font = GameFont.Small; + if (tabs == null) + BuildTabs(); + + // Header. + Rect header = new(inRect.x, inRect.y, inRect.width, 32f); + Text.Font = GameFont.Medium; + string kindLabel = Current.IsGlobal ? "FactionLoadout_GlobalLabel".Translate().ToString() : Current.Def.LabelCap.ToString(); + Widgets.Label(header, "FactionLoadout_TC_PawnHeader".Translate(kindLabel)); + Text.Font = GameFont.Small; + + float colsY = inRect.y + 36f; + float colsH = inRect.yMax - colsY; + + if ((tabs?.Count ?? 0) == 0) + { + Widgets.Label(new Rect(inRect.x, colsY, inRect.width, 40f), "FactionLoadout_NoEditableProperties".Translate()); + return; + } + + selectedCategory = Mathf.Clamp(selectedCategory, 0, tabs.Count - 1); + + // Three columns: preview | category nav | options. + float previewW = inRect.width * 0.38f; + float navW = inRect.width * 0.16f; + const float gap = 8f; + Rect previewRect = new(inRect.x, colsY, previewW, colsH); + Rect navRect = new(previewRect.xMax + gap, colsY, navW, colsH); + Rect optsRect = new(navRect.xMax + gap, colsY, inRect.xMax - (navRect.xMax + gap), colsH); + + DrawPreviewColumn(previewRect); + DrawCategoryNav(navRect); + DrawOptions(optsRect); + } + + private void DrawPreviewColumn(Rect rect) + { + previewWidget.Draw(rect, Preview); + } + + private void DrawCategoryNav(Rect rect) + { + Widgets.DrawMenuSection(rect); + rect = rect.ContractedBy(4f); + + const float rowH = 34f; + float contentH = tabs.Count * rowH; + bool needsScroll = contentH > rect.height; + Rect view = new(0, 0, rect.width - (needsScroll ? 16f : 0f), Mathf.Max(contentH, rect.height)); + + Widgets.BeginScrollView(rect, ref navScroll, view); + for (int i = 0; i < tabs.Count; i++) + { + Rect btn = new(0, i * rowH, view.width, rowH - 2f); + Color bg = selectedCategory == i ? new Color32(49, 82, 133, 255) : new Color(0.2f, 0.2f, 0.2f, 1f); + Rect r = btn; + if (Widgets.CustomButtonText(ref r, $"{tabs[i].Name}", bg, Color.white, Color.white)) + selectedCategory = i; + } + Widgets.EndScrollView(); + } + + private void DrawOptions(Rect rect) + { + Widgets.DrawMenuSection(rect); + rect = rect.ContractedBy(6f); + + // Clipboard toolbar (copy / paste-all). Wired identically to the old editor: + // the reset callback clears the active tab's text/scroll buffers after a paste. + Rect toolbarRect = new(rect.x, rect.y, rect.width, 28f); + ClipboardToolbar.Draw( + toolbarRect, + Current, + () => + { + if (selectedCategory >= 0 && selectedCategory < tabs.Count && tabs[selectedCategory] is EditTab et) + et.ResetBuffers(); + } + ); + + // Selected category content, with its OWN scroll position (per-category bug fix). + Tab tab = tabs[selectedCategory]; + Rect scrollOut = new(rect.x, toolbarRect.yMax + 4f, rect.width, rect.yMax - (toolbarRect.yMax + 4f)); + float innerW = scrollOut.width - 24f; + float contentH = tabHeights.TryGetValue(tab, out float storedH) ? Mathf.Max(storedH, scrollOut.height) : scrollOut.height; + + Vector2 scroll = categoryScroll.TryGetValue(tab, out Vector2 s) ? s : Vector2.zero; + Widgets.BeginScrollView(scrollOut, ref scroll, new Rect(0, 0, innerW, contentH)); + + Listing_Standard ui = new() { ColumnWidth = innerW }; + ui.Begin(new Rect(0, 0, innerW, 1000000)); + + // Detect inline option changes (checkboxes/sliders/text) to auto-refresh the + // preview. Float-menu pickers apply on a later frame and are covered by the + // explicit Regenerate button. + bool prevChanged = GUI.changed; + GUI.changed = false; + tab.Draw(ui); + if (GUI.changed) + Preview.NotifyEditChanged(); + GUI.changed = prevChanged || GUI.changed; + + tabHeights[tab] = ui.CurHeight; + ui.End(); + + Widgets.EndScrollView(); + categoryScroll[tab] = scroll; + } + + private void BuildTabs() + { + PawnKindDef dk = DefaultKind; + tabs = [new GeneralTab(Current, dk)]; + + bool isAnimal = dk.RaceProps.Animal; + if (!isAnimal) + { + tabs.AddRange( + [ + new BackstoryTab(Current, dk), + new AppearanceTab(Current, dk), + new ApparelTab(Current, dk), + new WeaponTab(Current, dk), + new ImplantsTab(Current, dk), + new InventoryTab(Current, dk), + new RaidPointsTab(Current, dk), + new RaidLootTab(Current, dk), + ] + ); + if (VFEAncientsReflectionModule.ModLoaded.Value) + tabs.Add(new AncientsTab(Current, dk)); + if (VEPsycastsReflectionModule.ModLoaded.Value) + tabs.Add(new PsycastsTab(Current, dk)); + if (ModsConfig.BiotechActive) + tabs.Add(new XenotypeTab(Current, dk)); + + foreach (ITotalControlModule module in ModuleRegistry.Modules) + { + if (module.IsActive) + module.AddTabs(Current, dk, tabs); + } + } + } +} diff --git a/1.6/Source/UISupport/TotalControlController.cs b/1.6/Source/UISupport/TotalControlController.cs new file mode 100644 index 0000000..8b995d9 --- /dev/null +++ b/1.6/Source/UISupport/TotalControlController.cs @@ -0,0 +1,302 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FactionLoadout.UISupport.Screens; +using RimWorld; +using UnityEngine; +using Verse; + +namespace FactionLoadout.UISupport; + +public enum TCScreen +{ + FactionView, + FactionEdit, + PawnEdit, +} + +/// +/// Owns the navigation state (which screen + the selected preset/faction/pawnkind) and +/// renders the shared chrome (title bar, top tab strip, breadcrumb) before dispatching +/// to the active screen. Screen objects hold their own scroll/buffer state so it persists +/// across frames; the controller rebuilds a screen only when its subject changes. +/// +[HotSwappable] +public class TotalControlController +{ + public readonly Dialog_TotalControl Owner; + + public TCScreen Screen { get; private set; } = TCScreen.FactionView; + public Preset SelectedPreset; + public FactionEdit SelectedFaction { get; private set; } + public PawnKindEdit SelectedKind { get; private set; } + + private readonly FactionViewScreen factionView; + private FactionEditScreen factionEdit; + private PawnEditScreen pawnEdit; + + public TotalControlController(Dialog_TotalControl owner, Preset initialPreset) + { + Owner = owner; + SelectedPreset = ResolveInitialPreset(initialPreset); + factionView = new FactionViewScreen(this); + } + + public static Preset ResolveInitialPreset(Preset given) + { + if (given != null) + return given; + + if (!MySettings.ActivePreset.NullOrEmpty()) + { + Preset active = Preset.LoadedPresets.FirstOrDefault(p => p.GUID == MySettings.ActivePreset); + if (active != null) + return active; + } + + return Preset.LoadedPresets.FirstOrDefault(); + } + + // ---------------------------------------------------------------- navigation + + public void SelectPreset(Preset p) + { + SelectedPreset = p; + SelectedFaction = null; + SelectedKind = null; + factionEdit = null; + DisposePawnEdit(); + TCEditContext.CurrentFaction = null; + Screen = TCScreen.FactionView; + } + + public void OpenFaction(FactionEdit fe) + { + if (fe == null) + return; + + if (SelectedFaction != fe) + { + SelectedFaction = fe; + SelectedKind = null; + factionEdit = new FactionEditScreen(this, fe); + DisposePawnEdit(); + } + + TCEditContext.CurrentFaction = fe; + Screen = TCScreen.FactionEdit; + } + + public void OpenKind(PawnKindEdit pke) + { + if (pke == null) + return; + + if (SelectedKind != pke) + { + DisposePawnEdit(); + SelectedKind = pke; + pawnEdit = new PawnEditScreen(this, SelectedFaction, pke); + } + + Screen = TCScreen.PawnEdit; + } + + public void GoTo(TCScreen s) + { + if (IsEnabled(s)) + Screen = s; + } + + public bool IsEnabled(TCScreen s) => + s switch + { + TCScreen.FactionView => true, + TCScreen.FactionEdit => SelectedFaction is { DeletedOrClosed: false }, + TCScreen.PawnEdit => SelectedKind is { DeletedOrClosed: false }, + _ => false, + }; + + public bool HandleEscape() + { + switch (Screen) + { + case TCScreen.PawnEdit: + Screen = TCScreen.FactionEdit; + return true; + case TCScreen.FactionEdit: + Screen = TCScreen.FactionView; + return true; + default: + return false; // at Home -> let the window close + } + } + + public void Dispose() + { + DisposePawnEdit(); + TCEditContext.CurrentFaction = null; + } + + private void DisposePawnEdit() + { + pawnEdit?.Dispose(); + pawnEdit = null; + } + + // ------------------------------------------------------------------- drawing + + public void Draw(Rect inRect) + { + // Validate the selection context; fall back if something was deleted underneath us. + if (Screen == TCScreen.PawnEdit && SelectedKind is not { DeletedOrClosed: false }) + { + SelectedKind = null; + DisposePawnEdit(); + Screen = IsEnabled(TCScreen.FactionEdit) ? TCScreen.FactionEdit : TCScreen.FactionView; + } + if (Screen == TCScreen.FactionEdit && SelectedFaction is not { DeletedOrClosed: false }) + { + SelectedFaction = null; + SelectedKind = null; + factionEdit = null; + DisposePawnEdit(); + Screen = TCScreen.FactionView; + } + + Rect titleBar = new(inRect.x, inRect.y, inRect.width, 38f); + Rect tabStrip = new(inRect.x, titleBar.yMax + 2f, inRect.width, 32f); + Rect crumb = new(inRect.x, tabStrip.yMax + 4f, inRect.width, 20f); + Rect content = new(inRect.x, crumb.yMax + 6f, inRect.width, inRect.yMax - (crumb.yMax + 6f)); + + DrawTitleBar(titleBar); + DrawTabStrip(tabStrip); + DrawBreadcrumb(crumb); + + switch (Screen) + { + case TCScreen.FactionView: + factionView.Draw(content); + break; + case TCScreen.FactionEdit: + factionEdit?.Draw(content); + break; + case TCScreen.PawnEdit: + pawnEdit?.Draw(content); + break; + } + } + + private void DrawTitleBar(Rect rect) + { + Text.Font = GameFont.Medium; + Rect titleRect = new(rect.x, rect.y, 360f, rect.height); + Widgets.Label(titleRect, "FactionLoadout_TC_Title".Translate()); + Text.Font = GameFont.Small; + + // Active-preset dropdown. Leave ~34px on the right for the window's close X. + const float dropdownW = 300f; + Rect dropdown = new(rect.xMax - dropdownW - 34f, rect.y + 4f, dropdownW, 30f); + string presetLabel = SelectedPreset != null ? "FactionLoadout_TC_PresetLabel".Translate(SelectedPreset.Name) : "FactionLoadout_TC_NoPreset".Translate(); + if (Widgets.ButtonText(dropdown, presetLabel)) + OpenPresetDropdown(); + } + + private void OpenPresetDropdown() + { + List options = new(); + foreach (Preset p in Preset.LoadedPresets) + { + Preset captured = p; + bool active = MySettings.ActivePreset == p.GUID; + string label = active ? $"{p.Name} ({"FactionLoadout_Active".Translate()})" : p.Name; + options.Add(new FloatMenuOption(label, () => SelectPreset(captured))); + } + + if (options.Count == 0) + options.Add(new FloatMenuOption("FactionLoadout_NothingHere".Translate(), null)); + + Find.WindowStack.Add(new FloatMenu(options)); + } + + private void DrawTabStrip(Rect rect) + { + const float tabW = 170f; + const float gap = 4f; + DrawTab(new Rect(rect.x, rect.y, tabW, rect.height), TCScreen.FactionView, "FactionLoadout_Tab_FactionView".Translate()); + DrawTab(new Rect(rect.x + (tabW + gap), rect.y, tabW, rect.height), TCScreen.FactionEdit, "FactionLoadout_Tab_FactionEdit".Translate()); + DrawTab(new Rect(rect.x + (tabW + gap) * 2f, rect.y, tabW, rect.height), TCScreen.PawnEdit, "FactionLoadout_Tab_PawnEdit".Translate()); + Widgets.DrawLineHorizontal(rect.x, rect.yMax, rect.width); + } + + private void DrawTab(Rect rect, TCScreen screen, string label) + { + bool selected = Screen == screen; + Color bg = selected ? new Color32(49, 82, 133, 255) : new Color(0.2f, 0.2f, 0.2f, 1f); + + if (!IsEnabled(screen)) + { + Widgets.DrawBoxSolid(rect, new Color(0.14f, 0.14f, 0.14f, 1f)); + GUI.color = new Color(1f, 1f, 1f, 0.35f); + Text.Anchor = TextAnchor.MiddleCenter; + Widgets.Label(rect, $"{label}"); + Text.Anchor = TextAnchor.UpperLeft; + GUI.color = Color.white; + return; + } + + Rect r = rect; + if (Widgets.CustomButtonText(ref r, $"{label}", bg, Color.white, Color.white)) + GoTo(screen); + } + + private void DrawBreadcrumb(Rect rect) + { + Text.Font = GameFont.Tiny; + float x = rect.x; + + // Home is always clickable (unless already there). + DrawCrumbSegment(ref x, rect, "FactionLoadout_Breadcrumb_Home".Translate(), Screen != TCScreen.FactionView, () => GoTo(TCScreen.FactionView)); + + if (SelectedFaction is { DeletedOrClosed: false }) + { + DrawCrumbSeparator(ref x, rect); + DrawCrumbSegment(ref x, rect, SelectedFaction.Faction.LabelCap.ToString(), Screen != TCScreen.FactionEdit, () => GoTo(TCScreen.FactionEdit)); + + if (SelectedKind is { DeletedOrClosed: false }) + { + DrawCrumbSeparator(ref x, rect); + string kindName = SelectedKind.IsGlobal ? "FactionLoadout_GlobalLabel".Translate().ToString() : SelectedKind.Def?.LabelCap.ToString(); + DrawCrumbSegment(ref x, rect, kindName, clickable: false, onClick: null); + } + } + + Text.Font = GameFont.Small; + } + + private static void DrawCrumbSegment(ref float x, Rect bar, string text, bool clickable, Action onClick) + { + float w = Text.CalcSize(text).x; + Rect r = new(x, bar.y, w, bar.height); + GUI.color = clickable ? new Color(0.6f, 0.8f, 1f) : Color.white; + Widgets.Label(r, text); + if (clickable) + { + Widgets.DrawHighlightIfMouseover(r); + if (Widgets.ButtonInvisible(r)) + onClick?.Invoke(); + } + GUI.color = Color.white; + x += w; + } + + private static void DrawCrumbSeparator(ref float x, Rect bar) + { + const string sep = " › "; + float w = Text.CalcSize(sep).x; + GUI.color = new Color(0.6f, 0.6f, 0.6f); + Widgets.Label(new Rect(x, bar.y, w, bar.height), sep); + GUI.color = Color.white; + x += w; + } +} diff --git a/Common/Languages/English/Keyed/FactionLoadout_Keys.xml b/Common/Languages/English/Keyed/FactionLoadout_Keys.xml index a166017..9df1739 100644 --- a/Common/Languages/English/Keyed/FactionLoadout_Keys.xml +++ b/Common/Languages/English/Keyed/FactionLoadout_Keys.xml @@ -403,4 +403,22 @@ Add new faction edit... WARNING: This preset has missing factions, probably because they are added by a mod that is not loaded: Missing factions + + + Total Control + Preset: {0} + (no preset selected) + Faction + Pawn: {0} + Generating preview... + Preview failed to generate. Click Regenerate to retry. + Regenerate + Headgear + Clothes + Faction View + Faction Edit + Pawn Edit + Home + Open Total Control + Total Control opens in a fullscreen editor where you can manage presets, edit factions, and customise pawns with a live preview.