init mod
This commit is contained in:
commit
5eb002e67a
17 files changed
+1631
No files matched your search
@@ -0,0 +1,2 @@
|
||||
mod_skills_base_name = Skills Engine
|
||||
mod_skills_base_desc = Engine skills subsystem: card-based skill definitions, the grant service (skill + granted action pairing), the skills panel UI and the collection effect plate.
|
||||
@@ -0,0 +1,2 @@
|
||||
mod_skills_base_name = Движок умений
|
||||
mod_skills_base_desc = Подсистема умений движка: карточные определения умений, сервис выдачи (умение + связанное действие), панель умений и плашка эффекта коллекции.
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "skills_base",
|
||||
"name": "Skills Engine",
|
||||
"version": "1.0.0",
|
||||
"author": "ct",
|
||||
"description": "Engine skills subsystem: card-based skill definitions, the grant service (skill + granted action pairing), the skills panel UI and the collection effect plate. Replaces the former Core/Skills implementations and Core/Factories/Impls/SkillFactory.",
|
||||
"category": "engine, library",
|
||||
"sources": [
|
||||
"src/**/*.cs"
|
||||
],
|
||||
"depends": ["interface>=1"],
|
||||
"ui": []
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace Cthangover.Mods.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the rarity tier of a skill card, determining its drop probability,
|
||||
/// visual frame treatment, and relative power budget. Higher tiers correspond to
|
||||
/// more potent effects and rarer acquisition chances.
|
||||
/// </summary>
|
||||
public enum RareType
|
||||
{
|
||||
/// <summary>Standard skill with the highest drop rate and baseline power.</summary>
|
||||
Common,
|
||||
/// <summary>Slightly improved stats and a lower drop rate than <c>Common</c>.</summary>
|
||||
Uncommon,
|
||||
/// <summary>Notably stronger, appearing infrequently in loot pools.</summary>
|
||||
Rare,
|
||||
/// <summary>Powerful skill with significant gameplay impact; very low drop chance.</summary>
|
||||
Epic,
|
||||
/// <summary>The highest tier — game-changing abilities with extremely rare availability.</summary>
|
||||
Legendary
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
using Cthangover.Mods.Interface.Base.Lists;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
|
||||
using Cthangover.Mods.Interface.Theming;
|
||||
namespace Cthangover.Mods.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// Grid card of one collected skill. Pure presentation: the panel binds a
|
||||
/// <see cref="SkillInfo"/> through <see cref="Construct"/> and toggles
|
||||
/// selection through <see cref="SetSelected"/>, while every colour comes
|
||||
/// from <see cref="SkillPalette"/> so the same rarity reads identically on
|
||||
/// the card, the study plate and the rarity filter tabs.
|
||||
/// <para/>
|
||||
/// Clicking is intentionally not handled here: the panel subscribes to
|
||||
/// <c>GuiInput</c>, so the card keeps a single responsibility and can be
|
||||
/// hosted by any list without knowing its owner. Hover is handled inside
|
||||
/// because it only repaints the card itself.
|
||||
/// </summary>
|
||||
public partial class SkillCardBehaviour : ListItem<SkillInfo>
|
||||
{
|
||||
[Export] private Panel bg;
|
||||
[Export] private Panel accent;
|
||||
[Export] private PanelContainer art;
|
||||
[Export] private TextureRect imgCard;
|
||||
[Export] private Label txtName;
|
||||
[Export] private Label txtGems;
|
||||
[Export] private PanelContainer badgeBox;
|
||||
[Export] private Label badgeLabel;
|
||||
[Export] private HBoxContainer effectRow;
|
||||
[Export] private Label effectValue;
|
||||
[Export] private Label effectLabel;
|
||||
|
||||
private bool _selected;
|
||||
private bool _hover;
|
||||
|
||||
/// <summary>Configures the card to accept mouse input for hover/selection feedback.</summary>
|
||||
public override void _Ready()
|
||||
{
|
||||
MouseFilter = MouseFilterEnum.Stop;
|
||||
MouseEntered += OnMouseEntered;
|
||||
MouseExited += OnMouseExited;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Binds the skill definition to the card: artwork, translated name,
|
||||
/// rarity gems (one diamond per tier), the type badge and the first
|
||||
/// numeric effect as the card's short line. Skills without effects hide
|
||||
/// the effect row instead of leaving an empty band; a skill without
|
||||
/// artwork keeps the empty well, which is a valid authored state.
|
||||
/// </summary>
|
||||
public override void Construct(SkillInfo skillInfo)
|
||||
{
|
||||
base.Construct(skillInfo);
|
||||
ResolveNodes();
|
||||
|
||||
if (art != null)
|
||||
art.AddThemeStyleboxOverride("panel", UiKit.Frame(UiTokens.RadiusSm, UiTokens.Well, UiTokens.Line));
|
||||
|
||||
if (skillInfo == null)
|
||||
return;
|
||||
|
||||
if (imgCard != null)
|
||||
imgCard.Texture = skillInfo.Sprite;
|
||||
|
||||
if (txtName != null)
|
||||
{
|
||||
txtName.Text = TranslationServer.Translate(skillInfo.Name);
|
||||
txtName.AddThemeFontSizeOverride("font_size", UiTokens.FontBodySmall);
|
||||
txtName.AddThemeColorOverride("font_color", UiTokens.InkBright);
|
||||
}
|
||||
|
||||
if (txtGems != null)
|
||||
{
|
||||
txtGems.Text = new string('◆', Mathf.Clamp((int)skillInfo.RareType + 1, 1, 5));
|
||||
txtGems.AddThemeFontSizeOverride("font_size", UiTokens.FontMicro);
|
||||
txtGems.AddThemeColorOverride("font_color", SkillPalette.RarityColor(skillInfo.RareType));
|
||||
}
|
||||
|
||||
ApplyTypeBadge(skillInfo);
|
||||
ApplyEffect(skillInfo);
|
||||
RefreshStyle();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the panel-owned selection state: the selected card gets the
|
||||
/// golden surface, rarity border and a soft rarity glow, while hover
|
||||
/// only lifts the border. Both are repainted from scratch so the card
|
||||
/// can never end up half-styled after a filter rebuild.
|
||||
/// </summary>
|
||||
public void SetSelected(bool selected)
|
||||
{
|
||||
_selected = selected;
|
||||
RefreshStyle();
|
||||
}
|
||||
|
||||
/// <summary>Frees the node; the panel owns the card's lifetime and rebuilds the grid.</summary>
|
||||
public override void Destruct()
|
||||
{
|
||||
QueueFree();
|
||||
}
|
||||
|
||||
private void ResolveNodes()
|
||||
{
|
||||
bg ??= GetNodeOrNull<Panel>("Bg");
|
||||
accent ??= GetNodeOrNull<Panel>("Accent");
|
||||
art ??= GetNodeOrNull<PanelContainer>("Art");
|
||||
imgCard ??= GetNodeOrNull<TextureRect>("Art/ImgCard");
|
||||
txtName ??= GetNodeOrNull<Label>("TxtName");
|
||||
txtGems ??= GetNodeOrNull<Label>("Gems");
|
||||
badgeBox ??= GetNodeOrNull<PanelContainer>("BadgeBox");
|
||||
badgeLabel ??= GetNodeOrNull<Label>("BadgeBox/BadgeLabel");
|
||||
effectRow ??= GetNodeOrNull<HBoxContainer>("EffectRow");
|
||||
effectValue ??= GetNodeOrNull<Label>("EffectRow/EffectValue");
|
||||
effectLabel ??= GetNodeOrNull<Label>("EffectRow/EffectLabel");
|
||||
}
|
||||
|
||||
private void ApplyTypeBadge(SkillInfo skillInfo)
|
||||
{
|
||||
var color = SkillPalette.TypeColor(skillInfo.SkillType);
|
||||
|
||||
if (badgeBox != null)
|
||||
{
|
||||
var style = UiKit.Frame(UiTokens.RadiusXs, UiTokens.BadgeFill(color), UiTokens.BadgeLine(color));
|
||||
style.ContentMarginLeft = 6;
|
||||
style.ContentMarginRight = 6;
|
||||
style.ContentMarginTop = 1;
|
||||
style.ContentMarginBottom = 1;
|
||||
badgeBox.AddThemeStyleboxOverride("panel", style);
|
||||
}
|
||||
|
||||
if (badgeLabel != null)
|
||||
{
|
||||
badgeLabel.Text = TranslationServer.Translate(SkillPalette.TypeLocaleKey(skillInfo.SkillType));
|
||||
badgeLabel.Uppercase = true;
|
||||
badgeLabel.AddThemeFontSizeOverride("font_size", UiTokens.FontMicro);
|
||||
badgeLabel.AddThemeColorOverride("font_color", color);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyEffect(SkillInfo skillInfo)
|
||||
{
|
||||
var effect = skillInfo.Effects != null && skillInfo.Effects.Count > 0 ? skillInfo.Effects[0] : null;
|
||||
var hasEffect = effect != null && !string.IsNullOrEmpty(effect.Key);
|
||||
|
||||
if (effectRow != null)
|
||||
effectRow.Visible = hasEffect;
|
||||
|
||||
if (!hasEffect)
|
||||
return;
|
||||
|
||||
if (effectValue != null)
|
||||
{
|
||||
effectValue.Text = effect.FormatValue();
|
||||
effectValue.AddThemeFontSizeOverride("font_size", UiTokens.FontTiny);
|
||||
effectValue.AddThemeColorOverride("font_color", UiTokens.AccentBright);
|
||||
}
|
||||
|
||||
if (effectLabel != null)
|
||||
{
|
||||
effectLabel.Text = TranslationServer.Translate(effect.Key);
|
||||
effectLabel.AddThemeFontSizeOverride("font_size", UiTokens.FontTiny);
|
||||
effectLabel.AddThemeColorOverride("font_color", UiTokens.Muted);
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshStyle()
|
||||
{
|
||||
var rarity = SkillPalette.RarityColor(Model?.RareType ?? RareType.Common);
|
||||
|
||||
if (accent != null)
|
||||
{
|
||||
var accentStyle = UiKit.Frame(UiTokens.RadiusXs, rarity);
|
||||
accentStyle.CornerRadiusBottomLeft = 0;
|
||||
accentStyle.CornerRadiusBottomRight = 0;
|
||||
accent.AddThemeStyleboxOverride("panel", accentStyle);
|
||||
}
|
||||
|
||||
if (bg == null)
|
||||
return;
|
||||
|
||||
var background = _selected ? UiTokens.SurfaceSelected : _hover ? UiTokens.SurfaceHover : UiTokens.Surface1;
|
||||
var border = _selected || _hover ? rarity : UiTokens.LineCard;
|
||||
var style = UiKit.Frame(UiTokens.RadiusMd, background, border);
|
||||
if (_selected)
|
||||
{
|
||||
style.ShadowColor = UiTokens.Alpha(rarity, 0.35f);
|
||||
style.ShadowSize = 4;
|
||||
}
|
||||
bg.AddThemeStyleboxOverride("panel", style);
|
||||
}
|
||||
|
||||
private void OnMouseEntered()
|
||||
{
|
||||
_hover = true;
|
||||
RefreshStyle();
|
||||
}
|
||||
|
||||
private void OnMouseExited()
|
||||
{
|
||||
_hover = false;
|
||||
RefreshStyle();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Cthangover.Mods.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// Sums skill effects across a set of owned skills into the totals shown by
|
||||
/// the skills panel's global "collection effect" plate. The result is what
|
||||
/// the player actually gains right now — a card collected from any mod
|
||||
/// changes it immediately, without touching the individual definitions.
|
||||
/// <para/>
|
||||
/// Aggregation is keyed by <see cref="SkillEffectInfo.Key"/> and preserves
|
||||
/// first-seen order, so the plate does not reshuffle between openings and a
|
||||
/// newly granted skill appends its unique effects at the end. Null skills,
|
||||
/// null effect lists and effects without a key are skipped: mod JSON is not
|
||||
/// trusted to be complete.
|
||||
/// </summary>
|
||||
public static class SkillEffectAggregator
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds the collection-wide totals from the given skills. Effects with
|
||||
/// the same key are summed; the <c>Percent</c> flag survives if any
|
||||
/// contributor set it, so a mixed authoring mistake degrades to the
|
||||
/// percent display instead of silently dropping the suffix.
|
||||
/// </summary>
|
||||
/// <param name="skills">Owned skills, usually resolved from <see cref="SkillData.Skills"/>.</param>
|
||||
/// <returns>One entry per unique effect key, in first-seen order. Empty when nothing contributes.</returns>
|
||||
public static List<SkillEffectInfo> Aggregate(IEnumerable<SkillInfo> skills)
|
||||
{
|
||||
var order = new List<string>();
|
||||
var totals = new Dictionary<string, SkillEffectInfo>();
|
||||
|
||||
if (skills != null)
|
||||
{
|
||||
foreach (var skill in skills)
|
||||
{
|
||||
if (skill?.Effects == null)
|
||||
continue;
|
||||
|
||||
foreach (var effect in skill.Effects)
|
||||
{
|
||||
if (effect == null || string.IsNullOrEmpty(effect.Key))
|
||||
continue;
|
||||
|
||||
if (!totals.TryGetValue(effect.Key, out var total))
|
||||
{
|
||||
total = new SkillEffectInfo { Key = effect.Key, Percent = effect.Percent };
|
||||
totals[effect.Key] = total;
|
||||
order.Add(effect.Key);
|
||||
}
|
||||
|
||||
total.Value += effect.Value;
|
||||
total.Percent |= effect.Percent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var result = new List<SkillEffectInfo>();
|
||||
foreach (var key in order)
|
||||
result.Add(totals[key]);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Cthangover.Mods.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// One numeric bonus granted by a skill card — the gameplay payload behind
|
||||
/// the collection. <see cref="Key"/> is the locale key naming the affected
|
||||
/// stat (e.g. <c>characters/skills/effects/recruit</c>), <see cref="Value"/>
|
||||
/// its magnitude and <see cref="Percent"/> the display unit.
|
||||
/// <para/>
|
||||
/// Effects sharing a key are summed by <see cref="SkillEffectAggregator"/>
|
||||
/// into the panel's global "collection effect" plate, so one key must keep
|
||||
/// the same unit across every skill: two effects of <c>+10%</c> and
|
||||
/// <c>+5%</c> become <c>+15%</c>, never <c>+15</c>. Mods author these
|
||||
/// values in the skill JSON next to the card's rarity and type.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class SkillEffectInfo
|
||||
{
|
||||
/// <summary>Locale key of the affected stat, shared by all skills that touch it.</summary>
|
||||
[JsonPropertyName("Key")]
|
||||
public string Key { get; set; }
|
||||
|
||||
/// <summary>Magnitude in the unit encoded by <see cref="Percent"/>. Negative values are penalties.</summary>
|
||||
[JsonPropertyName("Value")]
|
||||
public float Value { get; set; }
|
||||
|
||||
/// <summary>When true the value is displayed with a <c>%</c> suffix; still summed as a plain number.</summary>
|
||||
[JsonPropertyName("Percent")]
|
||||
public bool Percent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Formats the magnitude for cards, chips and the collection plate. An
|
||||
/// explicit <c>+</c> marks positive values so a bonus never reads as a
|
||||
/// subtracted amount; trailing zeros are trimmed (<c>+50%</c>, not
|
||||
/// <c>+50.0%</c>); <c>%</c> is appended when <see cref="Percent"/> is
|
||||
/// set.
|
||||
/// </summary>
|
||||
public string FormatValue()
|
||||
{
|
||||
var number = Value.ToString("0.##", CultureInfo.InvariantCulture);
|
||||
if (Value > 0f)
|
||||
number = "+" + number;
|
||||
return Percent ? number + "%" : number;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using Cthangover.Core.Factories;
|
||||
|
||||
namespace Cthangover.Mods.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// Thin <c>FileFactory</c> for skill definitions. Skills are collected by
|
||||
/// the shared <c>EntityResolver</c> through the DTO's
|
||||
/// <c>[EntityKind("skill")]</c> attribute, so this factory carries no
|
||||
/// directory knowledge at all — a skill file is found wherever a mod
|
||||
/// decides to put it, as long as its envelope declares the skill kind.
|
||||
/// <para/>
|
||||
/// The single instance is a self-contained lazy singleton owned by this
|
||||
/// mod assembly; consumers resolve it through <see cref="Instance"/>.
|
||||
/// </summary>
|
||||
public class SkillFactory : FileFactory<SkillInfo>
|
||||
{
|
||||
private static readonly Lazy<SkillFactory> instance = new(() => new SkillFactory());
|
||||
public static SkillFactory Instance => instance.Value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using Cthangover.Core.Factories;
|
||||
using Cthangover.Core.Utils;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Mods.Skills
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Immutable data model for a single skill definition, populated by
|
||||
/// <see cref="SkillFactory"/> from JSON assets. Each instance carries the
|
||||
/// metadata required to render a skill card in the UI (<see cref="Sprite"/>,
|
||||
/// <see cref="Name"/>, <see cref="Description"/>) and to resolve its gameplay
|
||||
/// behaviour through <see cref="SkillType"/> and <see cref="RareType"/>.
|
||||
/// Instances are shared across the runtime and should not be mutated after
|
||||
/// construction.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
[EntityKind("skill")]
|
||||
public class SkillInfo : IIdentifiable
|
||||
{
|
||||
/// <summary>
|
||||
/// The unique string key of this skill, used as the primary lookup in
|
||||
/// <see cref="SkillFactory"/> and as the identity token in
|
||||
/// <see cref="SkillData.Skills"/>.
|
||||
/// </summary>
|
||||
[JsonPropertyName("Id")]
|
||||
public string ID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The display name shown on the skill card. This value is passed through
|
||||
/// <see cref="TranslationServer.Translate"/> before rendering, allowing
|
||||
/// localisation via Godot's translation system.
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A longer textual description explaining what the skill does in gameplay
|
||||
/// terms. Displayed in tooltip and detail views alongside the card artwork.
|
||||
/// </summary>
|
||||
public string Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The relative path to the skill's artwork texture, resolved inside the
|
||||
/// mod-group "characters/skills" by <see cref="TextureUtils.LoadFromModGroup"/>
|
||||
/// when the <see cref="Sprite"/> property is first accessed.
|
||||
/// </summary>
|
||||
public string Image { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional ID of the action card this skill gifts to the player when
|
||||
/// acquired (e.g. an active skill that hands over a battle card).
|
||||
/// Resolved against <see cref="Cthangover.Core.Factories.Impls.ActionCharacterFactory"/>
|
||||
/// when the skills panel renders the detail view, and pushed into
|
||||
/// <see cref="Cthangover.Core.Settings.ActionPoolData.ActionIds"/> by
|
||||
/// <see cref="SkillService"/> so the card becomes assignable to a
|
||||
/// character. Empty for skills that only act as passive gates and
|
||||
/// never produce a card.
|
||||
/// </summary>
|
||||
public string GrantsAction { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Numeric bonuses this skill grants while owned. They are the card's
|
||||
/// actual payload: the skills panel shows the first effect on the grid
|
||||
/// card, lists all of them in the study plate, and sums every owned
|
||||
/// skill's effects into the global "collection effect" plate via
|
||||
/// <see cref="SkillEffectAggregator"/>. Empty for skills that only gate
|
||||
/// content (e.g. unlock an action) without touching stats.
|
||||
/// </summary>
|
||||
[JsonPropertyName("Effects")]
|
||||
public List<SkillEffectInfo> Effects { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The gameplay classification of this skill (<c>Active</c>, <c>Passive</c>,
|
||||
/// or <c>ForCharacter</c>), serialised as a string in JSON and converted
|
||||
/// via <see cref="JsonStringEnumConverter"/>.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
[JsonPropertyName("Type")]
|
||||
public SkillType SkillType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The rarity tier of this skill, serialised as a string in JSON and
|
||||
/// converted via <see cref="JsonStringEnumConverter"/>. Drives the colour
|
||||
/// of the card frame, study plate and rarity badge through
|
||||
/// <see cref="SkillPalette"/>.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
[JsonPropertyName("Rare")]
|
||||
public RareType RareType { get; set; }
|
||||
|
||||
private Texture2D cacheSprite;
|
||||
|
||||
/// <summary>
|
||||
/// Lazily loads and caches the <see cref="Texture2D"/> for this skill's
|
||||
/// card artwork. On first access the texture is resolved from the mod-group
|
||||
/// "characters/skills" using the <see cref="Image"/> path; subsequent
|
||||
/// accesses return the cached reference.
|
||||
/// </summary>
|
||||
public Texture2D Sprite
|
||||
{
|
||||
get
|
||||
{
|
||||
if (cacheSprite == null)
|
||||
cacheSprite = TextureUtils.LoadFromModGroup("characters/skills", Image);
|
||||
return cacheSprite;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
namespace Cthangover.Mods.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// Enumerates every skill available in the game by its canonical identifier.
|
||||
/// Each member corresponds to a JSON-definition entry loaded by
|
||||
/// <see cref="SkillFactory"/> and used to construct <see cref="SkillInfo"/> instances.
|
||||
/// The underlying <c>int</c> values are not manually assigned; the enum serves
|
||||
/// purely as a compile-time-safe skill reference.
|
||||
/// </summary>
|
||||
public enum SkillName : int
|
||||
{
|
||||
/// <summary>
|
||||
/// A domination-type skill that allows the player to command enslaved units,
|
||||
/// overriding enemy AI and redirecting attacks under the player's control.
|
||||
/// </summary>
|
||||
SLAVE_MASTER
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides conversion helpers for <see cref="SkillName"/> values, used when
|
||||
/// bridging the enum-based API with string-based lookups in
|
||||
/// <see cref="SkillData"/> and factory registries.
|
||||
/// </summary>
|
||||
public static class SkillNameExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a <see cref="SkillName"/> enum value to its lowercase string
|
||||
/// identifier, matching the format expected by <see cref="SkillData.Skills"/>
|
||||
/// and serialization layers.
|
||||
/// </summary>
|
||||
/// <param name="skill">The skill enum value to convert.</param>
|
||||
/// <returns>The lowercase name of the enum constant (e.g. "slave_master").</returns>
|
||||
public static string ID(this SkillName skill)
|
||||
{
|
||||
return skill.ToString().ToLower();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Mods.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps skill card enums to their visual identity. The five rarity colours
|
||||
/// are fixed by the UI style guide (Common→muted, Uncommon→success,
|
||||
/// Rare→info, Epic→recipe, Legendary→accent) and are shared by the grid
|
||||
/// card frame, the study plate, the rarity filter tabs and the rarity
|
||||
/// badge — a card can never appear in two different colours in one screen.
|
||||
/// The type accents reuse the action-card palette so the same "passive",
|
||||
/// "active" and "self" semantics read identically across panels, and the
|
||||
/// locale keys follow both lists for badge labels.
|
||||
/// </summary>
|
||||
public static class SkillPalette
|
||||
{
|
||||
private static readonly Color[] RarityColors =
|
||||
{
|
||||
UiTokens.Muted,
|
||||
UiTokens.Success,
|
||||
UiTokens.Info,
|
||||
UiTokens.TypeRecipe,
|
||||
UiTokens.Accent,
|
||||
};
|
||||
|
||||
/// <summary>Rarity colour clipped to the five known tiers; unknown values degrade to Common.</summary>
|
||||
public static Color RarityColor(RareType rarity)
|
||||
{
|
||||
var index = Mathf.Clamp((int)rarity, 0, RarityColors.Length - 1);
|
||||
return RarityColors[index];
|
||||
}
|
||||
|
||||
/// <summary>Locale key of the rarity name shown on the badge and the rarity filter tabs.</summary>
|
||||
public static string RarityLocaleKey(RareType rarity)
|
||||
=> "ui/skills/rarity/" + rarity.ToString().ToLowerInvariant();
|
||||
|
||||
/// <summary>Accent colour of the skill type badge, matching the action-card badges.</summary>
|
||||
public static Color TypeColor(SkillType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case SkillType.Active:
|
||||
return UiTokens.TypeActive;
|
||||
case SkillType.ForCharacter:
|
||||
return UiTokens.TypeSelf;
|
||||
default:
|
||||
return UiTokens.TypePassive;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Locale key of the type name; <c>ForCharacter</c> needs an explicit snake_case key.</summary>
|
||||
public static string TypeLocaleKey(SkillType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case SkillType.Active:
|
||||
return "ui/skills/type/active";
|
||||
case SkillType.ForCharacter:
|
||||
return "ui/skills/type/for_character";
|
||||
default:
|
||||
return "ui/skills/type/passive";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using Cthangover.Core.Factories.Impls;
|
||||
using Cthangover.Core.Settings;
|
||||
using Cthangover.Core.Utils;
|
||||
|
||||
namespace Cthangover.Mods.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// Single entry point for acquiring a skill at runtime. A skill is more
|
||||
/// than a checkbox in <see cref="SkillData"/>: an active skill can hand
|
||||
/// the player an action card through <see cref="SkillInfo.GrantsAction"/>,
|
||||
/// and that card is what the player later assigns to a character in the
|
||||
/// character panel. Granting the skill and its card together keeps the two
|
||||
/// collections from drifting apart — a skill without its card (or a card
|
||||
/// without its skill) would survive save/restore inconsistently and the
|
||||
/// skills panel could not explain where the card came from.
|
||||
/// <para/>
|
||||
/// Granting is idempotent: once <see cref="SkillData"/> owns the skill ID,
|
||||
/// repeated calls are no-ops, so replayed scenario events cannot duplicate
|
||||
/// action cards in the pool.
|
||||
/// </summary>
|
||||
public static class SkillService
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves the skill definition through <see cref="SkillFactory"/> and
|
||||
/// grants it to the current runtime. This is the path used by scenario
|
||||
/// actions and mod initializers. Unknown IDs are logged and rejected
|
||||
/// instead of partially mutating the runtime state.
|
||||
/// </summary>
|
||||
/// <param name="id">Skill ID as authored in the skill's JSON file (e.g. "slave_master").</param>
|
||||
/// <returns><c>true</c> when the skill was newly granted; <c>false</c> when it was already owned or could not be resolved.</returns>
|
||||
public static bool GrantSkill(string id)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id))
|
||||
return false;
|
||||
|
||||
var skill = SkillFactory.Instance.Get(id);
|
||||
if (skill == null)
|
||||
{
|
||||
GameLogger.Log("SKILL", $"GrantSkill: unknown skill '{id}'", LogLevel.Warning);
|
||||
return false;
|
||||
}
|
||||
|
||||
return GrantSkill(GameData.Instance.Runtime, skill);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Core grant routine operating on an explicit runtime, so the
|
||||
/// skill/card pairing can be exercised without the Godot autoload.
|
||||
/// Adds the skill to <see cref="RuntimeData.SkillData"/> and, when the
|
||||
/// definition names one, appends its granted action to
|
||||
/// <see cref="RuntimeData.ActionPool"/> where the character panel can
|
||||
/// pick it up.
|
||||
/// </summary>
|
||||
/// <param name="runtime">Session state that owns both the skill set and the action pool.</param>
|
||||
/// <param name="skill">Resolved skill definition to grant.</param>
|
||||
/// <returns><c>true</c> when the skill was newly granted; <c>false</c> when it was already owned or the arguments are invalid.</returns>
|
||||
public static bool GrantSkill(RuntimeData runtime, SkillInfo skill)
|
||||
{
|
||||
if (runtime == null || skill == null || string.IsNullOrEmpty(skill.ID))
|
||||
return false;
|
||||
|
||||
if (runtime.SkillData.HasSkill(skill.ID))
|
||||
return false;
|
||||
|
||||
runtime.SkillData.AddSkill(skill.ID);
|
||||
|
||||
if (!string.IsNullOrEmpty(skill.GrantsAction))
|
||||
runtime.ActionPool.ActionIds.Add(skill.GrantsAction);
|
||||
|
||||
GameLogger.Log("SKILL", string.IsNullOrEmpty(skill.GrantsAction)
|
||||
? $"GrantSkill: granted '{skill.ID}'"
|
||||
: $"GrantSkill: granted '{skill.ID}', action '{skill.GrantsAction}' added to pool");
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
namespace Cthangover.Mods.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// Classifies how a skill behaves during gameplay. This distinction drives
|
||||
/// which battle-phase the skill can be used in and how its effects are resolved
|
||||
/// by the combat engine.
|
||||
/// </summary>
|
||||
public enum SkillType
|
||||
{
|
||||
/// <summary>
|
||||
/// Must be intentionally triggered during the player's turn, consuming an action
|
||||
/// resource and targeting a specific ally or enemy.
|
||||
/// </summary>
|
||||
Active,
|
||||
/// <summary>
|
||||
/// Remains in effect continuously once acquired, providing a persistent buff
|
||||
/// or aura without requiring any player input.
|
||||
/// </summary>
|
||||
Passive,
|
||||
/// <summary>
|
||||
/// Applies directly to the owning character as a stat modifier or intrinsic trait,
|
||||
/// rather than being a card the player selects each turn.
|
||||
/// </summary>
|
||||
ForCharacter,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Cthangover.Core.Factories.Impls;
|
||||
using Cthangover.Core.Settings;
|
||||
using Cthangover.Mods.Interface.Base.Lists.Impls;
|
||||
|
||||
namespace Cthangover.Mods.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// UI widget that displays all skills currently owned by the player in a
|
||||
/// column-based card grid. Extends
|
||||
/// <see cref="ColumnCellListWidget{TItem, TModel}"/> to reuse the generic
|
||||
/// list-rendering pipeline. When the widget is populated it reads the set
|
||||
/// of owned skill IDs from <see cref="SkillData.Skills"/> in the current
|
||||
/// <see cref="GameData"/> runtime, resolves each ID to a
|
||||
/// <see cref="SkillInfo"/> via <see cref="SkillFactory.Instance"/>, and
|
||||
/// hands the resulting models to the list for rendering as
|
||||
/// <see cref="SkillCardBehaviour"/> items.
|
||||
/// </summary>
|
||||
public partial class SkillWidget : ColumnCellListWidget<SkillCardBehaviour, SkillInfo>
|
||||
{
|
||||
/// <summary>
|
||||
/// Produces the collection of <see cref="SkillInfo"/> models that the
|
||||
/// list widget will render. Each ownedskill ID from
|
||||
/// <see cref="GameData.Instance.Runtime.SkillData"/> is resolved through
|
||||
/// <see cref="SkillFactory.Instance.Get"/> and collected into a
|
||||
/// <see cref="List{T}"/>.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// A snapshot of all currently owned skills, each as a fully-populated
|
||||
/// <see cref="SkillInfo"/> instance.
|
||||
/// </returns>
|
||||
public override ICollection<SkillInfo> CreateModels()
|
||||
{
|
||||
return GameData.Instance.Runtime.SkillData.Skills.Select(x => SkillFactory.Instance.Get(x)).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Cthangover.Core.Actions;
|
||||
using Cthangover.Core.Mods;
|
||||
|
||||
namespace Cthangover.Mods.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// Binds the skills subsystem into the kernel bridge. Kernel entry points
|
||||
/// (scenario actions, the character service) route grants through
|
||||
/// <see cref="SkillsBridge"/>; the actual grant logic — resolving the
|
||||
/// definition and pairing the skill with its granted action card — is
|
||||
/// owned by this mod's <see cref="SkillService"/>. The binding happens on
|
||||
/// <c>OnModResourcesReady</c>, when skill JSON data is guaranteed to be
|
||||
/// collectable through the factory.
|
||||
/// </summary>
|
||||
public class SkillsModInitializer : IModInitializer
|
||||
{
|
||||
/// <summary>No per-mod work: registration is idempotent and resources-ready only.</summary>
|
||||
public void OnModLoaded(string modId)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Installs the grant sink (first registration wins).</summary>
|
||||
public void OnModResourcesReady()
|
||||
{
|
||||
SkillsBridge.Register(SkillService.GrantSkill);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,858 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Characters;
|
||||
using Cthangover.Core.Factories.Impls;
|
||||
using Cthangover.Core.Scenes;
|
||||
using Cthangover.Core.UI;
|
||||
using Cthangover.Mods.Interface.Base.Lists;
|
||||
using Cthangover.Core.Settings;
|
||||
using Cthangover.Core.Skills;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Cthangover.Core.Utils;
|
||||
using Godot;
|
||||
|
||||
using Cthangover.Mods.Interface.Theming;
|
||||
namespace Cthangover.Mods.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// Skills collection panel: the showcase grid of collected skill cards on
|
||||
/// the left, the study plate of the selected card on the right and the
|
||||
/// global "collection effect" plate hanging in the screen corner. The
|
||||
/// window structure lives in <c>skills_panel.tscn</c>; this behaviour only
|
||||
/// resolves the nodes, paints them with <see cref="UiTokens"/> and fills
|
||||
/// them with data.
|
||||
/// <para/>
|
||||
/// The grid never reserves slots for cards the player does not own: the
|
||||
/// total card count in the world depends on the loaded mods and is unknown
|
||||
/// at runtime, so only owned skills are shown and the header counter
|
||||
/// reports the collected amount. The effect plate sums every owned skill's
|
||||
/// <see cref="SkillEffectInfo"/> through <see cref="SkillEffectAggregator"/>,
|
||||
/// which is the "what the collection gives me right now" readout.
|
||||
/// <para/>
|
||||
/// Selecting a card only repaints the study plate; filters, search and sort
|
||||
/// rebuild the grid and keep the selection on the first visible card. All
|
||||
/// rarity colours come from <see cref="SkillPalette"/>, shared with the
|
||||
/// card scene, so a card and its plate can never disagree.
|
||||
/// </summary>
|
||||
public partial class SkillsPanelBehaviour : Widget
|
||||
{
|
||||
private enum SortMode
|
||||
{
|
||||
Rarity,
|
||||
Type,
|
||||
Name,
|
||||
}
|
||||
|
||||
private const int Columns = 6;
|
||||
private const float CellGap = 10f;
|
||||
private const float SideWidth = 400f;
|
||||
private const float CardAspect = 210f / 160f;
|
||||
|
||||
private readonly List<SkillCardBehaviour> _cards = new();
|
||||
private ModScenePrefab _cardPrefab;
|
||||
|
||||
private ColorRect _backdrop;
|
||||
private Panel _window;
|
||||
private Label _title;
|
||||
private HBoxContainer _tabsRow;
|
||||
private Label _progressLabel;
|
||||
private Label _progressValue;
|
||||
private Button _closeButton;
|
||||
private Panel _rule;
|
||||
private LineEdit _searchEdit;
|
||||
private OptionButton _sortOption;
|
||||
private HFlowContainer _rarityTabs;
|
||||
private ScrollContainer _gridScroll;
|
||||
private Control _gridContent;
|
||||
private Label _emptyLabel;
|
||||
private Label _hint;
|
||||
private Button _resetButton;
|
||||
private PanelContainer _side;
|
||||
private Label _sideLabel;
|
||||
private Control _plate;
|
||||
private Panel _plateBg;
|
||||
private PanelContainer _plateArt;
|
||||
private TextureRect _plateImage;
|
||||
private Label _plateGems;
|
||||
private PanelContainer _medal;
|
||||
private Label _medalGlyph;
|
||||
private PanelContainer _plateNameBox;
|
||||
private Label _plateName;
|
||||
private HFlowContainer _badges;
|
||||
private Label _desc;
|
||||
private HFlowContainer _chips;
|
||||
private PanelContainer _grant;
|
||||
private TextureRect _grantIcon;
|
||||
private Label _grantCaption;
|
||||
private Label _grantName;
|
||||
private Label _passportEmpty;
|
||||
private Panel _hud;
|
||||
private Panel _hudAccent;
|
||||
private Label _hudTitle;
|
||||
private Label _hudCount;
|
||||
private Panel _hudRule;
|
||||
private ScrollContainer _hudScroll;
|
||||
private GridContainer _hudList;
|
||||
private Label _hudEmpty;
|
||||
private Label _hudFoot;
|
||||
|
||||
private SkillInfo _selected;
|
||||
private SkillType? _typeFilter;
|
||||
private RareType? _rarityFilter;
|
||||
private SortMode _sort = SortMode.Rarity;
|
||||
private string _search = string.Empty;
|
||||
|
||||
/// <summary>Resolves nodes, applies styles and texts, loads the skill card prefab and wires search/sort/close handlers.</summary>
|
||||
public override void _Ready()
|
||||
{
|
||||
base._Ready();
|
||||
ResolveNodes();
|
||||
ApplyStaticStyles();
|
||||
ApplyStaticTexts();
|
||||
|
||||
_cardPrefab = new ModScenePrefab("scenes/ui/skills_panel/skill_card.tscn");
|
||||
|
||||
if (_closeButton != null)
|
||||
_closeButton.Pressed += OnClosePressed;
|
||||
if (_searchEdit != null)
|
||||
_searchEdit.TextChanged += OnSearchChanged;
|
||||
if (_sortOption != null)
|
||||
_sortOption.ItemSelected += OnSortSelected;
|
||||
if (_resetButton != null)
|
||||
_resetButton.Pressed += OnResetPressed;
|
||||
|
||||
GameLogger.Log("SKILLSPANEL", $"Ready: grid={_gridContent != null} side={_side != null} hud={_hudList != null} prefab={_cardPrefab != null}");
|
||||
|
||||
// A scene that starts visible never goes through Widget.Show (the
|
||||
// lifecycle only fires on the typed Show/Switch calls), so build the
|
||||
// panel here too — ShowConstruct rebuilds from scratch, and the next
|
||||
// open just repeats that rebuild.
|
||||
if (Visible)
|
||||
{
|
||||
EnsureConstructed();
|
||||
ShowConstruct();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rebuilds the whole panel on every open with a clean filter state, so
|
||||
/// the player always lands on the full collection. Newly collected
|
||||
/// skills appear here because the grid is rebuilt from
|
||||
/// <see cref="SkillData.Skills"/> rather than cached.
|
||||
/// </summary>
|
||||
protected override void ShowConstruct()
|
||||
{
|
||||
_typeFilter = null;
|
||||
_rarityFilter = null;
|
||||
_sort = SortMode.Rarity;
|
||||
_search = string.Empty;
|
||||
|
||||
if (_searchEdit != null)
|
||||
_searchEdit.Text = string.Empty;
|
||||
if (_sortOption != null)
|
||||
_sortOption.Selected = 0;
|
||||
|
||||
var owned = OwnedSkills();
|
||||
RebuildTypeTabs(owned);
|
||||
RebuildRarityTabs(owned);
|
||||
RebuildGrid(owned);
|
||||
RebuildHud(owned);
|
||||
|
||||
GameLogger.Log("SKILLSPANEL", $"ShowConstruct: {owned.Count} skill(s) owned");
|
||||
}
|
||||
|
||||
protected override void HideDestruct()
|
||||
{
|
||||
ClearCards();
|
||||
_selected = null;
|
||||
ClearChildren(_badges);
|
||||
ClearChildren(_chips);
|
||||
}
|
||||
|
||||
private void ResolveNodes()
|
||||
{
|
||||
const string root = "Window/Margin/Root";
|
||||
_backdrop = GetNodeOrNull<ColorRect>("Bg");
|
||||
_window = GetNodeOrNull<Panel>("Window");
|
||||
_title = GetNodeOrNull<Label>(root + "/Header/Title");
|
||||
_tabsRow = GetNodeOrNull<HBoxContainer>(root + "/Header/TabsRow");
|
||||
_progressLabel = GetNodeOrNull<Label>(root + "/Header/ProgressLabel");
|
||||
_progressValue = GetNodeOrNull<Label>(root + "/Header/ProgressValue");
|
||||
_closeButton = GetNodeOrNull<Button>(root + "/Header/CloseButton");
|
||||
_rule = GetNodeOrNull<Panel>(root + "/Rule");
|
||||
_searchEdit = GetNodeOrNull<LineEdit>(root + "/Body/Showcase/Toolbar/SearchEdit");
|
||||
_sortOption = GetNodeOrNull<OptionButton>(root + "/Body/Showcase/Toolbar/SortOption");
|
||||
_rarityTabs = GetNodeOrNull<HFlowContainer>(root + "/Body/Showcase/RarityTabs");
|
||||
_gridScroll = GetNodeOrNull<ScrollContainer>(root + "/Body/Showcase/GridScroll");
|
||||
_gridContent = GetNodeOrNull<Control>(root + "/Body/Showcase/GridScroll/GridContent");
|
||||
_emptyLabel = GetNodeOrNull<Label>(root + "/Body/Showcase/EmptyLabel");
|
||||
_hint = GetNodeOrNull<Label>(root + "/Body/Showcase/Footer/Hint");
|
||||
_resetButton = GetNodeOrNull<Button>(root + "/Body/Showcase/Footer/ResetButton");
|
||||
_side = GetNodeOrNull<PanelContainer>(root + "/Body/Side");
|
||||
_sideLabel = GetNodeOrNull<Label>(root + "/Body/Side/SideScroll/Passport/SideLabel");
|
||||
_plate = GetNodeOrNull<Control>(root + "/Body/Side/SideScroll/Passport/Plate");
|
||||
_plateBg = GetNodeOrNull<Panel>(root + "/Body/Side/SideScroll/Passport/Plate/PlateBg");
|
||||
_plateArt = GetNodeOrNull<PanelContainer>(root + "/Body/Side/SideScroll/Passport/Plate/PlateArt");
|
||||
_plateImage = GetNodeOrNull<TextureRect>(root + "/Body/Side/SideScroll/Passport/Plate/PlateArt/PlateImage");
|
||||
_plateGems = GetNodeOrNull<Label>(root + "/Body/Side/SideScroll/Passport/Plate/PlateGems");
|
||||
_medal = GetNodeOrNull<PanelContainer>(root + "/Body/Side/SideScroll/Passport/Plate/PlateMedal");
|
||||
_medalGlyph = GetNodeOrNull<Label>(root + "/Body/Side/SideScroll/Passport/Plate/PlateMedal/MedalGlyph");
|
||||
_plateNameBox = GetNodeOrNull<PanelContainer>(root + "/Body/Side/SideScroll/Passport/Plate/PlateNameBox");
|
||||
_plateName = GetNodeOrNull<Label>(root + "/Body/Side/SideScroll/Passport/Plate/PlateNameBox/PlateName");
|
||||
_badges = GetNodeOrNull<HFlowContainer>(root + "/Body/Side/SideScroll/Passport/Badges");
|
||||
_desc = GetNodeOrNull<Label>(root + "/Body/Side/SideScroll/Passport/Desc");
|
||||
_chips = GetNodeOrNull<HFlowContainer>(root + "/Body/Side/SideScroll/Passport/Chips");
|
||||
_grant = GetNodeOrNull<PanelContainer>(root + "/Body/Side/SideScroll/Passport/Grant");
|
||||
_grantIcon = GetNodeOrNull<TextureRect>(root + "/Body/Side/SideScroll/Passport/Grant/GrantRow/GrantIcon");
|
||||
_grantCaption = GetNodeOrNull<Label>(root + "/Body/Side/SideScroll/Passport/Grant/GrantRow/GrantInfo/GrantCaption");
|
||||
_grantName = GetNodeOrNull<Label>(root + "/Body/Side/SideScroll/Passport/Grant/GrantRow/GrantInfo/GrantName");
|
||||
_passportEmpty = GetNodeOrNull<Label>(root + "/Body/Side/SideScroll/Passport/Empty");
|
||||
_hud = GetNodeOrNull<Panel>("Hud");
|
||||
_hudAccent = GetNodeOrNull<Panel>("Hud/HudAccent");
|
||||
_hudTitle = GetNodeOrNull<Label>("Hud/HudMargin/HudColumn/HudHead/HudTitle");
|
||||
_hudCount = GetNodeOrNull<Label>("Hud/HudMargin/HudColumn/HudHead/HudCount");
|
||||
_hudRule = GetNodeOrNull<Panel>("Hud/HudMargin/HudColumn/HudRule");
|
||||
_hudScroll = GetNodeOrNull<ScrollContainer>("Hud/HudMargin/HudColumn/HudScroll");
|
||||
_hudList = GetNodeOrNull<GridContainer>("Hud/HudMargin/HudColumn/HudScroll/HudList");
|
||||
_hudEmpty = GetNodeOrNull<Label>("Hud/HudMargin/HudColumn/HudEmpty");
|
||||
_hudFoot = GetNodeOrNull<Label>("Hud/HudMargin/HudColumn/HudFoot");
|
||||
}
|
||||
|
||||
private void ApplyStaticStyles()
|
||||
{
|
||||
if (_backdrop != null)
|
||||
UiKit.ApplyPanelBackdrop(_backdrop);
|
||||
if (_window != null)
|
||||
_window.AddThemeStyleboxOverride("panel", MakePanelStyle(UiTokens.Surface0, UiTokens.Line, UiTokens.RadiusLg, true));
|
||||
if (_rule != null)
|
||||
_rule.AddThemeStyleboxOverride("panel", MakePanelStyle(UiTokens.Line, UiTokens.Line, 0));
|
||||
if (_side != null)
|
||||
_side.AddThemeStyleboxOverride("panel", MakeSideStyle());
|
||||
if (_plateBg != null)
|
||||
_plateBg.AddThemeStyleboxOverride("panel", MakePanelStyle(UiTokens.Surface1, UiTokens.Line, UiTokens.RadiusMd));
|
||||
if (_plateArt != null)
|
||||
_plateArt.AddThemeStyleboxOverride("panel", MakePanelStyle(UiTokens.Well, UiTokens.Line, UiTokens.RadiusSm));
|
||||
if (_grant != null)
|
||||
{
|
||||
var grantStyle = MakePanelStyle(UiTokens.Well, UiTokens.Line, UiTokens.RadiusMd);
|
||||
ApplyContentMargins(grantStyle, 10, 9);
|
||||
_grant.AddThemeStyleboxOverride("panel", grantStyle);
|
||||
}
|
||||
if (_hud != null)
|
||||
_hud.AddThemeStyleboxOverride("panel", MakePanelStyle(UiTokens.Surface0, UiTokens.Line, UiTokens.RadiusLg, true));
|
||||
if (_hudAccent != null)
|
||||
_hudAccent.AddThemeStyleboxOverride("panel", UiKit.Frame(0, UiTokens.AccentLine));
|
||||
if (_hudRule != null)
|
||||
_hudRule.AddThemeStyleboxOverride("panel", MakePanelStyle(UiTokens.LineSoft, UiTokens.LineSoft, 0));
|
||||
|
||||
StyleLabel(_title, UiTokens.FontTitle, UiTokens.InkBright);
|
||||
if (_title != null)
|
||||
_title.Uppercase = true;
|
||||
StyleLabel(_progressLabel, UiTokens.FontTiny, UiTokens.Faint);
|
||||
if (_progressLabel != null)
|
||||
_progressLabel.Uppercase = true;
|
||||
StyleLabel(_progressValue, UiTokens.FontBodyLarge, UiTokens.AccentBright);
|
||||
StyleLabel(_hint, UiTokens.FontBodySmall, UiTokens.Faint);
|
||||
StyleLabel(_sideLabel, UiTokens.FontTiny, UiTokens.Faint);
|
||||
if (_sideLabel != null)
|
||||
_sideLabel.Uppercase = true;
|
||||
StyleLabel(_plateGems, UiTokens.FontSmall, UiTokens.Muted);
|
||||
StyleLabel(_medalGlyph, UiTokens.FontBodyLarge, UiTokens.Muted);
|
||||
StyleLabel(_plateName, UiTokens.FontBodyLarge, UiTokens.InkBright);
|
||||
StyleLabel(_desc, UiTokens.FontBody, UiTokens.Body);
|
||||
StyleLabel(_grantCaption, UiTokens.FontTiny, UiTokens.Faint);
|
||||
if (_grantCaption != null)
|
||||
_grantCaption.Uppercase = true;
|
||||
StyleLabel(_grantName, UiTokens.FontBodyLarge, UiTokens.InkBright);
|
||||
StyleLabel(_passportEmpty, UiTokens.FontBodyLarge, UiTokens.Faint);
|
||||
StyleLabel(_hudTitle, UiTokens.FontTiny, UiTokens.AccentDim);
|
||||
if (_hudTitle != null)
|
||||
_hudTitle.Uppercase = true;
|
||||
StyleLabel(_hudCount, UiTokens.FontSmall, UiTokens.Muted);
|
||||
StyleLabel(_hudEmpty, UiTokens.FontBodySmall, UiTokens.Faint);
|
||||
StyleLabel(_hudFoot, UiTokens.FontTiny, UiTokens.Faint);
|
||||
|
||||
if (_closeButton != null)
|
||||
{
|
||||
_closeButton.Text = "✕";
|
||||
_closeButton.TooltipText = Translate("ui/journal/close");
|
||||
CthangoverTheme.ApplyIcon(_closeButton);
|
||||
}
|
||||
|
||||
if (_resetButton != null)
|
||||
{
|
||||
_resetButton.FocusMode = FocusModeEnum.None;
|
||||
_resetButton.AddThemeFontSizeOverride("font_size", UiTokens.FontBody);
|
||||
}
|
||||
|
||||
if (_medal != null)
|
||||
_medal.AddThemeStyleboxOverride("panel", MakeCircleStyle(UiTokens.Line));
|
||||
if (_plateNameBox != null)
|
||||
_plateNameBox.AddThemeStyleboxOverride("panel", MakeNamePlateStyle(UiTokens.Line));
|
||||
}
|
||||
|
||||
private void ApplyStaticTexts()
|
||||
{
|
||||
if (_title != null)
|
||||
_title.Text = Translate("ui/skills/title");
|
||||
if (_progressLabel != null)
|
||||
_progressLabel.Text = Translate("ui/skills/collection");
|
||||
if (_hint != null)
|
||||
_hint.Text = Translate("ui/skills/hint");
|
||||
if (_resetButton != null)
|
||||
_resetButton.Text = Translate("ui/skills/reset");
|
||||
if (_sideLabel != null)
|
||||
_sideLabel.Text = Translate("ui/skills/side_title");
|
||||
if (_hudTitle != null)
|
||||
_hudTitle.Text = Translate("ui/skills/collection_effect");
|
||||
if (_hudFoot != null)
|
||||
_hudFoot.Text = Translate("ui/skills/collection_effect_hint");
|
||||
if (_hudEmpty != null)
|
||||
_hudEmpty.Text = Translate("ui/skills/no_effects");
|
||||
if (_grantCaption != null)
|
||||
_grantCaption.Text = Translate("ui/skills/grants_action");
|
||||
|
||||
if (_searchEdit != null)
|
||||
_searchEdit.PlaceholderText = Translate("ui/skills/search");
|
||||
if (_sortOption != null)
|
||||
{
|
||||
_sortOption.Clear();
|
||||
_sortOption.AddItem(Translate("ui/skills/sort_rarity"), (int)SortMode.Rarity);
|
||||
_sortOption.AddItem(Translate("ui/skills/sort_type"), (int)SortMode.Type);
|
||||
_sortOption.AddItem(Translate("ui/skills/sort_name"), (int)SortMode.Name);
|
||||
_sortOption.Selected = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private List<SkillInfo> OwnedSkills()
|
||||
{
|
||||
var result = new List<SkillInfo>();
|
||||
foreach (var id in GameData.Instance.Runtime.SkillData.Skills)
|
||||
{
|
||||
var skill = SkillFactory.Instance.Get(id);
|
||||
if (skill != null)
|
||||
result.Add(skill);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void RebuildTypeTabs(List<SkillInfo> owned)
|
||||
{
|
||||
ClearChildren(_tabsRow);
|
||||
if (_tabsRow == null)
|
||||
return;
|
||||
|
||||
AddTypeTab(null, "ui/skills/filter_all", owned.Count);
|
||||
AddTypeTab(SkillType.Passive, SkillPalette.TypeLocaleKey(SkillType.Passive), CountType(owned, SkillType.Passive));
|
||||
AddTypeTab(SkillType.Active, SkillPalette.TypeLocaleKey(SkillType.Active), CountType(owned, SkillType.Active));
|
||||
AddTypeTab(SkillType.ForCharacter, SkillPalette.TypeLocaleKey(SkillType.ForCharacter), CountType(owned, SkillType.ForCharacter));
|
||||
}
|
||||
|
||||
private void AddTypeTab(SkillType? type, string localeKey, int count)
|
||||
{
|
||||
var active = _typeFilter == type;
|
||||
var button = new Button
|
||||
{
|
||||
Text = Translate(localeKey) + " " + count,
|
||||
FocusMode = FocusModeEnum.None,
|
||||
};
|
||||
button.AddThemeFontSizeOverride("font_size", UiTokens.FontBody);
|
||||
button.AddThemeStyleboxOverride("normal", MakeTypeTabStyle(active, false));
|
||||
button.AddThemeStyleboxOverride("hover", MakeTypeTabStyle(active, true));
|
||||
button.AddThemeStyleboxOverride("pressed", MakeTypeTabStyle(active, true));
|
||||
button.AddThemeStyleboxOverride("focus", new StyleBoxEmpty());
|
||||
button.AddThemeColorOverride("font_color", active ? UiTokens.AccentBright : UiTokens.Muted);
|
||||
button.AddThemeColorOverride("font_hover_color", active ? UiTokens.AccentBright : UiTokens.Ink);
|
||||
button.AddThemeColorOverride("font_pressed_color", UiTokens.AccentBright);
|
||||
button.Pressed += () => OnTypePressed(type);
|
||||
_tabsRow.AddChild(button);
|
||||
}
|
||||
|
||||
private void RebuildRarityTabs(List<SkillInfo> owned)
|
||||
{
|
||||
ClearChildren(_rarityTabs);
|
||||
if (_rarityTabs == null)
|
||||
return;
|
||||
|
||||
AddRarityTab(null, "ui/skills/filter_rarities", owned.Count, UiTokens.Accent, false);
|
||||
foreach (RareType rarity in Enum.GetValues(typeof(RareType)))
|
||||
AddRarityTab(rarity, SkillPalette.RarityLocaleKey(rarity), CountRarity(owned, rarity), SkillPalette.RarityColor(rarity), true);
|
||||
}
|
||||
|
||||
private void AddRarityTab(RareType? rarity, string localeKey, int count, Color color, bool withGlyph)
|
||||
{
|
||||
var active = _rarityFilter == rarity;
|
||||
var button = new Button
|
||||
{
|
||||
Text = (withGlyph ? "◆ " : string.Empty) + Translate(localeKey) + " " + count,
|
||||
FocusMode = FocusModeEnum.None,
|
||||
};
|
||||
button.AddThemeFontSizeOverride("font_size", UiTokens.FontSmall);
|
||||
button.AddThemeStyleboxOverride("normal", MakeRarityTabStyle(active, false, color));
|
||||
button.AddThemeStyleboxOverride("hover", MakeRarityTabStyle(active, true, color));
|
||||
button.AddThemeStyleboxOverride("pressed", MakeRarityTabStyle(active, true, color));
|
||||
button.AddThemeStyleboxOverride("focus", new StyleBoxEmpty());
|
||||
button.AddThemeColorOverride("font_color", active ? color : UiTokens.Alpha(color, 0.75f));
|
||||
button.AddThemeColorOverride("font_hover_color", color);
|
||||
button.AddThemeColorOverride("font_pressed_color", color);
|
||||
button.Pressed += () => OnRarityPressed(rarity);
|
||||
_rarityTabs.AddChild(button);
|
||||
}
|
||||
|
||||
private void RebuildGrid(List<SkillInfo> owned)
|
||||
{
|
||||
var previous = _selected;
|
||||
ClearCards();
|
||||
|
||||
if (_gridContent == null)
|
||||
return;
|
||||
|
||||
var skills = FilterAndSort(owned);
|
||||
if (skills.Count == 0)
|
||||
{
|
||||
if (_gridScroll != null)
|
||||
_gridScroll.Visible = false;
|
||||
if (_emptyLabel != null)
|
||||
{
|
||||
_emptyLabel.Visible = true;
|
||||
_emptyLabel.Text = Translate(owned.Count == 0 ? "ui/skills/empty" : "ui/skills/nothing_found");
|
||||
}
|
||||
HidePassport(owned.Count == 0 ? "ui/skills/empty" : "ui/skills/nothing_found");
|
||||
GameLogger.Log("SKILLSPANEL", $"RebuildGrid: no cards ({owned.Count} owned)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_gridScroll != null)
|
||||
_gridScroll.Visible = true;
|
||||
if (_emptyLabel != null)
|
||||
_emptyLabel.Visible = false;
|
||||
|
||||
var cardSize = CalculateCardSize();
|
||||
for (var i = 0; i < skills.Count; i++)
|
||||
{
|
||||
var skill = skills[i];
|
||||
var card = _cardPrefab?.Instantiate<SkillCardBehaviour>();
|
||||
if (card == null)
|
||||
continue;
|
||||
|
||||
_gridContent.AddChild(card);
|
||||
card.Construct(skill);
|
||||
PlaceCard(card, i, cardSize);
|
||||
|
||||
card.GuiInput += @event =>
|
||||
{
|
||||
if (@event is InputEventMouseButton mouse && mouse.Pressed && mouse.ButtonIndex == MouseButton.Left)
|
||||
SelectSkill(skill);
|
||||
};
|
||||
|
||||
_cards.Add(card);
|
||||
}
|
||||
|
||||
if (_cards.Count == 0)
|
||||
{
|
||||
HidePassport("ui/skills/nothing_found");
|
||||
return;
|
||||
}
|
||||
|
||||
var last = _cards[_cards.Count - 1];
|
||||
_gridContent.CustomMinimumSize = new Vector2(0, last.Position.Y + last.Size.Y + CellGap);
|
||||
|
||||
var keep = previous == null ? null : skills.Find(candidate => candidate.ID == previous.ID);
|
||||
SelectSkill(keep ?? _cards[0].Model);
|
||||
|
||||
GameLogger.Log("SKILLSPANEL", $"RebuildGrid: {_cards.Count} card(s) from {owned.Count} owned");
|
||||
}
|
||||
|
||||
private List<SkillInfo> FilterAndSort(List<SkillInfo> owned)
|
||||
{
|
||||
var result = new List<SkillInfo>();
|
||||
foreach (var skill in owned)
|
||||
{
|
||||
if (_typeFilter.HasValue && skill.SkillType != _typeFilter.Value)
|
||||
continue;
|
||||
if (_rarityFilter.HasValue && skill.RareType != _rarityFilter.Value)
|
||||
continue;
|
||||
if (!MatchesSearch(skill))
|
||||
continue;
|
||||
result.Add(skill);
|
||||
}
|
||||
result.Sort(CompareSkills);
|
||||
return result;
|
||||
}
|
||||
|
||||
private bool MatchesSearch(SkillInfo skill)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_search))
|
||||
return true;
|
||||
|
||||
var needle = _search.Trim().ToLowerInvariant();
|
||||
if (Translate(skill.Name).ToLowerInvariant().Contains(needle))
|
||||
return true;
|
||||
return skill.ID != null && skill.ID.ToLowerInvariant().Contains(needle);
|
||||
}
|
||||
|
||||
private int CompareSkills(SkillInfo a, SkillInfo b)
|
||||
{
|
||||
switch (_sort)
|
||||
{
|
||||
case SortMode.Name:
|
||||
return string.Compare(Translate(a.Name), Translate(b.Name), StringComparison.OrdinalIgnoreCase);
|
||||
case SortMode.Type:
|
||||
{
|
||||
var type = ((int)a.SkillType).CompareTo((int)b.SkillType);
|
||||
return type != 0 ? type : string.Compare(Translate(a.Name), Translate(b.Name), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
default:
|
||||
{
|
||||
var rarity = ((int)b.RareType).CompareTo((int)a.RareType);
|
||||
return rarity != 0 ? rarity : string.Compare(Translate(a.Name), Translate(b.Name), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Vector2 CalculateCardSize()
|
||||
{
|
||||
var viewportWidth = GetViewportRect().Size.X;
|
||||
if (viewportWidth <= 0f)
|
||||
viewportWidth = 1920f;
|
||||
|
||||
var panelWidth = viewportWidth - 80f - 48f;
|
||||
var showcaseWidth = Mathf.Max(320f, panelWidth - SideWidth - 14f);
|
||||
var cardWidth = (showcaseWidth - (Columns - 1) * CellGap) / Columns;
|
||||
cardWidth = Mathf.Clamp(cardWidth, 72f, 220f);
|
||||
return new Vector2(cardWidth, cardWidth * CardAspect);
|
||||
}
|
||||
|
||||
private static void PlaceCard(SkillCardBehaviour card, int index, Vector2 size)
|
||||
{
|
||||
var column = index % Columns;
|
||||
var row = index / Columns;
|
||||
card.Position = new Vector2(column * (size.X + CellGap), row * (size.Y + CellGap));
|
||||
card.AnchorRight = 0f;
|
||||
card.AnchorBottom = 0f;
|
||||
card.Size = size;
|
||||
}
|
||||
|
||||
private void SelectSkill(SkillInfo skill)
|
||||
{
|
||||
_selected = skill;
|
||||
foreach (var card in _cards)
|
||||
card.SetSelected(ReferenceEquals(card.Model, skill));
|
||||
BuildPassport(skill);
|
||||
}
|
||||
|
||||
private void BuildPassport(SkillInfo skill)
|
||||
{
|
||||
if (skill == null)
|
||||
{
|
||||
HidePassport("ui/skills/nothing_found");
|
||||
return;
|
||||
}
|
||||
|
||||
var rarityColor = SkillPalette.RarityColor(skill.RareType);
|
||||
var rarityLine = UiTokens.Alpha(rarityColor, 0.5f);
|
||||
|
||||
SetPassportBlockVisible(true);
|
||||
if (_passportEmpty != null)
|
||||
_passportEmpty.Visible = false;
|
||||
|
||||
if (_plateBg != null)
|
||||
_plateBg.AddThemeStyleboxOverride("panel", MakePanelStyle(UiTokens.Surface1, rarityColor, UiTokens.RadiusMd, true));
|
||||
if (_plateImage != null)
|
||||
_plateImage.Texture = skill.Sprite;
|
||||
if (_plateGems != null)
|
||||
{
|
||||
_plateGems.Text = new string('◆', Mathf.Clamp((int)skill.RareType + 1, 1, 5));
|
||||
_plateGems.AddThemeColorOverride("font_color", rarityColor);
|
||||
}
|
||||
if (_medal != null)
|
||||
_medal.AddThemeStyleboxOverride("panel", MakeCircleStyle(rarityColor));
|
||||
if (_medalGlyph != null)
|
||||
_medalGlyph.AddThemeColorOverride("font_color", rarityColor);
|
||||
if (_plateName != null)
|
||||
_plateName.Text = Translate(skill.Name);
|
||||
if (_plateNameBox != null)
|
||||
_plateNameBox.AddThemeStyleboxOverride("panel", MakeNamePlateStyle(rarityLine));
|
||||
|
||||
ClearChildren(_badges);
|
||||
if (_badges != null)
|
||||
{
|
||||
_badges.AddChild(UiKit.Badge(Translate(SkillPalette.RarityLocaleKey(skill.RareType)), rarityColor));
|
||||
_badges.AddChild(UiKit.Badge(Translate(SkillPalette.TypeLocaleKey(skill.SkillType)), SkillPalette.TypeColor(skill.SkillType)));
|
||||
}
|
||||
|
||||
if (_desc != null)
|
||||
_desc.Text = Translate(skill.Description);
|
||||
|
||||
ClearChildren(_chips);
|
||||
if (_chips != null && skill.Effects != null)
|
||||
{
|
||||
foreach (var effect in skill.Effects)
|
||||
{
|
||||
if (effect == null || string.IsNullOrEmpty(effect.Key))
|
||||
continue;
|
||||
_chips.AddChild(UiKit.PropertyChip(Translate(effect.Key), effect.FormatValue()));
|
||||
}
|
||||
}
|
||||
|
||||
ApplyGrant(skill.GrantsAction);
|
||||
}
|
||||
|
||||
private void HidePassport(string messageKey)
|
||||
{
|
||||
SetPassportBlockVisible(false);
|
||||
if (_passportEmpty != null)
|
||||
{
|
||||
_passportEmpty.Visible = true;
|
||||
_passportEmpty.Text = Translate(messageKey);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetPassportBlockVisible(bool visible)
|
||||
{
|
||||
if (_plate != null)
|
||||
_plate.Visible = visible;
|
||||
if (_badges != null)
|
||||
_badges.Visible = visible;
|
||||
if (_desc != null)
|
||||
_desc.Visible = visible;
|
||||
if (_chips != null)
|
||||
_chips.Visible = visible;
|
||||
if (_grant != null && !visible)
|
||||
_grant.Visible = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the action card a skill gifts to the player. Skills without a
|
||||
/// granted action (pure passives and gates) keep the block hidden, and
|
||||
/// a broken cross-mod reference degrades to a plain skill card instead
|
||||
/// of failing the whole study plate.
|
||||
/// </summary>
|
||||
private void ApplyGrant(string actionId)
|
||||
{
|
||||
var action = string.IsNullOrEmpty(actionId) ? null : ActionTemplateRegistry.Get(actionId);
|
||||
if (_grant != null)
|
||||
_grant.Visible = action != null;
|
||||
if (action == null)
|
||||
return;
|
||||
|
||||
if (_grantIcon != null)
|
||||
_grantIcon.Texture = action.Image;
|
||||
if (_grantName != null)
|
||||
_grantName.Text = Translate(action.Name);
|
||||
}
|
||||
|
||||
private void RebuildHud(List<SkillInfo> owned)
|
||||
{
|
||||
if (_hudCount != null)
|
||||
_hudCount.Text = owned.Count.ToString();
|
||||
if (_progressValue != null)
|
||||
_progressValue.Text = owned.Count.ToString();
|
||||
|
||||
var totals = SkillEffectAggregator.Aggregate(owned);
|
||||
ClearChildren(_hudList);
|
||||
|
||||
var hasTotals = totals.Count > 0;
|
||||
if (_hudScroll != null)
|
||||
_hudScroll.Visible = hasTotals;
|
||||
if (_hudEmpty != null)
|
||||
_hudEmpty.Visible = !hasTotals;
|
||||
|
||||
if (_hudList == null)
|
||||
return;
|
||||
|
||||
foreach (var total in totals)
|
||||
{
|
||||
var cell = UiKit.Row(10);
|
||||
var value = UiKit.Label(total.FormatValue(), UiTokens.FontBodyLarge, UiTokens.AccentBright);
|
||||
value.CustomMinimumSize = new Vector2(54, 0);
|
||||
value.HorizontalAlignment = HorizontalAlignment.Right;
|
||||
cell.AddChild(value);
|
||||
|
||||
var key = UiKit.Label(Translate(total.Key), UiTokens.FontBodySmall, UiTokens.Body);
|
||||
key.SizeFlagsHorizontal = SizeFlags.ExpandFill;
|
||||
key.ClipText = true;
|
||||
cell.AddChild(key);
|
||||
|
||||
_hudList.AddChild(cell);
|
||||
}
|
||||
}
|
||||
|
||||
private static int CountType(List<SkillInfo> skills, SkillType type)
|
||||
{
|
||||
var count = 0;
|
||||
foreach (var skill in skills)
|
||||
{
|
||||
if (skill.SkillType == type)
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private static int CountRarity(List<SkillInfo> skills, RareType rarity)
|
||||
{
|
||||
var count = 0;
|
||||
foreach (var skill in skills)
|
||||
{
|
||||
if (skill.RareType == rarity)
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private void ClearCards()
|
||||
{
|
||||
foreach (var card in _cards)
|
||||
card.QueueFree();
|
||||
_cards.Clear();
|
||||
}
|
||||
|
||||
private void OnTypePressed(SkillType? type)
|
||||
{
|
||||
_typeFilter = type;
|
||||
var owned = OwnedSkills();
|
||||
RebuildTypeTabs(owned);
|
||||
RebuildGrid(owned);
|
||||
}
|
||||
|
||||
private void OnRarityPressed(RareType? rarity)
|
||||
{
|
||||
_rarityFilter = rarity;
|
||||
var owned = OwnedSkills();
|
||||
RebuildRarityTabs(owned);
|
||||
RebuildGrid(owned);
|
||||
}
|
||||
|
||||
private void OnSearchChanged(string text)
|
||||
{
|
||||
_search = text;
|
||||
RebuildGrid(OwnedSkills());
|
||||
}
|
||||
|
||||
private void OnSortSelected(long index)
|
||||
{
|
||||
_sort = (SortMode)index;
|
||||
RebuildGrid(OwnedSkills());
|
||||
}
|
||||
|
||||
private void OnResetPressed()
|
||||
{
|
||||
ShowConstruct();
|
||||
}
|
||||
|
||||
private void OnClosePressed()
|
||||
{
|
||||
Hide();
|
||||
}
|
||||
|
||||
private static string Translate(string key)
|
||||
=> TranslationServer.Translate(key).ToString();
|
||||
|
||||
private static void StyleLabel(Label label, int fontSize, Color color)
|
||||
{
|
||||
if (label == null)
|
||||
return;
|
||||
label.AddThemeFontSizeOverride("font_size", fontSize);
|
||||
label.AddThemeColorOverride("font_color", color);
|
||||
}
|
||||
|
||||
private static void ApplyContentMargins(StyleBoxFlat style, int horizontal, int vertical)
|
||||
{
|
||||
style.ContentMarginLeft = horizontal;
|
||||
style.ContentMarginRight = horizontal;
|
||||
style.ContentMarginTop = vertical;
|
||||
style.ContentMarginBottom = vertical;
|
||||
}
|
||||
|
||||
private static StyleBoxFlat MakePanelStyle(Color background, Color border, int radius, bool shadow = false)
|
||||
{
|
||||
var style = UiKit.Frame(radius, background, border);
|
||||
if (shadow)
|
||||
{
|
||||
style.ShadowColor = new Color(0f, 0f, 0f, 0.6f);
|
||||
style.ShadowSize = 18;
|
||||
style.ShadowOffset = new Vector2(0f, 8f);
|
||||
}
|
||||
return style;
|
||||
}
|
||||
|
||||
private static StyleBoxFlat MakeSideStyle()
|
||||
{
|
||||
var style = new StyleBoxFlat
|
||||
{
|
||||
BgColor = UiTokens.SideBg,
|
||||
BorderColor = UiTokens.Line,
|
||||
BorderWidthLeft = 1,
|
||||
ContentMarginLeft = 20,
|
||||
ContentMarginRight = 18,
|
||||
ContentMarginTop = 16,
|
||||
ContentMarginBottom = 16,
|
||||
};
|
||||
return style;
|
||||
}
|
||||
|
||||
private static StyleBoxFlat MakeCircleStyle(Color border)
|
||||
{
|
||||
var style = UiKit.Frame(17, UiTokens.Surface0, border);
|
||||
ApplyContentMargins(style, 5, 4);
|
||||
return style;
|
||||
}
|
||||
|
||||
private static StyleBoxFlat MakeNamePlateStyle(Color border)
|
||||
{
|
||||
var style = UiKit.Frame(UiTokens.RadiusXs, UiTokens.SlotTagBg, border);
|
||||
ApplyContentMargins(style, 10, 4);
|
||||
return style;
|
||||
}
|
||||
|
||||
private static StyleBoxFlat MakeTypeTabStyle(bool active, bool hover)
|
||||
{
|
||||
var style = new StyleBoxFlat();
|
||||
style.BgColor = active ? UiTokens.AccentSoft : hover ? UiTokens.WhiteTrace : Colors.Transparent;
|
||||
if (active)
|
||||
{
|
||||
style.BorderColor = UiTokens.AccentLine;
|
||||
style.BorderWidthLeft = 1;
|
||||
style.BorderWidthRight = 1;
|
||||
style.BorderWidthTop = 1;
|
||||
style.BorderWidthBottom = 1;
|
||||
}
|
||||
style.CornerRadiusTopLeft = UiTokens.RadiusSm;
|
||||
style.CornerRadiusTopRight = UiTokens.RadiusSm;
|
||||
style.CornerRadiusBottomLeft = UiTokens.RadiusSm;
|
||||
style.CornerRadiusBottomRight = UiTokens.RadiusSm;
|
||||
ApplyContentMargins(style, 13, 7);
|
||||
return style;
|
||||
}
|
||||
|
||||
private static StyleBoxFlat MakeRarityTabStyle(bool active, bool hover, Color color)
|
||||
{
|
||||
var style = UiKit.Frame(UiTokens.RadiusXs, Colors.Transparent, Colors.Transparent);
|
||||
if (active)
|
||||
{
|
||||
style.BgColor = UiTokens.Alpha(color, 0.09f);
|
||||
style.BorderColor = UiTokens.Alpha(color, 0.5f);
|
||||
}
|
||||
else if (hover)
|
||||
{
|
||||
style.BgColor = UiTokens.WhiteTrace;
|
||||
}
|
||||
ApplyContentMargins(style, 9, 4);
|
||||
return style;
|
||||
}
|
||||
|
||||
private static void ClearChildren(Node container)
|
||||
{
|
||||
if (container == null)
|
||||
return;
|
||||
foreach (var child in container.GetChildren())
|
||||
{
|
||||
if (child is Node node)
|
||||
{
|
||||
container.RemoveChild(node);
|
||||
node.QueueFree();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Cthangover.Mods.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// Marks the skills engine mod for subsystem coverage discovery: the
|
||||
/// coverage test scans loaded assemblies for this marker and requires a
|
||||
/// declared integration test for the subsystem.
|
||||
/// </summary>
|
||||
public static class SubsystemRegistration
|
||||
{
|
||||
public static string SubsystemId => "skills";
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user