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:
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b5d4e612df874934a2fe5e4ce5cf43de
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 296f57316cdb4f9ba2227b877b091c23
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
%#%#%#%#%#
|
||||||
|
%#%#%#%%#%#%#%#%#%
|
||||||
|
%#%#%%#%#%#% *%#%
|
||||||
|
#%#%%#%#%#% ***%#
|
||||||
|
%%#%#%#%%#% %#%#%#****%#
|
||||||
|
%#%#%#%#%#% %#%%#%%***%%#
|
||||||
|
#%#%%#%#%#%# %#%#%#%***%#%%
|
||||||
|
#%%#%#%%#%#% %#%#%****#%#%#
|
||||||
|
%#%#**** ****%#%#%#
|
||||||
|
%#%****%#%#% %#%#%%%#%#%%#%
|
||||||
|
#%***%%#%#%# %#%%#%#%#%#%#
|
||||||
|
%#***#%#%%#% %#%#%#%#%#%#
|
||||||
|
%%****%%#%#% #%#%#%%#%#%#
|
||||||
|
#%%*** %%#%#%#%#%%#
|
||||||
|
%#%#%* %#%#%%#%#%# *
|
||||||
|
%#%#%#%#%#%#%#%%#%#
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f2373174b6954f2d9088d83d10e0d9e4
|
||||||
|
TextScriptImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
|
||||||
|
:======:
|
||||||
|
==============
|
||||||
|
.==================.
|
||||||
|
=== =================
|
||||||
|
==== === ====
|
||||||
|
==== ==: == ..== .====
|
||||||
|
===== .==: == .== =====
|
||||||
|
===== ==: == .== =====
|
||||||
|
==== ==: == ..== .====
|
||||||
|
==== ========== ====
|
||||||
|
===== .-====
|
||||||
|
.==================.
|
||||||
|
==============
|
||||||
|
:======:
|
||||||
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 412c9be402ec4d8c8510e71066ec22ff
|
||||||
|
TextScriptImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
|
||||||
|
============
|
||||||
|
==================
|
||||||
|
============== =====
|
||||||
|
========= = ======
|
||||||
|
======= ===== =======
|
||||||
|
====== ======== ======
|
||||||
|
== = ========== =====
|
||||||
|
== = ========== =====
|
||||||
|
====== ======== ======
|
||||||
|
======= ===== =======
|
||||||
|
========= = ======
|
||||||
|
============== =====
|
||||||
|
==================
|
||||||
|
============
|
||||||
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 975f1f5de289435eb81c91c3eddd31dc
|
||||||
|
TextScriptImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -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:
|
||||||
@@ -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: []
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 245dfb73afe047d5a3a3ef54c30d5135
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -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: []
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 366e71361387449abcfaa1a50c9af87b
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -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: []
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 1b0892a0d4c34b03a54c3d511ae59356
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -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: {}
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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:
|
||||||
@@ -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:
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user