Implement animations, pipeline for duplicates and merging.
This commit is contained in:
@@ -18,11 +18,14 @@ namespace KernelPanic.UI
|
||||
private readonly List<VisualElement> _rows = new();
|
||||
|
||||
private VisualElement _list;
|
||||
private ScrollView _detailScroll;
|
||||
private VisualElement _detail;
|
||||
private FontAsset _monospaceFont;
|
||||
private LanguageDeckDatabase _languageDeckDatabase;
|
||||
private CardDatabase _cardDatabase;
|
||||
private PlayerCollection _playerCollection;
|
||||
private Func<int> _getMergesBalance;
|
||||
private Func<DistroDefinition, VersionUpgradeResult> _tryUpgradeUnit;
|
||||
private IReadOnlyList<DistroDefinition> _units = Array.Empty<DistroDefinition>();
|
||||
private IReadOnlyList<LanguageCatalogEntry> _languages = LanguageCatalog.All;
|
||||
private IReadOnlyList<CardEntry> _cards = Array.Empty<CardEntry>();
|
||||
@@ -35,14 +38,17 @@ namespace KernelPanic.UI
|
||||
|
||||
public event Action ViewChanged;
|
||||
|
||||
public void Bind(VisualElement root, FontAsset artFont, LanguageDeckDatabase deckDatabase, CardDatabase cardsDatabase, PlayerCollection collection)
|
||||
public void Bind(VisualElement root, FontAsset artFont, LanguageDeckDatabase deckDatabase, CardDatabase cardsDatabase, PlayerCollection collection, Func<int> getMergesBalance = null, Func<DistroDefinition, VersionUpgradeResult> tryUpgradeUnit = null)
|
||||
{
|
||||
_monospaceFont = artFont;
|
||||
_languageDeckDatabase = deckDatabase;
|
||||
_cardDatabase = cardsDatabase;
|
||||
_playerCollection = collection;
|
||||
_getMergesBalance = getMergesBalance;
|
||||
_tryUpgradeUnit = tryUpgradeUnit;
|
||||
_list = root.Q<VisualElement>("CollectionList");
|
||||
_detail = root.Q<VisualElement>("CollectionDetail");
|
||||
_detailScroll = root.Q<ScrollView>("CollectionDetail");
|
||||
_detail = _detailScroll?.contentContainer;
|
||||
}
|
||||
|
||||
public bool IsCardSubview => _mode == CollectionMode.CardSubview;
|
||||
@@ -282,7 +288,79 @@ namespace KernelPanic.UI
|
||||
|
||||
readout.Add(details);
|
||||
_detail.Add(readout);
|
||||
_detail.Add(BuildSubviewCommand($"> cat ~/units/{unit.Id}/cards", "exclusive packages", HasListableCards(unit), () => OpenUnitCards(unit), "no packages installed"));
|
||||
AddVersionUpgradePanel(_detail, unit);
|
||||
_detail.Add(BuildSubviewCommand($"cat ~/units/{unit.Id}/cards", "exclusive packages", HasListableCards(unit), () => OpenUnitCards(unit), "no packages installed"));
|
||||
}
|
||||
|
||||
private void AddVersionUpgradePanel(VisualElement target, DistroDefinition unit)
|
||||
{
|
||||
int version = _playerCollection == null ? 1 : Mathf.Clamp(_playerCollection.GetVersion(unit.Id), 1, GachaTuning.MaxVersion);
|
||||
string release = DistroVersionCatalog.GetReleaseLabel(unit.Id, version);
|
||||
int merges = _getMergesBalance?.Invoke() ?? 0;
|
||||
|
||||
VisualElement panel = new();
|
||||
panel.AddToClassList("version-upgrade-panel");
|
||||
|
||||
VisualElement summary = new();
|
||||
summary.AddToClassList("version-upgrade-summary");
|
||||
|
||||
Label current = new($"{DistroPresentation.DisplayName(unit)} {release} v{version}/{GachaTuning.MaxVersion}");
|
||||
current.AddToClassList("version-upgrade-current");
|
||||
summary.Add(current);
|
||||
|
||||
Label wallet = new(version < GachaTuning.MaxVersion ? $"merges {merges} / {GachaTuning.GetVersionUpgradeCost(version + 1)}" : $"merges {merges}");
|
||||
wallet.AddToClassList("version-upgrade-wallet");
|
||||
summary.Add(wallet);
|
||||
panel.Add(summary);
|
||||
|
||||
if (version < GachaTuning.MaxVersion)
|
||||
{
|
||||
int targetVersion = version + 1;
|
||||
int cost = GachaTuning.GetVersionUpgradeCost(targetVersion);
|
||||
Label next = new($"next {DistroVersionCatalog.GetReleaseLabel(unit.Id, targetVersion)} - {DistroVersionCatalog.GetEffectSummary(unit.Id, targetVersion)}");
|
||||
next.AddToClassList("version-upgrade-next");
|
||||
panel.Add(next);
|
||||
|
||||
bool affordable = merges >= cost;
|
||||
wallet.EnableInClassList(affordable ? "version-upgrade-wallet-affordable" : "version-upgrade-wallet-warning", true);
|
||||
panel.Add(BuildCompactUpgradeCommand($"> apt full-upgrade ({cost} merges)", affordable ? "ready" : "insufficient merges", affordable && _tryUpgradeUnit != null, () => UpgradeUnit(unit)));
|
||||
}
|
||||
else
|
||||
{
|
||||
Label next = new("latest release installed");
|
||||
next.AddToClassList("version-upgrade-next");
|
||||
panel.Add(next);
|
||||
panel.Add(BuildCompactUpgradeCommand("> apt full-upgrade", "latest release", false, null));
|
||||
}
|
||||
|
||||
VisualElement path = new();
|
||||
path.AddToClassList("version-path");
|
||||
for (int i = 1; i <= GachaTuning.MaxVersion; i++)
|
||||
{
|
||||
string className = i <= version ? "version-path-owned" : i == version + 1 ? "version-path-next" : "version-path-locked";
|
||||
Label step = new($"v{i} {DistroVersionCatalog.GetReleaseLabel(unit.Id, i)}");
|
||||
step.AddToClassList("version-path-step");
|
||||
step.AddToClassList(className);
|
||||
path.Add(step);
|
||||
}
|
||||
|
||||
panel.Add(path);
|
||||
target.Add(panel);
|
||||
}
|
||||
|
||||
private void UpgradeUnit(DistroDefinition unit)
|
||||
{
|
||||
if (unit == null || _tryUpgradeUnit == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
VersionUpgradeResult result = _tryUpgradeUnit(unit);
|
||||
RenderUnitDetail(unit);
|
||||
if (!result.Success)
|
||||
{
|
||||
_detail.Add(BuildDetailLine("upgrade", FormatUpgradeFailure(result.FailureReason)));
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderLanguageDetail(LanguageCatalogEntry language)
|
||||
@@ -310,7 +388,7 @@ namespace KernelPanic.UI
|
||||
return;
|
||||
}
|
||||
|
||||
_detail.Add(BuildSubviewCommand($"> cat ~/lang/{GetLanguageId(language.Language)}/deck", "starter deck", true, () => OpenLanguageDeck(language), null));
|
||||
_detail.Add(BuildSubviewCommand($"cat ~/lang/{GetLanguageId(language.Language)}/deck", "starter deck", true, () => OpenLanguageDeck(language), null));
|
||||
}
|
||||
|
||||
private void OpenUnitCards(DistroDefinition unit)
|
||||
@@ -463,6 +541,28 @@ namespace KernelPanic.UI
|
||||
return row;
|
||||
}
|
||||
|
||||
private static VisualElement BuildCompactUpgradeCommand(string command, string state, bool enabled, Action action)
|
||||
{
|
||||
VisualElement row = new();
|
||||
row.AddToClassList("version-upgrade-command");
|
||||
row.EnableInClassList("disabled-row", !enabled);
|
||||
|
||||
Label commandLabel = new(command);
|
||||
commandLabel.AddToClassList("version-upgrade-command-text");
|
||||
Label stateLabel = new(state);
|
||||
stateLabel.AddToClassList("version-upgrade-command-state");
|
||||
|
||||
row.Add(commandLabel);
|
||||
row.Add(stateLabel);
|
||||
|
||||
if (enabled)
|
||||
{
|
||||
row.RegisterCallback<ClickEvent>(_ => action?.Invoke());
|
||||
}
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
private static void AddPassiveDetails(VisualElement target, DistroDefinition unit)
|
||||
{
|
||||
if (unit.Passive == null)
|
||||
@@ -600,6 +700,17 @@ namespace KernelPanic.UI
|
||||
return card == null ? "--" : $"{card.Language} / {card.CycleCost}c";
|
||||
}
|
||||
|
||||
private static string FormatUpgradeFailure(VersionUpgradeFailureReason reason)
|
||||
{
|
||||
return reason switch
|
||||
{
|
||||
VersionUpgradeFailureReason.NotOwned => "not owned",
|
||||
VersionUpgradeFailureReason.MaxVersion => "latest release",
|
||||
VersionUpgradeFailureReason.InsufficientMerges => "insufficient merges",
|
||||
_ => "upgrade failed"
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetLanguageId(Language language)
|
||||
{
|
||||
return language switch
|
||||
|
||||
@@ -20,6 +20,7 @@ namespace KernelPanic.UI
|
||||
public sealed class CombatSceneController : MonoBehaviour
|
||||
{
|
||||
private const string StyleResourcePath = "CombatScene";
|
||||
private const string SharedScrollbarStyleResourcePath = "TerminalScrollbars";
|
||||
private const string FilledCycle = "●";
|
||||
private const string EmptyCycle = "○";
|
||||
|
||||
@@ -48,8 +49,18 @@ namespace KernelPanic.UI
|
||||
private VisualElement handRow;
|
||||
private VisualElement turnResourceGrid;
|
||||
private Label logLabel;
|
||||
private VisualElement feedbackLayer;
|
||||
private VisualElement damageVignette;
|
||||
private VisualElement overlay;
|
||||
private Color distroAccent;
|
||||
private readonly Dictionary<CombatantState, VisualElement> combatantElements = new();
|
||||
private readonly Dictionary<CardInstance, VisualElement> handCardElements = new();
|
||||
private readonly Dictionary<CardInstance, VisualElement> queueChipElements = new();
|
||||
private readonly Dictionary<CardInstance, VisualElement> stackChipElements = new();
|
||||
private readonly Dictionary<CombatantState, int> previousUptime = new();
|
||||
private readonly Dictionary<CombatantState, int> previousShield = new();
|
||||
private readonly HashSet<CardInstance> knownHandCards = new();
|
||||
private int queueCascadeIndex;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
@@ -64,9 +75,18 @@ namespace KernelPanic.UI
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
root?.EnableInClassList("reduced-motion", UIPreferences.ReducedMotion);
|
||||
combatManager.StateChanged += Refresh;
|
||||
combatManager.CombatLog += HandleCombatLog;
|
||||
runManager.RepositoryChanged += Refresh;
|
||||
GameEvents.CardPlayed += HandleCardPlayed;
|
||||
GameEvents.CardResolved += HandleCardResolved;
|
||||
GameEvents.PhaseChanged += HandlePhaseChanged;
|
||||
GameEvents.DamageDealt += HandleDamageDealt;
|
||||
GameEvents.CombatantDefeated += HandleCombatantDefeated;
|
||||
GameEvents.EnemyWouldAct += HandleEnemyWouldAct;
|
||||
GameEvents.PlayerDamaged += HandlePlayerDamaged;
|
||||
GameEvents.StatusApplied += HandleStatusApplied;
|
||||
}
|
||||
|
||||
private void Start()
|
||||
@@ -101,6 +121,14 @@ namespace KernelPanic.UI
|
||||
combatManager.StateChanged -= Refresh;
|
||||
combatManager.CombatLog -= HandleCombatLog;
|
||||
runManager.RepositoryChanged -= Refresh;
|
||||
GameEvents.CardPlayed -= HandleCardPlayed;
|
||||
GameEvents.CardResolved -= HandleCardResolved;
|
||||
GameEvents.PhaseChanged -= HandlePhaseChanged;
|
||||
GameEvents.DamageDealt -= HandleDamageDealt;
|
||||
GameEvents.CombatantDefeated -= HandleCombatantDefeated;
|
||||
GameEvents.EnemyWouldAct -= HandleEnemyWouldAct;
|
||||
GameEvents.PlayerDamaged -= HandlePlayerDamaged;
|
||||
GameEvents.StatusApplied -= HandleStatusApplied;
|
||||
}
|
||||
|
||||
private bool TryBuildRunConfig(out RunConfig config)
|
||||
@@ -140,7 +168,7 @@ namespace KernelPanic.UI
|
||||
|
||||
AddLanguageDeck(Language.Python, startingDeck);
|
||||
AddLanguageDeck(Language.JavaScript, startingDeck);
|
||||
config = new RunConfig(distro, Language.Python, Language.JavaScript, startingDeck, Environment.TickCount);
|
||||
config = new RunConfig(distro, Language.Python, Language.JavaScript, startingDeck, Environment.TickCount, 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -182,6 +210,12 @@ namespace KernelPanic.UI
|
||||
{
|
||||
root.styleSheets.Add(styleSheet);
|
||||
}
|
||||
|
||||
StyleSheet scrollbarStyleSheet = Resources.Load<StyleSheet>(SharedScrollbarStyleResourcePath);
|
||||
if (scrollbarStyleSheet != null)
|
||||
{
|
||||
root.styleSheets.Add(scrollbarStyleSheet);
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildLayout()
|
||||
@@ -239,6 +273,16 @@ namespace KernelPanic.UI
|
||||
logLabel.AddToClassList("log-line");
|
||||
commandPanel.Add(logLabel);
|
||||
|
||||
feedbackLayer = new();
|
||||
feedbackLayer.AddToClassList("feedback-layer");
|
||||
feedbackLayer.pickingMode = PickingMode.Ignore;
|
||||
root.Add(feedbackLayer);
|
||||
|
||||
damageVignette = new();
|
||||
damageVignette.AddToClassList("damage-vignette");
|
||||
damageVignette.pickingMode = PickingMode.Ignore;
|
||||
root.Add(damageVignette);
|
||||
|
||||
overlay = new();
|
||||
overlay.AddToClassList("overlay");
|
||||
overlay.style.display = DisplayStyle.None;
|
||||
@@ -336,10 +380,13 @@ namespace KernelPanic.UI
|
||||
private void RefreshPlayer()
|
||||
{
|
||||
playerPanel.Clear();
|
||||
combatantElements.Remove(combatManager.PlayerState);
|
||||
combatantElements[combatManager.PlayerState] = playerPanel;
|
||||
playerPanel.Add(PanelHeader(DisplayName(runManager.CurrentConfig.Distro), "htop"));
|
||||
ApplyAccent(playerPanel);
|
||||
|
||||
CombatantState state = combatManager.PlayerState;
|
||||
DetectBeneficialResourceFeedback(state, playerPanel);
|
||||
playerPanel.Add(MeterBlock("uptime", state.CurrentUptime, state.MaxUptime, MeterTone.Uptime));
|
||||
playerPanel.Add(MeterBlock("shield", state.Shield, Mathf.Max(1, state.Shield), state.Shield > 0 ? MeterTone.Beneficial : MeterTone.Muted));
|
||||
playerPanel.Add(CycleBlock(state.Cycles, state.MaxCycles));
|
||||
@@ -352,6 +399,8 @@ namespace KernelPanic.UI
|
||||
|
||||
private void RefreshTracks()
|
||||
{
|
||||
queueChipElements.Clear();
|
||||
stackChipElements.Clear();
|
||||
FillCardStrip(interpreterStrip, combatManager.InterpreterQueue.Cards, "queue empty");
|
||||
FillCardStrip(lazyStackPile, combatManager.LazyStack.Cards, "stack empty");
|
||||
tokenArea.Clear();
|
||||
@@ -361,6 +410,12 @@ namespace KernelPanic.UI
|
||||
private void RefreshEnemies()
|
||||
{
|
||||
enemyRow.Clear();
|
||||
combatantElements.Clear();
|
||||
if (combatManager.PlayerState != null)
|
||||
{
|
||||
combatantElements[combatManager.PlayerState] = playerPanel;
|
||||
}
|
||||
|
||||
for (int i = 0; i < combatManager.Enemies.Count; i++)
|
||||
{
|
||||
int index = i;
|
||||
@@ -377,6 +432,8 @@ namespace KernelPanic.UI
|
||||
Label name = new(enemy.Name);
|
||||
name.AddToClassList("enemy-name");
|
||||
card.Add(name);
|
||||
combatantElements[enemy.State] = card;
|
||||
DetectBeneficialResourceFeedback(enemy.State, card);
|
||||
card.Add(MeterBlock("uptime", enemy.CurrentUptime, enemy.MaxUptime, MeterTone.Uptime));
|
||||
if (enemy.State.Shield > 0)
|
||||
{
|
||||
@@ -394,11 +451,27 @@ namespace KernelPanic.UI
|
||||
private void RefreshHand()
|
||||
{
|
||||
handRow.Clear();
|
||||
handCardElements.Clear();
|
||||
HashSet<CardInstance> currentHandCards = new();
|
||||
IReadOnlyList<CardInstance> hand = combatManager.HandController.Cards;
|
||||
for (int i = 0; i < hand.Count; i++)
|
||||
{
|
||||
CardInstance card = hand[i];
|
||||
handRow.Add(CardFace(card));
|
||||
currentHandCards.Add(card);
|
||||
VisualElement face = CardFace(card);
|
||||
handCardElements[card] = face;
|
||||
if (!knownHandCards.Contains(card))
|
||||
{
|
||||
PlayElementBeat(face, "hand-card-drawn", 260);
|
||||
}
|
||||
|
||||
handRow.Add(face);
|
||||
}
|
||||
|
||||
knownHandCards.Clear();
|
||||
foreach (CardInstance card in currentHandCards)
|
||||
{
|
||||
knownHandCards.Add(card);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -694,7 +767,7 @@ namespace KernelPanic.UI
|
||||
|
||||
private VisualElement CardFace(CardInstance card)
|
||||
{
|
||||
Button button = new(() => combatManager.PlayCard(card));
|
||||
Button button = new(() => PlayCardWithFeedback(card));
|
||||
button.text = string.Empty;
|
||||
button.AddToClassList("hand-card");
|
||||
if (combatManager.PendingTargetCard == card)
|
||||
@@ -885,10 +958,350 @@ namespace KernelPanic.UI
|
||||
|
||||
for (int i = 0; i < cards.Count; i++)
|
||||
{
|
||||
target.Add(CardChip(cards[i]));
|
||||
VisualElement chip = CardChip(cards[i]);
|
||||
if (target == interpreterStrip)
|
||||
{
|
||||
queueChipElements[cards[i]] = chip;
|
||||
}
|
||||
else if (target == lazyStackPile)
|
||||
{
|
||||
stackChipElements[cards[i]] = chip;
|
||||
}
|
||||
|
||||
target.Add(chip);
|
||||
}
|
||||
}
|
||||
|
||||
private bool PlayCardWithFeedback(CardInstance card)
|
||||
{
|
||||
int cyclesBefore = combatManager.PlayerState == null ? 0 : combatManager.PlayerState.Cycles;
|
||||
bool played = combatManager.PlayCard(card);
|
||||
if (played)
|
||||
{
|
||||
int spent = Mathf.Max(0, cyclesBefore - (combatManager.PlayerState == null ? cyclesBefore : combatManager.PlayerState.Cycles));
|
||||
if (spent > 0)
|
||||
{
|
||||
PlayElementBeat(turnResourceGrid, "cycles-spent-beat", 220);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (combatManager.PendingTargetCard != card && handCardElements.TryGetValue(card, out VisualElement face))
|
||||
{
|
||||
PlayElementBeat(face, "feedback-denied", 220);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void HandleCardPlayed(CardPlayedEvent payload)
|
||||
{
|
||||
if (payload.Card == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
VisualElement source = handCardElements.TryGetValue(payload.Card, out VisualElement cardElement) ? cardElement : null;
|
||||
VisualElement destination = payload.Track switch
|
||||
{
|
||||
ResolutionTrack.InterpreterQueue => interpreterStrip,
|
||||
ResolutionTrack.LazyStack => lazyStackPile,
|
||||
_ => FirstTargetElement(payload.Card)
|
||||
};
|
||||
|
||||
FlyCardGhost(payload.Card, source, destination, payload.Track == ResolutionTrack.Native ? "feedback-card-strike" : "feedback-card-route");
|
||||
}
|
||||
|
||||
private void HandleCardResolved(CardResolvedEvent payload)
|
||||
{
|
||||
if (payload.Card == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.Track == ResolutionTrack.InterpreterQueue)
|
||||
{
|
||||
int delay = UIPreferences.ReducedMotion ? 0 : Mathf.Min(queueCascadeIndex * 120, 600);
|
||||
queueCascadeIndex++;
|
||||
ScheduleFeedback(() =>
|
||||
{
|
||||
VisualElement chip = queueChipElements.TryGetValue(payload.Card, out VisualElement queuedChip) ? queuedChip : interpreterStrip;
|
||||
PlayElementBeat(chip, "queue-chip-resolve", 260);
|
||||
FlyCardGhost(payload.Card, chip, turnResourceGrid, "feedback-card-discard");
|
||||
}, delay);
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.Track == ResolutionTrack.LazyStack)
|
||||
{
|
||||
VisualElement chip = stackChipElements.TryGetValue(payload.Card, out VisualElement stackChip) ? stackChip : lazyStackPile;
|
||||
PlayElementBeat(chip, "stack-chip-resolve", 320);
|
||||
FlyCardGhost(payload.Card, chip, turnResourceGrid, "feedback-card-discard");
|
||||
}
|
||||
}
|
||||
|
||||
private void HandlePhaseChanged(PhaseChangedEvent payload)
|
||||
{
|
||||
if (payload.NextPhase == TurnPhase.Interpret)
|
||||
{
|
||||
queueCascadeIndex = 0;
|
||||
}
|
||||
|
||||
ScheduleFeedback(() => PlayElementBeat(phaseLabel, "phase-pulse", 260), 0);
|
||||
}
|
||||
|
||||
private void HandleDamageDealt(DamageDealtEvent payload)
|
||||
{
|
||||
ScheduleFeedback(() =>
|
||||
{
|
||||
bool isPlayer = payload.Target == combatManager.PlayerState;
|
||||
VisualElement target = CombatantElement(payload.Target);
|
||||
string text = payload.Amount <= 0 ? "blocked" : $"-{Mathf.RoundToInt(payload.Amount)}";
|
||||
string tone = payload.Amount <= 0 ? "float-muted" : "float-damage";
|
||||
if (payload.Amount >= 8)
|
||||
{
|
||||
tone += " float-large";
|
||||
}
|
||||
|
||||
SpawnFloatingText(target, text, tone);
|
||||
PlayElementBeat(target, payload.Amount <= 0 ? "feedback-block" : "feedback-hit", 240);
|
||||
if (isPlayer && payload.Amount > 0)
|
||||
{
|
||||
FlashDamageVignette();
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
private void HandleCombatantDefeated(CombatantDefeatedEvent payload)
|
||||
{
|
||||
ScheduleFeedback(() =>
|
||||
{
|
||||
VisualElement target = CombatantElement(payload.Combatant);
|
||||
SpawnFloatingText(target, "killed", "float-kill float-large");
|
||||
SpawnDeathGhost(target);
|
||||
PlayElementBeat(target, "feedback-killed", 260);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
private void HandleEnemyWouldAct(EnemyWouldActEvent payload)
|
||||
{
|
||||
ScheduleFeedback(() => PlayElementBeat(CombatantElement(payload.Enemy?.State), "enemy-acting", 260), 0);
|
||||
}
|
||||
|
||||
private void HandlePlayerDamaged(PlayerDamagedEvent payload)
|
||||
{
|
||||
if (payload.Amount > 0)
|
||||
{
|
||||
ScheduleFeedback(FlashDamageVignette, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleStatusApplied(StatusAppliedEvent payload)
|
||||
{
|
||||
ScheduleFeedback(() =>
|
||||
{
|
||||
StatusDescriptor descriptor = StatusEffectController.GetDescriptor(payload.StatusType);
|
||||
SpawnFloatingText(CombatantElement(payload.Target), $"{descriptor.IconKey} x{payload.Stacks}", descriptor.IsBeneficial ? "float-heal" : "float-status");
|
||||
PlayElementBeat(CombatantElement(payload.Target), descriptor.IsBeneficial ? "feedback-boost" : "feedback-status", 220);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
private void DetectBeneficialResourceFeedback(CombatantState state, VisualElement element)
|
||||
{
|
||||
if (state == null || element == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (previousUptime.TryGetValue(state, out int oldUptime) && state.CurrentUptime > oldUptime)
|
||||
{
|
||||
SpawnFloatingText(element, $"+{state.CurrentUptime - oldUptime}", "float-heal");
|
||||
PlayElementBeat(element, "feedback-boost", 220);
|
||||
}
|
||||
|
||||
if (previousShield.TryGetValue(state, out int oldShield) && state.Shield > oldShield)
|
||||
{
|
||||
SpawnFloatingText(element, $"+{state.Shield - oldShield} shield", "float-heal");
|
||||
PlayElementBeat(element, "feedback-block", 220);
|
||||
}
|
||||
|
||||
previousUptime[state] = state.CurrentUptime;
|
||||
previousShield[state] = state.Shield;
|
||||
}
|
||||
|
||||
private VisualElement CombatantElement(CombatantState state)
|
||||
{
|
||||
if (state != null && combatantElements.TryGetValue(state, out VisualElement element))
|
||||
{
|
||||
return element;
|
||||
}
|
||||
|
||||
return state == combatManager.PlayerState ? playerPanel : enemyRow;
|
||||
}
|
||||
|
||||
private VisualElement FirstTargetElement(CardInstance card)
|
||||
{
|
||||
if (card?.TargetSnapshot == null)
|
||||
{
|
||||
return enemyRow;
|
||||
}
|
||||
|
||||
for (int i = 0; i < card.TargetSnapshot.Count; i++)
|
||||
{
|
||||
VisualElement element = CombatantElement(card.TargetSnapshot[i]);
|
||||
if (element != null)
|
||||
{
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
return enemyRow;
|
||||
}
|
||||
|
||||
private void PlayElementBeat(VisualElement element, string className, int durationMs)
|
||||
{
|
||||
if (element == null || string.IsNullOrWhiteSpace(className))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
element.RemoveFromClassList(className);
|
||||
element.AddToClassList(className);
|
||||
ScheduleFeedback(() => element.RemoveFromClassList(className), UIPreferences.ReducedMotion ? 80 : durationMs);
|
||||
}
|
||||
|
||||
private void SpawnFloatingText(VisualElement anchor, string text, string className)
|
||||
{
|
||||
if (feedbackLayer == null || anchor == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Rect anchorRect = anchor.worldBound;
|
||||
Rect rootRect = root.worldBound;
|
||||
Label label = new(text);
|
||||
label.AddToClassList("float-number");
|
||||
if (!string.IsNullOrWhiteSpace(className))
|
||||
{
|
||||
string[] classes = className.Split(' ');
|
||||
for (int i = 0; i < classes.Length; i++)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(classes[i]))
|
||||
{
|
||||
label.AddToClassList(classes[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
label.style.left = anchorRect.center.x - rootRect.x - 30f;
|
||||
label.style.top = anchorRect.center.y - rootRect.y - 16f;
|
||||
feedbackLayer.Add(label);
|
||||
|
||||
if (UIPreferences.ReducedMotion)
|
||||
{
|
||||
label.AddToClassList("float-static");
|
||||
ScheduleFeedback(() => label.RemoveFromHierarchy(), 420);
|
||||
return;
|
||||
}
|
||||
|
||||
ScheduleFeedback(() =>
|
||||
{
|
||||
label.style.top = anchorRect.center.y - rootRect.y - 42f;
|
||||
label.style.opacity = 0f;
|
||||
}, 20);
|
||||
ScheduleFeedback(() => label.RemoveFromHierarchy(), 520);
|
||||
}
|
||||
|
||||
private void FlyCardGhost(CardInstance card, VisualElement source, VisualElement destination, string className)
|
||||
{
|
||||
if (feedbackLayer == null || card == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Rect rootRect = root.worldBound;
|
||||
Rect sourceRect = source == null ? new Rect(rootRect.center.x - 70f, rootRect.center.y - 50f, 140f, 110f) : source.worldBound;
|
||||
Rect destinationRect = destination == null ? sourceRect : destination.worldBound;
|
||||
|
||||
VisualElement ghost = CardFaceView(card);
|
||||
ghost.AddToClassList("feedback-card-ghost");
|
||||
ghost.AddToClassList(className);
|
||||
ghost.style.position = Position.Absolute;
|
||||
ghost.style.left = sourceRect.x - rootRect.x;
|
||||
ghost.style.top = sourceRect.y - rootRect.y;
|
||||
ghost.style.width = Mathf.Max(110f, sourceRect.width);
|
||||
ghost.style.height = Mathf.Max(94f, sourceRect.height);
|
||||
feedbackLayer.Add(ghost);
|
||||
|
||||
if (UIPreferences.ReducedMotion)
|
||||
{
|
||||
ghost.AddToClassList("feedback-card-instant");
|
||||
ScheduleFeedback(() => ghost.RemoveFromHierarchy(), 140);
|
||||
return;
|
||||
}
|
||||
|
||||
ScheduleFeedback(() =>
|
||||
{
|
||||
ghost.style.left = destinationRect.center.x - rootRect.x - (sourceRect.width * 0.5f);
|
||||
ghost.style.top = destinationRect.center.y - rootRect.y - (sourceRect.height * 0.5f);
|
||||
ghost.style.opacity = 0f;
|
||||
}, 20);
|
||||
ScheduleFeedback(() => ghost.RemoveFromHierarchy(), 360);
|
||||
}
|
||||
|
||||
private void SpawnDeathGhost(VisualElement source)
|
||||
{
|
||||
if (feedbackLayer == null || source == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Rect rootRect = root.worldBound;
|
||||
Rect sourceRect = source.worldBound;
|
||||
VisualElement ghost = new();
|
||||
ghost.AddToClassList("death-ghost");
|
||||
ghost.style.position = Position.Absolute;
|
||||
ghost.style.left = sourceRect.x - rootRect.x;
|
||||
ghost.style.top = sourceRect.y - rootRect.y;
|
||||
ghost.style.width = sourceRect.width;
|
||||
ghost.style.height = sourceRect.height;
|
||||
feedbackLayer.Add(ghost);
|
||||
|
||||
if (UIPreferences.ReducedMotion)
|
||||
{
|
||||
ScheduleFeedback(() => ghost.RemoveFromHierarchy(), 180);
|
||||
return;
|
||||
}
|
||||
|
||||
ScheduleFeedback(() =>
|
||||
{
|
||||
ghost.style.opacity = 0f;
|
||||
ghost.style.scale = new Scale(new Vector3(0.92f, 0.92f, 1f));
|
||||
}, 20);
|
||||
ScheduleFeedback(() => ghost.RemoveFromHierarchy(), 360);
|
||||
}
|
||||
|
||||
private void FlashDamageVignette()
|
||||
{
|
||||
if (damageVignette == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PlayElementBeat(damageVignette, "damage-vignette-on", UIPreferences.ReducedMotion ? 120 : 240);
|
||||
}
|
||||
|
||||
private void ScheduleFeedback(Action action, int delayMs)
|
||||
{
|
||||
if (action == null || root == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
root.schedule.Execute(action).StartingIn(Mathf.Max(0, delayMs));
|
||||
}
|
||||
|
||||
private void HandleCombatLog(string message)
|
||||
{
|
||||
if (logLabel != null)
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace KernelPanic.UI
|
||||
private GachaService _gachaService;
|
||||
private PlayerCollection _playerCollection;
|
||||
private EntropyWallet _wallet;
|
||||
private Func<IReadOnlyList<DistroDefinition>, PullResolutionResult> _resolvePulledDistros;
|
||||
private Action _onChanged;
|
||||
private Action _requestRootCreditExchange;
|
||||
private int _selectedBannerIndex;
|
||||
@@ -40,13 +41,14 @@ namespace KernelPanic.UI
|
||||
private string _resultBannerId;
|
||||
private string _resultText;
|
||||
|
||||
public void Bind(VisualElement root, DistroDatabase database, FontAsset artFont, GachaService service, PlayerCollection collection, EntropyWallet entropyWallet, Action changedCallback, Action rootCreditExchangeCallback)
|
||||
public void Bind(VisualElement root, DistroDatabase database, FontAsset artFont, GachaService service, PlayerCollection collection, EntropyWallet entropyWallet, Func<IReadOnlyList<DistroDefinition>, PullResolutionResult> pullResolver, Action changedCallback, Action rootCreditExchangeCallback)
|
||||
{
|
||||
_distroDatabase = database;
|
||||
_monospaceFont = artFont;
|
||||
_gachaService = service;
|
||||
_playerCollection = collection;
|
||||
_wallet = entropyWallet;
|
||||
_resolvePulledDistros = pullResolver;
|
||||
_onChanged = changedCallback;
|
||||
_requestRootCreditExchange = rootCreditExchangeCallback;
|
||||
_bannerList = root.Q<VisualElement>("GachaBannerList");
|
||||
@@ -252,18 +254,30 @@ namespace KernelPanic.UI
|
||||
lines.Add($"entropy fallback: spent {result.EntropySpent} entropy");
|
||||
}
|
||||
|
||||
List<DistroDefinition> distroRewards = new();
|
||||
for (int i = 0; i < result.Rewards.Count; i++)
|
||||
{
|
||||
if (result.Rewards[i].Distro != null)
|
||||
{
|
||||
distroRewards.Add(result.Rewards[i].Distro);
|
||||
}
|
||||
}
|
||||
|
||||
PullResolutionResult resolution = _resolvePulledDistros?.Invoke(distroRewards);
|
||||
int distroOutcomeIndex = 0;
|
||||
for (int i = 0; i < result.Rewards.Count; i++)
|
||||
{
|
||||
GachaReward reward = result.Rewards[i];
|
||||
bool duplicate = reward.Distro != null && _playerCollection.GetOwnedCount(reward.Distro.Id) > 0;
|
||||
if (reward.Distro != null)
|
||||
PullResolutionOutcome? outcome = null;
|
||||
if (reward.Distro != null && resolution != null && distroOutcomeIndex < resolution.Outcomes.Count)
|
||||
{
|
||||
_playerCollection.Add(reward.Distro);
|
||||
outcome = resolution.Outcomes[distroOutcomeIndex];
|
||||
distroOutcomeIndex++;
|
||||
}
|
||||
|
||||
string suffix = reward.Guaranteed ? " guaranteed" : reward.PityTriggered ? " pity" : string.Empty;
|
||||
string duplicateText = duplicate ? " duplicate" : string.Empty;
|
||||
lines.Add($"{i + 1:00}: {reward.StarRating}-star {reward.DisplayName}{suffix}{duplicateText}");
|
||||
string outcomeText = FormatPullOutcome(outcome);
|
||||
lines.Add($"{i + 1:00}: {reward.StarRating}-star {reward.DisplayName}{suffix}{outcomeText}");
|
||||
}
|
||||
|
||||
_resultBannerId = bannerId;
|
||||
@@ -272,6 +286,23 @@ namespace KernelPanic.UI
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private static string FormatPullOutcome(PullResolutionOutcome? outcome)
|
||||
{
|
||||
if (!outcome.HasValue)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
PullResolutionOutcome value = outcome.Value;
|
||||
return value.Kind switch
|
||||
{
|
||||
PullOutcomeKind.Granted => " granted",
|
||||
PullOutcomeKind.Dupe => $" duplicate +{value.MergesAwarded} merges",
|
||||
PullOutcomeKind.DupeOverflow => $" max-version duplicate +{value.MergesAwarded} merges",
|
||||
_ => string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
private void RequestPullSelectedBanner(int pullCount)
|
||||
{
|
||||
string bannerId = _bannerIds[_selectedBannerIndex];
|
||||
|
||||
@@ -23,6 +23,7 @@ namespace KernelPanic.UI
|
||||
private const string HiddenClassName = "hidden";
|
||||
private const string SelectedClassName = "selected";
|
||||
private const string CursorOnClassName = "cursor-on";
|
||||
private const string SharedScrollbarStyleResourcePath = "TerminalScrollbars";
|
||||
private const float BootIntroSeconds = 1.5f;
|
||||
private static bool _bootIntroPlayed;
|
||||
|
||||
@@ -133,13 +134,14 @@ namespace KernelPanic.UI
|
||||
_cardLoadout = new CardLoadout(_playerCollection.OwnedUnits);
|
||||
Initialize(new EntropyWallet()); // TODO: Replace with persistent wallet service composition.
|
||||
BindElements();
|
||||
LoadSharedStyles();
|
||||
BindCommandEntries();
|
||||
RegisterCommandEntryCallbacks();
|
||||
featuredUnitPanel.Bind(_root, monospaceFont);
|
||||
collectionScreen.Bind(_root, monospaceFont, languageDeckDatabase, cardDatabase, _playerCollection);
|
||||
collectionScreen.Bind(_root, monospaceFont, languageDeckDatabase, cardDatabase, _playerCollection, GetMergesBalance, UpgradeCollectionUnit);
|
||||
BindScreenFrames();
|
||||
starterSelection.Bind(_root, distroDatabase, HandleStarterConfirmed);
|
||||
gachaScreen.Bind(_root, distroDatabase, monospaceFont, _gachaService, _playerCollection, _wallet, HandleMetaStateChanged, OpenRootCreditExchange);
|
||||
gachaScreen.Bind(_root, distroDatabase, monospaceFont, _gachaService, _playerCollection, _wallet, ResolvePulledDistros, HandleMetaStateChanged, OpenRootCreditExchange);
|
||||
LoadMetaState();
|
||||
_playerCollection.Changed += HandleMetaStateChanged;
|
||||
_gachaService.Changed += HandleMetaStateChanged;
|
||||
@@ -221,6 +223,15 @@ namespace KernelPanic.UI
|
||||
_collectionLanguagesButton = _root.Q<Button>("CollectionLanguagesButton");
|
||||
}
|
||||
|
||||
private void LoadSharedStyles()
|
||||
{
|
||||
StyleSheet scrollbarStyleSheet = Resources.Load<StyleSheet>(SharedScrollbarStyleResourcePath);
|
||||
if (scrollbarStyleSheet != null)
|
||||
{
|
||||
_root.styleSheets.Add(scrollbarStyleSheet);
|
||||
}
|
||||
}
|
||||
|
||||
private void BindScreenFrames()
|
||||
{
|
||||
_runSetupFrame.Bind(_runSetupPanel, "$ ./start_run --configure", "[esc] back [arrows] navigate [enter] select [b] boot run", ShowMainMenu);
|
||||
@@ -554,12 +565,13 @@ namespace KernelPanic.UI
|
||||
_saveData.EnsureLists();
|
||||
_wallet.SetBalance(_saveData.entropyBalance);
|
||||
|
||||
for (int i = 0; i < _saveData.ownedUnitIds.Count; i++)
|
||||
for (int i = 0; i < _saveData.ownedUnits.Count; i++)
|
||||
{
|
||||
DistroDefinition unit = ResolveSavedDistro(_saveData.ownedUnitIds[i]);
|
||||
OwnedUnitSaveEntry entry = _saveData.ownedUnits[i];
|
||||
DistroDefinition unit = ResolveSavedDistro(entry?.id);
|
||||
if (unit != null)
|
||||
{
|
||||
_playerCollection.Add(unit);
|
||||
_playerCollection.Add(unit, entry.version);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -624,7 +636,7 @@ namespace KernelPanic.UI
|
||||
|
||||
private void HandleStarterConfirmed(DistroDefinition picked, IReadOnlyList<DistroDefinition> remaining)
|
||||
{
|
||||
_playerCollection.Add(picked);
|
||||
_playerCollection.Add(picked, 1);
|
||||
AddAllFourStarDistrosToBeginnerBannerPool();
|
||||
_gachaService.SetBeginnerGuaranteedDistros(remaining);
|
||||
_saveData.starterChosen = true;
|
||||
@@ -639,6 +651,7 @@ namespace KernelPanic.UI
|
||||
_saveData ??= SaveData.CreateDefault();
|
||||
_saveData.EnsureLists();
|
||||
_saveData.entropyBalance = _wallet == null ? 0 : _wallet.Balance;
|
||||
_saveData.ownedUnits.Clear();
|
||||
_saveData.ownedUnitIds.Clear();
|
||||
_saveData.bannerPoolIds.Clear();
|
||||
_gachaService.WriteProgress(_saveData);
|
||||
@@ -648,7 +661,11 @@ namespace KernelPanic.UI
|
||||
DistroDefinition unit = _playerCollection.OwnedUnits[i];
|
||||
if (unit != null && !string.IsNullOrWhiteSpace(unit.Id))
|
||||
{
|
||||
_saveData.ownedUnitIds.Add(unit.Id);
|
||||
_saveData.ownedUnits.Add(new OwnedUnitSaveEntry
|
||||
{
|
||||
id = unit.Id,
|
||||
version = Mathf.Clamp(_playerCollection.GetVersion(unit.Id), 1, GachaTuning.MaxVersion)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -664,6 +681,38 @@ namespace KernelPanic.UI
|
||||
_saveService.Save(_saveData);
|
||||
}
|
||||
|
||||
private PullResolutionResult ResolvePulledDistros(IReadOnlyList<DistroDefinition> pulledDistros)
|
||||
{
|
||||
PullResolutionContext context = new(_saveData, _playerCollection, distroDatabase, _playerCollection.FeaturedUnit?.Id);
|
||||
return PullResolver.Resolve(pulledDistros, context);
|
||||
}
|
||||
|
||||
private int GetMergesBalance()
|
||||
{
|
||||
_saveData ??= SaveData.CreateDefault();
|
||||
_saveData.EnsureLists();
|
||||
return _saveData.merges;
|
||||
}
|
||||
|
||||
private VersionUpgradeResult UpgradeCollectionUnit(DistroDefinition unit)
|
||||
{
|
||||
if (unit == null)
|
||||
{
|
||||
return VersionUpgradeResult.Failed(null, VersionUpgradeFailureReason.NotOwned);
|
||||
}
|
||||
|
||||
VersionUpgradeResult result = VersionUpgrader.TryUpgrade(unit.Id, _saveData, _playerCollection);
|
||||
if (result.Success)
|
||||
{
|
||||
SaveCurrentState();
|
||||
featuredUnitPanel.Refresh(_playerCollection.OwnedUnits, _playerCollection.FeaturedUnit);
|
||||
collectionScreen.RefreshUnits(_playerCollection.OwnedUnits);
|
||||
RefreshCurrencyReadouts();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void SaveLastRunLoadout(DistroDefinition unit, IReadOnlyList<string> cardIds)
|
||||
{
|
||||
_saveData ??= SaveData.CreateDefault();
|
||||
@@ -708,7 +757,7 @@ namespace KernelPanic.UI
|
||||
_rootCreditsToEntropyButton?.SetEnabled(true);
|
||||
bool showPullTokens = IsGachaVisible();
|
||||
_pullTokensLabel.text = showPullTokens
|
||||
? $"bandwidth={_gachaService.PullTokens} compute-credits={_gachaService.LimitedPullTokens}"
|
||||
? $"bandwidth={_gachaService.PullTokens} compute-credits={_gachaService.LimitedPullTokens} merges={_saveData.merges}"
|
||||
: string.Empty;
|
||||
_pullTokensLabel.EnableInClassList(HiddenClassName, !showPullTokens);
|
||||
}
|
||||
@@ -1452,7 +1501,7 @@ namespace KernelPanic.UI
|
||||
}
|
||||
|
||||
SaveLastRunLoadout(unit, equippedCardIds);
|
||||
RunContext.Set(unit, BuildEquippedCardDefinitions(unit, equippedCardIds), _selectedRunLanguages[0], _selectedRunLanguages[1]);
|
||||
RunContext.Set(unit, BuildEquippedCardDefinitions(unit, equippedCardIds), _selectedRunLanguages[0], _selectedRunLanguages[1], _playerCollection.GetVersion(unit.Id));
|
||||
SceneLoader.LoadGame();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user