diff --git a/Assets/Data/Distros.meta b/Assets/Data/Distros.meta
new file mode 100644
index 0000000..9fd6c94
--- /dev/null
+++ b/Assets/Data/Distros.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: b5d4e612df874934a2fe5e4ce5cf43de
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Data/Distros/Art.meta b/Assets/Data/Distros/Art.meta
new file mode 100644
index 0000000..d7a6f11
--- /dev/null
+++ b/Assets/Data/Distros/Art.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 296f57316cdb4f9ba2227b877b091c23
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Data/Distros/Art/fedora.txt b/Assets/Data/Distros/Art/fedora.txt
new file mode 100644
index 0000000..88e22d5
--- /dev/null
+++ b/Assets/Data/Distros/Art/fedora.txt
@@ -0,0 +1,16 @@
+ %#%#%#%#%#
+ %#%#%#%%#%#%#%#%#%
+ %#%#%%#%#%#% *%#%
+ #%#%%#%#%#% ***%#
+ %%#%#%#%%#% %#%#%#****%#
+ %#%#%#%#%#% %#%%#%%***%%#
+#%#%%#%#%#%# %#%#%#%***%#%%
+#%%#%#%%#%#% %#%#%****#%#%#
+%#%#**** ****%#%#%#
+%#%****%#%#% %#%#%%%#%#%%#%
+#%***%%#%#%# %#%%#%#%#%#%#
+%#***#%#%%#% %#%#%#%#%#%#
+%%****%%#%#% #%#%#%%#%#%#
+#%%*** %%#%#%#%#%%#
+%#%#%* %#%#%%#%#%# *
+ %#%#%#%#%#%#%#%%#%#
diff --git a/Assets/Data/Distros/Art/fedora.txt.meta b/Assets/Data/Distros/Art/fedora.txt.meta
new file mode 100644
index 0000000..ccb460c
--- /dev/null
+++ b/Assets/Data/Distros/Art/fedora.txt.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: f2373174b6954f2d9088d83d10e0d9e4
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Data/Distros/Art/mint.txt b/Assets/Data/Distros/Art/mint.txt
new file mode 100644
index 0000000..9eb5bc0
--- /dev/null
+++ b/Assets/Data/Distros/Art/mint.txt
@@ -0,0 +1,16 @@
+
+ :======:
+ ==============
+ .==================.
+ === =================
+ ==== === ====
+ ==== ==: == ..== .====
+ ===== .==: == .== =====
+ ===== ==: == .== =====
+ ==== ==: == ..== .====
+ ==== ========== ====
+ ===== .-====
+ .==================.
+ ==============
+ :======:
+
diff --git a/Assets/Data/Distros/Art/mint.txt.meta b/Assets/Data/Distros/Art/mint.txt.meta
new file mode 100644
index 0000000..1c8a843
--- /dev/null
+++ b/Assets/Data/Distros/Art/mint.txt.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 412c9be402ec4d8c8510e71066ec22ff
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Data/Distros/Art/ubuntu.txt b/Assets/Data/Distros/Art/ubuntu.txt
new file mode 100644
index 0000000..3cf5c3e
--- /dev/null
+++ b/Assets/Data/Distros/Art/ubuntu.txt
@@ -0,0 +1,16 @@
+
+ ============
+ ==================
+ ============== =====
+ ========= = ======
+ ======= ===== =======
+ ====== ======== ======
+ == = ========== =====
+ == = ========== =====
+ ====== ======== ======
+ ======= ===== =======
+ ========= = ======
+ ============== =====
+ ==================
+ ============
+
diff --git a/Assets/Data/Distros/Art/ubuntu.txt.meta b/Assets/Data/Distros/Art/ubuntu.txt.meta
new file mode 100644
index 0000000..fa21f82
--- /dev/null
+++ b/Assets/Data/Distros/Art/ubuntu.txt.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 975f1f5de289435eb81c91c3eddd31dc
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Data/Distros/DistroDatabase.asset b/Assets/Data/Distros/DistroDatabase.asset
new file mode 100644
index 0000000..a8d085e
--- /dev/null
+++ b/Assets/Data/Distros/DistroDatabase.asset
@@ -0,0 +1,18 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 5af93d291e7d4d51b312bdf1e47dc6ef, type: 3}
+ m_Name: DistroDatabase
+ m_EditorClassIdentifier:
+ allDistros:
+ - {fileID: 11400000, guid: 1b0892a0d4c34b03a54c3d511ae59356, type: 2}
+ - {fileID: 11400000, guid: 245dfb73afe047d5a3a3ef54c30d5135, type: 2}
+ - {fileID: 11400000, guid: 366e71361387449abcfaa1a50c9af87b, type: 2}
diff --git a/Assets/Data/Distros/DistroDatabase.asset.meta b/Assets/Data/Distros/DistroDatabase.asset.meta
new file mode 100644
index 0000000..6453a4c
--- /dev/null
+++ b/Assets/Data/Distros/DistroDatabase.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 4d88ca5446fa4d80b2fbd4b12697cb7c
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Data/Distros/Fedora.asset b/Assets/Data/Distros/Fedora.asset
new file mode 100644
index 0000000..69cf57b
--- /dev/null
+++ b/Assets/Data/Distros/Fedora.asset
@@ -0,0 +1,26 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: bd550368fd0c75d43a53aa4fb3b6541e, type: 3}
+ m_Name: Fedora
+ m_EditorClassIdentifier:
+ id: fedora
+ displayName: Fedora
+ primaryLanguage: 2
+ secondaryLanguage: 8
+ passiveName: Bleeding Edge
+ description: First to every new feature. Occasionally first to the bugs that come with it.
+ asciiArt: {fileID: 4900000, guid: f2373174b6954f2d9088d83d10e0d9e4, type: 3}
+ accentColor: {r: 0.23529412, g: 0.43137255, b: 0.7058824, a: 1}
+ baseUptime: 55
+ baseRam: 5
+ baseCyclesPerTurn: 3
+ exclusiveCards: []
diff --git a/Assets/Data/Distros/Fedora.asset.meta b/Assets/Data/Distros/Fedora.asset.meta
new file mode 100644
index 0000000..dda654c
--- /dev/null
+++ b/Assets/Data/Distros/Fedora.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 245dfb73afe047d5a3a3ef54c30d5135
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Data/Distros/Mint.asset b/Assets/Data/Distros/Mint.asset
new file mode 100644
index 0000000..2cb1df9
--- /dev/null
+++ b/Assets/Data/Distros/Mint.asset
@@ -0,0 +1,26 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: bd550368fd0c75d43a53aa4fb3b6541e, type: 3}
+ m_Name: Mint
+ m_EditorClassIdentifier:
+ id: mint
+ displayName: Mint
+ primaryLanguage: 3
+ secondaryLanguage: 4
+ passiveName: It Just Works
+ description: Comfort incarnate. No surprises, no drama, ever.
+ asciiArt: {fileID: 4900000, guid: 412c9be402ec4d8c8510e71066ec22ff, type: 3}
+ accentColor: {r: 0.5294118, g: 0.8117647, b: 0.24313726, a: 1}
+ baseUptime: 65
+ baseRam: 5
+ baseCyclesPerTurn: 3
+ exclusiveCards: []
diff --git a/Assets/Data/Distros/Mint.asset.meta b/Assets/Data/Distros/Mint.asset.meta
new file mode 100644
index 0000000..0b6c5ea
--- /dev/null
+++ b/Assets/Data/Distros/Mint.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 366e71361387449abcfaa1a50c9af87b
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Data/Distros/Ubuntu.asset b/Assets/Data/Distros/Ubuntu.asset
new file mode 100644
index 0000000..ba174de
--- /dev/null
+++ b/Assets/Data/Distros/Ubuntu.asset
@@ -0,0 +1,26 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: bd550368fd0c75d43a53aa4fb3b6541e, type: 3}
+ m_Name: Ubuntu
+ m_EditorClassIdentifier:
+ id: ubuntu
+ displayName: Ubuntu
+ primaryLanguage: 3
+ secondaryLanguage: 4
+ passiveName: Long Term Support
+ description: The friendly one. Everything works out of the box, and she will not stop helping.
+ asciiArt: {fileID: 4900000, guid: 975f1f5de289435eb81c91c3eddd31dc, type: 3}
+ accentColor: {r: 0.9137255, g: 0.32941177, b: 0.1254902, a: 1}
+ baseUptime: 60
+ baseRam: 5
+ baseCyclesPerTurn: 3
+ exclusiveCards: []
diff --git a/Assets/Data/Distros/Ubuntu.asset.meta b/Assets/Data/Distros/Ubuntu.asset.meta
new file mode 100644
index 0000000..76aaed8
--- /dev/null
+++ b/Assets/Data/Distros/Ubuntu.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 1b0892a0d4c34b03a54c3d511ae59356
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scenes/MainMenuScene.unity b/Assets/Scenes/MainMenuScene.unity
index c8a1d4a..27ca081 100644
--- a/Assets/Scenes/MainMenuScene.unity
+++ b/Assets/Scenes/MainMenuScene.unity
@@ -305,4 +305,5 @@ MonoBehaviour:
m_EditorClassIdentifier:
monospaceFont: {fileID: 0}
motdBody: unstable userspace detected; keep a rollback shell open.
+ distroDatabase: {fileID: 11400000, guid: 4d88ca5446fa4d80b2fbd4b12697cb7c, type: 2}
featuredUnitPanel: {}
diff --git a/Assets/Scripts/Data/DistroDatabase.cs b/Assets/Scripts/Data/DistroDatabase.cs
new file mode 100644
index 0000000..d388af3
--- /dev/null
+++ b/Assets/Scripts/Data/DistroDatabase.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace KernelPanic.Data
+{
+ ///
+ /// Resolves persisted distro ids to playable distro definitions.
+ ///
+ public sealed class DistroDatabase : ScriptableObject
+ {
+ [SerializeField] private List allDistros = new();
+
+ public IReadOnlyList AllDistros => allDistros;
+
+ public DistroDefinition FindById(string id)
+ {
+ if (string.IsNullOrWhiteSpace(id))
+ {
+ return null;
+ }
+
+ for (int i = 0; i < allDistros.Count; i++)
+ {
+ DistroDefinition distro = allDistros[i];
+ if (distro != null && string.Equals(distro.Id, id, StringComparison.OrdinalIgnoreCase))
+ {
+ return distro;
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/Assets/Scripts/Data/DistroDatabase.cs.meta b/Assets/Scripts/Data/DistroDatabase.cs.meta
new file mode 100644
index 0000000..edaf02c
--- /dev/null
+++ b/Assets/Scripts/Data/DistroDatabase.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 5af93d291e7d4d51b312bdf1e47dc6ef
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/Data/DistroDefinition.cs b/Assets/Scripts/Data/DistroDefinition.cs
index ff6f480..8adda18 100644
--- a/Assets/Scripts/Data/DistroDefinition.cs
+++ b/Assets/Scripts/Data/DistroDefinition.cs
@@ -14,6 +14,8 @@ namespace KernelPanic.Data
[SerializeField] private Language primaryLanguage;
[SerializeField] private Language secondaryLanguage;
[SerializeField] private string passiveName;
+ [SerializeField, TextArea] private string description;
+ [SerializeField] private TextAsset asciiArt;
[SerializeField] private Color accentColor = new(0.36f, 1f, 0.57f);
[SerializeField] private int baseUptime;
[SerializeField] private int baseRam;
@@ -25,6 +27,8 @@ namespace KernelPanic.Data
public Language PrimaryLanguage => primaryLanguage;
public Language SecondaryLanguage => secondaryLanguage;
public string PassiveName => passiveName;
+ public string Description => description;
+ public TextAsset AsciiArt => asciiArt;
public Color AccentColor => accentColor;
public int BaseUptime => baseUptime;
public int BaseRam => baseRam;
diff --git a/Assets/Scripts/Meta/GachaService.cs b/Assets/Scripts/Meta/GachaService.cs
index 3c16949..4236f45 100644
--- a/Assets/Scripts/Meta/GachaService.cs
+++ b/Assets/Scripts/Meta/GachaService.cs
@@ -1,4 +1,6 @@
using System;
+using System.Collections.Generic;
+using KernelPanic.Data;
namespace KernelPanic.Meta
{
@@ -7,7 +9,23 @@ namespace KernelPanic.Meta
///
public sealed class GachaService
{
+ private readonly List bannerPool = new();
+
public int PullTokens => 0; // TODO: Replace with the real pull-token wallet when that meta system exists.
+ public IReadOnlyList BannerPool => bannerPool;
+
+ public event Action BannerPoolChanged;
+
+ public void AddToBannerPool(DistroDefinition distro)
+ {
+ if (distro == null || bannerPool.Contains(distro))
+ {
+ return;
+ }
+
+ bannerPool.Add(distro);
+ BannerPoolChanged?.Invoke();
+ }
public GachaPullResult PerformPull(EntropyWallet wallet, int cost)
{
diff --git a/Assets/Scripts/Meta/PlayerCollection.cs b/Assets/Scripts/Meta/PlayerCollection.cs
new file mode 100644
index 0000000..61dee38
--- /dev/null
+++ b/Assets/Scripts/Meta/PlayerCollection.cs
@@ -0,0 +1,32 @@
+using System.Collections.Generic;
+using KernelPanic.Data;
+using System;
+
+namespace KernelPanic.Meta
+{
+ ///
+ /// Tracks player-owned persistent unlocks for menu presentation and collection views.
+ ///
+ public sealed class PlayerCollection
+ {
+ private readonly List ownedUnits = new();
+
+ // TODO: Populate owned units from SaveService during bootstrap.
+ // TODO: Add newly unlocked units from GachaService rewards when pulls exist.
+ public IReadOnlyList OwnedUnits => ownedUnits;
+
+ public event Action Changed;
+
+ public void Add(DistroDefinition unit)
+ {
+ if (unit == null || ownedUnits.Contains(unit))
+ {
+ return;
+ }
+
+ // Persistence is wired by the owner so this domain class stays IO-free.
+ ownedUnits.Add(unit);
+ Changed?.Invoke();
+ }
+ }
+}
diff --git a/Assets/Scripts/Meta/PlayerCollection.cs.meta b/Assets/Scripts/Meta/PlayerCollection.cs.meta
new file mode 100644
index 0000000..eeac422
--- /dev/null
+++ b/Assets/Scripts/Meta/PlayerCollection.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 4a10ff65b6e9478d9fe638f8a39f8144
diff --git a/Assets/Scripts/Meta/SaveService.cs b/Assets/Scripts/Meta/SaveService.cs
index 76fc6cf..84c0229 100644
--- a/Assets/Scripts/Meta/SaveService.cs
+++ b/Assets/Scripts/Meta/SaveService.cs
@@ -1,4 +1,7 @@
using System;
+using System.Collections.Generic;
+using System.IO;
+using UnityEngine;
namespace KernelPanic.Meta
{
@@ -7,22 +10,66 @@ namespace KernelPanic.Meta
///
public sealed class SaveService
{
+ private const string SaveFileName = "kernel-panic-save.json";
+
+ public string SavePath => Path.Combine(Application.persistentDataPath, SaveFileName);
+
public void Save(SaveData data)
{
- throw new NotImplementedException();
+ data ??= new SaveData();
+ data.EnsureLists();
+
+ string directory = Path.GetDirectoryName(SavePath);
+ if (!string.IsNullOrEmpty(directory))
+ {
+ Directory.CreateDirectory(directory);
+ }
+
+ string json = JsonUtility.ToJson(data, true);
+ File.WriteAllText(SavePath, json);
}
public SaveData Load()
{
- throw new NotImplementedException();
+ if (!File.Exists(SavePath))
+ {
+ return SaveData.CreateDefault();
+ }
+
+ try
+ {
+ SaveData data = JsonUtility.FromJson(File.ReadAllText(SavePath));
+ data ??= SaveData.CreateDefault();
+ data.EnsureLists();
+ return data;
+ }
+ catch (Exception ex)
+ {
+ Debug.LogWarning($"Save file could not be loaded; starting fresh. {ex.Message}");
+ return SaveData.CreateDefault();
+ }
}
}
///
/// Stores persistent progression data for serialization.
///
+ [Serializable]
public sealed class SaveData
{
- public int EntropyBalance { get; set; }
+ public bool starterChosen;
+ public List ownedUnitIds = new();
+ public List bannerPoolIds = new();
+
+ public static SaveData CreateDefault()
+ {
+ return new SaveData();
+ }
+
+ public void EnsureLists()
+ {
+ ownedUnitIds ??= new List();
+ bannerPoolIds ??= new List();
+ }
}
}
diff --git a/Assets/Scripts/UI/AsciiArtFitter.cs b/Assets/Scripts/UI/AsciiArtFitter.cs
new file mode 100644
index 0000000..2953fbc
--- /dev/null
+++ b/Assets/Scripts/UI/AsciiArtFitter.cs
@@ -0,0 +1,139 @@
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+using UnityEngine.TextCore.Text;
+using UnityEngine.UIElements;
+
+namespace KernelPanic.UI
+{
+ ///
+ /// Fits monospace ASCII art into a fixed UI Toolkit label region.
+ ///
+ public sealed class AsciiArtFitter
+ {
+ private const float ReferenceFontSize = 20f;
+ private const float LineHeightRatio = 1.05f;
+ private const float MinFontSize = 6f;
+ private const float MaxFontSize = 24f;
+ private const float MonospaceTolerance = 0.02f;
+ private static readonly Dictionary AdvanceRatioByFont = new();
+ private static readonly HashSet WarnedFonts = new();
+ private static bool hasDefaultAdvanceRatio;
+ private static float defaultAdvanceRatio;
+
+ private readonly Label label;
+ private readonly FontAsset font;
+ private string art;
+
+ public AsciiArtFitter(Label label, FontAsset font)
+ {
+ this.label = label;
+ this.font = font;
+ label.RegisterCallback(_ => Fit());
+ }
+
+ public void SetArt(string value)
+ {
+ art = value;
+ Fit();
+ }
+
+ private void Fit()
+ {
+ if (string.IsNullOrWhiteSpace(art))
+ {
+ return;
+ }
+
+ Rect region = label.layout;
+ if (!IsUsable(region.width) || !IsUsable(region.height))
+ {
+ return;
+ }
+
+ GetArtDimensions(art, out int rows, out int columns);
+ if (rows <= 0 || columns <= 0)
+ {
+ return;
+ }
+
+ float advanceRatio = GetAdvanceRatio();
+ float widthFit = region.width / (columns * advanceRatio);
+ float heightFit = region.height / (rows * LineHeightRatio);
+ float fontSize = Mathf.Clamp(Mathf.Floor(Mathf.Min(widthFit, heightFit)), MinFontSize, MaxFontSize);
+ float blockWidth = columns * fontSize * advanceRatio;
+ float blockHeight = rows * fontSize * LineHeightRatio;
+
+ label.style.fontSize = fontSize;
+ label.style.paddingLeft = Mathf.Max(0f, (region.width - blockWidth) * 0.5f);
+ label.style.paddingTop = Mathf.Max(0f, (region.height - blockHeight) * 0.5f);
+ label.style.paddingRight = 0f;
+ label.style.paddingBottom = 0f;
+ }
+
+ private float GetAdvanceRatio()
+ {
+ if (font == null && hasDefaultAdvanceRatio)
+ {
+ return defaultAdvanceRatio;
+ }
+
+ if (font != null && AdvanceRatioByFont.TryGetValue(font, out float cached))
+ {
+ return cached;
+ }
+
+ float previousFontSize = label.resolvedStyle.fontSize;
+ label.style.fontSize = ReferenceFontSize;
+
+ float mWidth = label.MeasureTextSize("MMMMMMMMMM", 0f, VisualElement.MeasureMode.Undefined, 0f, VisualElement.MeasureMode.Undefined).x;
+ float iWidth = label.MeasureTextSize("iiiiiiiiii", 0f, VisualElement.MeasureMode.Undefined, 0f, VisualElement.MeasureMode.Undefined).x;
+ label.style.fontSize = previousFontSize;
+
+ float advanceRatio = mWidth > 0f ? mWidth / (10f * ReferenceFontSize) : 0.6f;
+ if (font == null)
+ {
+ defaultAdvanceRatio = advanceRatio;
+ hasDefaultAdvanceRatio = true;
+ }
+ else
+ {
+ AdvanceRatioByFont[font] = advanceRatio;
+ }
+
+ if (font != null && mWidth > 0f && iWidth > 0f)
+ {
+ float difference = Mathf.Abs(mWidth - iWidth) / Mathf.Max(mWidth, iWidth);
+ if (difference > MonospaceTolerance && WarnedFonts.Add(font))
+ {
+ Debug.LogWarning($"ASCII art font '{font.name}' is not monospace; art may misalign.");
+ }
+ }
+
+ return advanceRatio;
+ }
+
+ private static void GetArtDimensions(string value, out int rows, out int columns)
+ {
+ string normalized = value.Replace("\r\n", "\n").Replace('\r', '\n');
+ if (normalized.EndsWith("\n", StringComparison.Ordinal))
+ {
+ normalized = normalized[..^1];
+ }
+
+ rows = 0;
+ columns = 0;
+ string[] lines = normalized.Split('\n');
+ for (int i = 0; i < lines.Length; i++)
+ {
+ rows++;
+ columns = Mathf.Max(columns, lines[i].Length);
+ }
+ }
+
+ private static bool IsUsable(float value)
+ {
+ return value > 0f && !float.IsNaN(value) && !float.IsInfinity(value);
+ }
+ }
+}
diff --git a/Assets/Scripts/UI/AsciiArtFitter.cs.meta b/Assets/Scripts/UI/AsciiArtFitter.cs.meta
new file mode 100644
index 0000000..71deae5
--- /dev/null
+++ b/Assets/Scripts/UI/AsciiArtFitter.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 6f677d4ac6de4dca8e92a3efa687ff1b
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/UI/BackgroundLogRingBuffer.cs b/Assets/Scripts/UI/BackgroundLogRingBuffer.cs
new file mode 100644
index 0000000..4ea252d
--- /dev/null
+++ b/Assets/Scripts/UI/BackgroundLogRingBuffer.cs
@@ -0,0 +1,134 @@
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+using UnityEngine.UIElements;
+
+namespace KernelPanic.UI
+{
+ ///
+ /// Keeps the ambient boot-log background filled by rewriting a fixed stack of labels.
+ ///
+ public sealed class BackgroundLogRingBuffer
+ {
+ private const float LineHeight = 18f;
+ private const int ExtraLines = 2;
+ private const int TickMilliseconds = 450;
+
+ private readonly VisualElement layer;
+ private readonly IReadOnlyList sourceLines;
+ private readonly List