371 lines
11 KiB
C#
371 lines
11 KiB
C#
using NeoModLoader.services;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
|
|
namespace IdleSpectator;
|
|
|
|
/// <summary>
|
|
/// Compact dossier portrait that reuses vanilla <see cref="UiUnitAvatarElement"/>
|
|
/// (stone frame + floor tile), falling back to a plain live sprite if needed.
|
|
/// </summary>
|
|
public static class DossierAvatar
|
|
{
|
|
private static UiUnitAvatarElement _template;
|
|
private static UiUnitAvatarElement _instance;
|
|
private static RectTransform _host;
|
|
private static Image _fallbackBg;
|
|
private static Image _fallbackSprite;
|
|
private static bool _usingVanilla;
|
|
private static bool _loggedMode;
|
|
private static long _shownActorId = -1;
|
|
|
|
public static bool UsingVanilla => _usingVanilla;
|
|
|
|
public static void EnsureHost(Transform parent, float size)
|
|
{
|
|
if (_host != null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
GameObject hostGo = new GameObject("DossierAvatarHost", typeof(RectTransform));
|
|
hostGo.transform.SetParent(parent, false);
|
|
_host = hostGo.GetComponent<RectTransform>();
|
|
_host.anchorMin = new Vector2(0f, 1f);
|
|
_host.anchorMax = new Vector2(0f, 1f);
|
|
_host.pivot = new Vector2(0f, 1f);
|
|
_host.sizeDelta = new Vector2(size, size);
|
|
|
|
TrySpawnVanilla(size);
|
|
if (!_usingVanilla)
|
|
{
|
|
BuildFallback(size);
|
|
}
|
|
}
|
|
|
|
public static void Place(float padX, float yFromTop, float size)
|
|
{
|
|
if (_host == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_host.anchoredPosition = new Vector2(padX, -yFromTop);
|
|
_host.sizeDelta = new Vector2(size, size);
|
|
if (_instance != null)
|
|
{
|
|
RectTransform rt = _instance.GetComponent<RectTransform>();
|
|
if (rt != null)
|
|
{
|
|
rt.anchorMin = Vector2.zero;
|
|
rt.anchorMax = Vector2.one;
|
|
rt.offsetMin = Vector2.zero;
|
|
rt.offsetMax = Vector2.zero;
|
|
rt.localScale = Vector3.one;
|
|
}
|
|
}
|
|
}
|
|
|
|
public static void SetActive(bool active)
|
|
{
|
|
if (_host != null)
|
|
{
|
|
_host.gameObject.SetActive(active);
|
|
}
|
|
}
|
|
|
|
public static void Show(Actor actor)
|
|
{
|
|
if (_host == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (actor == null || !actor.isAlive())
|
|
{
|
|
ClearActor();
|
|
return;
|
|
}
|
|
|
|
long id = actor.getID();
|
|
if (_usingVanilla && _instance != null && _shownActorId == id && _instance.gameObject.activeSelf)
|
|
{
|
|
try
|
|
{
|
|
_instance.updateTileSprite();
|
|
}
|
|
catch
|
|
{
|
|
// ignore tile refresh failures
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (_usingVanilla && _instance != null)
|
|
{
|
|
try
|
|
{
|
|
_instance.show_banner_kingdom = false;
|
|
_instance.show_banner_clan = false;
|
|
if (_instance.kingdomBanner != null)
|
|
{
|
|
_instance.kingdomBanner.gameObject.SetActive(false);
|
|
}
|
|
|
|
if (_instance.clanBanner != null)
|
|
{
|
|
_instance.clanBanner.gameObject.SetActive(false);
|
|
}
|
|
|
|
if (_instance.avatarLoader != null)
|
|
{
|
|
// Keep unit readable inside the compact frame.
|
|
_instance.avatarLoader.avatarSize = 0.55f;
|
|
}
|
|
|
|
_instance.show(actor);
|
|
_shownActorId = id;
|
|
DisableInteractions(_instance.gameObject);
|
|
return;
|
|
}
|
|
catch (System.Exception ex)
|
|
{
|
|
LogService.LogInfo("[IdleSpectator] Vanilla avatar show failed, using fallback: " + ex.Message);
|
|
FallbackToSprite(actor);
|
|
return;
|
|
}
|
|
}
|
|
|
|
FallbackToSprite(actor);
|
|
_shownActorId = id;
|
|
}
|
|
|
|
public static void ClearActor()
|
|
{
|
|
_shownActorId = -1;
|
|
if (_usingVanilla && _instance != null)
|
|
{
|
|
try
|
|
{
|
|
_instance.gameObject.SetActive(false);
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
if (_fallbackSprite != null)
|
|
{
|
|
_fallbackSprite.enabled = false;
|
|
}
|
|
}
|
|
|
|
private static void TrySpawnVanilla(float size)
|
|
{
|
|
UiUnitAvatarElement template = FindTemplate();
|
|
if (template == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
GameObject clone = Object.Instantiate(template.gameObject, _host, false);
|
|
clone.name = "VanillaUnitAvatar";
|
|
clone.SetActive(true);
|
|
|
|
_instance = clone.GetComponent<UiUnitAvatarElement>();
|
|
if (_instance == null)
|
|
{
|
|
Object.Destroy(clone);
|
|
return;
|
|
}
|
|
|
|
_template = template;
|
|
_usingVanilla = true;
|
|
_instance.show_banner_kingdom = false;
|
|
_instance.show_banner_clan = false;
|
|
|
|
RectTransform rt = clone.GetComponent<RectTransform>();
|
|
if (rt != null)
|
|
{
|
|
rt.anchorMin = Vector2.zero;
|
|
rt.anchorMax = Vector2.one;
|
|
rt.offsetMin = Vector2.zero;
|
|
rt.offsetMax = Vector2.zero;
|
|
rt.localScale = Vector3.one;
|
|
rt.sizeDelta = Vector2.zero;
|
|
}
|
|
|
|
DisableInteractions(clone);
|
|
if (!_loggedMode)
|
|
{
|
|
_loggedMode = true;
|
|
LogService.LogInfo("[IdleSpectator] Dossier portrait using vanilla UiUnitAvatarElement frame");
|
|
}
|
|
}
|
|
catch (System.Exception ex)
|
|
{
|
|
_usingVanilla = false;
|
|
_instance = null;
|
|
LogService.LogInfo("[IdleSpectator] Could not clone unit avatar: " + ex.Message);
|
|
}
|
|
}
|
|
|
|
private static UiUnitAvatarElement FindTemplate()
|
|
{
|
|
if (_template != null)
|
|
{
|
|
return _template;
|
|
}
|
|
|
|
UiUnitAvatarElement prefab = null;
|
|
UiUnitAvatarElement scene = null;
|
|
UiUnitAvatarElement[] all = Resources.FindObjectsOfTypeAll<UiUnitAvatarElement>();
|
|
for (int i = 0; i < all.Length; i++)
|
|
{
|
|
UiUnitAvatarElement el = all[i];
|
|
if (el == null || el.gameObject == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Skip our own clone if already present.
|
|
if (el.gameObject.name.StartsWith("VanillaUnitAvatar"))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!el.gameObject.scene.IsValid())
|
|
{
|
|
prefab = el;
|
|
break;
|
|
}
|
|
|
|
if (scene == null)
|
|
{
|
|
scene = el;
|
|
}
|
|
}
|
|
|
|
_template = prefab != null ? prefab : scene;
|
|
return _template;
|
|
}
|
|
|
|
private static void BuildFallback(float size)
|
|
{
|
|
_fallbackBg = new GameObject("FallbackBg", typeof(RectTransform), typeof(Image)).GetComponent<Image>();
|
|
_fallbackBg.transform.SetParent(_host, false);
|
|
RectTransform bgRt = _fallbackBg.GetComponent<RectTransform>();
|
|
bgRt.anchorMin = Vector2.zero;
|
|
bgRt.anchorMax = Vector2.one;
|
|
bgRt.offsetMin = Vector2.zero;
|
|
bgRt.offsetMax = Vector2.zero;
|
|
_fallbackBg.color = new Color(0.04f, 0.045f, 0.055f, 0.95f);
|
|
_fallbackBg.raycastTarget = false;
|
|
|
|
_fallbackSprite = HudCanvas.MakeIcon(_host, "FallbackLive", size);
|
|
RectTransform spRt = _fallbackSprite.GetComponent<RectTransform>();
|
|
spRt.anchorMin = new Vector2(0.5f, 0.5f);
|
|
spRt.anchorMax = new Vector2(0.5f, 0.5f);
|
|
spRt.pivot = new Vector2(0.5f, 0.5f);
|
|
spRt.anchoredPosition = Vector2.zero;
|
|
_fallbackSprite.preserveAspect = true;
|
|
|
|
if (!_loggedMode)
|
|
{
|
|
_loggedMode = true;
|
|
LogService.LogInfo("[IdleSpectator] Dossier portrait using fallback live sprite (no vanilla avatar template)");
|
|
}
|
|
}
|
|
|
|
private static void FallbackToSprite(Actor actor)
|
|
{
|
|
if (_fallbackSprite == null && _host != null)
|
|
{
|
|
BuildFallback(_host.sizeDelta.x);
|
|
}
|
|
|
|
if (_instance != null)
|
|
{
|
|
_instance.gameObject.SetActive(false);
|
|
}
|
|
|
|
if (_fallbackBg != null)
|
|
{
|
|
_fallbackBg.gameObject.SetActive(true);
|
|
// Prefer the unit's current floor tile as portrait ground, like vanilla.
|
|
try
|
|
{
|
|
if (actor != null && actor.current_tile != null && actor.current_tile.Type != null
|
|
&& actor.current_tile.Type.sprites != null && actor.current_tile.Type.sprites.main != null)
|
|
{
|
|
Sprite tile = actor.current_tile.Type.sprites.main.sprite;
|
|
if (tile != null)
|
|
{
|
|
_fallbackBg.sprite = tile;
|
|
_fallbackBg.color = Color.white;
|
|
_fallbackBg.type = Image.Type.Simple;
|
|
_fallbackBg.preserveAspect = false;
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// keep flat fill
|
|
}
|
|
}
|
|
|
|
if (_fallbackSprite != null)
|
|
{
|
|
Sprite live = HudIcons.FromActorLive(actor);
|
|
HudIcons.Apply(_fallbackSprite, live, preserveAspect: true);
|
|
if (live != null)
|
|
{
|
|
float w = Mathf.Max(1f, live.rect.width);
|
|
float h = Mathf.Max(1f, live.rect.height);
|
|
float max = _host != null ? _host.sizeDelta.x : 44f;
|
|
float scale = (max * 0.85f) / Mathf.Max(w, h);
|
|
_fallbackSprite.rectTransform.sizeDelta = new Vector2(w * scale, h * scale);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void DisableInteractions(GameObject root)
|
|
{
|
|
if (root == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Graphic[] graphics = root.GetComponentsInChildren<Graphic>(true);
|
|
for (int i = 0; i < graphics.Length; i++)
|
|
{
|
|
if (graphics[i] != null)
|
|
{
|
|
graphics[i].raycastTarget = false;
|
|
}
|
|
}
|
|
|
|
Behaviour[] tips = root.GetComponentsInChildren<TipButton>(true);
|
|
for (int i = 0; i < tips.Length; i++)
|
|
{
|
|
if (tips[i] != null)
|
|
{
|
|
tips[i].enabled = false;
|
|
}
|
|
}
|
|
|
|
Button[] buttons = root.GetComponentsInChildren<Button>(true);
|
|
for (int i = 0; i < buttons.Length; i++)
|
|
{
|
|
if (buttons[i] != null)
|
|
{
|
|
buttons[i].enabled = false;
|
|
buttons[i].interactable = false;
|
|
}
|
|
}
|
|
}
|
|
}
|