Add actual ASCII-Art for distros, add first three distros and being able to choose a starting-distro. Update UI to fix multiple small bugs.

This commit is contained in:
2026-07-04 00:12:03 +02:00
parent 36ddc8fa1c
commit 00e990ac18
36 changed files with 1421 additions and 13 deletions
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b5d4e612df874934a2fe5e4ce5cf43de
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 296f57316cdb4f9ba2227b877b091c23
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+16
View File
@@ -0,0 +1,16 @@
%#%#%#%#%#
%#%#%#%%#%#%#%#%#%
%#%#%%#%#%#% *%#%
#%#%%#%#%#% ***%#
%%#%#%#%%#% %#%#%#****%#
%#%#%#%#%#% %#%%#%%***%%#
#%#%%#%#%#%# %#%#%#%***%#%%
#%%#%#%%#%#% %#%#%****#%#%#
%#%#**** ****%#%#%#
%#%****%#%#% %#%#%%%#%#%%#%
#%***%%#%#%# %#%%#%#%#%#%#
%#***#%#%%#% %#%#%#%#%#%#
%%****%%#%#% #%#%#%%#%#%#
#%%*** %%#%#%#%#%%#
%#%#%* %#%#%%#%#%# *
%#%#%#%#%#%#%#%%#%#
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: f2373174b6954f2d9088d83d10e0d9e4
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+16
View File
@@ -0,0 +1,16 @@
:======:
==============
.==================.
=== =================
==== === ====
==== ==: == ..== .====
===== .==: == .== =====
===== ==: == .== =====
==== ==: == ..== .====
==== ========== ====
===== .-====
.==================.
==============
:======:
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 412c9be402ec4d8c8510e71066ec22ff
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+16
View File
@@ -0,0 +1,16 @@
============
==================
============== =====
========= = ======
======= ===== =======
====== ======== ======
== = ========== =====
== = ========== =====
====== ======== ======
======= ===== =======
========= = ======
============== =====
==================
============
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 975f1f5de289435eb81c91c3eddd31dc
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+18
View File
@@ -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}
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4d88ca5446fa4d80b2fbd4b12697cb7c
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
+26
View File
@@ -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: []
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 245dfb73afe047d5a3a3ef54c30d5135
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
+26
View File
@@ -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: []
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 366e71361387449abcfaa1a50c9af87b
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
+26
View File
@@ -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: []
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1b0892a0d4c34b03a54c3d511ae59356
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
+1
View File
@@ -305,4 +305,5 @@ MonoBehaviour:
m_EditorClassIdentifier: m_EditorClassIdentifier:
monospaceFont: {fileID: 0} monospaceFont: {fileID: 0}
motdBody: unstable userspace detected; keep a rollback shell open. motdBody: unstable userspace detected; keep a rollback shell open.
distroDatabase: {fileID: 11400000, guid: 4d88ca5446fa4d80b2fbd4b12697cb7c, type: 2}
featuredUnitPanel: {} featuredUnitPanel: {}
+35
View File
@@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace KernelPanic.Data
{
/// <summary>
/// Resolves persisted distro ids to playable distro definitions.
/// </summary>
public sealed class DistroDatabase : ScriptableObject
{
[SerializeField] private List<DistroDefinition> allDistros = new();
public IReadOnlyList<DistroDefinition> 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;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5af93d291e7d4d51b312bdf1e47dc6ef
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+4
View File
@@ -14,6 +14,8 @@ namespace KernelPanic.Data
[SerializeField] private Language primaryLanguage; [SerializeField] private Language primaryLanguage;
[SerializeField] private Language secondaryLanguage; [SerializeField] private Language secondaryLanguage;
[SerializeField] private string passiveName; [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 Color accentColor = new(0.36f, 1f, 0.57f);
[SerializeField] private int baseUptime; [SerializeField] private int baseUptime;
[SerializeField] private int baseRam; [SerializeField] private int baseRam;
@@ -25,6 +27,8 @@ namespace KernelPanic.Data
public Language PrimaryLanguage => primaryLanguage; public Language PrimaryLanguage => primaryLanguage;
public Language SecondaryLanguage => secondaryLanguage; public Language SecondaryLanguage => secondaryLanguage;
public string PassiveName => passiveName; public string PassiveName => passiveName;
public string Description => description;
public TextAsset AsciiArt => asciiArt;
public Color AccentColor => accentColor; public Color AccentColor => accentColor;
public int BaseUptime => baseUptime; public int BaseUptime => baseUptime;
public int BaseRam => baseRam; public int BaseRam => baseRam;
+18
View File
@@ -1,4 +1,6 @@
using System; using System;
using System.Collections.Generic;
using KernelPanic.Data;
namespace KernelPanic.Meta namespace KernelPanic.Meta
{ {
@@ -7,7 +9,23 @@ namespace KernelPanic.Meta
/// </summary> /// </summary>
public sealed class GachaService public sealed class GachaService
{ {
private readonly List<DistroDefinition> bannerPool = new();
public int PullTokens => 0; // TODO: Replace with the real pull-token wallet when that meta system exists. public int PullTokens => 0; // TODO: Replace with the real pull-token wallet when that meta system exists.
public IReadOnlyList<DistroDefinition> 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) public GachaPullResult PerformPull(EntropyWallet wallet, int cost)
{ {
+32
View File
@@ -0,0 +1,32 @@
using System.Collections.Generic;
using KernelPanic.Data;
using System;
namespace KernelPanic.Meta
{
/// <summary>
/// Tracks player-owned persistent unlocks for menu presentation and collection views.
/// </summary>
public sealed class PlayerCollection
{
private readonly List<DistroDefinition> ownedUnits = new();
// TODO: Populate owned units from SaveService during bootstrap.
// TODO: Add newly unlocked units from GachaService rewards when pulls exist.
public IReadOnlyList<DistroDefinition> 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();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4a10ff65b6e9478d9fe638f8a39f8144
+50 -3
View File
@@ -1,4 +1,7 @@
using System; using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
namespace KernelPanic.Meta namespace KernelPanic.Meta
{ {
@@ -7,22 +10,66 @@ namespace KernelPanic.Meta
/// </summary> /// </summary>
public sealed class SaveService 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) 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() public SaveData Load()
{ {
throw new NotImplementedException(); if (!File.Exists(SavePath))
{
return SaveData.CreateDefault();
}
try
{
SaveData data = JsonUtility.FromJson<SaveData>(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();
}
} }
} }
/// <summary> /// <summary>
/// Stores persistent progression data for serialization. /// Stores persistent progression data for serialization.
/// </summary> /// </summary>
[Serializable]
public sealed class SaveData public sealed class SaveData
{ {
public int EntropyBalance { get; set; } public bool starterChosen;
public List<string> ownedUnitIds = new();
public List<string> bannerPoolIds = new();
public static SaveData CreateDefault()
{
return new SaveData();
}
public void EnsureLists()
{
ownedUnitIds ??= new List<string>();
bannerPoolIds ??= new List<string>();
}
} }
} }
+139
View File
@@ -0,0 +1,139 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.TextCore.Text;
using UnityEngine.UIElements;
namespace KernelPanic.UI
{
/// <summary>
/// Fits monospace ASCII art into a fixed UI Toolkit label region.
/// </summary>
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<FontAsset, float> AdvanceRatioByFont = new();
private static readonly HashSet<FontAsset> 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<GeometryChangedEvent>(_ => 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);
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6f677d4ac6de4dca8e92a3efa687ff1b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,134 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
namespace KernelPanic.UI
{
/// <summary>
/// Keeps the ambient boot-log background filled by rewriting a fixed stack of labels.
/// </summary>
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<string> sourceLines;
private readonly List<Label> labels = new();
private IVisualElementScheduledItem tickSchedule;
private float lastRegionHeight;
private int cursor;
private bool reducedMotion;
private bool started;
public BackgroundLogRingBuffer(VisualElement layer, IReadOnlyList<string> sourceLines)
{
this.layer = layer;
this.sourceLines = sourceLines ?? Array.Empty<string>();
this.layer.RegisterCallback<GeometryChangedEvent>(HandleGeometryChanged);
}
public void Start(bool useReducedMotion)
{
reducedMotion = useReducedMotion;
started = true;
cursor = 0;
if (lastRegionHeight > 0f)
{
RebuildLabelsForHeight(lastRegionHeight);
Render();
}
RestartTick();
}
public void Stop()
{
started = false;
tickSchedule?.Pause();
tickSchedule = null;
}
private void HandleGeometryChanged(GeometryChangedEvent evt)
{
if (Mathf.Approximately(evt.newRect.height, evt.oldRect.height))
{
return;
}
lastRegionHeight = evt.newRect.height;
if (!started)
{
return;
}
RebuildLabelsForHeight(evt.newRect.height);
Render();
}
private void RebuildLabelsForHeight(float regionHeight)
{
int targetCount = Mathf.Max(ExtraLines, Mathf.CeilToInt(regionHeight / LineHeight) + ExtraLines);
if (labels.Count == targetCount)
{
return;
}
layer.Clear();
labels.Clear();
for (int i = 0; i < targetCount; i++)
{
Label label = new() { pickingMode = PickingMode.Ignore };
label.AddToClassList("background-log-line");
layer.Add(label);
labels.Add(label);
}
}
private void RestartTick()
{
tickSchedule?.Pause();
tickSchedule = null;
if (reducedMotion || sourceLines.Count == 0)
{
return;
}
tickSchedule = layer.schedule.Execute(Advance).Every(TickMilliseconds);
}
private void Advance()
{
if (sourceLines.Count == 0)
{
return;
}
cursor = (cursor + 1) % sourceLines.Count;
Render();
}
private void Render()
{
if (sourceLines.Count == 0)
{
foreach (Label label in labels)
{
label.text = string.Empty;
}
return;
}
for (int i = 0; i < labels.Count; i++)
{
labels[i].text = sourceLines[(cursor + i) % sourceLines.Count];
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 03ad9dd5bf1d46ee9cd2a6f54d17b5fa
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+108
View File
@@ -0,0 +1,108 @@
using KernelPanic.Data;
using UnityEngine;
using UnityEngine.TextCore.Text;
using UnityEngine.UIElements;
namespace KernelPanic.UI
{
/// <summary>
/// Applies terminal-safe rendering rules for distro ASCII art.
/// </summary>
public static class DistroArtPresenter
{
private static readonly string[][] DashClasses =
{
new[] { "dash", "dash-top", "dash-01" },
new[] { "dash", "dash-top", "dash-02" },
new[] { "dash", "dash-top", "dash-03" },
new[] { "dash", "dash-top", "dash-04" },
new[] { "dash", "dash-top", "dash-05" },
new[] { "dash", "dash-top", "dash-06" },
new[] { "dash", "dash-top", "dash-07" },
new[] { "dash", "dash-top", "dash-08" },
new[] { "dash", "dash-bottom", "dash-01" },
new[] { "dash", "dash-bottom", "dash-02" },
new[] { "dash", "dash-bottom", "dash-03" },
new[] { "dash", "dash-bottom", "dash-04" },
new[] { "dash", "dash-bottom", "dash-05" },
new[] { "dash", "dash-bottom", "dash-06" },
new[] { "dash", "dash-bottom", "dash-07" },
new[] { "dash", "dash-bottom", "dash-08" },
new[] { "dash", "dash-left", "dash-v01" },
new[] { "dash", "dash-left", "dash-v02" },
new[] { "dash", "dash-left", "dash-v03" },
new[] { "dash", "dash-left", "dash-v04" },
new[] { "dash", "dash-left", "dash-v05" },
new[] { "dash", "dash-left", "dash-v06" },
new[] { "dash", "dash-right", "dash-v01" },
new[] { "dash", "dash-right", "dash-v02" },
new[] { "dash", "dash-right", "dash-v03" },
new[] { "dash", "dash-right", "dash-v04" },
new[] { "dash", "dash-right", "dash-v05" },
new[] { "dash", "dash-right", "dash-v06" }
};
public static VisualElement CreatePlaceholder()
{
VisualElement placeholder = new();
placeholder.AddToClassList("ascii-placeholder");
for (int i = 0; i < DashClasses.Length; i++)
{
VisualElement dash = new();
for (int classIndex = 0; classIndex < DashClasses[i].Length; classIndex++)
{
dash.AddToClassList(DashClasses[i][classIndex]);
}
placeholder.Add(dash);
}
return placeholder;
}
public static void ConfigureArtLabel(Label label, FontAsset font)
{
label.enableRichText = false;
label.AddToClassList("ascii-art");
if (font != null)
{
label.style.unityFontDefinition = new StyleFontDefinition(font);
}
}
public static string Render(Label label, VisualElement placeholder, DistroDefinition unit)
{
string art = TryGetArtText(unit);
bool hasArt = !string.IsNullOrWhiteSpace(art);
label.text = hasArt ? art : string.Empty;
label.EnableInClassList("hidden", !hasArt);
placeholder.EnableInClassList("hidden", hasArt);
return hasArt ? art : null;
}
private static string TryGetArtText(DistroDefinition unit)
{
if (unit == null)
{
return null;
}
UnityEngine.TextAsset asciiArt = unit.AsciiArt;
if (asciiArt == null)
{
return null;
}
try
{
return asciiArt.text;
}
catch (MissingReferenceException)
{
return null;
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4bf334307f22465cad853cb6b3757468
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+13 -3
View File
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using KernelPanic.Data; using KernelPanic.Data;
using UnityEngine; using UnityEngine;
using UnityEngine.TextCore.Text;
using UnityEngine.UIElements; using UnityEngine.UIElements;
namespace KernelPanic.UI namespace KernelPanic.UI
@@ -13,11 +14,11 @@ namespace KernelPanic.UI
public sealed class FeaturedUnitPanel public sealed class FeaturedUnitPanel
{ {
private const string EmptyAccent = "5cff91"; private const string EmptyAccent = "5cff91";
private const string EmptyAscii = " _\n | |\n ___| |__\n / __| '_ \\\n \\__ \\ | | |\n |___/_| |_|";
private VisualElement panel; private VisualElement panel;
private Label titleLabel; private Label titleLabel;
private Label asciiLabel; private Label asciiLabel;
private VisualElement asciiPlaceholder;
private Label unitNameLabel; private Label unitNameLabel;
private Label languagesLabel; private Label languagesLabel;
private Label passiveLabel; private Label passiveLabel;
@@ -26,11 +27,14 @@ namespace KernelPanic.UI
private Label emptyHintLabel; private Label emptyHintLabel;
private VisualElement populatedState; private VisualElement populatedState;
private VisualElement emptyState; private VisualElement emptyState;
private FontAsset monospaceFont;
private AsciiArtFitter asciiFitter;
private IReadOnlyList<DistroDefinition> units = Array.Empty<DistroDefinition>(); private IReadOnlyList<DistroDefinition> units = Array.Empty<DistroDefinition>();
private int selectedIndex; private int selectedIndex;
public void Bind(VisualElement root) public void Bind(VisualElement root, FontAsset artFont)
{ {
monospaceFont = artFont;
panel = root.Q<VisualElement>("FeaturedUnitPanel"); panel = root.Q<VisualElement>("FeaturedUnitPanel");
titleLabel = root.Q<Label>("FeaturedUnitTitle"); titleLabel = root.Q<Label>("FeaturedUnitTitle");
asciiLabel = root.Q<Label>("FeaturedUnitAscii"); asciiLabel = root.Q<Label>("FeaturedUnitAscii");
@@ -42,6 +46,12 @@ namespace KernelPanic.UI
emptyHintLabel = root.Q<Label>("FeaturedUnitEmptyHint"); emptyHintLabel = root.Q<Label>("FeaturedUnitEmptyHint");
populatedState = root.Q<VisualElement>("FeaturedUnitPopulatedState"); populatedState = root.Q<VisualElement>("FeaturedUnitPopulatedState");
emptyState = root.Q<VisualElement>("FeaturedUnitEmptyState"); emptyState = root.Q<VisualElement>("FeaturedUnitEmptyState");
DistroArtPresenter.ConfigureArtLabel(asciiLabel, monospaceFont);
asciiFitter = new AsciiArtFitter(asciiLabel, monospaceFont);
asciiPlaceholder = DistroArtPresenter.CreatePlaceholder();
asciiPlaceholder.AddToClassList("hidden");
populatedState.Insert(0, asciiPlaceholder);
} }
public void Refresh(IReadOnlyList<DistroDefinition> ownedUnits) public void Refresh(IReadOnlyList<DistroDefinition> ownedUnits)
@@ -92,7 +102,7 @@ namespace KernelPanic.UI
string displayName = string.IsNullOrWhiteSpace(unit.DisplayName) ? unit.name : unit.DisplayName; string displayName = string.IsNullOrWhiteSpace(unit.DisplayName) ? unit.name : unit.DisplayName;
titleLabel.text = "[ neofetch ]"; titleLabel.text = "[ neofetch ]";
asciiLabel.text = EmptyAscii; // TODO: Replace with unit-specific ASCII copy when unit presentation data exists. asciiFitter.SetArt(DistroArtPresenter.Render(asciiLabel, asciiPlaceholder, unit));
unitNameLabel.text = displayName; unitNameLabel.text = displayName;
languagesLabel.text = $"{unit.PrimaryLanguage} / {unit.SecondaryLanguage}"; languagesLabel.text = $"{unit.PrimaryLanguage} / {unit.SecondaryLanguage}";
passiveLabel.text = string.IsNullOrWhiteSpace(unit.PassiveName) ? "--" : unit.PassiveName; passiveLabel.text = string.IsNullOrWhiteSpace(unit.PassiveName) ? "--" : unit.PassiveName;
+433 -1
View File
@@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using KernelPanic.Core; using KernelPanic.Core;
using KernelPanic.Data;
using KernelPanic.Meta; using KernelPanic.Meta;
using UnityEngine; using UnityEngine;
using UnityEngine.TextCore.Text; using UnityEngine.TextCore.Text;
@@ -26,9 +27,15 @@ namespace KernelPanic.UI
[SerializeField] private FontAsset monospaceFont; // TODO: Assign a real monospace FontAsset when typography assets exist. [SerializeField] private FontAsset monospaceFont; // TODO: Assign a real monospace FontAsset when typography assets exist.
[SerializeField] private string motdBody = "unstable userspace detected; keep a rollback shell open."; [SerializeField] private string motdBody = "unstable userspace detected; keep a rollback shell open.";
[SerializeField] private DistroDatabase distroDatabase;
[SerializeField] private FeaturedUnitPanel featuredUnitPanel = new(); [SerializeField] private FeaturedUnitPanel featuredUnitPanel = new();
private readonly List<CommandMenuEntry> commandEntries = new(); private readonly List<CommandMenuEntry> commandEntries = new();
private readonly List<VisualElement> starterCards = new();
private readonly List<Label> starterNames = new();
private readonly List<Label> starterLanguages = new();
private readonly List<Label> starterDescriptions = new();
private readonly List<VisualElement> collectionRows = new();
private UIDocument document; private UIDocument document;
private VisualElement root; private VisualElement root;
private VisualElement shellRoot; private VisualElement shellRoot;
@@ -39,6 +46,9 @@ namespace KernelPanic.UI
private VisualElement settingsPanel; private VisualElement settingsPanel;
private VisualElement eventBanner; private VisualElement eventBanner;
private VisualElement backgroundLogLayer; private VisualElement backgroundLogLayer;
private VisualElement starterModal;
private VisualElement collectionList;
private VisualElement collectionDetail;
private Label appIdLabel; private Label appIdLabel;
private Label entropyLabel; private Label entropyLabel;
private Label pullTokensLabel; private Label pullTokensLabel;
@@ -46,23 +56,32 @@ namespace KernelPanic.UI
private Label promptCursorLabel; private Label promptCursorLabel;
private Label bootIntroLogLabel; private Label bootIntroLogLabel;
private Label motdBodyLabel; private Label motdBodyLabel;
private Label starterConfirmLabel;
private VisualElement motdBlock; private VisualElement motdBlock;
private Button collectionBackButton; private Button collectionBackButton;
private Button gachaBackButton; private Button gachaBackButton;
private Button settingsBackButton; private Button settingsBackButton;
private SaveService saveService;
private SaveData saveData;
private EntropyWallet wallet; private EntropyWallet wallet;
private GachaService gachaService; private GachaService gachaService;
private PlayerCollection playerCollection; private PlayerCollection playerCollection;
private BackgroundLogRingBuffer backgroundLog; private BackgroundLogRingBuffer backgroundLog;
private IEventBannerSource eventBannerSource; private IEventBannerSource eventBannerSource;
private int selectedCommandIndex; private int selectedCommandIndex;
private int selectedStarterIndex;
private int selectedCollectionIndex;
private float bootIntroElapsed; private float bootIntroElapsed;
private int bootIntroCharacterCount; private int bootIntroCharacterCount;
private bool cursorVisible; private bool cursorVisible;
private bool suppressNextClick; private bool suppressNextClick;
private bool starterModalActive;
private bool starterConfirming;
private bool warnedUnresolvedSaveId;
private string bootIntroCopy; private string bootIntroCopy;
private IVisualElementScheduledItem blinkSchedule; private IVisualElementScheduledItem blinkSchedule;
private IVisualElementScheduledItem bootIntroSchedule; private IVisualElementScheduledItem bootIntroSchedule;
private IVisualElementScheduledItem starterCloseSchedule;
public void Initialize(EntropyWallet initializedWallet) public void Initialize(EntropyWallet initializedWallet)
{ {
@@ -73,13 +92,18 @@ namespace KernelPanic.UI
private void Awake() private void Awake()
{ {
document = GetComponent<UIDocument>(); document = GetComponent<UIDocument>();
saveService = new SaveService();
gachaService = new GachaService(); gachaService = new GachaService();
playerCollection = new PlayerCollection(); // TODO: Replace with persistent player-collection service composition. playerCollection = new PlayerCollection(); // TODO: Replace with persistent player-collection service composition.
Initialize(new EntropyWallet()); // TODO: Replace with persistent wallet service composition. Initialize(new EntropyWallet()); // TODO: Replace with persistent wallet service composition.
BindElements(); BindElements();
BindCommandEntries(); BindCommandEntries();
RegisterCommandEntryCallbacks(); RegisterCommandEntryCallbacks();
featuredUnitPanel.Bind(root); RegisterStarterCallbacks();
featuredUnitPanel.Bind(root, monospaceFont);
LoadMetaState();
playerCollection.Changed += HandleMetaStateChanged;
gachaService.BannerPoolChanged += HandleMetaStateChanged;
ApplyOptionalFont(); ApplyOptionalFont();
} }
@@ -92,6 +116,7 @@ namespace KernelPanic.UI
RefreshCurrencyReadouts(); RefreshCurrencyReadouts();
RefreshEventBanner(); RefreshEventBanner();
featuredUnitPanel.Refresh(playerCollection.OwnedUnits); featuredUnitPanel.Refresh(playerCollection.OwnedUnits);
RefreshCollection();
SelectCommand(0); SelectCommand(0);
ShowMainMenu(); ShowMainMenu();
StartAmbientSchedules(); StartAmbientSchedules();
@@ -104,6 +129,7 @@ namespace KernelPanic.UI
blinkSchedule?.Pause(); blinkSchedule?.Pause();
backgroundLog?.Stop(); backgroundLog?.Stop();
bootIntroSchedule?.Pause(); bootIntroSchedule?.Pause();
starterCloseSchedule?.Pause();
} }
private void BindElements() private void BindElements()
@@ -119,6 +145,9 @@ namespace KernelPanic.UI
motdBlock = root.Q<VisualElement>("MotdBlock"); motdBlock = root.Q<VisualElement>("MotdBlock");
backgroundLogLayer = root.Q<VisualElement>("BackgroundLogLayer"); backgroundLogLayer = root.Q<VisualElement>("BackgroundLogLayer");
backgroundLog = new BackgroundLogRingBuffer(backgroundLogLayer, BootLogCopy.Lines); backgroundLog = new BackgroundLogRingBuffer(backgroundLogLayer, BootLogCopy.Lines);
starterModal = root.Q<VisualElement>("StarterModal");
collectionList = root.Q<VisualElement>("CollectionList");
collectionDetail = root.Q<VisualElement>("CollectionDetail");
appIdLabel = root.Q<Label>("AppIdLabel"); appIdLabel = root.Q<Label>("AppIdLabel");
entropyLabel = root.Q<Label>("EntropyLabel"); entropyLabel = root.Q<Label>("EntropyLabel");
@@ -127,6 +156,15 @@ namespace KernelPanic.UI
promptCursorLabel = root.Q<Label>("PromptCursorLabel"); promptCursorLabel = root.Q<Label>("PromptCursorLabel");
bootIntroLogLabel = root.Q<Label>("BootIntroLogLabel"); bootIntroLogLabel = root.Q<Label>("BootIntroLogLabel");
motdBodyLabel = root.Q<Label>("MotdBodyLabel"); motdBodyLabel = root.Q<Label>("MotdBodyLabel");
starterConfirmLabel = root.Q<Label>("StarterConfirmLabel");
for (int i = 0; i < 3; i++)
{
starterCards.Add(root.Q<VisualElement>($"StarterOption{i}"));
starterNames.Add(root.Q<Label>($"StarterName{i}"));
starterLanguages.Add(root.Q<Label>($"StarterLanguages{i}"));
starterDescriptions.Add(root.Q<Label>($"StarterDescription{i}"));
}
collectionBackButton = root.Q<Button>("CollectionBackButton"); collectionBackButton = root.Q<Button>("CollectionBackButton");
gachaBackButton = root.Q<Button>("GachaBackButton"); gachaBackButton = root.Q<Button>("GachaBackButton");
@@ -171,6 +209,77 @@ namespace KernelPanic.UI
settingsBackButton.clicked -= ShowMainMenu; settingsBackButton.clicked -= ShowMainMenu;
} }
private void LoadMetaState()
{
saveData = saveService.Load();
saveData.EnsureLists();
for (int i = 0; i < saveData.ownedUnitIds.Count; i++)
{
DistroDefinition unit = ResolveSavedDistro(saveData.ownedUnitIds[i]);
if (unit != null)
{
playerCollection.Add(unit);
}
}
for (int i = 0; i < saveData.bannerPoolIds.Count; i++)
{
DistroDefinition unit = ResolveSavedDistro(saveData.bannerPoolIds[i]);
if (unit != null)
{
gachaService.AddToBannerPool(unit);
}
}
}
private DistroDefinition ResolveSavedDistro(string id)
{
DistroDefinition unit = distroDatabase == null ? null : distroDatabase.FindById(id);
if (unit == null && !warnedUnresolvedSaveId)
{
warnedUnresolvedSaveId = true;
Debug.LogWarning($"Save references a distro id that is not in DistroDatabase: {id}");
}
return unit;
}
private void HandleMetaStateChanged()
{
SaveCurrentState();
featuredUnitPanel.Refresh(playerCollection.OwnedUnits);
RefreshCollection();
}
private void SaveCurrentState()
{
saveData ??= SaveData.CreateDefault();
saveData.EnsureLists();
saveData.ownedUnitIds.Clear();
saveData.bannerPoolIds.Clear();
for (int i = 0; i < playerCollection.OwnedUnits.Count; i++)
{
DistroDefinition unit = playerCollection.OwnedUnits[i];
if (unit != null && !string.IsNullOrWhiteSpace(unit.Id))
{
saveData.ownedUnitIds.Add(unit.Id);
}
}
for (int i = 0; i < gachaService.BannerPool.Count; i++)
{
DistroDefinition unit = gachaService.BannerPool[i];
if (unit != null && !string.IsNullOrWhiteSpace(unit.Id))
{
saveData.bannerPoolIds.Add(unit.Id);
}
}
saveService.Save(saveData);
}
private void RefreshStaticText() private void RefreshStaticText()
{ {
appIdLabel.text = $"kernel-panic v{Application.version} - tty1"; appIdLabel.text = $"kernel-panic v{Application.version} - tty1";
@@ -189,6 +298,20 @@ namespace KernelPanic.UI
} }
} }
private void RegisterStarterCallbacks()
{
for (int i = 0; i < starterCards.Count; i++)
{
int index = i;
starterCards[i].RegisterCallback<PointerEnterEvent>(_ => SelectStarter(index));
starterCards[i].RegisterCallback<ClickEvent>(_ =>
{
SelectStarter(index);
ConfirmStarterSelectionIntent();
});
}
}
private void RefreshCurrencyReadouts() private void RefreshCurrencyReadouts()
{ {
if (entropyLabel == null || pullTokensLabel == null || gachaService == null || wallet == null) if (entropyLabel == null || pullTokensLabel == null || gachaService == null || wallet == null)
@@ -215,6 +338,131 @@ namespace KernelPanic.UI
: banner.Text; : banner.Text;
} }
private void RefreshCollection()
{
if (collectionList == null || collectionDetail == null)
{
return;
}
collectionRows.Clear();
collectionList.Clear();
IReadOnlyList<DistroDefinition> units = playerCollection.OwnedUnits;
if (units.Count == 0)
{
collectionDetail.Clear();
collectionDetail.Add(new Label("no units installed") { name = "CollectionEmptyTitle" });
collectionDetail.Add(new Label("run: curl gacha.sh | sh") { name = "CollectionEmptyHint" });
return;
}
selectedCollectionIndex = Mathf.Clamp(selectedCollectionIndex, 0, units.Count - 1);
for (int i = 0; i < units.Count; i++)
{
int index = i;
DistroDefinition unit = units[i];
VisualElement row = new();
row.AddToClassList("collection-row");
row.RegisterCallback<PointerEnterEvent>(_ => SelectCollectionUnit(index));
row.RegisterCallback<ClickEvent>(_ => SelectCollectionUnit(index));
Label name = new(DisplayName(unit));
name.AddToClassList("collection-row-name");
name.style.color = new StyleColor(unit.AccentColor);
Label languages = new(FormatLanguages(unit));
languages.AddToClassList("collection-row-languages");
row.Add(name);
row.Add(languages);
collectionList.Add(row);
collectionRows.Add(row);
}
SelectCollectionUnit(selectedCollectionIndex);
}
private void SelectCollectionUnit(int index)
{
IReadOnlyList<DistroDefinition> units = playerCollection.OwnedUnits;
if (units.Count == 0)
{
return;
}
selectedCollectionIndex = Mathf.Clamp(index, 0, units.Count - 1);
for (int i = 0; i < collectionRows.Count; i++)
{
collectionRows[i].EnableInClassList(SelectedClassName, i == selectedCollectionIndex);
}
RenderCollectionDetail(units[selectedCollectionIndex]);
}
private void RenderCollectionDetail(DistroDefinition unit)
{
collectionDetail.Clear();
// TODO: Unify this readout with FeaturedUnitPanel when unit presentation grows beyond labels.
VisualElement readout = new();
readout.AddToClassList("collection-detail-readout");
Label artLabel = new();
DistroArtPresenter.ConfigureArtLabel(artLabel, monospaceFont);
AsciiArtFitter artFitter = new(artLabel, monospaceFont);
VisualElement artPlaceholder = DistroArtPresenter.CreatePlaceholder();
artFitter.SetArt(DistroArtPresenter.Render(artLabel, artPlaceholder, unit));
readout.Add(artPlaceholder);
readout.Add(artLabel);
VisualElement details = new();
details.AddToClassList("collection-detail-values");
Label name = new(DisplayName(unit));
name.AddToClassList("collection-detail-name");
name.style.color = new StyleColor(unit.AccentColor);
details.Add(name);
details.Add(BuildDetailLine("lang", FormatLanguages(unit)));
details.Add(BuildDetailLine("passive", string.IsNullOrWhiteSpace(unit.PassiveName) ? "--" : unit.PassiveName));
details.Add(BuildDetailLine("uptime", unit.BaseUptime.ToString()));
details.Add(BuildDetailLine("ram", unit.BaseRam.ToString()));
details.Add(BuildDetailLine("cycles", unit.BaseCyclesPerTurn.ToString()));
readout.Add(details);
collectionDetail.Add(readout);
Label description = new(string.IsNullOrWhiteSpace(unit.Description) ? "--" : unit.Description);
description.AddToClassList("collection-detail-description");
collectionDetail.Add(description);
}
private static VisualElement BuildDetailLine(string key, string value)
{
VisualElement row = new();
row.AddToClassList("kv-row");
Label keyLabel = new(key);
keyLabel.AddToClassList("kv-key");
Label valueLabel = new(value);
valueLabel.AddToClassList("kv-value");
row.Add(keyLabel);
row.Add(valueLabel);
return row;
}
private static string DisplayName(DistroDefinition unit)
{
return string.IsNullOrWhiteSpace(unit.DisplayName) ? unit.name : unit.DisplayName;
}
private static string FormatLanguages(DistroDefinition unit)
{
return $"{unit.PrimaryLanguage} / {unit.SecondaryLanguage}";
}
private void StartAmbientSchedules() private void StartAmbientSchedules()
{ {
blinkSchedule?.Pause(); blinkSchedule?.Pause();
@@ -277,6 +525,7 @@ namespace KernelPanic.UI
shellRoot.RemoveFromClassList("boot-hidden"); shellRoot.RemoveFromClassList("boot-hidden");
shellRoot.AddToClassList("boot-visible"); shellRoot.AddToClassList("boot-visible");
backgroundLog?.Start(UIPreferences.ReducedMotion); backgroundLog?.Start(UIPreferences.ReducedMotion);
ShowStarterModalIfNeeded();
root.Focus(); root.Focus();
} }
@@ -299,6 +548,13 @@ namespace KernelPanic.UI
return; return;
} }
if (starterModalActive)
{
HandleStarterKeyDown(evt);
evt.StopPropagation();
return;
}
if (evt.keyCode == KeyCode.UpArrow) if (evt.keyCode == KeyCode.UpArrow)
{ {
SelectCommand(selectedCommandIndex - 1); SelectCommand(selectedCommandIndex - 1);
@@ -342,6 +598,181 @@ namespace KernelPanic.UI
suppressNextClick = SkipBootIntro(); suppressNextClick = SkipBootIntro();
} }
private void ShowStarterModalIfNeeded()
{
if (saveData == null || saveData.starterChosen)
{
return;
}
IReadOnlyList<DistroDefinition> starters = GetStarterDistros();
if (starters.Count < 3)
{
Debug.LogWarning("Starter distro selection needs three distros in DistroDatabase.");
return;
}
starterModalActive = true;
starterConfirming = false;
starterConfirmLabel.AddToClassList(HiddenClassName);
starterModal.RemoveFromClassList(HiddenClassName);
for (int i = 0; i < starterCards.Count; i++)
{
DistroDefinition unit = starters[i];
starterNames[i].text = DisplayName(unit);
starterNames[i].style.color = new StyleColor(unit.AccentColor);
starterLanguages[i].text = FormatLanguages(unit);
starterDescriptions[i].text = unit.Description;
}
SelectStarter(0);
root.Focus();
}
private IReadOnlyList<DistroDefinition> GetStarterDistros()
{
return distroDatabase == null ? Array.Empty<DistroDefinition>() : distroDatabase.AllDistros;
}
private void SelectStarter(int index)
{
IReadOnlyList<DistroDefinition> starters = GetStarterDistros();
if (starters.Count == 0)
{
return;
}
selectedStarterIndex = (index + Mathf.Min(starters.Count, starterCards.Count)) % Mathf.Min(starters.Count, starterCards.Count);
starterConfirming = false;
starterConfirmLabel.AddToClassList(HiddenClassName);
for (int i = 0; i < starterCards.Count; i++)
{
bool selected = i == selectedStarterIndex;
starterCards[i].EnableInClassList(SelectedClassName, selected);
if (i < starters.Count && selected)
{
StyleColor accent = new(starters[i].AccentColor);
starterCards[i].style.borderTopColor = accent;
starterCards[i].style.borderRightColor = accent;
starterCards[i].style.borderBottomColor = accent;
starterCards[i].style.borderLeftColor = accent;
}
else
{
starterCards[i].style.borderTopColor = StyleKeyword.Null;
starterCards[i].style.borderRightColor = StyleKeyword.Null;
starterCards[i].style.borderBottomColor = StyleKeyword.Null;
starterCards[i].style.borderLeftColor = StyleKeyword.Null;
}
}
}
private void HandleStarterKeyDown(KeyDownEvent evt)
{
if (evt.keyCode == KeyCode.LeftArrow)
{
SelectStarter(selectedStarterIndex - 1);
return;
}
if (evt.keyCode == KeyCode.RightArrow)
{
SelectStarter(selectedStarterIndex + 1);
return;
}
int digitIndex = GetDigitCommandIndex(evt.keyCode);
if (digitIndex >= 0 && digitIndex < 3)
{
SelectStarter(digitIndex);
return;
}
if (evt.keyCode == KeyCode.Return || evt.keyCode == KeyCode.KeypadEnter)
{
if (starterConfirming)
{
ConfirmStarterSelection();
return;
}
ConfirmStarterSelectionIntent();
return;
}
if (evt.keyCode == KeyCode.Y && starterConfirming)
{
ConfirmStarterSelection();
return;
}
if (evt.keyCode == KeyCode.Escape || evt.keyCode == KeyCode.N)
{
starterConfirming = false;
starterConfirmLabel.AddToClassList(HiddenClassName);
}
}
private void ConfirmStarterSelectionIntent()
{
IReadOnlyList<DistroDefinition> starters = GetStarterDistros();
if (selectedStarterIndex >= starters.Count)
{
return;
}
starterConfirming = true;
starterConfirmLabel.text = $"install {DisplayName(starters[selectedStarterIndex])}? this cannot be undone [Y/n]";
starterConfirmLabel.RemoveFromClassList(HiddenClassName);
}
private void ConfirmStarterSelection()
{
IReadOnlyList<DistroDefinition> starters = GetStarterDistros();
if (selectedStarterIndex >= starters.Count)
{
return;
}
DistroDefinition picked = starters[selectedStarterIndex];
playerCollection.Add(picked);
for (int i = 0; i < starters.Count; i++)
{
if (i != selectedStarterIndex)
{
gachaService.AddToBannerPool(starters[i]);
}
}
saveData.starterChosen = true;
SaveCurrentState();
featuredUnitPanel.Refresh(playerCollection.OwnedUnits);
RefreshCollection();
starterConfirmLabel.text = $"installing {picked.Id} (1/1)... done";
starterConfirmLabel.RemoveFromClassList(HiddenClassName);
if (UIPreferences.ReducedMotion)
{
CloseStarterModal();
return;
}
starterCloseSchedule?.Pause();
starterCloseSchedule = root.schedule.Execute(CloseStarterModal).StartingIn(600);
}
private void CloseStarterModal()
{
starterCloseSchedule?.Pause();
starterCloseSchedule = null;
starterModalActive = false;
starterConfirming = false;
starterModal.AddToClassList(HiddenClassName);
root.Focus();
}
private static int GetDigitCommandIndex(KeyCode keyCode) private static int GetDigitCommandIndex(KeyCode keyCode)
{ {
return keyCode switch return keyCode switch
@@ -402,6 +833,7 @@ namespace KernelPanic.UI
private void ShowCollection() private void ShowCollection()
{ {
RefreshCollection();
ShowPanel(collectionPanel); ShowPanel(collectionPanel);
} }
+4 -4
View File
@@ -1,10 +1,10 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="False"> <ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="False">
<ui:VisualElement name="CollectionPanel" class="sub-panel hidden"> <ui:VisualElement name="CollectionPanel" class="sub-panel hidden">
<ui:Label text="COLLECTION" class="screen-title" /> <ui:Label text="COLLECTION" class="screen-title" />
<ui:ScrollView name="CollectionScrollView" class="stub-scroll"> <ui:VisualElement name="CollectionContent" class="collection-shell">
<!-- TODO: Render owned distros and cards here when collection data exists. --> <ui:ScrollView name="CollectionList" class="collection-list" />
<ui:VisualElement name="CollectionContent" class="collection-content" /> <ui:VisualElement name="CollectionDetail" class="collection-detail" />
</ui:ScrollView> </ui:VisualElement>
<ui:Button name="CollectionBackButton" text="Back" class="terminal-button back-button" /> <ui:Button name="CollectionBackButton" text="Back" class="terminal-button back-button" />
</ui:VisualElement> </ui:VisualElement>
</ui:UXML> </ui:UXML>
+165 -1
View File
@@ -277,8 +277,13 @@ Button {
.ascii-art { .ascii-art {
width: 150px; width: 150px;
min-width: 150px; min-width: 150px;
height: 150px;
color: var(--kp-primary); color: var(--kp-primary);
white-space: normal; font-size: 14px;
line-height: 105%;
white-space: nowrap;
overflow: hidden;
-unity-paragraph-spacing: 0;
} }
.ascii-placeholder { .ascii-placeholder {
@@ -470,6 +475,165 @@ Button {
min-height: 280px; min-height: 280px;
} }
.collection-shell {
width: 760px;
height: 330px;
flex-direction: row;
}
.collection-list {
width: 300px;
margin-right: 16px;
border-left-width: 1px;
border-right-width: 1px;
border-top-width: 1px;
border-bottom-width: 1px;
border-left-color: var(--kp-border);
border-right-color: var(--kp-border);
border-top-color: var(--kp-border);
border-bottom-color: var(--kp-border);
background-color: var(--kp-panel);
}
.collection-detail {
flex-grow: 1;
padding: 16px;
border-left-width: 1px;
border-right-width: 1px;
border-top-width: 1px;
border-bottom-width: 1px;
border-left-color: var(--kp-border);
border-right-color: var(--kp-border);
border-top-color: var(--kp-border);
border-bottom-color: var(--kp-border);
background-color: var(--kp-panel);
}
.collection-detail-readout {
flex-direction: row;
}
.collection-detail-values {
flex-grow: 1;
min-width: 0;
}
.collection-row {
min-height: 44px;
padding: 8px 10px;
border-left-width: 2px;
border-left-color: transparent;
}
.collection-row.selected {
background-color: var(--kp-hover);
border-left-color: var(--kp-primary);
}
.collection-row-name {
font-size: 16px;
-unity-font-style: bold;
}
.collection-row-languages,
.collection-detail-muted {
color: var(--kp-muted);
}
.collection-detail-name {
font-size: 22px;
-unity-font-style: bold;
margin-bottom: 8px;
}
.collection-detail-description {
color: var(--kp-primary);
white-space: normal;
margin-top: 14px;
}
.modal-overlay {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
align-items: center;
justify-content: center;
background-color: rgba(5, 8, 7, 0.88);
}
.starter-dialog {
width: 900px;
padding: 22px;
border-left-width: 1px;
border-right-width: 1px;
border-top-width: 1px;
border-bottom-width: 1px;
border-left-color: var(--kp-primary);
border-right-color: var(--kp-primary);
border-top-color: var(--kp-primary);
border-bottom-color: var(--kp-primary);
background-color: var(--kp-bg-solid);
}
.starter-title {
color: var(--kp-primary);
font-size: 24px;
-unity-font-style: bold;
margin-bottom: 8px;
}
.starter-instruction {
color: var(--kp-muted);
margin-bottom: 18px;
}
.starter-options {
flex-direction: row;
}
.starter-card {
width: 270px;
min-height: 190px;
margin-right: 14px;
padding: 14px;
border-left-width: 1px;
border-right-width: 1px;
border-top-width: 1px;
border-bottom-width: 1px;
border-left-color: var(--kp-border);
border-right-color: var(--kp-border);
border-top-color: var(--kp-border);
border-bottom-color: var(--kp-border);
background-color: var(--kp-panel);
}
.starter-card.selected {
background-color: var(--kp-hover);
}
.starter-name {
font-size: 20px;
-unity-font-style: bold;
margin-bottom: 8px;
}
.starter-languages {
color: var(--kp-muted);
margin-bottom: 12px;
}
.starter-description {
color: var(--kp-primary);
white-space: normal;
}
.starter-confirm {
margin-top: 18px;
color: var(--kp-amber);
}
.terminal-button { .terminal-button {
height: 42px; height: 42px;
margin-top: 24px; margin-top: 24px;
+25
View File
@@ -144,5 +144,30 @@
<ui:Label text="daily seed: --------" class="footer-muted" /> <ui:Label text="daily seed: --------" class="footer-muted" />
</ui:VisualElement> </ui:VisualElement>
</ui:VisualElement> </ui:VisualElement>
<ui:VisualElement name="StarterModal" class="modal-overlay hidden">
<ui:VisualElement class="starter-dialog">
<ui:Label text="select starter distro" class="starter-title" />
<ui:Label text="choose one base system to install into persistent userspace" class="starter-instruction" />
<ui:VisualElement name="StarterOptions" class="starter-options">
<ui:VisualElement name="StarterOption0" class="starter-card">
<ui:Label name="StarterName0" class="starter-name" />
<ui:Label name="StarterLanguages0" class="starter-languages" />
<ui:Label name="StarterDescription0" class="starter-description" />
</ui:VisualElement>
<ui:VisualElement name="StarterOption1" class="starter-card">
<ui:Label name="StarterName1" class="starter-name" />
<ui:Label name="StarterLanguages1" class="starter-languages" />
<ui:Label name="StarterDescription1" class="starter-description" />
</ui:VisualElement>
<ui:VisualElement name="StarterOption2" class="starter-card">
<ui:Label name="StarterName2" class="starter-name" />
<ui:Label name="StarterLanguages2" class="starter-languages" />
<ui:Label name="StarterDescription2" class="starter-description" />
</ui:VisualElement>
</ui:VisualElement>
<ui:Label name="StarterConfirmLabel" class="starter-confirm hidden" />
</ui:VisualElement>
</ui:VisualElement>
</ui:VisualElement> </ui:VisualElement>
</ui:UXML> </ui:UXML>
+1 -1
View File
@@ -678,7 +678,7 @@ PlayerSettings:
qnxGraphicConfPath: qnxGraphicConfPath:
apiCompatibilityLevel: 6 apiCompatibilityLevel: 6
captureStartupLogs: {} captureStartupLogs: {}
activeInputHandler: 1 activeInputHandler: 2
windowsGamepadBackendHint: 0 windowsGamepadBackendHint: 0
enableDirectStorage: 0 enableDirectStorage: 0
cloudProjectId: f53bdd3c-833a-46a8-ad19-0180dc4af0dd cloudProjectId: f53bdd3c-833a-46a8-ad19-0180dc4af0dd