Handbuch Scroll-Listen entfernt. Handbuch offen und tabs eingefügt. Upgrades eingefügt. "Garage" eingefügt. Anbindung eines echten Gameplaymanager möglich (theoretisch)
This commit is contained in:
@@ -1,103 +1,48 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace DepthsOfGreed.HandbookUI
|
||||
{
|
||||
public sealed class BuildMenuController : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private Button[] categoryButtons;
|
||||
[SerializeField] private TowerCardView[] cardSlots;
|
||||
public TestPlacementController Placement { get; set; }
|
||||
private readonly List<TowerCardView> cards = new List<TowerCardView>();
|
||||
private Button[] categories;
|
||||
private int selectedCategory = -1;
|
||||
private readonly List<HandbookTowerEntry>[] groups = { new List<HandbookTowerEntry>(), new List<HandbookTowerEntry>(), new List<HandbookTowerEntry>(), new List<HandbookTowerEntry>() };
|
||||
private int selected = -1;
|
||||
private TowerCardView revealed;
|
||||
private HandbookTowerEntry[][] entries;
|
||||
|
||||
public int SelectedCategory => selected;
|
||||
public void Initialize(HandbookTowerCatalog catalog)
|
||||
{
|
||||
categories = new[] {
|
||||
transform.Find("LeftPage/CategoryBar/Category_Ammo").GetComponent<Button>(),
|
||||
transform.Find("LeftPage/CategoryBar/Category_Energy").GetComponent<Button>(),
|
||||
transform.Find("LeftPage/CategoryBar/Category_Special").GetComponent<Button>()
|
||||
};
|
||||
var left = transform.Find("LeftPage/Cards_Left");
|
||||
var right = transform.Find("RightPage/Cards_Right");
|
||||
int count = Mathf.Max(left.childCount, right.childCount);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
if (i < left.childCount) AddCard(left.GetChild(i));
|
||||
if (i < right.childCount) AddCard(right.GetChild(i));
|
||||
}
|
||||
var groups = new[] { new List<HandbookTowerEntry>(), new List<HandbookTowerEntry>(), new List<HandbookTowerEntry>() };
|
||||
if (catalog && catalog.towers != null)
|
||||
foreach (var entry in catalog.towers)
|
||||
if (entry) groups[(int)entry.category].Add(entry);
|
||||
entries = new[] { groups[0].ToArray(), groups[1].ToArray(), groups[2].ToArray() };
|
||||
foreach (var group in groups)
|
||||
if (group.Count > cards.Count)
|
||||
Debug.LogWarning("Handbuch: Mehr Turmtypen als Kartenplätze in einer Kategorie.", this);
|
||||
for (int i = 0; i < categories.Length; i++)
|
||||
{
|
||||
int index = i;
|
||||
categories[i].onClick.AddListener(() => SelectCategory(index));
|
||||
}
|
||||
foreach (var card in cardSlots) card.Initialize(this);
|
||||
foreach (var group in groups) group.Clear();
|
||||
if (catalog.towers != null) foreach (var entry in catalog.towers) if (entry) groups[(int)entry.menuGroup].Add(entry);
|
||||
for (int i = 0; i < categoryButtons.Length; i++) { int n = i; categoryButtons[i].onClick.AddListener(() => SelectCategory(n)); }
|
||||
SelectCategory(0);
|
||||
}
|
||||
|
||||
private void AddCard(Transform card)
|
||||
{
|
||||
var view = card.gameObject.AddComponent<TowerCardView>();
|
||||
view.Initialize(this);
|
||||
cards.Add(view);
|
||||
}
|
||||
|
||||
public void SelectCategory(int index)
|
||||
{
|
||||
if (index == selectedCategory) return;
|
||||
selectedCategory = index;
|
||||
revealed = null;
|
||||
for (int i = 0; i < cards.Count; i++)
|
||||
if (index < 0 || index >= groups.Length || index == selected) return;
|
||||
selected = index; ResetCards(); Bind();
|
||||
for (int i = 0; i < categoryButtons.Length; i++)
|
||||
{
|
||||
cards[i].SetBack(false, true);
|
||||
bool visible = i < entries[index].Length;
|
||||
cards[i].gameObject.SetActive(visible);
|
||||
if (visible) cards[i].Bind(entries[index][i]);
|
||||
}
|
||||
for (int i = 0; i < categories.Length; i++)
|
||||
{
|
||||
var colors = categories[i].colors;
|
||||
colors.normalColor = i == index ? new Color(1f, 0.72f, 0.2f) : new Color(0.82f, 0.82f, 0.82f);
|
||||
colors.highlightedColor = i == index ? new Color(1f, 0.82f, 0.4f) : Color.white;
|
||||
colors.selectedColor = colors.normalColor;
|
||||
colors.pressedColor = new Color(0.7f, 0.7f, 0.7f);
|
||||
categories[i].colors = colors;
|
||||
categories[i].image.color = Color.white;
|
||||
categoryButtons[i].image.color = Color.white; var colors = categoryButtons[i].colors;
|
||||
colors.normalColor = i == index ? new Color(1, 0.76f, 0.35f) : new Color(0.87f, 0.84f, 0.75f);
|
||||
colors.highlightedColor = i == index ? new Color(1, 0.86f, 0.55f) : new Color(0.97f, 0.94f, 0.85f);
|
||||
colors.selectedColor = colors.normalColor; categoryButtons[i].colors = colors;
|
||||
}
|
||||
}
|
||||
|
||||
private void Bind()
|
||||
{
|
||||
if (groups[selected].Count > cardSlots.Length) Debug.LogError("Handbuch: Mehr Einträge als vorbereitete Baukarten; weitere Slots im Inspector ergänzen.", this);
|
||||
for (int i = 0; i < cardSlots.Length; i++) { bool visible = i < groups[selected].Count; cardSlots[i].gameObject.SetActive(visible); if (visible) cardSlots[i].Bind(groups[selected][i]); }
|
||||
}
|
||||
public void ToggleCard(TowerCardView card)
|
||||
{
|
||||
if (revealed == card)
|
||||
{
|
||||
card.SetBack(false);
|
||||
revealed = null;
|
||||
return;
|
||||
}
|
||||
if (revealed) revealed.SetBack(false);
|
||||
revealed = card;
|
||||
card.SetBack(true);
|
||||
}
|
||||
|
||||
public void ResetCards()
|
||||
{
|
||||
revealed = null;
|
||||
foreach (var card in cards) card.SetBack(false, true);
|
||||
}
|
||||
|
||||
public void ResetForClose()
|
||||
{
|
||||
ResetCards();
|
||||
SelectCategory(0);
|
||||
if (revealed == card) { card.SetBack(false); revealed = null; return; }
|
||||
if (revealed) revealed.SetBack(false); revealed = card; card.SetBack(true);
|
||||
}
|
||||
public void ResetCards() { revealed = null; foreach (var card in cardSlots) card.SetBack(false, true); }
|
||||
public void ResetForClose() { ResetCards(); selected = -1; SelectCategory(0); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using DepthsOfGreed.HandbookUI;
|
||||
[InitializeOnLoad]
|
||||
public static class HandbookInspectorValidation
|
||||
{
|
||||
static HandbookInspectorValidation(){if(SessionState.GetBool("InspectorValidation",false))EditorApplication.playModeStateChanged+=Ready;}
|
||||
public static void Validate()
|
||||
{
|
||||
try
|
||||
{
|
||||
var scene=EditorSceneManager.OpenScene("Assets/Scenes/Tim_Test_UI.unity");
|
||||
var main=UnityEngine.Object.FindFirstObjectByType<HandbookController>(FindObjectsInactive.Include);var root=main.transform;
|
||||
if(!root.Find("GeneralFeedback")||!root.Find("OpenBook/ActiveTabLayer"))throw new Exception("Missing permanent UI.");
|
||||
if(root.GetComponentsInChildren<ScrollRect>(true).Length>0)throw new Exception("Scroll views remain in handbook.");
|
||||
var index=UnityEngine.Object.FindFirstObjectByType<HandbookIndexController>(FindObjectsInactive.Include);var iso=new SerializedObject(index);
|
||||
if(iso.FindProperty("entryButtons").arraySize!=24||iso.FindProperty("filterButtons").arraySize!=5)throw new Exception("Index inspector references invalid");
|
||||
var build=UnityEngine.Object.FindFirstObjectByType<BuildMenuController>(FindObjectsInactive.Include);if(new SerializedObject(build).FindProperty("categoryButtons").arraySize!=4)throw new Exception("Four build categories missing");
|
||||
foreach(var sceneRoot in scene.GetRootGameObjects())foreach(var tr in sceneRoot.GetComponentsInChildren<Transform>(true))if(GameObjectUtility.GetMonoBehavioursWithMissingScriptCount(tr.gameObject)>0)throw new Exception("Missing script: "+tr.name);
|
||||
var authorCount=root.GetComponentsInChildren<RectTransform>(true).Count(r=>!r.name.StartsWith("TMP"));SessionState.SetInt("InspectorRectCount",authorCount);
|
||||
SessionState.SetBool("InspectorValidation",true);EditorApplication.playModeStateChanged+=Ready;EditorApplication.EnterPlaymode();
|
||||
}catch(Exception e){Debug.LogException(e);EditorApplication.Exit(1);}
|
||||
}
|
||||
private static void Ready(PlayModeStateChange state)
|
||||
{
|
||||
if(state!=PlayModeStateChange.EnteredPlayMode)return;SessionState.SetBool("InspectorValidation",false);EditorApplication.playModeStateChanged-=Ready;EditorApplication.delayCall+=Run;
|
||||
}
|
||||
private static void Run()
|
||||
{
|
||||
try
|
||||
{
|
||||
var c=UnityEngine.Object.FindFirstObjectByType<HandbookController>();c.Open(0);var root=c.transform;
|
||||
if(root.GetComponentsInChildren<RectTransform>(true).Count(r=>!r.name.StartsWith("TMP"))!=SessionState.GetInt("InspectorRectCount",0))throw new Exception("UI generated at runtime");
|
||||
var menu=UnityEngine.Object.FindFirstObjectByType<BuildMenuController>();int[] expected={6,6,3,4};for(int i=0;i<4;i++){menu.SelectCategory(i);if(menu.GetComponentsInChildren<TowerCardView>().Length!=expected[i])throw new Exception("Category count incorrect: "+i);}
|
||||
var p=root.GetComponent<TestPlacementController>();var catalog=AssetDatabase.LoadAssetAtPath<HandbookTowerCatalog>("Assets/HandbookData/TestTurmkatalog.asset");var gun=catalog.towers.First(t=>t.id=="ammo_0");var energy=catalog.towers.First(t=>t.id=="energy_0");
|
||||
if(!p.Backend.ExecuteBuild(new BuildRequest(gun,new Vector3(3.5f,0,0))).Allowed||!p.Backend.ExecuteBuild(new BuildRequest(energy,new Vector3(0,3.5f,0))).Allowed)throw new Exception("Base placement failed");
|
||||
var obj=p.Backend.BuiltObjects[0].instance;var renderer=obj.GetComponent<SpriteRenderer>();if(!renderer || !renderer.sprite || (gun.baseSprite && renderer.sprite!=gun.baseSprite) || renderer.bounds.size.x>0.8f)throw new Exception("Base sprite or placeholder scaling incorrect");
|
||||
int before=p.Backend.Energy;if(!p.Backend.ExecuteUpgrade(p.Backend.BuiltObjects[0].instanceId).Allowed||p.Backend.Energy>=before)throw new Exception("Upgrade failed");
|
||||
c.Open(2);var upgrades=UnityEngine.Object.FindFirstObjectByType<HandbookUpgradesController>();if(upgrades.VisibleUpgradeCount!=2)throw new Exception("Upgrade listing failed");
|
||||
c.Open(3);var index=UnityEngine.Object.FindFirstObjectByType<HandbookIndexController>();if(index.VisibleEntryCount!=24)throw new Exception("All entries not displayed");int[] counts={24,12,3,4,5};for(int i=0;i<5;i++){index.SelectFilter(i);if(index.VisibleEntryCount!=counts[i])throw new Exception("Index filter count incorrect");}
|
||||
index.SelectEntry(0);if(!index.SelectedEnemy)throw new Exception("Enemy detail missing");c.Open(0);c.Open(3);if(index.SelectedEnemy||index.SelectedEntry)throw new Exception("Index selection not reset");
|
||||
c.Close();Debug.Log("INSPECTOR_RUNTIME_VALIDATION_OK: no runtime UI generation, sprites, 4 build categories, 5 index filters, bases, placement and upgrades passed.");EditorApplication.Exit(0);
|
||||
}catch(Exception e){Debug.LogException(e);EditorApplication.Exit(1);}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d29615975ab3eb04e88d9ffe0715404f
|
||||
@@ -1,242 +0,0 @@
|
||||
using System;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
|
||||
[InitializeOnLoad]
|
||||
public static class HandbookSceneValidation
|
||||
{
|
||||
public static void ConfigureFeedbackAndValidate()
|
||||
{
|
||||
try
|
||||
{
|
||||
var scene = EditorSceneManager.OpenScene("Assets/Scenes/Tim_Test_UI.unity");
|
||||
var controller = UnityEngine.Object.FindFirstObjectByType<DepthsOfGreed.HandbookUI.HandbookController>(FindObjectsInactive.Include);
|
||||
var serialized = new SerializedObject(controller);
|
||||
serialized.FindProperty("successfulPlacementDelay").floatValue = 0.5f;
|
||||
serialized.ApplyModifiedPropertiesWithoutUndo();
|
||||
EditorSceneManager.SaveScene(scene);
|
||||
Validate();
|
||||
}
|
||||
catch (Exception e) { Debug.LogException(e); EditorApplication.Exit(1); }
|
||||
}
|
||||
public static void SetupAndValidate()
|
||||
{
|
||||
try
|
||||
{
|
||||
const string dataFolder = "Assets/HandbookData";
|
||||
const string towerFolder = dataFolder + "/TestTowers";
|
||||
if (!AssetDatabase.IsValidFolder(dataFolder)) AssetDatabase.CreateFolder("Assets", "HandbookData");
|
||||
if (!AssetDatabase.IsValidFolder(towerFolder)) AssetDatabase.CreateFolder(dataFolder, "TestTowers");
|
||||
var names = new[] {
|
||||
new[] { "Gewehr", "Minigun", "Sniper", "Granatenwerfer", "Howitzer", "Flammenwerfer" },
|
||||
new[] { "Laser-Piercer", "Bouncer", "Pusher", "Knocker", "Laser-Beam" },
|
||||
new[] { "Falle", "Generator", "Staubsauger", "Spiegel" }
|
||||
};
|
||||
var prefixes = new[] { "ammo", "energy", "special" };
|
||||
var entries = new System.Collections.Generic.List<DepthsOfGreed.HandbookUI.HandbookTowerEntry>();
|
||||
for (int category = 0; category < names.Length; category++)
|
||||
for (int i = 0; i < names[category].Length; i++)
|
||||
{
|
||||
string path = towerFolder + "/" + prefixes[category] + "_" + i + "_" + names[category][i] + ".asset";
|
||||
var entry = AssetDatabase.LoadAssetAtPath<DepthsOfGreed.HandbookUI.HandbookTowerEntry>(path);
|
||||
if (!entry)
|
||||
{
|
||||
entry = ScriptableObject.CreateInstance<DepthsOfGreed.HandbookUI.HandbookTowerEntry>();
|
||||
entry.id = prefixes[category] + "_" + i;
|
||||
entry.displayName = names[category][i];
|
||||
entry.category = (DepthsOfGreed.HandbookUI.HandbookTowerCategory)category;
|
||||
entry.energyCost = 100 + i * 25;
|
||||
entry.summary = category == 0 ? "Schaden: 20 · Schüsse/s: 2" : category == 1 ? "Energieverbrauch: 5/s" :
|
||||
new[] { "Falle · Testwirkung", "Energieerzeugung · Testwert: 5/s", "Sammelfunktion · Testeintrag", "Umlenkung · Testeintrag" }[i];
|
||||
entry.description = entry.displayName + " – Testeintrag\n\nHier steht später eine kurze Erklärung der Funktion. Kosten und Werte sind vorläufige Testdaten.\n\nKlicken: zurück zur Baukarte";
|
||||
AssetDatabase.CreateAsset(entry, path);
|
||||
}
|
||||
entries.Add(entry);
|
||||
}
|
||||
const string catalogPath = dataFolder + "/TestTurmkatalog.asset";
|
||||
var catalog = AssetDatabase.LoadAssetAtPath<DepthsOfGreed.HandbookUI.HandbookTowerCatalog>(catalogPath);
|
||||
if (!catalog)
|
||||
{
|
||||
catalog = ScriptableObject.CreateInstance<DepthsOfGreed.HandbookUI.HandbookTowerCatalog>();
|
||||
catalog.towers = entries.ToArray();
|
||||
AssetDatabase.CreateAsset(catalog, catalogPath);
|
||||
}
|
||||
AssetDatabase.SaveAssets();
|
||||
var scene = EditorSceneManager.OpenScene("Assets/Scenes/Tim_Test_UI.unity");
|
||||
catalog = AssetDatabase.LoadAssetAtPath<DepthsOfGreed.HandbookUI.HandbookTowerCatalog>(catalogPath);
|
||||
if (!catalog) throw new Exception("Catalog asset could not be loaded after scene switch.");
|
||||
var controller = UnityEngine.Object.FindFirstObjectByType<DepthsOfGreed.HandbookUI.HandbookController>(FindObjectsInactive.Include);
|
||||
var serialized = new SerializedObject(controller);
|
||||
serialized.FindProperty("towerCatalog").objectReferenceValue = catalog;
|
||||
serialized.FindProperty("successfulPlacementDelay").floatValue = 1f;
|
||||
serialized.ApplyModifiedPropertiesWithoutUndo();
|
||||
EditorSceneManager.SaveScene(scene);
|
||||
Debug.Log("HANDBOOK_DATA_SETUP_OK: 15 editable tower assets and catalog connected; delay set to 1 second.");
|
||||
Validate();
|
||||
}
|
||||
catch (Exception exception) { Debug.LogException(exception); EditorApplication.Exit(1); }
|
||||
}
|
||||
static HandbookSceneValidation()
|
||||
{
|
||||
if (SessionState.GetBool("HandbookValidationPending", false))
|
||||
EditorApplication.playModeStateChanged += CheckPlayMode;
|
||||
}
|
||||
public static void Validate()
|
||||
{
|
||||
try
|
||||
{
|
||||
var scene = EditorSceneManager.OpenScene("Assets/Scenes/Tim_Test_UI.unity");
|
||||
var controllers = UnityEngine.Object.FindObjectsByType<DepthsOfGreed.HandbookUI.HandbookController>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||||
if (controllers.Length != 1) throw new Exception("Expected exactly one handbook controller.");
|
||||
var serialized = new SerializedObject(controllers[0]);
|
||||
foreach (var field in new[] { "closedBook", "openBook", "bookButton", "closeButton", "closedTabsRoot", "openTabsRoot", "towerCatalog", "testTowerPrefab" })
|
||||
if (!serialized.FindProperty(field).objectReferenceValue) throw new Exception("Missing reference: " + field);
|
||||
foreach (var field in new[] { "closedTabs", "openTabs", "pages" })
|
||||
{
|
||||
var array = serialized.FindProperty(field);
|
||||
if (array.arraySize != 4) throw new Exception("Invalid array: " + field);
|
||||
for (int i = 0; i < array.arraySize; i++)
|
||||
if (!array.GetArrayElementAtIndex(i).objectReferenceValue) throw new Exception("Missing array reference: " + field);
|
||||
}
|
||||
foreach (var root in scene.GetRootGameObjects())
|
||||
foreach (var transform in root.GetComponentsInChildren<Transform>(true))
|
||||
if (GameObjectUtility.GetMonoBehavioursWithMissingScriptCount(transform.gameObject) != 0)
|
||||
throw new Exception("Missing script on " + transform.name);
|
||||
Debug.Log("HANDBOOK_SCENE_VALIDATION_OK: Scene loaded; all controller references resolved; no missing scripts.");
|
||||
EditorApplication.playModeStateChanged += CheckPlayMode;
|
||||
SessionState.SetBool("HandbookValidationPending", true);
|
||||
EditorApplication.EnterPlaymode();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.LogException(exception);
|
||||
EditorApplication.Exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CheckPlayMode(PlayModeStateChange state)
|
||||
{
|
||||
if (state != PlayModeStateChange.EnteredPlayMode) return;
|
||||
SessionState.SetBool("HandbookValidationPending", false);
|
||||
EditorApplication.playModeStateChanged -= CheckPlayMode;
|
||||
try
|
||||
{
|
||||
var controller = UnityEngine.Object.FindFirstObjectByType<DepthsOfGreed.HandbookUI.HandbookController>();
|
||||
controller.Open(0);
|
||||
var menu = UnityEngine.Object.FindFirstObjectByType<DepthsOfGreed.HandbookUI.BuildMenuController>();
|
||||
if (!menu) throw new Exception("Build menu did not initialize.");
|
||||
foreach (var pair in new[] { (0, 6), (1, 5), (2, 4) })
|
||||
{
|
||||
menu.SelectCategory(pair.Item1);
|
||||
var cards = menu.GetComponentsInChildren<DepthsOfGreed.HandbookUI.TowerCardView>();
|
||||
if (cards.Length != pair.Item2) throw new Exception("Wrong category card count.");
|
||||
foreach (var card in cards)
|
||||
if (card.Entry == null || !card.transform.Find("Front").gameObject.activeSelf || card.transform.Find("Back").gameObject.activeSelf)
|
||||
throw new Exception("Invalid initial card state.");
|
||||
}
|
||||
menu.SelectCategory(0);
|
||||
var first = menu.GetComponentsInChildren<DepthsOfGreed.HandbookUI.TowerCardView>()[0];
|
||||
menu.ToggleCard(first);
|
||||
double deadline = EditorApplication.timeSinceStartup + 0.6;
|
||||
EditorApplication.CallbackFunction check = null;
|
||||
check = () => {
|
||||
if (EditorApplication.timeSinceStartup < deadline) return;
|
||||
EditorApplication.update -= check;
|
||||
try {
|
||||
if (!first.transform.Find("Back").gameObject.activeSelf) throw new Exception("Card flip did not complete.");
|
||||
menu.SelectCategory(1);
|
||||
if (first.transform.Find("Back").gameObject.activeSelf) throw new Exception("Category did not reset card.");
|
||||
controller.Close();
|
||||
controller.Open(0);
|
||||
if (menu.GetComponentsInChildren<DepthsOfGreed.HandbookUI.TowerCardView>().Length != 6)
|
||||
throw new Exception("Closing did not restore the default build category.");
|
||||
controller.Close();
|
||||
foreach (var card in menu.GetComponentsInChildren<DepthsOfGreed.HandbookUI.TowerCardView>(true))
|
||||
if (card.transform.Find("Back").gameObject.activeSelf) throw new Exception("Book close did not reset cards.");
|
||||
controller.Open(3);
|
||||
var indexMenu = UnityEngine.Object.FindFirstObjectByType<DepthsOfGreed.HandbookUI.HandbookIndexController>();
|
||||
if (!indexMenu || indexMenu.VisibleEntryCount != 15 || indexMenu.SelectedEntry != null)
|
||||
throw new Exception("Index initialization failed.");
|
||||
indexMenu.SelectEntry(0);
|
||||
if (!indexMenu.SelectedEntry) throw new Exception("Index selection failed.");
|
||||
indexMenu.ListScroll.verticalNormalizedPosition = 0f;
|
||||
controller.Open(0);
|
||||
controller.Open(3);
|
||||
if (indexMenu.SelectedEntry != null || indexMenu.ListScroll.verticalNormalizedPosition < 0.99f)
|
||||
throw new Exception("Index re-entry did not reset selection and scroll.");
|
||||
indexMenu.SelectFilter(2);
|
||||
if (indexMenu.VisibleEntryCount != 0 || indexMenu.SelectedEntry != null)
|
||||
throw new Exception("Enemy filter failed.");
|
||||
indexMenu.SelectFilter(1);
|
||||
if (indexMenu.VisibleEntryCount != 15) throw new Exception("Tower filter failed.");
|
||||
indexMenu.SelectEntry(1);
|
||||
controller.Close();
|
||||
controller.Open(3);
|
||||
if (indexMenu.SelectedEntry != null || indexMenu.ListScroll.verticalNormalizedPosition < 0.99f)
|
||||
throw new Exception("Reopening index did not reset selection and scroll.");
|
||||
controller.Close();
|
||||
var placement = controller.GetComponent<DepthsOfGreed.HandbookUI.TestPlacementController>();
|
||||
var entry = first.Entry;
|
||||
if (!placement.enabled) throw new Exception("Placement initialization failed.");
|
||||
controller.Open(0);
|
||||
var validScreen = (Vector2)Camera.main.WorldToScreenPoint(new Vector3(3.5f, 0, 0));
|
||||
if (!placement.Begin(entry, validScreen) || !placement.IsDragging) throw new Exception("Drag did not begin.");
|
||||
placement.Cancel();
|
||||
if (placement.IsDragging || placement.TowerCount != 0) throw new Exception("Cancel created a tower.");
|
||||
if (placement.TryPlaceAt(entry, Vector3.zero, out _)) throw new Exception("Center must be invalid.");
|
||||
if (placement.TryPlaceAt(entry, new Vector3(3.1f, 0, 0), out _)) throw new Exception("Base crosses inner wall.");
|
||||
if (placement.TryPlaceAt(entry, new Vector3(3.9f, 0, 0), out _)) throw new Exception("Base crosses outer wall.");
|
||||
int initialEnergy = placement.Energy;
|
||||
if (!placement.Begin(entry, validScreen)) throw new Exception("Second drag did not begin.");
|
||||
placement.End(validScreen);
|
||||
if (placement.TowerCount != 1 || placement.IsDragging) throw new Exception("Drag release failed.");
|
||||
if (!placement.IsWaitingForReturn) throw new Exception("Successful placement did not start delay.");
|
||||
for (int i = 1; i < 8; i++) {
|
||||
float angle = i * Mathf.PI / 4f;
|
||||
if (!placement.TryPlaceAt(entry, new Vector3(Mathf.Cos(angle) * 3.5f, Mathf.Sin(angle) * 3.5f, 0), out string reason))
|
||||
throw new Exception("Valid placement failed: " + reason);
|
||||
}
|
||||
if (placement.TowerCount != 8 || placement.Energy != initialEnergy - entry.energyCost * 8)
|
||||
throw new Exception("Wrong tower count or resource deduction.");
|
||||
if (placement.TryPlaceAt(entry, new Vector3(3.5f, 0, 0), out _)) throw new Exception("Overlap allowed.");
|
||||
float freeAngle = Mathf.PI / 8f;
|
||||
if (placement.TryPlaceAt(entry, new Vector3(Mathf.Cos(freeAngle) * 3.5f, Mathf.Sin(freeAngle) * 3.5f, 0), out string limitReason) || limitReason != "Turmlimit erreicht")
|
||||
throw new Exception("Tower limit failed.");
|
||||
double delayDeadline = EditorApplication.timeSinceStartup + 1.5;
|
||||
EditorApplication.CallbackFunction delayCheck = null;
|
||||
delayCheck = () => {
|
||||
if (EditorApplication.timeSinceStartup < delayDeadline) return;
|
||||
EditorApplication.update -= delayCheck;
|
||||
try {
|
||||
if (placement.IsWaitingForReturn) throw new Exception("Book delay never expired.");
|
||||
var group = menu.GetComponentInParent<CanvasGroup>();
|
||||
if (!group.blocksRaycasts) throw new Exception("Book input not restored after delay.");
|
||||
if (placement.Begin(entry, validScreen)) throw new Exception("Limit should prevent drag.");
|
||||
var toast = controller.transform.Find("FeedbackBox") ?? controller.transform.Find("Feedbackbox");
|
||||
var message = controller.transform.Find("FeedbackMessage").GetComponent<TMPro.TMP_Text>();
|
||||
if (!toast || !toast.gameObject.activeSelf || message.text != "Turmlimit erreicht")
|
||||
throw new Exception("Limit notice did not show background and message.");
|
||||
double toastDeadline = EditorApplication.timeSinceStartup + 2.9;
|
||||
EditorApplication.CallbackFunction toastCheck = null;
|
||||
toastCheck = () => {
|
||||
if (EditorApplication.timeSinceStartup < toastDeadline) return;
|
||||
EditorApplication.update -= toastCheck;
|
||||
try {
|
||||
if (toast.gameObject.activeSelf || message.text != "") throw new Exception("Notice did not disappear.");
|
||||
controller.Close();
|
||||
Debug.Log("HANDBOOK_RUNTIME_VALIDATION_OK: Category reset, 0.5-second placement delay, limit notice background and automatic dismissal passed; existing placement checks passed.");
|
||||
EditorApplication.Exit(0);
|
||||
} catch (Exception e) { Debug.LogException(e); EditorApplication.Exit(1); }
|
||||
};
|
||||
EditorApplication.update += toastCheck;
|
||||
} catch (Exception e) { Debug.LogException(e); EditorApplication.Exit(1); }
|
||||
};
|
||||
EditorApplication.update += delayCheck;
|
||||
} catch (Exception e) { Debug.LogException(e); EditorApplication.Exit(1); }
|
||||
};
|
||||
EditorApplication.update += check;
|
||||
}
|
||||
catch (Exception e) { Debug.LogException(e); EditorApplication.Exit(1); }
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 79121ad9902be844f8f2b736dbf9adb5
|
||||
@@ -0,0 +1,89 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
using UnityEngine.Rendering.Universal;
|
||||
using DepthsOfGreed.HandbookUI;
|
||||
|
||||
[InitializeOnLoad]
|
||||
public static class SupplyBasementValidation
|
||||
{
|
||||
static SupplyBasementValidation(){if(SessionState.GetBool("SupplyValidate",false))EditorApplication.playModeStateChanged+=Ready;}
|
||||
public static void Validate()
|
||||
{
|
||||
EditorSceneManager.OpenScene("Assets/Scenes/Tim_Test_UI.unity");
|
||||
SessionState.SetBool("SupplyValidate",true);EditorApplication.playModeStateChanged+=Ready;EditorApplication.EnterPlaymode();
|
||||
}
|
||||
static void Ready(PlayModeStateChange state)
|
||||
{
|
||||
if(state!=PlayModeStateChange.EnteredPlayMode)return;
|
||||
SessionState.SetBool("SupplyValidate",false);EditorApplication.playModeStateChanged-=Ready;
|
||||
double due=EditorApplication.timeSinceStartup+1;int stage=0;SupplyBasementController c=null;int count=0;long first=0;Vector3 bookScale=Vector3.one;
|
||||
EditorApplication.CallbackFunction run=null;run=()=>
|
||||
{
|
||||
if(EditorApplication.timeSinceStartup<due)return;
|
||||
try
|
||||
{
|
||||
if(stage==0)
|
||||
{
|
||||
c=UnityEngine.Object.FindFirstObjectByType<SupplyBasementController>();
|
||||
Assert(c && c.connectionButtons.Length==8 && c.priorityButtons.Length==3,"Authored references");
|
||||
Assert(!c.basementView.activeSelf && c.handbookRoot.activeSelf,"Starts on tower floor");
|
||||
count=Count(c);bookScale=c.handbook.transform.Find("OpenBook").localScale;
|
||||
var catalog=AssetDatabase.LoadAssetAtPath<HandbookTowerCatalog>("Assets/HandbookData/TestTurmkatalog.asset");
|
||||
var result=c.placement.Backend.ExecuteBuild(new BuildRequest(catalog.towers.First(t=>t.id=="ammo_0"),new Vector3(3.5f,0,0)));
|
||||
Assert(result.Allowed,"Test tower build");first=c.placement.Backend.BuiltObjects.Last().instanceId;
|
||||
c.UseLift();c.UseLift();stage=1;due=EditorApplication.timeSinceStartup+1.7;
|
||||
}
|
||||
else if(stage==1)
|
||||
{
|
||||
Assert(c.IsInBasement && !c.IsTravelling && c.basementView.activeSelf && c.handbookVisibility.alpha==0,"Lift arrival");
|
||||
Assert(c.handbookRoot.activeSelf && c.placement.isActiveAndEnabled,"Backend keeps updating in basement");
|
||||
Assert(c.connectionButtons[0].interactable && c.IsConnectionEnabled(first),"Automatic connection");
|
||||
c.ToggleConnection(0);Assert(!c.IsConnectionEnabled(first),"Disconnect");
|
||||
c.ToggleConnection(0);Assert(c.IsConnectionEnabled(first),"Reconnect");
|
||||
c.SetPriority(1);Assert(c.Priority==1,"Set priority");
|
||||
c.SetPriority(1);Assert(c.Priority==-1,"Return to automatic priority");
|
||||
// Force both travel directions independently of the loading pause.
|
||||
var pause=typeof(SupplyBasementController).GetField("truckPauseUntil",System.Reflection.BindingFlags.Instance|System.Reflection.BindingFlags.NonPublic);
|
||||
var truckPosition=c.truck.anchoredPosition;
|
||||
pause.SetValue(c,0f);c.truck.anchoredPosition=new Vector2(10000,truckPosition.y);c.SendMessage("Update");
|
||||
Assert(c.truck.localScale.x<0,"Truck faces left while driving left");
|
||||
pause.SetValue(c,0f);c.truck.anchoredPosition=new Vector2(-10000,truckPosition.y);c.SendMessage("Update");
|
||||
Assert(c.truck.localScale.x>0,"Truck faces right while driving right");
|
||||
c.truck.anchoredPosition=truckPosition;
|
||||
Assert(c.placement.Backend.BuiltObjects.Count==1,"No duplicate towers");
|
||||
if (SystemInfo.graphicsDeviceType != GraphicsDeviceType.Null && Environment.GetCommandLineArgs().Contains("-captureSupplyPreview")) Capture(c);
|
||||
Assert(Count(c)==count,"No generated room UI");
|
||||
c.UseLift();stage=2;due=EditorApplication.timeSinceStartup+1.7;
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert(!c.IsInBasement && !c.IsTravelling && c.handbookRoot.activeSelf && c.handbookVisibility.alpha==1,"Return to tower");
|
||||
Assert(c.handbook.transform.Find("OpenBook").localScale==bookScale,"Book scale preserved");
|
||||
Assert(c.placement.Backend.BuiltObjects.Count==1,"Tower retained across trip");
|
||||
Assert(Count(c)==count,"No UI created on second trip");
|
||||
Debug.Log("SUPPLY_BASEMENT_RUNTIME_OK: native hierarchy, auto connection, toggle, priority, two-way lift and preserved book layout.");
|
||||
EditorApplication.update-=run;EditorApplication.Exit(0);
|
||||
}
|
||||
}
|
||||
catch(Exception e){EditorApplication.update-=run;Debug.LogException(e);EditorApplication.Exit(1);}
|
||||
};EditorApplication.update+=run;
|
||||
}
|
||||
static int Count(SupplyBasementController c)=>c.GetComponentInParent<Canvas>().GetComponentsInChildren<RectTransform>(true).Count(t=>!t.name.StartsWith("TMP"));
|
||||
static void Assert(bool condition,string message){if(!condition)throw new Exception(message);}
|
||||
static void Capture(SupplyBasementController c)
|
||||
{
|
||||
var camera=Camera.main;var canvas=c.GetComponentInParent<Canvas>();
|
||||
var rt=new RenderTexture(1920,1080,24);rt.Create();camera.targetTexture=rt;camera.aspect=1920f/1080;
|
||||
canvas.renderMode=RenderMode.ScreenSpaceCamera;canvas.worldCamera=camera;canvas.planeDistance=1;canvas.sortingOrder=200;
|
||||
Canvas.ForceUpdateCanvases();RenderPipeline.SubmitRenderRequest(camera,new UniversalRenderPipeline.SingleCameraRequest{destination=rt});
|
||||
var previous=RenderTexture.active;RenderTexture.active=rt;var tex=new Texture2D(1920,1080,TextureFormat.RGB24,false);tex.ReadPixels(new Rect(0,0,1920,1080),0,0);tex.Apply();RenderTexture.active=previous;
|
||||
var folder=Path.Combine(Path.GetDirectoryName(Application.dataPath),"Temp","HandbookValidation");
|
||||
Directory.CreateDirectory(folder);File.WriteAllBytes(Path.Combine(folder,"basement-preview.png"),tex.EncodeToPNG());
|
||||
UnityEngine.Object.DestroyImmediate(tex);camera.targetTexture=null;rt.Release();UnityEngine.Object.DestroyImmediate(rt);canvas.renderMode=RenderMode.ScreenSpaceOverlay;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5351dea0200aeb346b62d793f21fde75
|
||||
@@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using DepthsOfGreed.HandbookUI;
|
||||
|
||||
[InitializeOnLoad]
|
||||
public static class SupplyBindingValidation
|
||||
{
|
||||
static SupplyBindingValidation(){if(SessionState.GetBool("SupplyBindingTest",false))EditorApplication.playModeStateChanged+=Ready;}
|
||||
public static void Validate(){EditorSceneManager.OpenScene("Assets/Scenes/Tim_Test_UI.unity");SessionState.SetBool("SupplyBindingTest",true);EditorApplication.playModeStateChanged+=Ready;EditorApplication.EnterPlaymode();}
|
||||
static void Check(bool value,string message){if(!value)throw new Exception(message);}
|
||||
static void Ready(PlayModeStateChange change)
|
||||
{
|
||||
if(change!=PlayModeStateChange.EnteredPlayMode)return;SessionState.SetBool("SupplyBindingTest",false);EditorApplication.playModeStateChanged-=Ready;
|
||||
double due=EditorApplication.timeSinceStartup+1;int stage=0;SupplyBasementController c=null;SupplyContractTestBackend fake=null;
|
||||
EditorApplication.CallbackFunction run=null;run=()=>
|
||||
{
|
||||
if(EditorApplication.timeSinceStartup<due)return;
|
||||
try
|
||||
{
|
||||
if(stage==0)
|
||||
{
|
||||
c=UnityEngine.Object.FindFirstObjectByType<SupplyBasementController>();fake=new GameObject("TEST_ONLY_SupplyAdapter").AddComponent<SupplyContractTestBackend>();
|
||||
var catalog=AssetDatabase.LoadAssetAtPath<HandbookTowerCatalog>("Assets/HandbookData/TestTurmkatalog.asset");
|
||||
Check(catalog.towers.Count(t=>t.supply.kind!=SupplyKind.None)==12,"Twelve supplied weapons");
|
||||
fake.state.towers=new[]{new TowerSupplyState{instanceId=987,type=catalog.towers.First(t=>t.id=="ammo_0"),magazine=17,magazineCapacity=40}};
|
||||
fake.state.channels=new[]{new SupplyChannelState{kind=SupplyKind.Physical,storage=180,storageCapacity=300,buffer=23,bufferCapacity=50}};
|
||||
fake.state.truck=new SupplyTruckState{phase=SupplyTruckPhase.Driving,from=SupplyStop.PhysicalTank,to=SupplyStop.Distributor,progress=.5f,cargoKind=SupplyKind.Physical,cargo=20,cargoCapacity=40};
|
||||
c.BindSupplyBackend(fake);c.UseLift();stage=1;due=EditorApplication.timeSinceStartup+1.7;
|
||||
}
|
||||
else
|
||||
{
|
||||
Check(c.IsInBasement && c.HasSupplyBackend,"Adapter attached");
|
||||
Check(c.connectionNames[0].text.Contains("987"),"Backend tower IDs, independent of prototype placement");
|
||||
Check(c.storageAmounts[0].text=="180 / 300" && c.bufferAmounts[0].text=="23 / 50","Channel snapshots rendered");
|
||||
Check(c.magazineAmounts[0].gameObject.activeSelf && c.magazineAmounts[0].text.Contains("17 / 40"),"Live magazine rendered");
|
||||
Check(Mathf.Abs(c.truck.anchoredPosition.x-Mathf.Lerp(c.loadingPositions[0],c.unloadingPosition,.5f))<.1f,"Backend drives truck; preview suppressed");
|
||||
c.ToggleConnection(0);Check(!fake.state.towers[0].connected && !c.IsConnectionEnabled(987),"Connection command accepted");
|
||||
fake.reject=true;c.ToggleConnection(0);Check(!c.IsConnectionEnabled(987),"Rejected command does not change connection");
|
||||
fake.reject=false;c.SetPriority(1);Check(fake.state.priority==SupplyKind.EnergyCells && c.Priority==1,"Priority command accepted");
|
||||
fake.reject=true;c.SetPriority(2);Check(c.Priority==1,"Rejected priority remains authoritative");
|
||||
fake.state.towers=Array.Empty<TowerSupplyState>();c.SendMessage("RefreshConnections");Check(!c.connectionButtons[0].interactable,"Removed tower frees slot");
|
||||
Debug.Log("SUPPLY_BINDING_VALIDATION_OK: fields, live amounts, external tower IDs, commands, rejection, truck route and removal.");EditorApplication.update-=run;EditorApplication.Exit(0);
|
||||
}
|
||||
}
|
||||
catch(Exception e){EditorApplication.update-=run;Debug.LogException(e);EditorApplication.Exit(1);}
|
||||
};EditorApplication.update+=run;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0d55a01e5b6f36446890591cb0761214
|
||||
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
namespace DepthsOfGreed.HandbookUI
|
||||
{
|
||||
public enum BuildUse { Structure, Trap, Ability, RepairCrew }
|
||||
public enum BuildRegion { WallRing, Interior, Gate }
|
||||
public enum BaseShape { Circle, Rectangle }
|
||||
public enum AmmoKind { None, Bullets, Explosives, Oil, ExplosivesAndOil, Energy }
|
||||
public enum AttackShape { None, Line, Cone, Area, Contact }
|
||||
public enum TargetMode { None, Single, Area, PlayerSelectedTarget, PlayerSelectedArea, Walls }
|
||||
public enum RotationMode { Free, Fixed, PlayerAimed, NotApplicable }
|
||||
public enum SpecialEffect { None, Ricochet, PushBack, Slow, Piercing, EnergyGeneration, CorpseCollection, Reflection, Repair, Bombardment, ContactTrap, PathBlock }
|
||||
[Serializable] public sealed class CombatDefinition
|
||||
{
|
||||
public AmmoKind ammunition;
|
||||
[Min(0)] public float ammoPerShot, energyPerSecond, damagePerHit, attacksPerSecond, range;
|
||||
[Tooltip("Used for continuous weapons instead of damagePerHit / ammoPerShot.")]
|
||||
[Min(0)] public float continuousDamagePerSecond, ammoPerSecond;
|
||||
public bool continuousAttack, unlimitedRange;
|
||||
public AttackShape attackShape;
|
||||
public TargetMode targets;
|
||||
public RotationMode rotation;
|
||||
[Range(0, 180)] public float coneAngle = 45;
|
||||
[Min(0)] public float areaRadius;
|
||||
public SpecialEffect effect;
|
||||
[Min(0)] public float effectStrength, effectDuration, attackCooldown;
|
||||
[Min(0)] public float knockbackDistance, energyGeneratedPerSecond, energyCapacityBonus, repairHealthPerSecond, corpsesCollectedPerSecond;
|
||||
[Range(0, 1)] public float slowFraction;
|
||||
[Min(0)] public float reflectionDamageMultiplier = 1;
|
||||
[Min(0)] public int ricochetCount;
|
||||
}
|
||||
[Serializable] public sealed class UpgradeDefinition
|
||||
{
|
||||
public string title;
|
||||
[Min(0)] public int energyCost, entrailsCost;
|
||||
[TextArea] public string description;
|
||||
[Min(0)] public float damageMultiplier = 1, fireRateMultiplier = 1, rangeMultiplier = 1, effectMultiplier = 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c6a1c1f7e6144c84491522bd463af59d
|
||||
@@ -0,0 +1,17 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
namespace DepthsOfGreed.HandbookUI
|
||||
{
|
||||
public sealed class GeneralFeedbackController : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private RectTransform panel;
|
||||
[SerializeField] private TMP_Text message;
|
||||
private bool error;
|
||||
private float until;
|
||||
public void Initialize(RectTransform book, TMP_FontAsset font) { if (font) message.font = font; panel.gameObject.SetActive(false); }
|
||||
public void ShowControls(string text) { error = false; until = 0; message.text = text; panel.gameObject.SetActive(true); }
|
||||
public void ShowError(string text, bool bookOpen) { error = true; until = Time.unscaledTime + 3; message.text = text; panel.gameObject.SetActive(true); }
|
||||
public void HideControls() { if (!error) panel.gameObject.SetActive(false); }
|
||||
private void Update() { if (error && Time.unscaledTime > until) { panel.gameObject.SetActive(false); error = false; } }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cff0fbda783c6bf4da55a7f73449094f
|
||||
@@ -19,15 +19,24 @@ namespace DepthsOfGreed.HandbookUI
|
||||
[SerializeField] private GameObject[] pages;
|
||||
[SerializeField] private GameObject testTowerPrefab;
|
||||
[SerializeField] private HandbookTowerCatalog towerCatalog;
|
||||
[Tooltip("Optional: component implementing IHandbookGameplay. Empty uses the prototype backend.")]
|
||||
[SerializeField] private MonoBehaviour gameplayBackend;
|
||||
[Tooltip("Wartezeit nach erfolgreichem Platzieren, bevor das Buch hochfährt.")]
|
||||
[SerializeField, Min(0)] private float successfulPlacementDelay = 0.5f;
|
||||
[SerializeField] private float hoverDistance = 115f;
|
||||
[Header("Tab movement (Build, Statistics, Upgrades, Index)")]
|
||||
[Tooltip("Distance left per closed tab. -1 automatically uses its width. Rest position is the authored RectTransform position.")]
|
||||
[SerializeField] private float[] closedTabRevealDistances = { -1f, -1f, -1f, -1f };
|
||||
[Tooltip("Distance up per open tab. -1 automatically uses its height.")]
|
||||
[SerializeField] private float[] openTabRevealDistances = { -1f, -1f, -1f, -1f };
|
||||
[Tooltip("Overlap retained when an automatically sized tab is fully revealed.")]
|
||||
[SerializeField, Min(0)] private float closedTabOverlap = 10f;
|
||||
[SerializeField, Min(0)] private float openTabOverlap = 25f;
|
||||
[SerializeField] private float animationSpeed = 14f;
|
||||
|
||||
private readonly string[] titles = { "Bauen", "Statistik", "Upgrades", "Index" };
|
||||
private Vector2[] closedPositions, openPositions;
|
||||
private TMP_Text[] closedLabels, openLabels;
|
||||
private RectTransform activeLayer;
|
||||
[SerializeField] private RectTransform activeLayer;
|
||||
private int selected, hoveredClosed = -1, hoveredOpen = -1;
|
||||
private bool isOpen, bookHovered;
|
||||
private Vector3 bookScale;
|
||||
@@ -48,27 +57,19 @@ namespace DepthsOfGreed.HandbookUI
|
||||
closedPositions = PrepareTabs(closedTabs, out closedLabels, false);
|
||||
openPositions = PrepareTabs(openTabs, out openLabels, true);
|
||||
bookScale = bookButton.transform.localScale;
|
||||
var layer = new GameObject("ActiveTabLayer", typeof(RectTransform));
|
||||
activeLayer = layer.GetComponent<RectTransform>();
|
||||
activeLayer.SetParent(openTabsRoot.parent, false);
|
||||
activeLayer.anchorMin = openTabsRoot.anchorMin;
|
||||
activeLayer.anchorMax = openTabsRoot.anchorMax;
|
||||
activeLayer.pivot = openTabsRoot.pivot;
|
||||
activeLayer.offsetMin = openTabsRoot.offsetMin;
|
||||
activeLayer.offsetMax = openTabsRoot.offsetMax;
|
||||
var background = openBook.transform.Find("Background");
|
||||
if (background) activeLayer.SetSiblingIndex(background.GetSiblingIndex() + 1);
|
||||
bookButton.onClick.AddListener(() => Open(0));
|
||||
closeButton.onClick.AddListener(Close);
|
||||
AddHover(bookButton.gameObject, () => bookHovered = true, () => bookHovered = false);
|
||||
buildMenu = pages[0].AddComponent<BuildMenuController>();
|
||||
buildMenu = pages[0].GetComponent<BuildMenuController>();
|
||||
buildMenu.Initialize(towerCatalog);
|
||||
placement = gameObject.AddComponent<TestPlacementController>();
|
||||
placement.Initialize(this, buildMenu, (RectTransform)openBook.transform, testTowerPrefab);
|
||||
placement = gameObject.GetComponent<TestPlacementController>();
|
||||
placement.Initialize(this, buildMenu, (RectTransform)openBook.transform, testTowerPrefab, gameplayBackend);
|
||||
placement.SuccessfulPlacementDelay = successfulPlacementDelay;
|
||||
buildMenu.Placement = placement;
|
||||
indexMenu = pages[3].AddComponent<HandbookIndexController>();
|
||||
indexMenu = pages[3].GetComponent<HandbookIndexController>();
|
||||
indexMenu.Initialize(towerCatalog);
|
||||
var upgrades = pages[2].GetComponent<HandbookUpgradesController>();
|
||||
upgrades.Initialize(placement);
|
||||
Close();
|
||||
}
|
||||
|
||||
@@ -79,31 +80,9 @@ namespace DepthsOfGreed.HandbookUI
|
||||
for (int i = 0; i < tabs.Length; i++)
|
||||
{
|
||||
int index = i;
|
||||
if (!opened)
|
||||
{
|
||||
var marker = (RectTransform)tabs[i].transform;
|
||||
marker.sizeDelta = new Vector2(marker.sizeDelta.x + 25f, marker.sizeDelta.y);
|
||||
marker.anchoredPosition += Vector2.right * 25f;
|
||||
}
|
||||
positions[i] = ((RectTransform)tabs[i].transform).anchoredPosition;
|
||||
labels[i] = tabs[i].GetComponentInChildren<TMP_Text>(true);
|
||||
if (labels[i])
|
||||
{
|
||||
var text = labels[i];
|
||||
text.text = titles[i];
|
||||
text.raycastTarget = false;
|
||||
text.fontSize = opened ? 20 : 18;
|
||||
text.enableAutoSizing = true;
|
||||
text.fontSizeMin = 14;
|
||||
text.fontSizeMax = opened ? 20 : 18;
|
||||
text.alignment = TextAlignmentOptions.MidlineLeft;
|
||||
var rect = text.rectTransform;
|
||||
rect.anchorMin = Vector2.zero;
|
||||
rect.anchorMax = Vector2.one;
|
||||
rect.offsetMin = new Vector2(opened ? 65 : 48, 4);
|
||||
rect.offsetMax = new Vector2(-6, -4);
|
||||
text.gameObject.SetActive(false);
|
||||
}
|
||||
if (labels[i]) labels[i].gameObject.SetActive(false);
|
||||
tabs[i].onClick.AddListener(() => Open(index));
|
||||
AddHover(tabs[i].gameObject,
|
||||
() => { if (opened) hoveredOpen = index; else hoveredClosed = index; },
|
||||
@@ -143,9 +122,7 @@ namespace DepthsOfGreed.HandbookUI
|
||||
rect.SetParent(i == selected ? activeLayer : openTabsRoot, false);
|
||||
}
|
||||
if (index == 3 && !samePage && indexMenu) indexMenu.ResetForEntry();
|
||||
if (samePage)
|
||||
foreach (var scroll in pages[index].GetComponentsInChildren<ScrollRect>(true))
|
||||
scroll.verticalNormalizedPosition = 1f;
|
||||
if (samePage && index == 3 && indexMenu) indexMenu.ResetForEntry();
|
||||
}
|
||||
|
||||
public void Close()
|
||||
@@ -189,11 +166,18 @@ namespace DepthsOfGreed.HandbookUI
|
||||
bool show = i == hovered || i == active;
|
||||
var rect = (RectTransform)tabs[i].transform;
|
||||
// Reveal the full marker while retaining a small overlap with the page.
|
||||
float distance = Mathf.Clamp(hoverDistance, 0f, Mathf.Max(0f, positions[i].x - 10f));
|
||||
var target = positions[i] + (show ? Vector2.left * distance : Vector2.zero);
|
||||
bool opened = tabs == openTabs;
|
||||
var distances = opened ? openTabRevealDistances : closedTabRevealDistances;
|
||||
float configured = distances != null && i < distances.Length ? distances[i] : -1f;
|
||||
float automatic = opened
|
||||
? Mathf.Max(0f, -positions[i].y - openTabOverlap)
|
||||
: Mathf.Max(0f, positions[i].x - closedTabOverlap);
|
||||
float distance = configured >= 0f ? configured : automatic;
|
||||
var target = positions[i] + (show ? (opened ? Vector2.up : Vector2.left) * distance : Vector2.zero);
|
||||
rect.anchoredPosition = Vector2.Lerp(rect.anchoredPosition, target, blend);
|
||||
if (labels[i]) labels[i].gameObject.SetActive(show);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace DepthsOfGreed.HandbookUI
|
||||
{
|
||||
public static class HandbookCostText
|
||||
{
|
||||
public static string Format(int energy, int entrails)
|
||||
{
|
||||
string a = energy > 0 ? energy + " Energie" : "";
|
||||
string b = entrails > 0 ? entrails + " Gedärme" : "";
|
||||
return a.Length > 0 && b.Length > 0 ? a + " · " + b : a + b;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 65b5d287df87fcc4690d572fdedb81f6
|
||||
@@ -0,0 +1,23 @@
|
||||
using UnityEngine;
|
||||
namespace DepthsOfGreed.HandbookUI
|
||||
{
|
||||
public enum EnemyTrait { None, GroupSpawn, AlternatingInvulnerability, CorpseConsumer, EnergyCharged }
|
||||
[CreateAssetMenu(menuName = "Depths of Greed/Handbuch/Gegnertyp")]
|
||||
public sealed class HandbookEnemyEntry : ScriptableObject
|
||||
{
|
||||
public string id, displayName;
|
||||
public Sprite icon;
|
||||
public GameObject gameplayPrefab;
|
||||
[TextArea(3, 8)] public string description;
|
||||
[Min(1)] public float maxHealth = 100;
|
||||
[Min(0)] public float damage = 10, speed = 1, attacksPerSecond = 1, spawnWeight = 1;
|
||||
public Vector2Int dropRange;
|
||||
public Vector2Int spawnGroup = Vector2Int.one;
|
||||
public EnemyTrait trait;
|
||||
public float invulnerableSeconds, vulnerableSeconds, corpseHeal, corpseBuffMultiplier = 1;
|
||||
public float energyChargeThreshold, explosionRadius;
|
||||
public bool provisional = true;
|
||||
public int sourceBalanceScore;
|
||||
[TextArea(3, 10)] public string designNotes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d97d07f7d8008a1438cb14fe4a1b425a
|
||||
@@ -1,150 +1,69 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace DepthsOfGreed.HandbookUI
|
||||
{
|
||||
public sealed class HandbookIndexController : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private Button[] filterButtons, entryButtons;
|
||||
[SerializeField] private TMP_Text[] entryNames;
|
||||
[SerializeField] private Image[] entryIcons;
|
||||
[SerializeField] private TMP_Text detailName, description, emptyMessage;
|
||||
[SerializeField] private Image detailIcon;
|
||||
[SerializeField] private Sprite placeholderIcon;
|
||||
private sealed class Item { public HandbookTowerEntry tower; public HandbookEnemyEntry enemy; public string Name => tower ? tower.displayName : enemy.displayName; public Sprite Icon => tower ? tower.icon : enemy.icon; }
|
||||
private readonly List<Item> all = new List<Item>(), filtered = new List<Item>();
|
||||
public HandbookTowerEntry SelectedEntry { get; private set; }
|
||||
public int VisibleEntryCount => currentFilter == 2 ? 0 : entries.Count;
|
||||
public ScrollRect ListScroll => scroll;
|
||||
private ScrollRect scroll;
|
||||
private Button[] filters;
|
||||
private readonly List<HandbookTowerEntry> entries = new List<HandbookTowerEntry>();
|
||||
private readonly List<Button> rows = new List<Button>();
|
||||
private TMP_Text detailName, description, emptyMessage;
|
||||
private Image detailIcon;
|
||||
private Sprite placeholder;
|
||||
private Color placeholderColor;
|
||||
private int currentFilter;
|
||||
|
||||
public HandbookEnemyEntry SelectedEnemy { get; private set; }
|
||||
public int VisibleEntryCount => filtered.Count;
|
||||
private int filter = -1;
|
||||
public void Initialize(HandbookTowerCatalog catalog)
|
||||
{
|
||||
scroll = transform.Find("IndexLeft/EntryScrollView").GetComponent<ScrollRect>();
|
||||
filters = new[] {
|
||||
transform.Find("IndexLeft/FilterBar/Filter_All").GetComponent<Button>(),
|
||||
transform.Find("IndexLeft/FilterBar/Filter_Towers").GetComponent<Button>(),
|
||||
transform.Find("IndexLeft/FilterBar/Filter_Enemies").GetComponent<Button>()
|
||||
};
|
||||
detailName = transform.Find("IndexRight/DetailNameText").GetComponent<TMP_Text>();
|
||||
description = transform.Find("IndexRight/DetailDescriptionText").GetComponent<TMP_Text>();
|
||||
detailIcon = transform.Find("IndexRight/DetailIcon").GetComponent<Image>();
|
||||
placeholder = detailIcon.sprite;
|
||||
placeholderColor = detailIcon.color;
|
||||
var template = scroll.content.GetChild(0).GetComponent<Button>();
|
||||
if (catalog && catalog.towers != null)
|
||||
foreach (var entry in catalog.towers)
|
||||
{
|
||||
if (!entry) continue;
|
||||
int index = entries.Count;
|
||||
entries.Add(entry);
|
||||
var row = Instantiate(template, scroll.content);
|
||||
row.name = "IndexEntry_" + entry.id;
|
||||
row.gameObject.SetActive(true);
|
||||
row.image.color = Color.white;
|
||||
var label = row.transform.Find("NameText").GetComponent<TMP_Text>();
|
||||
label.text = entry.displayName;
|
||||
label.color = Color.black;
|
||||
label.raycastTarget = false;
|
||||
label.enableAutoSizing = true;
|
||||
label.fontSizeMin = 16;
|
||||
label.fontSizeMax = 24;
|
||||
var icon = row.transform.Find("Icon").GetComponent<Image>();
|
||||
if (entry.icon) { icon.sprite = entry.icon; icon.color = Color.white; }
|
||||
icon.preserveAspect = true;
|
||||
icon.raycastTarget = false;
|
||||
row.onClick.RemoveAllListeners();
|
||||
row.onClick.AddListener(() => SelectEntry(index));
|
||||
rows.Add(row);
|
||||
}
|
||||
template.gameObject.SetActive(false);
|
||||
var empty = new GameObject("EmptyListMessage", typeof(RectTransform), typeof(TextMeshProUGUI), typeof(LayoutElement));
|
||||
empty.transform.SetParent(scroll.content, false);
|
||||
emptyMessage = empty.GetComponent<TMP_Text>();
|
||||
emptyMessage.font = description.font;
|
||||
emptyMessage.fontSize = 22;
|
||||
emptyMessage.color = new Color(0.3f, 0.3f, 0.3f);
|
||||
emptyMessage.alignment = TextAlignmentOptions.MidlineLeft;
|
||||
emptyMessage.raycastTarget = false;
|
||||
empty.GetComponent<LayoutElement>().preferredHeight = 70;
|
||||
for (int i = 0; i < filters.Length; i++)
|
||||
{
|
||||
int index = i;
|
||||
filters[i].onClick.AddListener(() => SelectFilter(index));
|
||||
}
|
||||
if (catalog.towers != null) foreach (var t in catalog.towers) if (t) all.Add(new Item { tower = t });
|
||||
if (catalog.enemies != null) foreach (var e in catalog.enemies) if (e) all.Add(new Item { enemy = e });
|
||||
for (int i = 0; i < filterButtons.Length; i++) { int n = i; filterButtons[i].onClick.AddListener(() => SelectFilter(n)); }
|
||||
for (int i = 0; i < entryButtons.Length; i++) { int n = i; entryButtons[i].onClick.AddListener(() => SelectEntry(n)); }
|
||||
ResetForEntry();
|
||||
}
|
||||
|
||||
public void ResetForEntry()
|
||||
public void ResetForEntry() { SelectFilter(0, true); }
|
||||
public void SelectFilter(int value, bool force = false)
|
||||
{
|
||||
SelectFilter(0, true);
|
||||
if (value < 0 || value > 4 || (!force && value == filter)) return;
|
||||
filter = value; filtered.Clear();
|
||||
foreach (var item in all)
|
||||
if (value == 0 || (value == 1 && item.tower && (int)item.tower.menuGroup < 2) || (value == 2 && item.tower && item.tower.menuGroup == BuildMenuGroup.Anlagen) || (value == 3 && item.tower && item.tower.menuGroup == BuildMenuGroup.Einsaetze) || (value == 4 && item.enemy)) filtered.Add(item);
|
||||
ClearSelection(); Render();
|
||||
for (int i = 0; i < filterButtons.Length; i++) { filterButtons[i].image.color = Color.white; var c = filterButtons[i].colors; c.normalColor = i == value ? new Color(1, 0.76f, 0.35f) : new Color(0.87f, 0.84f, 0.75f); c.selectedColor = c.normalColor; filterButtons[i].colors = c; }
|
||||
}
|
||||
|
||||
public void SelectFilter(int index, bool force = false)
|
||||
private void Render()
|
||||
{
|
||||
if (index < 0 || index > 2 || (!force && currentFilter == index)) return;
|
||||
currentFilter = index;
|
||||
foreach (var row in rows) row.gameObject.SetActive(index != 2);
|
||||
bool empty = index == 2 || rows.Count == 0;
|
||||
emptyMessage.gameObject.SetActive(empty);
|
||||
emptyMessage.text = index == 2 ? "Noch keine Gegner-Einträge" : "Noch keine Einträge";
|
||||
for (int i = 0; i < filters.Length; i++)
|
||||
if (filtered.Count > entryButtons.Length) Debug.LogError("Index: Anzahl vorbereiteter Auswahlfelder erhöhen.", this);
|
||||
for (int i = 0; i < entryButtons.Length; i++)
|
||||
{
|
||||
filters[i].image.color = Color.white;
|
||||
var colors = filters[i].colors;
|
||||
colors.normalColor = i == index ? new Color(1, 0.72f, 0.2f) : new Color(0.82f, 0.82f, 0.82f);
|
||||
colors.highlightedColor = i == index ? new Color(1, 0.82f, 0.4f) : Color.white;
|
||||
colors.selectedColor = colors.normalColor;
|
||||
filters[i].colors = colors;
|
||||
entryButtons[i].gameObject.SetActive(i < filtered.Count);
|
||||
if (i >= filtered.Count) continue;
|
||||
var item = filtered[i]; entryNames[i].text = item.Name; entryIcons[i].sprite = item.Icon ? item.Icon : placeholderIcon; entryIcons[i].color = item.Icon ? Color.white : Color.gray;
|
||||
bool active = (item.tower && item.tower == SelectedEntry) || (item.enemy && item.enemy == SelectedEnemy);
|
||||
entryButtons[i].image.color = active ? new Color(0.58f, 0.58f, 0.58f) : new Color(0.9f, 0.88f, 0.81f);
|
||||
}
|
||||
ClearSelection();
|
||||
ResetScroll();
|
||||
emptyMessage.gameObject.SetActive(filtered.Count == 0);
|
||||
}
|
||||
|
||||
public void SelectEntry(int index)
|
||||
{
|
||||
if (currentFilter == 2 || index < 0 || index >= entries.Count) return;
|
||||
SelectedEntry = entries[index];
|
||||
detailName.text = SelectedEntry.displayName;
|
||||
description.text = SelectedEntry.description;
|
||||
detailIcon.gameObject.SetActive(true);
|
||||
detailIcon.sprite = SelectedEntry.icon ? SelectedEntry.icon : placeholder;
|
||||
detailIcon.color = SelectedEntry.icon ? Color.white : placeholderColor;
|
||||
detailIcon.preserveAspect = true;
|
||||
RefreshSelectionColors();
|
||||
}
|
||||
|
||||
public void ClearSelection()
|
||||
{
|
||||
SelectedEntry = null;
|
||||
detailIcon.gameObject.SetActive(false);
|
||||
detailName.text = "";
|
||||
description.text = "Wähle links einen Eintrag aus.";
|
||||
RefreshSelectionColors();
|
||||
}
|
||||
|
||||
private void RefreshSelectionColors()
|
||||
{
|
||||
for (int i = 0; i < rows.Count; i++)
|
||||
if (index < 0 || index >= filtered.Count) return;
|
||||
var item = filtered[index]; SelectedEntry = item.tower; SelectedEnemy = item.enemy;
|
||||
detailName.text = item.Name; detailIcon.gameObject.SetActive(true); detailIcon.sprite = item.Icon ? item.Icon : placeholderIcon; detailIcon.color = item.Icon ? Color.white : Color.gray;
|
||||
var text = new StringBuilder(item.tower ? item.tower.description : item.enemy.description);
|
||||
if (item.tower)
|
||||
{
|
||||
bool selected = entries[i] == SelectedEntry;
|
||||
var colors = rows[i].colors;
|
||||
colors.normalColor = selected ? new Color(0.58f, 0.58f, 0.58f) : new Color(0.86f, 0.86f, 0.86f);
|
||||
colors.highlightedColor = selected ? new Color(0.64f, 0.64f, 0.64f) : new Color(0.94f, 0.94f, 0.94f);
|
||||
colors.selectedColor = colors.normalColor;
|
||||
rows[i].colors = colors;
|
||||
var t = item.tower; text.Append("\n\nBaufläche: ").Append(t.region == BuildRegion.WallRing ? "Mauer" : t.region == BuildRegion.Interior ? "Innenfeld" : "Schleuse"); string price = HandbookCostText.Format(t.energyCost, t.entrailsCost); if (price.Length > 0) text.Append("\nKosten: ").Append(price);
|
||||
if (t.deploymentCooldown > 0) text.Append(" · Cooldown: ").Append(t.deploymentCooldown).Append(" s");
|
||||
if (t.upgradeable) { text.Append("\n\nVerbesserungen:"); foreach (var u in t.upgrades) text.Append("\n").Append(u.title).Append(": ").Append(u.description).Append(HandbookCostText.Format(u.energyCost, u.entrailsCost) is string upgradePrice && upgradePrice.Length > 0 ? " (" + upgradePrice + ")" : ""); }
|
||||
}
|
||||
description.text = text.ToString(); Render();
|
||||
}
|
||||
|
||||
public void ResetScroll()
|
||||
{
|
||||
scroll.StopMovement();
|
||||
Canvas.ForceUpdateCanvases();
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(scroll.content);
|
||||
scroll.verticalNormalizedPosition = 1f;
|
||||
}
|
||||
public void ClearSelection() { SelectedEntry = null; SelectedEnemy = null; detailIcon.gameObject.SetActive(false); detailName.text = ""; description.text = "Wähle links einen Eintrag aus."; }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
namespace DepthsOfGreed.HandbookUI
|
||||
{
|
||||
public enum PageFrameStyle { Flat, CurvedLeft, CurvedRight }
|
||||
public sealed class HandbookPageSurface : MonoBehaviour
|
||||
{
|
||||
public PageFrameStyle frameStyle;
|
||||
public Image frameImage;
|
||||
public Sprite flatFrame, curvedLeftFrame, curvedRightFrame;
|
||||
private void OnValidate()
|
||||
{
|
||||
if (!frameImage) return;
|
||||
Sprite sprite = frameStyle == PageFrameStyle.CurvedLeft ? curvedLeftFrame : frameStyle == PageFrameStyle.CurvedRight ? curvedRightFrame : flatFrame;
|
||||
if (sprite) frameImage.sprite = sprite;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dea21c6ba39be2c48b1c70f82555a3ef
|
||||
@@ -7,5 +7,6 @@ namespace DepthsOfGreed.HandbookUI
|
||||
{
|
||||
[Tooltip("Die Reihenfolge innerhalb einer Kategorie bestimmt die Reihenfolge im Buch.")]
|
||||
public HandbookTowerEntry[] towers;
|
||||
public HandbookEnemyEntry[] enemies;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,28 +3,106 @@ using UnityEngine;
|
||||
namespace DepthsOfGreed.HandbookUI
|
||||
{
|
||||
public enum HandbookTowerCategory { Munition, Energie, Spezial }
|
||||
public enum BuildMenuGroup { Physisch, Energie, Anlagen, Einsaetze }
|
||||
|
||||
[CreateAssetMenu(menuName = "Depths of Greed/Handbuch/Turmtyp", fileName = "NeuerTurmtyp")]
|
||||
public sealed class HandbookTowerEntry : ScriptableObject
|
||||
{
|
||||
[Tooltip("Stabile ID für die spätere Gameplay-Anbindung. Nach Zuordnung nicht ändern.")]
|
||||
[Header("Name und Kategorie")]
|
||||
[Tooltip("Stabile ID für die Gameplay-Anbindung. Nach Zuordnung nicht ändern.")]
|
||||
public string id;
|
||||
public string displayName;
|
||||
public HandbookTowerCategory category;
|
||||
public Sprite icon;
|
||||
[InspectorName("Waffentechnik / Basisfarbe")] public HandbookTowerCategory category;
|
||||
[InspectorName("Buchkategorie")] public BuildMenuGroup menuGroup;
|
||||
public BuildUse use;
|
||||
|
||||
[Header("Kaufpreis")]
|
||||
[Tooltip("Energie-Kaufpreis. 0 wird in der Preisanzeige ausgeblendet.")]
|
||||
[Min(0)] public int energyCost;
|
||||
[Tooltip("Gedärme-Kaufpreis. 0 wird in der Preisanzeige ausgeblendet. Beide Ressourcen können gemeinsam erforderlich sein.")]
|
||||
[Min(0)] public int entrailsCost;
|
||||
|
||||
[Header("Texte im Handbuch")]
|
||||
public string summary;
|
||||
[TextArea(3, 8)] public string description;
|
||||
[Tooltip("Kreis, der die gesamte Basis umschließt. Muss vollständig auf den Ring passen.")]
|
||||
|
||||
[Header("Grafiken und Prefabs")]
|
||||
[Tooltip("Bild im Bau-, Index- und Upgrademenü.")]
|
||||
public Sprite icon;
|
||||
[Tooltip("Grafik, die während der Platzierung dem Mauszeiger folgt.")]
|
||||
public Sprite previewSprite;
|
||||
[Tooltip("Grafik des unbeweglichen Unterteils. Wird vom Platzierungsprototyp genutzt.")]
|
||||
public Sprite baseSprite;
|
||||
[Tooltip("Vorbereitetes Unterteil als GameObject. Noch nicht vom Platzierungsprototyp verwendet.")]
|
||||
public GameObject basePrefab;
|
||||
[Tooltip("Vollständiges Objekt für die spätere Gameplay-Anbindung.")]
|
||||
public GameObject gameplayPrefab;
|
||||
|
||||
[Header("Platzierung und Platzbedarf")]
|
||||
public BuildRegion region;
|
||||
public BaseShape baseShape;
|
||||
[Tooltip("Kreis, der die gesamte Basis umschließt. Muss vollständig in die Baufläche passen.")]
|
||||
[Min(0.001f)] public float baseRadius = 0.36f;
|
||||
public Vector2 baseSize = new Vector2(0.5f, 0.5f);
|
||||
[Tooltip("Abstandskreis: darf andere Abstandskreise nicht überlappen.")]
|
||||
[Min(0.001f)] public float placementRadius = 0.6f;
|
||||
public bool allowPlayerRotation;
|
||||
public bool blocksMovement, requirePathValidation;
|
||||
|
||||
[Header("Mengenlimits und Einsatzdauer")]
|
||||
public bool countsTowardWallLimit = true;
|
||||
[Tooltip("0 = kein zusätzliches Limit für diesen Typ.")]
|
||||
[Min(0)] public int maxPerType;
|
||||
[Min(0)] public float deploymentCooldown, lifetimeSeconds;
|
||||
[Min(0)] public int contactUses;
|
||||
|
||||
[Header("Kampf, Verbrauch und Sonderfunktion")]
|
||||
public CombatDefinition combat = new CombatDefinition();
|
||||
|
||||
[Header("Kellerversorgung und Turmmagazin")]
|
||||
public TowerSupplyDefinition supply = new TowerSupplyDefinition();
|
||||
|
||||
[Header("Angreifbarkeit und Haltbarkeit")]
|
||||
public bool targetableByEnemies, destroyableByDamage;
|
||||
[Min(0)] public float maxHealth;
|
||||
public bool removeOnFloorLoss = true;
|
||||
|
||||
[Header("Verbesserungen")]
|
||||
public bool upgradeable = true;
|
||||
public UpgradeDefinition[] upgrades = new UpgradeDefinition[0];
|
||||
|
||||
[Header("Vorläufige Werte und Designnotizen")]
|
||||
public bool provisional = true;
|
||||
[HideInInspector] public int dataVersion;
|
||||
public float sourceCostRating;
|
||||
public int sourceBalanceScore;
|
||||
[TextArea(3, 12)] public string designNotes;
|
||||
|
||||
public CombatDefinition CreateCombatSnapshot(int level)
|
||||
{
|
||||
var result = JsonUtility.FromJson<CombatDefinition>(JsonUtility.ToJson(combat));
|
||||
for (int i = 0; i < Mathf.Min(Mathf.Max(0, level), upgrades.Length); i++)
|
||||
{
|
||||
var u = upgrades[i]; result.damagePerHit *= u.damageMultiplier; result.continuousDamagePerSecond *= u.damageMultiplier; result.attacksPerSecond *= u.fireRateMultiplier; result.range *= u.rangeMultiplier;
|
||||
result.effectStrength *= u.effectMultiplier; result.knockbackDistance *= u.effectMultiplier;
|
||||
result.energyGeneratedPerSecond *= u.effectMultiplier; result.energyCapacityBonus *= u.effectMultiplier;
|
||||
result.repairHealthPerSecond *= u.effectMultiplier; result.corpsesCollectedPerSecond *= u.effectMultiplier;
|
||||
result.slowFraction = Mathf.Clamp01(result.slowFraction * u.effectMultiplier);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
baseRadius = Mathf.Max(0.001f, baseRadius);
|
||||
placementRadius = Mathf.Max(baseRadius, placementRadius);
|
||||
energyCost = Mathf.Max(0, energyCost);
|
||||
entrailsCost = Mathf.Max(0, entrailsCost);
|
||||
baseSize = new Vector2(Mathf.Max(0.01f, baseSize.x), Mathf.Max(0.01f, baseSize.y));
|
||||
if (use == BuildUse.Ability) countsTowardWallLimit = false;
|
||||
if (category == HandbookTowerCategory.Munition) menuGroup = BuildMenuGroup.Physisch;
|
||||
else if (category == HandbookTowerCategory.Energie) menuGroup = BuildMenuGroup.Energie;
|
||||
else if ((int)menuGroup < 2) menuGroup = use == BuildUse.Structure ? BuildMenuGroup.Anlagen : BuildMenuGroup.Einsaetze;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityEngine.EventSystems;
|
||||
namespace DepthsOfGreed.HandbookUI
|
||||
{
|
||||
public sealed class HandbookUpgradesController : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private Button[] rows, buyButtons, mapMarkers;
|
||||
[SerializeField] private TMP_Text[] names, effects, buyLabels, markerLabels;
|
||||
[SerializeField] private Image[] icons;
|
||||
[SerializeField] private TMP_Text resources, emptyMessage;
|
||||
[SerializeField] private float mapWorldExtent = 170;
|
||||
private TestPlacementController placement;
|
||||
private IHandbookGameplay backend;
|
||||
private readonly List<BuiltObjectSnapshot> sorted = new List<BuiltObjectSnapshot>();
|
||||
private long selected, hovered;
|
||||
public int VisibleUpgradeCount => sorted.Count;
|
||||
public void Initialize(TestPlacementController p)
|
||||
{
|
||||
placement = p; backend = p.Backend;
|
||||
for (int i = 0; i < rows.Length; i++)
|
||||
{
|
||||
int n = i; rows[i].onClick.AddListener(() => { if (n < sorted.Count) { selected = sorted[n].instanceId; Highlights(); } });
|
||||
buyButtons[i].onClick.AddListener(() => { if (n < sorted.Count) { selected = sorted[n].instanceId; backend.ExecuteUpgrade(selected); } });
|
||||
var trigger = rows[i].GetComponent<EventTrigger>();
|
||||
var enter = new EventTrigger.Entry { eventID = EventTriggerType.PointerEnter }; enter.callback.AddListener(_ => { hovered = n < sorted.Count ? sorted[n].instanceId : 0; Highlights(); }); trigger.triggers.Add(enter);
|
||||
var exit = new EventTrigger.Entry { eventID = EventTriggerType.PointerExit }; exit.callback.AddListener(_ => { hovered = 0; Highlights(); }); trigger.triggers.Add(exit);
|
||||
}
|
||||
for (int i = 0; i < mapMarkers.Length; i++) { int n = i; mapMarkers[i].onClick.AddListener(() => { if (n < backend.BuiltObjects.Count) { selected = backend.BuiltObjects[n].instanceId; Highlights(); } }); }
|
||||
backend.StateChanged += Rebuild; Rebuild();
|
||||
}
|
||||
private void OnEnable() { if (backend != null) { selected = hovered = 0; Rebuild(); } }
|
||||
private void Rebuild()
|
||||
{
|
||||
resources.text = "Energie: " + backend.Energy + " · Gedärme: " + backend.Entrails;
|
||||
sorted.Clear(); foreach (var obj in backend.BuiltObjects) if (obj.type.upgradeable) sorted.Add(obj);
|
||||
sorted.Sort((a, b) => { int c = string.CompareOrdinal(a.type.id, b.type.id); return c != 0 ? c : a.instanceId.CompareTo(b.instanceId); });
|
||||
if (sorted.Count > rows.Length || backend.BuiltObjects.Count > mapMarkers.Length) Debug.LogError("Upgrades: Vorbereitete Karten-/Markeranzahl erhöhen.", this);
|
||||
emptyMessage.gameObject.SetActive(sorted.Count == 0);
|
||||
for (int i = 0; i < rows.Length; i++)
|
||||
{
|
||||
rows[i].gameObject.SetActive(i < sorted.Count); if (i >= sorted.Count) continue;
|
||||
var obj = sorted[i]; names[i].text = obj.type.displayName + " #" + obj.instanceId + " · Stufe " + obj.upgradeLevel;
|
||||
icons[i].sprite = obj.type.icon; icons[i].gameObject.SetActive(obj.type.icon);
|
||||
bool max = obj.upgradeLevel >= obj.type.upgrades.Length;
|
||||
effects[i].text = max ? "Maximal verbessert" : obj.type.upgrades[obj.upgradeLevel].description;
|
||||
buyLabels[i].text = max ? "Maximum" : HandbookCostText.Format(obj.type.upgrades[obj.upgradeLevel].energyCost, obj.type.upgrades[obj.upgradeLevel].entrailsCost) is string price && price.Length > 0 ? price : "Verbessern";
|
||||
buyButtons[i].interactable = !max && backend.CheckUpgrade(obj.instanceId).Allowed;
|
||||
}
|
||||
for (int i = 0; i < mapMarkers.Length; i++)
|
||||
{
|
||||
mapMarkers[i].gameObject.SetActive(i < backend.BuiltObjects.Count); if (i >= backend.BuiltObjects.Count) continue;
|
||||
var obj = backend.BuiltObjects[i]; markerLabels[i].text = obj.instanceId.ToString(); ((RectTransform)mapMarkers[i].transform).anchoredPosition = (Vector2)(obj.position - placement.AreaCenter) / placement.AreaRadius * mapWorldExtent;
|
||||
}
|
||||
Highlights();
|
||||
}
|
||||
private void Highlights()
|
||||
{
|
||||
for (int i = 0; i < mapMarkers.Length && i < backend.BuiltObjects.Count; i++) { long id = backend.BuiltObjects[i].instanceId; mapMarkers[i].image.color = id == selected ? new Color(1, 0.7f, 0.1f) : id == hovered ? Color.white : new Color(0.35f, 0.35f, 0.35f); mapMarkers[i].transform.localScale = Vector3.one * (id == selected || id == hovered ? 1.25f : 1); }
|
||||
for (int i = 0; i < rows.Length && i < sorted.Count; i++) rows[i].image.color = sorted[i].instanceId == selected ? new Color(0.7f, 0.7f, 0.68f) : new Color(0.9f, 0.88f, 0.81f);
|
||||
}
|
||||
private void OnDestroy() { if (backend != null) backend.StateChanged -= Rebuild; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0a278a7e05a0f534aad20b7c849e8934
|
||||
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
namespace DepthsOfGreed.HandbookUI
|
||||
{
|
||||
public enum BuildFailure { None, OutsideRegion, Overlap, WallLimit, InteriorLimit, TypeLimit, Cooldown, Energy, Entrails, InvalidType, PathBlocked }
|
||||
public readonly struct BuildCheck
|
||||
{
|
||||
public readonly BuildFailure failure;
|
||||
public readonly string message;
|
||||
public bool Allowed => failure == BuildFailure.None;
|
||||
public BuildCheck(BuildFailure code, string text = null) { failure = code; message = text; }
|
||||
}
|
||||
public readonly struct BuildRequest
|
||||
{
|
||||
public readonly HandbookTowerEntry type;
|
||||
public readonly Vector3 position;
|
||||
public readonly float rotationDegrees;
|
||||
public BuildRequest(HandbookTowerEntry entry, Vector3 point, float angle = 0) { type = entry; position = point; rotationDegrees = angle; }
|
||||
}
|
||||
public sealed class BuiltObjectSnapshot
|
||||
{
|
||||
public long instanceId;
|
||||
public HandbookTowerEntry type;
|
||||
public Vector3 position;
|
||||
public float rotationDegrees;
|
||||
public GameObject instance;
|
||||
public int upgradeLevel;
|
||||
public float expiresAt;
|
||||
}
|
||||
// Implement on a MonoBehaviour and assign HandbookController.gameplayBackend.
|
||||
public interface IHandbookGameplay
|
||||
{
|
||||
event Action StateChanged;
|
||||
int Energy { get; }
|
||||
int Entrails { get; }
|
||||
int WallCount { get; }
|
||||
int WallLimit { get; }
|
||||
IReadOnlyList<BuiltObjectSnapshot> BuiltObjects { get; }
|
||||
BuildCheck CheckAvailability(HandbookTowerEntry type);
|
||||
BuildCheck CheckBuild(BuildRequest request);
|
||||
BuildCheck ExecuteBuild(BuildRequest request);
|
||||
BuildCheck CheckUpgrade(long instanceId);
|
||||
BuildCheck ExecuteUpgrade(long instanceId);
|
||||
float GetCooldownRemaining(HandbookTowerEntry type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e1338929b382a5e4ab926061f8950250
|
||||
@@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace DepthsOfGreed.HandbookUI
|
||||
{
|
||||
public enum SupplyKind { None = -1, Physical = 0, EnergyCells = 1, Special = 2 }
|
||||
public enum SupplyTruckPhase { Idle, Loading, Driving, Unloading }
|
||||
public enum SupplyStop { PhysicalTank = 0, EnergyTank = 1, SpecialTank = 2, Distributor = 3 }
|
||||
public enum SupplyConsumption { PerShot, WhileFiring }
|
||||
|
||||
[Serializable]
|
||||
public sealed class TowerSupplyDefinition
|
||||
{
|
||||
[Tooltip("None = kein Kelleranschluss.")]
|
||||
public SupplyKind kind = SupplyKind.None;
|
||||
[Min(0)] public float magazineCapacity = 60;
|
||||
[Tooltip("Startfüllung pro neu gebauter Turminstanz, nicht der aktuelle Magazinstand.")]
|
||||
[Min(0)] public float initialAmmo = 60;
|
||||
[Tooltip("Maximale Nachfüllrate vom Verteiler ins Turmmagazin.")]
|
||||
[Min(0)] public float refillPerSecond = 10;
|
||||
public SupplyConsumption consumption = SupplyConsumption.PerShot;
|
||||
[Tooltip("Bei PerShot: nur bei einem tatsächlich abgefeuerten Schuss verbuchen.")]
|
||||
[Min(0)] public float unitsPerShot = 1;
|
||||
[Tooltip("Bei WhileFiring: nur während tatsächlicher Feuerzeit verbuchen.")]
|
||||
[Min(0)] public float unitsPerFiringSecond;
|
||||
}
|
||||
|
||||
// A detached display snapshot. Gameplay retains ownership of all live state.
|
||||
public sealed class SupplyState
|
||||
{
|
||||
public SupplyKind priority = SupplyKind.None;
|
||||
public IReadOnlyList<SupplyChannelState> channels = Array.Empty<SupplyChannelState>();
|
||||
public IReadOnlyList<TowerSupplyState> towers = Array.Empty<TowerSupplyState>();
|
||||
public SupplyTruckState truck;
|
||||
}
|
||||
public sealed class SupplyChannelState
|
||||
{
|
||||
public SupplyKind kind;
|
||||
public float storage, storageCapacity;
|
||||
public float buffer, bufferCapacity;
|
||||
// Optional, measured consumption; not theoretical fire rate.
|
||||
public float? consumedPerSecond;
|
||||
}
|
||||
public sealed class TowerSupplyState
|
||||
{
|
||||
// Must be the same stable instance ID as BuiltObjectSnapshot.instanceId.
|
||||
public long instanceId;
|
||||
public HandbookTowerEntry type;
|
||||
public bool connected = true;
|
||||
public float magazine, magazineCapacity;
|
||||
public float? consumedPerSecond;
|
||||
}
|
||||
public sealed class SupplyTruckState
|
||||
{
|
||||
public SupplyTruckPhase phase;
|
||||
public SupplyKind cargoKind = SupplyKind.None;
|
||||
public float cargo, cargoCapacity;
|
||||
public SupplyStop from, to;
|
||||
[Range(0,1)] public float progress;
|
||||
}
|
||||
public readonly struct SupplyCommandResult
|
||||
{
|
||||
public readonly bool accepted;
|
||||
public readonly string message;
|
||||
public SupplyCommandResult(bool accepted, string message = null) { this.accepted = accepted; this.message = message; }
|
||||
}
|
||||
|
||||
// Implement on a MonoBehaviour. The UI polls at 4 Hz and never consumes ammunition.
|
||||
public interface ISupplyGameplay
|
||||
{
|
||||
SupplyState ReadState();
|
||||
SupplyCommandResult SetConnected(long instanceId, bool connected);
|
||||
SupplyCommandResult SetPriority(SupplyKind priority);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 626058c40826b5c4baca12f83e521f56
|
||||
@@ -0,0 +1,125 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
namespace DepthsOfGreed.HandbookUI
|
||||
{
|
||||
public sealed class PrototypeHandbookGameplay : MonoBehaviour, IHandbookGameplay
|
||||
{
|
||||
public event Action StateChanged;
|
||||
public int Energy { get; private set; } = 2000;
|
||||
public int Entrails { get; private set; } = 100;
|
||||
public int WallLimit { get; private set; } = 8;
|
||||
public int InteriorLimit = 6;
|
||||
public int WallCount { get { int count = 0; foreach (var b in built) if (b.type.countsTowardWallLimit) count++; return count; } }
|
||||
public IReadOnlyList<BuiltObjectSnapshot> BuiltObjects => built;
|
||||
public Vector3 Center { get; private set; }
|
||||
public float InnerRadius { get; private set; }
|
||||
public float OuterRadius { get; private set; }
|
||||
private GameObject fallback;
|
||||
private Transform parent;
|
||||
private readonly List<BuiltObjectSnapshot> built = new List<BuiltObjectSnapshot>();
|
||||
private readonly Dictionary<string, float> readyAt = new Dictionary<string, float>();
|
||||
private long nextId = 1;
|
||||
public void Initialize(Transform container, Vector3 center, float inner, float outer, GameObject testPrefab)
|
||||
{ parent = container; Center = center; InnerRadius = inner; OuterRadius = outer; fallback = testPrefab; }
|
||||
public float GetCooldownRemaining(HandbookTowerEntry type) => type && readyAt.TryGetValue(type.id, out float time) ? Mathf.Max(0, time - Time.time) : 0;
|
||||
public BuildCheck CheckAvailability(HandbookTowerEntry type)
|
||||
{
|
||||
if (!type) return new BuildCheck(BuildFailure.InvalidType, "Kein Typ ausgewählt");
|
||||
if (type.countsTowardWallLimit && WallCount >= WallLimit) return new BuildCheck(BuildFailure.WallLimit, "Mauerlimit erreicht: 8/8");
|
||||
int same = 0, interior = 0;
|
||||
foreach (var b in built) { if (b.type.id == type.id) same++; if (!b.type.countsTowardWallLimit) interior++; }
|
||||
if (type.use != BuildUse.Ability && !type.countsTowardWallLimit && interior >= InteriorLimit) return new BuildCheck(BuildFailure.InteriorLimit, "Innenfeldlimit erreicht");
|
||||
if (type.maxPerType > 0 && same >= type.maxPerType) return new BuildCheck(BuildFailure.TypeLimit, "Limit für " + type.displayName + " erreicht");
|
||||
if (GetCooldownRemaining(type) > 0) return new BuildCheck(BuildFailure.Cooldown, "Bereit in " + Mathf.CeilToInt(GetCooldownRemaining(type)) + " s");
|
||||
if (Energy < type.energyCost) return new BuildCheck(BuildFailure.Energy, "Nicht genug Energie");
|
||||
if (Entrails < type.entrailsCost) return new BuildCheck(BuildFailure.Entrails, "Nicht genug Gedärme");
|
||||
return new BuildCheck(BuildFailure.None);
|
||||
}
|
||||
public BuildCheck CheckBuild(BuildRequest request)
|
||||
{
|
||||
var availability = CheckAvailability(request.type);
|
||||
if (!availability.Allowed) return availability;
|
||||
var t = request.type;
|
||||
Vector2 p = request.position - Center;
|
||||
float min, max;
|
||||
if (t.use == BuildUse.Ability) min = max = p.magnitude;
|
||||
else if (t.baseShape == BaseShape.Circle) { min = p.magnitude - t.baseRadius; max = p.magnitude + t.baseRadius; }
|
||||
else
|
||||
{
|
||||
Vector2 localCenter = Quaternion.Euler(0, 0, -request.rotationDegrees) * p;
|
||||
Vector2 half = t.baseSize * 0.5f;
|
||||
min = new Vector2(Mathf.Max(0, Mathf.Abs(localCenter.x) - half.x), Mathf.Max(0, Mathf.Abs(localCenter.y) - half.y)).magnitude;
|
||||
max = new Vector2(Mathf.Abs(localCenter.x) + half.x, Mathf.Abs(localCenter.y) + half.y).magnitude;
|
||||
}
|
||||
bool regionValid = t.region == BuildRegion.WallRing ? min >= InnerRadius && max <= OuterRadius : max <= InnerRadius;
|
||||
if (t.region == BuildRegion.Gate)
|
||||
{
|
||||
regionValid = false;
|
||||
for (int i = 0; i < 4; i++)
|
||||
if (Vector2.Distance(p, new Vector2(Mathf.Cos(i * Mathf.PI / 2), Mathf.Sin(i * Mathf.PI / 2)) * InnerRadius) < 0.65f) regionValid = true;
|
||||
}
|
||||
if (!regionValid) return new BuildCheck(BuildFailure.OutsideRegion, t.region == BuildRegion.WallRing ? "Nur auf der Mauer baubar" : t.region == BuildRegion.Gate ? "An einer Schleuse einsetzen" : "Nur im Innenfeld einsetzbar");
|
||||
if (t.use != BuildUse.Ability)
|
||||
{
|
||||
float radius = t.baseShape == BaseShape.Rectangle ? t.baseSize.magnitude / 2 : t.baseRadius;
|
||||
if (t.region == BuildRegion.Interior && min < 0.55f) return new BuildCheck(BuildFailure.OutsideRegion, "Kernbereich freihalten");
|
||||
foreach (var b in built)
|
||||
{
|
||||
float otherBase = b.type.baseShape == BaseShape.Rectangle ? b.type.baseSize.magnitude / 2 : b.type.baseRadius;
|
||||
float required = t.region == b.type.region ? t.placementRadius + b.type.placementRadius : radius + otherBase;
|
||||
if (Vector2.Distance(request.position, b.position) < required) return new BuildCheck(BuildFailure.Overlap, "Zu nah an einem anderen Objekt");
|
||||
}
|
||||
}
|
||||
// This prototype does not have the real pathfinder: the real backend must reject path-blocking placements.
|
||||
return new BuildCheck(BuildFailure.None);
|
||||
}
|
||||
public BuildCheck ExecuteBuild(BuildRequest request)
|
||||
{
|
||||
var check = CheckBuild(request);
|
||||
if (!check.Allowed) return check;
|
||||
var t = request.type;
|
||||
Energy -= t.energyCost; Entrails -= t.entrailsCost;
|
||||
readyAt[t.id] = Time.time + t.deploymentCooldown;
|
||||
if (t.use != BuildUse.Ability)
|
||||
{
|
||||
var obj = Instantiate(t.gameplayPrefab ? t.gameplayPrefab : fallback, request.position, Quaternion.Euler(0, 0, request.rotationDegrees), parent);
|
||||
obj.name = t.displayName + " #" + nextId;
|
||||
if (!t.gameplayPrefab)
|
||||
{
|
||||
var renderer = obj.GetComponent<SpriteRenderer>(); if (t.baseSprite && renderer) { renderer.sprite = t.baseSprite; renderer.color = Color.white; }
|
||||
Vector2 size = t.baseShape == BaseShape.Rectangle ? t.baseSize : Vector2.one * t.baseRadius * 1.4142136f;
|
||||
Vector2 source = renderer && renderer.sprite ? renderer.sprite.bounds.size : Vector2.one;
|
||||
obj.transform.localScale = new Vector3(size.x / source.x, size.y / source.y, 1);
|
||||
}
|
||||
built.Add(new BuiltObjectSnapshot { instanceId = nextId++, type = t, position = request.position, rotationDegrees = request.rotationDegrees, instance = obj, expiresAt = t.lifetimeSeconds > 0 ? Time.time + t.lifetimeSeconds : 0 });
|
||||
}
|
||||
StateChanged?.Invoke();
|
||||
return check;
|
||||
}
|
||||
public BuildCheck CheckUpgrade(long id)
|
||||
{
|
||||
var obj = built.Find(b => b.instanceId == id);
|
||||
if (obj == null || !obj.type.upgradeable || obj.upgradeLevel >= obj.type.upgrades.Length) return new BuildCheck(BuildFailure.InvalidType, "Maximal verbessert");
|
||||
var upgrade = obj.type.upgrades[obj.upgradeLevel];
|
||||
if (Energy < upgrade.energyCost) return new BuildCheck(BuildFailure.Energy, "Nicht genug Energie");
|
||||
if (Entrails < upgrade.entrailsCost) return new BuildCheck(BuildFailure.Entrails, "Nicht genug Gedärme");
|
||||
return new BuildCheck(BuildFailure.None);
|
||||
}
|
||||
public BuildCheck ExecuteUpgrade(long id)
|
||||
{
|
||||
var check = CheckUpgrade(id); if (!check.Allowed) return check;
|
||||
var obj = built.Find(b => b.instanceId == id); var u = obj.type.upgrades[obj.upgradeLevel];
|
||||
Energy -= u.energyCost; Entrails -= u.entrailsCost; obj.upgradeLevel++;
|
||||
StateChanged?.Invoke(); return check;
|
||||
}
|
||||
private void Update()
|
||||
{
|
||||
bool changed = false;
|
||||
for (int i = built.Count - 1; i >= 0; i--)
|
||||
if (!built[i].instance || (built[i].expiresAt > 0 && Time.time >= built[i].expiresAt))
|
||||
{ if (built[i].instance) Destroy(built[i].instance); built.RemoveAt(i); changed = true; }
|
||||
if (changed) StateChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 247c2436accc48d4681e34c06324e4a6
|
||||
@@ -0,0 +1,109 @@
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace DepthsOfGreed.HandbookUI
|
||||
{
|
||||
public sealed partial class SupplyBasementController
|
||||
{
|
||||
[Header("Versorgungssystem – optional")]
|
||||
[Tooltip("MonoBehaviour mit ISupplyGameplay. Leer = bisherige Bedien- und Fahrvorschau.")]
|
||||
[SerializeField] private MonoBehaviour supplyBackend;
|
||||
public TMP_Text[] storageAmounts, bufferAmounts, magazineAmounts;
|
||||
public bool HasSupplyBackend => supplyBackend && supplyBackend is ISupplyGameplay;
|
||||
public ISupplyGameplay SupplyBackend => HasSupplyBackend ? (ISupplyGameplay)supplyBackend : null;
|
||||
private SupplyState supplyState;
|
||||
private float commandMessageUntil;
|
||||
private readonly List<BuiltObjectSnapshot> supplyObjects = new List<BuiltObjectSnapshot>();
|
||||
|
||||
public void BindSupplyBackend(MonoBehaviour backend)
|
||||
{
|
||||
if (backend && !(backend is ISupplyGameplay))
|
||||
{ Debug.LogError("Versorgung: Komponente muss ISupplyGameplay implementieren.", backend); return; }
|
||||
supplyBackend = backend;
|
||||
// Filled on the next UI refresh; safe even before Awake.
|
||||
nextRefresh = 0;
|
||||
}
|
||||
private IReadOnlyList<BuiltObjectSnapshot> GetSupplyObjects()
|
||||
{
|
||||
if (!HasSupplyBackend)
|
||||
return placement && placement.Backend != null ? placement.Backend.BuiltObjects : null;
|
||||
supplyState = SupplyBackend.ReadState();
|
||||
supplyObjects.Clear();
|
||||
if (supplyState?.towers != null)
|
||||
foreach (var t in supplyState.towers)
|
||||
if (t != null && t.type) supplyObjects.Add(new BuiltObjectSnapshot { instanceId = t.instanceId, type = t.type });
|
||||
Priority = supplyState == null ? -1 : (int)supplyState.priority;
|
||||
RenderPriority(); RenderAmounts();
|
||||
return supplyObjects;
|
||||
}
|
||||
private TowerSupplyState FindTower(long id)
|
||||
{
|
||||
if (supplyState?.towers != null) foreach (var t in supplyState.towers) if (t != null && t.instanceId == id) return t;
|
||||
return null;
|
||||
}
|
||||
private bool TrySetBackendConnection(long id, bool connected)
|
||||
{
|
||||
if (!HasSupplyBackend) return false;
|
||||
var result = SupplyBackend.SetConnected(id, connected);
|
||||
if (result.accepted) ConnectionChanged?.Invoke(id, connected);
|
||||
else ShowCommandMessage(result.message);
|
||||
RefreshConnections(); return true;
|
||||
}
|
||||
private bool TrySetBackendPriority(int kind)
|
||||
{
|
||||
if (!HasSupplyBackend) return false;
|
||||
int desired = Priority == kind ? -1 : kind;
|
||||
var result = SupplyBackend.SetPriority((SupplyKind)desired);
|
||||
if (result.accepted) PriorityChanged?.Invoke(desired);
|
||||
else ShowCommandMessage(result.message);
|
||||
RefreshConnections(); return true;
|
||||
}
|
||||
private void ShowCommandMessage(string message)
|
||||
{ truckStatus.text = string.IsNullOrWhiteSpace(message) ? "Aktion derzeit nicht möglich" : message; commandMessageUntil = Time.unscaledTime + 2; }
|
||||
private void RenderPriority()
|
||||
{
|
||||
for (int i = 0; i < priorityButtons.Length; i++)
|
||||
{
|
||||
priorityButtons[i].GetComponent<UnityEngine.UI.Image>().color = Priority == i ? ColorFor(i) : new Color(.72f,.72f,.72f);
|
||||
priorityLabels[i].text = Priority == i ? "Vorrang aktiv" : "Vorrang geben";
|
||||
}
|
||||
}
|
||||
private static string Amount(float value, float capacity)
|
||||
=> Mathf.Max(0,value).ToString("0.#") + " / " + Mathf.Max(0,capacity).ToString("0.#");
|
||||
private void RenderAmounts()
|
||||
{
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
SupplyChannelState channel = null;
|
||||
if (supplyState?.channels != null) foreach (var item in supplyState.channels) if (item != null && (int)item.kind == i) { channel = item; break; }
|
||||
if (storageAmounts != null && i < storageAmounts.Length && storageAmounts[i]) storageAmounts[i].text = channel == null ? "— / —" : Amount(channel.storage, channel.storageCapacity);
|
||||
if (bufferAmounts != null && i < bufferAmounts.Length && bufferAmounts[i]) bufferAmounts[i].text = channel == null ? "Vorrat: —" : Amount(channel.buffer, channel.bufferCapacity);
|
||||
}
|
||||
}
|
||||
private void RenderMagazine(int slot, long id)
|
||||
{
|
||||
if (magazineAmounts == null || slot >= magazineAmounts.Length || !magazineAmounts[slot]) return;
|
||||
var state = HasSupplyBackend ? FindTower(id) : null;
|
||||
magazineAmounts[slot].gameObject.SetActive(state != null);
|
||||
if (state != null) magazineAmounts[slot].text = "Magazin: " + Amount(state.magazine, state.magazineCapacity);
|
||||
}
|
||||
private float StopX(SupplyStop stop) => stop == SupplyStop.Distributor ? unloadingPosition : loadingPositions[Mathf.Clamp((int)stop,0,2)];
|
||||
private void RenderBackendTruck()
|
||||
{
|
||||
var state = supplyState?.truck;
|
||||
if (state == null) { if(Time.unscaledTime >= commandMessageUntil)truckStatus.text = "LKW bereit"; return; }
|
||||
var p = truck.anchoredPosition;
|
||||
p.x = state.phase == SupplyTruckPhase.Driving
|
||||
? Mathf.Lerp(StopX(state.from), StopX(state.to), Mathf.Clamp01(state.progress)) : StopX(state.to);
|
||||
truck.anchoredPosition = p;
|
||||
float direction = StopX(state.to) - StopX(state.from);
|
||||
if (state.phase == SupplyTruckPhase.Driving && Mathf.Abs(direction) > .1f)
|
||||
truck.localScale = new Vector3(Mathf.Abs(truckScale.x)*Mathf.Sign(direction),truckScale.y,truckScale.z);
|
||||
if(Time.unscaledTime < commandMessageUntil)return;
|
||||
string phase = state.phase == SupplyTruckPhase.Loading ? "Beladen" : state.phase == SupplyTruckPhase.Unloading ? "Abladen" : state.phase == SupplyTruckPhase.Driving ? "Unterwegs" : "Bereit";
|
||||
int kind = (int)state.cargoKind;
|
||||
truckStatus.text = phase + (kind >= 0 && kind < kinds.Length ? " · " + kinds[kind] + " " + Amount(state.cargo,state.cargoCapacity) : "");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 944168cf2fe0e4a4fb64ba4e8d69ebf9
|
||||
@@ -0,0 +1,206 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace DepthsOfGreed.HandbookUI
|
||||
{
|
||||
// Presentation prototype. Connection intent is separate from real ammunition accounting.
|
||||
public sealed partial class SupplyBasementController : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler
|
||||
{
|
||||
public bool IsInBasement { get; private set; }
|
||||
public bool IsTravelling { get; private set; }
|
||||
public int Priority { get; private set; } = -1;
|
||||
public event System.Action<long, bool> ConnectionChanged;
|
||||
public event System.Action<int> PriorityChanged;
|
||||
[Header("Authored scene references")]
|
||||
public GameObject basementView;
|
||||
public GameObject handbookRoot;
|
||||
public CanvasGroup handbookVisibility;
|
||||
public HandbookController handbook;
|
||||
public TestPlacementController placement;
|
||||
public Button liftButton;
|
||||
public RectTransform liftSign;
|
||||
public TMP_Text liftLabel;
|
||||
public CanvasGroup doorGroup;
|
||||
public RectTransform leftDoor, rightDoor;
|
||||
public Button[] connectionButtons;
|
||||
public TMP_Text[] connectionNames, connectionStates;
|
||||
public Image[] connectionIcons, cables;
|
||||
public Button[] priorityButtons;
|
||||
public TMP_Text[] priorityLabels;
|
||||
public RectTransform truck;
|
||||
public TMP_Text truckStatus;
|
||||
[Header("Lift timing")]
|
||||
[Min(0.05f)] public float doorDuration = 0.3f;
|
||||
[Min(0)] public float travelDuration = 0.35f;
|
||||
[Min(0.05f)] public float signHalfFlipDuration = 0.1f;
|
||||
[Header("Truck display test (no ammunition is consumed)")]
|
||||
public bool animateTruckPreview = true;
|
||||
public float truckSpeed = 100f;
|
||||
public float[] loadingPositions = { -530f, 0f, 530f };
|
||||
public float unloadingPosition = 710f;
|
||||
public float loadingPause = 1.2f;
|
||||
public Color physicalColor = new Color(.35f,.35f,.35f);
|
||||
public Color cellColor = new Color(.48f,.48f,.48f);
|
||||
public Color specialColor = new Color(.60f,.60f,.60f);
|
||||
private readonly long[] slotIds = new long[8];
|
||||
private readonly Dictionary<long, bool> connections = new Dictionary<long, bool>();
|
||||
private bool hovered, labelBack;
|
||||
private Coroutine flipRoutine;
|
||||
private float nextRefresh, truckPauseUntil;
|
||||
private int previewKind;
|
||||
private bool unloading;
|
||||
private Vector3 signScale;
|
||||
private Vector3 truckScale;
|
||||
private readonly string[] kinds = { "Physisch", "Energiezellen", "Sondermunition" };
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
signScale = liftSign.localScale;
|
||||
truckScale = truck.localScale;
|
||||
basementView.SetActive(false);
|
||||
doorGroup.gameObject.SetActive(false);
|
||||
liftButton.onClick.AddListener(UseLift);
|
||||
for (int i = 0; i < connectionButtons.Length; i++)
|
||||
{ int slot = i; connectionButtons[i].onClick.AddListener(() => ToggleConnection(slot)); }
|
||||
for (int i = 0; i < priorityButtons.Length; i++)
|
||||
{ int kind = i; priorityButtons[i].onClick.AddListener(() => SetPriority(kind)); }
|
||||
if (!HasSupplyBackend) SetPriority(-1);
|
||||
else if (supplyBackend && !(supplyBackend is ISupplyGameplay)) Debug.LogError("Versorgung: ISupplyGameplay fehlt.", this);
|
||||
RefreshSign();
|
||||
}
|
||||
public void OnPointerEnter(PointerEventData e) { hovered = true; FlipSign(); }
|
||||
public void OnPointerExit(PointerEventData e) { hovered = false; FlipSign(); }
|
||||
private void FlipSign()
|
||||
{
|
||||
if (flipRoutine != null) StopCoroutine(flipRoutine);
|
||||
flipRoutine = StartCoroutine(Flip());
|
||||
}
|
||||
private IEnumerator Flip()
|
||||
{
|
||||
float start = liftSign.localScale.x;
|
||||
for (float t = 0; t < signHalfFlipDuration; t += Time.unscaledDeltaTime)
|
||||
{ liftSign.localScale = new Vector3(Mathf.Lerp(start, 0.02f, t / signHalfFlipDuration), signScale.y, signScale.z); yield return null; }
|
||||
labelBack = hovered; RefreshSign();
|
||||
for (float t = 0; t < signHalfFlipDuration; t += Time.unscaledDeltaTime)
|
||||
{ liftSign.localScale = new Vector3(Mathf.Lerp(0.02f, signScale.x, t / signHalfFlipDuration), signScale.y, signScale.z); yield return null; }
|
||||
liftSign.localScale = signScale; flipRoutine = null;
|
||||
}
|
||||
private void RefreshSign() => liftLabel.text = labelBack
|
||||
? (IsInBasement ? "ZUR\nTURMEBENE" : "ZUM\nVERSORGUNGS-\nKELLER")
|
||||
: (IsInBasement ? "▲\nAUFZUG" : "▼\nAUFZUG");
|
||||
public void UseLift()
|
||||
{
|
||||
if (IsTravelling) return;
|
||||
if (placement && (placement.IsDragging || placement.IsWaitingForReturn)) return;
|
||||
StartCoroutine(Travel());
|
||||
}
|
||||
private IEnumerator Travel()
|
||||
{
|
||||
IsTravelling = true; liftButton.interactable = false;
|
||||
doorGroup.gameObject.SetActive(true); doorGroup.blocksRaycasts = true;
|
||||
yield return Doors(true);
|
||||
if (handbook) handbook.Close();
|
||||
IsInBasement = !IsInBasement;
|
||||
basementView.SetActive(IsInBasement);
|
||||
// Hide only the presentation. The backend on HandbookRoot must keep updating.
|
||||
handbookVisibility.alpha = IsInBasement ? 0f : 1f;
|
||||
handbookVisibility.interactable = !IsInBasement;
|
||||
handbookVisibility.blocksRaycasts = !IsInBasement;
|
||||
hovered = false; labelBack = false; RefreshSign(); RefreshConnections();
|
||||
yield return new WaitForSecondsRealtime(travelDuration);
|
||||
yield return Doors(false);
|
||||
doorGroup.blocksRaycasts = false; doorGroup.gameObject.SetActive(false);
|
||||
liftButton.interactable = true; IsTravelling = false;
|
||||
}
|
||||
private IEnumerator Doors(bool close)
|
||||
{
|
||||
float width = ((RectTransform)leftDoor.parent).rect.width * 0.5f;
|
||||
for (float t = 0; t < doorDuration; t += Time.unscaledDeltaTime)
|
||||
{
|
||||
float p = Mathf.SmoothStep(0, 1, t / doorDuration); if (!close) p = 1 - p;
|
||||
leftDoor.anchoredPosition = new Vector2(-width * (1 - p), 0);
|
||||
rightDoor.anchoredPosition = new Vector2(width * (1 - p), 0);
|
||||
yield return null;
|
||||
}
|
||||
leftDoor.anchoredPosition = new Vector2(close ? 0 : -width, 0);
|
||||
rightDoor.anchoredPosition = new Vector2(close ? 0 : width, 0);
|
||||
}
|
||||
public bool IsConnectionEnabled(long instanceId) => HasSupplyBackend ? FindTower(instanceId)?.connected == true : !connections.TryGetValue(instanceId, out bool enabled) || enabled;
|
||||
public void ToggleConnection(int slot)
|
||||
{
|
||||
if (slot < 0 || slot >= slotIds.Length || slotIds[slot] == 0) return;
|
||||
long id = slotIds[slot];
|
||||
if (TrySetBackendConnection(id, !IsConnectionEnabled(id))) return;
|
||||
connections[id] = !IsConnectionEnabled(id);
|
||||
ConnectionChanged?.Invoke(id, connections[id]); RefreshConnections();
|
||||
}
|
||||
public void SetPriority(int kind)
|
||||
{
|
||||
if (kind < -1 || kind > 2) return;
|
||||
if (TrySetBackendPriority(kind)) return;
|
||||
Priority = Priority == kind ? -1 : kind;
|
||||
PriorityChanged?.Invoke(Priority);
|
||||
for (int i = 0; i < priorityButtons.Length; i++)
|
||||
{ priorityButtons[i].GetComponent<Image>().color = Priority == i ? ColorFor(i) : new Color(.72f,.72f,.72f); priorityLabels[i].color = new Color(.18f,.18f,.18f); priorityLabels[i].text = Priority == i ? "Vorrang aktiv" : "Vorrang geben"; }
|
||||
}
|
||||
private Color ColorFor(int kind) => kind == 0 ? physicalColor : kind == 1 ? cellColor : specialColor;
|
||||
private static bool UsesSupply(HandbookTowerEntry t) => t && t.use == BuildUse.Structure && t.supply != null && t.supply.kind != SupplyKind.None;
|
||||
private void RefreshConnections()
|
||||
{
|
||||
var list = GetSupplyObjects();
|
||||
for (int i = 0; i < slotIds.Length; i++)
|
||||
{
|
||||
bool exists = false;
|
||||
if (list != null) foreach (var obj in list) if (obj.instanceId == slotIds[i] && UsesSupply(obj.type)) { exists = true; break; }
|
||||
if (!exists) { connections.Remove(slotIds[i]); slotIds[i] = 0; }
|
||||
}
|
||||
if (list != null) foreach (var obj in list)
|
||||
{
|
||||
if (!UsesSupply(obj.type) || System.Array.IndexOf(slotIds, obj.instanceId) >= 0) continue;
|
||||
int free = System.Array.IndexOf(slotIds, 0L); if (free >= 0) slotIds[free] = obj.instanceId;
|
||||
}
|
||||
for (int i = 0; i < connectionButtons.Length; i++)
|
||||
{
|
||||
BuiltObjectSnapshot found = null;
|
||||
if (list != null) foreach (var obj in list) if (obj.instanceId == slotIds[i]) { found = obj; break; }
|
||||
RenderMagazine(i, found == null ? 0 : found.instanceId);
|
||||
connectionButtons[i].interactable = found != null;
|
||||
connectionIcons[i].gameObject.SetActive(found != null && found.type.icon);
|
||||
if (found == null) { connectionNames[i].text = ""; connectionStates[i].text = "Frei"; cables[i].color = new Color(.48f,.48f,.48f); continue; }
|
||||
bool enabled = IsConnectionEnabled(found.instanceId);
|
||||
int kind = Mathf.Clamp((int)found.type.supply.kind, 0, 2);
|
||||
connectionIcons[i].sprite = found.type.icon;
|
||||
connectionNames[i].text = found.type.displayName + " #" + found.instanceId;
|
||||
connectionStates[i].text = (enabled ? "Verbunden" : "Getrennt") + "\n" + kinds[kind];
|
||||
cables[i].color = enabled ? ColorFor(kind) : new Color(.48f,.48f,.48f);
|
||||
}
|
||||
}
|
||||
private void Update()
|
||||
{
|
||||
if (!IsInBasement) return;
|
||||
if (Time.unscaledTime > nextRefresh) { nextRefresh = Time.unscaledTime + 0.25f; RefreshConnections(); }
|
||||
if (HasSupplyBackend) { RenderBackendTruck(); return; }
|
||||
if (!animateTruckPreview) { truckStatus.text = "LKW bereit"; return; }
|
||||
if (Time.unscaledTime < truckPauseUntil) return;
|
||||
float destination = unloading ? unloadingPosition : loadingPositions[previewKind];
|
||||
var p = truck.anchoredPosition;
|
||||
float direction = destination - p.x;
|
||||
if (Mathf.Abs(direction) > 0.1f)
|
||||
truck.localScale = new Vector3(Mathf.Abs(truckScale.x) * Mathf.Sign(direction), truckScale.y, truckScale.z);
|
||||
p.x = Mathf.MoveTowards(p.x, destination, Mathf.Max(0, truckSpeed) * Time.unscaledDeltaTime); truck.anchoredPosition = p;
|
||||
truckStatus.text = (unloading ? "Zur Verteilerstation" : "Zum Behälter: " + kinds[previewKind]);
|
||||
if (Mathf.Abs(p.x - destination) < 0.1f)
|
||||
{
|
||||
truckStatus.text = (unloading ? "Abladen" : "Beladen: " + kinds[previewKind]);
|
||||
truckPauseUntil = Time.unscaledTime + loadingPause;
|
||||
if (unloading) previewKind = Priority >= 0 ? Priority : (previewKind + 1) % 3;
|
||||
unloading = !unloading;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fa505209e0481a842950f5e28ab02ab9
|
||||
@@ -0,0 +1,22 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using DepthsOfGreed.HandbookUI;
|
||||
using UnityEngine;
|
||||
|
||||
// Editor-only fixture. Never attached to the saved scene.
|
||||
public sealed class SupplyContractTestBackend : MonoBehaviour, ISupplyGameplay
|
||||
{
|
||||
public SupplyState state=new SupplyState();
|
||||
public bool reject;
|
||||
public SupplyState ReadState()=>state;
|
||||
public SupplyCommandResult SetConnected(long id,bool connected)
|
||||
{
|
||||
if(reject)return new SupplyCommandResult(false,"Test: Verbindung gesperrt");
|
||||
foreach(var t in state.towers)if(t.instanceId==id){t.connected=connected;return new SupplyCommandResult(true);}
|
||||
return new SupplyCommandResult(false,"Turm fehlt");
|
||||
}
|
||||
public SupplyCommandResult SetPriority(SupplyKind kind)
|
||||
{if(reject)return new SupplyCommandResult(false,"Test: Priorität gesperrt");state.priority=kind;return new SupplyCommandResult(true);}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6708cdceeee544046b6994aa3f896bbe
|
||||
@@ -11,8 +11,13 @@ namespace DepthsOfGreed.HandbookUI
|
||||
{
|
||||
public bool IsDragging { get; private set; }
|
||||
public bool ConsumedCancelThisFrame => cancelFrame == Time.frameCount;
|
||||
public int TowerCount => placed.Count;
|
||||
public int Energy { get; private set; } = 2000;
|
||||
public int TowerCount => Backend == null ? 0 : Backend.WallCount;
|
||||
public int Energy => Backend == null ? 0 : Backend.Energy;
|
||||
public IHandbookGameplay Backend { get; private set; }
|
||||
public Vector3 AreaCenter => area.position;
|
||||
public float AreaRadius => outerRadius;
|
||||
public string LastFailure { get; private set; }
|
||||
private string failedTypeId;
|
||||
public int MaximumTowers = 8;
|
||||
public float SuccessfulPlacementDelay = 0.5f;
|
||||
public bool IsWaitingForReturn => Time.unscaledTime < returnAfter;
|
||||
@@ -20,26 +25,30 @@ namespace DepthsOfGreed.HandbookUI
|
||||
private int cancelFrame = -1;
|
||||
private RectTransform book;
|
||||
private Vector2 bookHome;
|
||||
[Header("Book position while building")]
|
||||
[Tooltip("Automatic lowering accounts for the book scale. Disable to set an explicit anchored position.")]
|
||||
[SerializeField] private bool automaticLoweredBookPosition = true;
|
||||
[Tooltip("Height of the book left visible above the bottom edge, in parent UI units. Tabs can extend above it.")]
|
||||
[SerializeField, Min(0)] private float visibleBookHeightWhileBuilding = 15f;
|
||||
[SerializeField] private Vector2 loweredBookPosition = new Vector2(0f, -675f);
|
||||
private CanvasGroup bookGroup;
|
||||
private GameObject prefab, preview, controls;
|
||||
private Transform container, area;
|
||||
private SpriteRenderer ghost, footprint;
|
||||
private Sprite ghostPlaceholder;
|
||||
private TMP_Text controlsText, feedback, resources, buildHint;
|
||||
private Image feedbackBox;
|
||||
private RectTransform feedbackBoxRect;
|
||||
private float messageStarted;
|
||||
private Color feedbackBackground;
|
||||
private Camera worldCamera;
|
||||
private float innerRadius, outerRadius, messageUntil;
|
||||
private float innerRadius, outerRadius;
|
||||
private HandbookTowerEntry draggingEntry;
|
||||
private Vector3 target;
|
||||
private readonly List<PlacedRecord> placed = new List<PlacedRecord>();
|
||||
private readonly List<LineRenderer> guides = new List<LineRenderer>();
|
||||
private LineRenderer previewOutline, innerOutline, outerOutline;
|
||||
private Material lineMaterial;
|
||||
private struct PlacedRecord { public Vector3 position; public float radius; public GameObject instance; }
|
||||
private float placementAngle;
|
||||
private GeneralFeedbackController general;
|
||||
|
||||
public void Initialize(HandbookController owner, BuildMenuController menu, RectTransform openBook, GameObject testPrefab)
|
||||
public void Initialize(HandbookController owner, BuildMenuController menu, RectTransform openBook, GameObject testPrefab, MonoBehaviour gameplayBackend = null)
|
||||
{
|
||||
book = openBook;
|
||||
bookHome = book.anchoredPosition;
|
||||
@@ -57,6 +66,7 @@ namespace DepthsOfGreed.HandbookUI
|
||||
container = test.transform.Find("PlacedTowers") ?? test.transform.Find("PlaceTowers");
|
||||
preview = test.transform.Find("PlacementPreview").gameObject;
|
||||
ghost = preview.transform.Find("GhostVisual").GetComponent<SpriteRenderer>();
|
||||
ghostPlaceholder = ghost.sprite;
|
||||
footprint = preview.transform.Find("FootprintVisual").GetComponent<SpriteRenderer>();
|
||||
var controlTransform = transform.Find("PlacementControlBar") ?? transform.Find("PlacementController");
|
||||
if (!area || !container || !controlTransform)
|
||||
@@ -72,8 +82,6 @@ namespace DepthsOfGreed.HandbookUI
|
||||
if (boxTransform)
|
||||
{
|
||||
feedbackBox = boxTransform.GetComponent<Image>();
|
||||
feedbackBoxRect = (RectTransform)boxTransform;
|
||||
feedbackBackground = feedbackBox.color;
|
||||
feedbackBox.raycastTarget = false;
|
||||
feedbackBox.gameObject.SetActive(false);
|
||||
}
|
||||
@@ -81,6 +89,14 @@ namespace DepthsOfGreed.HandbookUI
|
||||
buildHint = menu.transform.Find("BuildHintText").GetComponent<TMP_Text>();
|
||||
innerRadius = RadiusOf("BackgroundTower", 3f);
|
||||
outerRadius = RadiusOf("BackgroundOuterwall", 4f);
|
||||
if (gameplayBackend && gameplayBackend is IHandbookGameplay real) Backend = real;
|
||||
else
|
||||
{
|
||||
var prototype = gameObject.GetComponent<PrototypeHandbookGameplay>();
|
||||
prototype.Initialize(container, area.position, innerRadius, outerRadius, prefab); Backend = prototype;
|
||||
}
|
||||
Backend.StateChanged += RefreshResources;
|
||||
general = gameObject.GetComponent<GeneralFeedbackController>(); general.Initialize(book, controlsText.font);
|
||||
lineMaterial = new Material(Shader.Find("Sprites/Default"));
|
||||
previewOutline = MakeCircle("PlacementOutline", preview.transform, 11);
|
||||
innerOutline = MakeCircle("BuildArea_Inner", area, 8);
|
||||
@@ -103,21 +119,25 @@ namespace DepthsOfGreed.HandbookUI
|
||||
public bool Begin(HandbookTowerEntry entry, Vector2 screenPosition)
|
||||
{
|
||||
if (!enabled || IsDragging || IsWaitingForReturn || entry == null) return false;
|
||||
if (placed.Count >= MaximumTowers) { Message("Turmlimit erreicht"); return false; }
|
||||
if (Energy < entry.energyCost) { Message("Nicht genug Energie"); return false; }
|
||||
draggingEntry = entry;
|
||||
var availability = Backend.CheckAvailability(entry);
|
||||
if (!availability.Allowed) { failedTypeId = entry.id; Message(availability.message); return false; }
|
||||
draggingEntry = entry; placementAngle = 0; LastFailure = null;
|
||||
ghost.sprite = entry.previewSprite ? entry.previewSprite : ghostPlaceholder;
|
||||
Vector2 visualSize = entry.baseShape == BaseShape.Rectangle ? entry.baseSize : Vector2.one * entry.baseRadius * 1.4142136f;
|
||||
ghost.transform.localScale = new Vector3(visualSize.x / ghost.sprite.bounds.size.x, visualSize.y / ghost.sprite.bounds.size.y, 1);
|
||||
IsDragging = true;
|
||||
preview.SetActive(true);
|
||||
footprint.transform.localScale = new Vector3(entry.placementRadius * 2f, entry.placementRadius * 2f, 1f);
|
||||
DrawCircle(previewOutline, entry.placementRadius);
|
||||
controls.SetActive(true);
|
||||
controls.SetActive(false);
|
||||
general.ShowControls("Loslassen: Bauen\nRechtsklick / Esc: Abbrechen" + (entry.allowPlayerRotation ? "\nR: Drehen" : ""));
|
||||
bookGroup.blocksRaycasts = false;
|
||||
ShowGuides(true);
|
||||
foreach (var record in placed)
|
||||
foreach (var record in Backend.BuiltObjects)
|
||||
{
|
||||
var guide = MakeCircle("ExistingTowerRadius", container, 8);
|
||||
guide.transform.position = record.position;
|
||||
DrawCircle(guide, record.radius);
|
||||
DrawCircle(guide, record.type.placementRadius);
|
||||
Tint(guide, new Color(1f, 1f, 1f, 0.5f));
|
||||
guides.Add(guide);
|
||||
}
|
||||
@@ -133,9 +153,9 @@ namespace DepthsOfGreed.HandbookUI
|
||||
if (!plane.Raycast(ray, out float distance)) return;
|
||||
target = ray.GetPoint(distance);
|
||||
target.z = 0;
|
||||
preview.transform.position = target;
|
||||
preview.transform.position = target; preview.transform.rotation = Quaternion.Euler(0, 0, placementAngle);
|
||||
string reason = Check(draggingEntry, target);
|
||||
bool outside = reason == "Außerhalb des Schienenbereichs";
|
||||
bool outside = Backend.CheckBuild(new BuildRequest(draggingEntry, target, placementAngle)).failure == BuildFailure.OutsideRegion;
|
||||
Color color = outside ? Color.gray : reason == null ? Color.green : Color.red;
|
||||
footprint.color = new Color(color.r, color.g, color.b, outside ? 0.08f : 0.18f);
|
||||
ghost.color = new Color(1f, 1f, 1f, outside ? 0.3f : 0.65f);
|
||||
@@ -160,29 +180,11 @@ namespace DepthsOfGreed.HandbookUI
|
||||
}
|
||||
|
||||
public string Check(HandbookTowerEntry entry, Vector3 position)
|
||||
{
|
||||
if (entry == null) return "Kein Turm ausgewählt";
|
||||
float distance = Vector2.Distance(position, area.position);
|
||||
if (distance - entry.baseRadius < innerRadius || distance + entry.baseRadius > outerRadius)
|
||||
return "Außerhalb des Schienenbereichs";
|
||||
foreach (var record in placed)
|
||||
if (Vector2.Distance(position, record.position) < entry.placementRadius + record.radius)
|
||||
return "Zu nah an einem anderen Turm";
|
||||
if (placed.Count >= MaximumTowers) return "Turmlimit erreicht";
|
||||
if (Energy < entry.energyCost) return "Nicht genug Energie";
|
||||
return null;
|
||||
}
|
||||
|
||||
{ return Backend.CheckBuild(new BuildRequest(entry, position, placementAngle)).message; }
|
||||
public bool TryPlaceAt(HandbookTowerEntry entry, Vector3 position, out string reason)
|
||||
{
|
||||
reason = Check(entry, position);
|
||||
if (reason != null) return false;
|
||||
var tower = Instantiate(prefab, position, Quaternion.identity, container);
|
||||
tower.name = entry.displayName + " (Test)";
|
||||
placed.Add(new PlacedRecord { position = position, radius = entry.placementRadius, instance = tower });
|
||||
Energy -= entry.energyCost;
|
||||
RefreshResources();
|
||||
return true;
|
||||
var result = Backend.ExecuteBuild(new BuildRequest(entry, position, placementAngle));
|
||||
reason = result.message; return result.Allowed;
|
||||
}
|
||||
|
||||
public void Cancel()
|
||||
@@ -198,6 +200,7 @@ namespace DepthsOfGreed.HandbookUI
|
||||
draggingEntry = null;
|
||||
preview.SetActive(false);
|
||||
controls.SetActive(false);
|
||||
general.HideControls();
|
||||
bookGroup.blocksRaycasts = true;
|
||||
ShowGuides(false);
|
||||
foreach (var guide in guides) Destroy(guide.gameObject);
|
||||
@@ -219,16 +222,20 @@ namespace DepthsOfGreed.HandbookUI
|
||||
{
|
||||
if (Mouse.current.rightButton.wasPressedThisFrame ||
|
||||
(Keyboard.current != null && Keyboard.current.escapeKey.wasPressedThisFrame)) Cancel();
|
||||
else Move(Mouse.current.position.ReadValue());
|
||||
else { if (draggingEntry.allowPlayerRotation && Keyboard.current != null && Keyboard.current.rKey.wasPressedThisFrame) placementAngle += 90; Move(Mouse.current.position.ReadValue()); }
|
||||
}
|
||||
bool lowered = IsDragging || IsWaitingForReturn;
|
||||
if (!lowered) bookGroup.blocksRaycasts = true;
|
||||
var position = lowered ? new Vector2(bookHome.x, -book.rect.height + 100f) : bookHome;
|
||||
float scaledHeight = book.rect.height * Mathf.Abs(book.localScale.y);
|
||||
var lowerTarget = automaticLoweredBookPosition
|
||||
? new Vector2(bookHome.x, -scaledHeight * (1f - book.pivot.y) + visibleBookHeightWhileBuilding)
|
||||
: loweredBookPosition;
|
||||
var position = lowered ? lowerTarget : bookHome;
|
||||
book.anchoredPosition = Vector2.Lerp(book.anchoredPosition, position,
|
||||
1f - Mathf.Exp(-18f * Time.unscaledDeltaTime));
|
||||
if (IsDragging)
|
||||
{
|
||||
bool outside = Check(draggingEntry, target) == "Außerhalb des Schienenbereichs";
|
||||
bool outside = Backend.CheckBuild(new BuildRequest(draggingEntry, target, placementAngle)).failure == BuildFailure.OutsideRegion;
|
||||
float alpha = outside ? 0.35f + 0.2f * Mathf.Sin(Time.unscaledTime * 5f) : 0.4f;
|
||||
Tint(innerOutline, new Color(1f, 0.8f, 0.3f, alpha));
|
||||
Tint(outerOutline, new Color(1f, 0.8f, 0.3f, alpha));
|
||||
@@ -238,61 +245,33 @@ namespace DepthsOfGreed.HandbookUI
|
||||
|
||||
private void Message(string reason)
|
||||
{
|
||||
feedback.gameObject.SetActive(true);
|
||||
feedback.transform.SetAsLastSibling();
|
||||
feedback.color = Color.white;
|
||||
feedback.text = reason;
|
||||
messageStarted = Time.unscaledTime;
|
||||
messageUntil = messageStarted + 2.65f;
|
||||
if (feedbackBox)
|
||||
{
|
||||
feedbackBox.gameObject.SetActive(true);
|
||||
feedbackBox.transform.SetAsLastSibling();
|
||||
feedback.transform.SetAsLastSibling();
|
||||
}
|
||||
UpdateFeedback();
|
||||
}
|
||||
|
||||
private void UpdateFeedback()
|
||||
{
|
||||
if (!feedback || messageUntil <= 0) return;
|
||||
float elapsed = Time.unscaledTime - messageStarted;
|
||||
if (Time.unscaledTime >= messageUntil) { ClearFeedback(); return; }
|
||||
float alpha = elapsed > 2.4f ? Mathf.Clamp01((messageUntil - Time.unscaledTime) / 0.25f) : 1f;
|
||||
feedback.color = new Color(1, 1, 1, alpha);
|
||||
if (feedbackBox)
|
||||
{
|
||||
float pulse = elapsed < 0.4f ? Mathf.Pow(Mathf.Sin(elapsed / 0.4f * Mathf.PI * 2f), 2f) : 0;
|
||||
Color color = Color.Lerp(feedbackBackground, new Color(0.48f, 0.34f, 0.15f, 1), pulse);
|
||||
color.a = alpha;
|
||||
feedbackBox.color = color;
|
||||
float canvasHeight = ((RectTransform)transform).rect.height;
|
||||
float y = Mathf.Min(-10f, book.anchoredPosition.y + book.rect.height - canvasHeight + 70f);
|
||||
feedbackBoxRect.anchoredPosition = new Vector2(0, y);
|
||||
feedback.rectTransform.anchoredPosition = new Vector2(0, y);
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearFeedback()
|
||||
{
|
||||
messageUntil = 0;
|
||||
if (feedback) { feedback.text = ""; feedback.color = Color.white; }
|
||||
if (feedbackBox) feedbackBox.gameObject.SetActive(false);
|
||||
LastFailure = reason;
|
||||
if (draggingEntry) failedTypeId = draggingEntry.id;
|
||||
general.ShowError(reason, !IsDragging);
|
||||
if (!IsDragging && buildHint) buildHint.text = reason;
|
||||
}
|
||||
private void UpdateFeedback() { }
|
||||
private void ClearFeedback() { if (feedback) feedback.text = ""; if (feedbackBox) feedbackBox.gameObject.SetActive(false); }
|
||||
private void RefreshResources()
|
||||
{
|
||||
resources.text = "Energie: " + Energy + " · Gedärme: 100 (Test)\nTürme: " + placed.Count + "/" + MaximumTowers;
|
||||
resources.text = "Energie: " + Energy + " · Gedärme: " + Backend.Entrails + "\nMauer: " + TowerCount + "/" + Backend.WallLimit;
|
||||
if (buildHint)
|
||||
{
|
||||
bool full = placed.Count >= MaximumTowers;
|
||||
buildHint.text = full ? "Turmlimit erreicht (" + placed.Count + "/" + MaximumTowers + ") · Klicken: Infos" : "Ziehen: Bauen · Klicken: Infos";
|
||||
bool full = TowerCount >= Backend.WallLimit;
|
||||
buildHint.text = full ? "Turmlimit erreicht (" + TowerCount + "/" + Backend.WallLimit + ") · Klicken: Infos" : "Ziehen: Bauen · Klicken: Infos";
|
||||
buildHint.color = full ? new Color(0.7f, 0.15f, 0.1f) : new Color(0.3f, 0.3f, 0.3f);
|
||||
}
|
||||
}
|
||||
public void ShowCardHint(HandbookTowerEntry entry)
|
||||
{
|
||||
var result = Backend.CheckAvailability(entry);
|
||||
if (buildHint) buildHint.text = !result.Allowed ? result.message : entry && entry.id == failedTypeId && LastFailure != null ? LastFailure : "Ziehen: Bauen · Klicken: Infos";
|
||||
}
|
||||
public void ClearCardHint() { RefreshResources(); }
|
||||
private void ShowGuides(bool show)
|
||||
{
|
||||
innerOutline.gameObject.SetActive(show);
|
||||
outerOutline.gameObject.SetActive(show);
|
||||
outerOutline.gameObject.SetActive(show && draggingEntry && draggingEntry.region == BuildRegion.WallRing);
|
||||
}
|
||||
private LineRenderer MakeCircle(string name, Transform parent, int order)
|
||||
{
|
||||
@@ -325,6 +304,6 @@ namespace DepthsOfGreed.HandbookUI
|
||||
}
|
||||
else line.widthCurve = AnimationCurve.Constant(0, 1, 1);
|
||||
}
|
||||
private void OnDestroy() { if (lineMaterial) Destroy(lineMaterial); }
|
||||
private void OnDestroy() { if (Backend != null) Backend.StateChanged -= RefreshResources; if (lineMaterial) Destroy(lineMaterial); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,13 @@ using UnityEngine.UI;
|
||||
|
||||
namespace DepthsOfGreed.HandbookUI
|
||||
{
|
||||
public sealed class TowerCardView : MonoBehaviour, IPointerClickHandler, IBeginDragHandler, IDragHandler, IEndDragHandler
|
||||
public sealed class TowerCardView : MonoBehaviour, IPointerClickHandler, IBeginDragHandler, IDragHandler, IEndDragHandler, IPointerEnterHandler, IPointerExitHandler
|
||||
{
|
||||
public HandbookTowerEntry Entry { get; private set; }
|
||||
private BuildMenuController menu;
|
||||
private GameObject front, back;
|
||||
private TMP_Text nameText, costText, statsText, infoText;
|
||||
private Coroutine animation;
|
||||
private Coroutine flipRoutine;
|
||||
private Vector3 originalScale;
|
||||
private bool showingBack, dragged;
|
||||
private Image icon;
|
||||
@@ -47,7 +47,9 @@ namespace DepthsOfGreed.HandbookUI
|
||||
{
|
||||
Entry = entry;
|
||||
nameText.text = entry.displayName;
|
||||
costText.text = "Kosten: " + entry.energyCost + " Energie (Test)";
|
||||
string price = HandbookCostText.Format(entry.energyCost, entry.entrailsCost);
|
||||
costText.text = price.Length > 0 ? "Kosten: " + price : "";
|
||||
costText.gameObject.SetActive(price.Length > 0);
|
||||
statsText.text = entry.summary;
|
||||
infoText.text = entry.description;
|
||||
icon.sprite = entry.icon ? entry.icon : placeholderSprite;
|
||||
@@ -61,6 +63,8 @@ namespace DepthsOfGreed.HandbookUI
|
||||
menu.ToggleCard(this);
|
||||
dragged = false;
|
||||
}
|
||||
public void OnPointerEnter(PointerEventData data) { if (menu.Placement) menu.Placement.ShowCardHint(Entry); }
|
||||
public void OnPointerExit(PointerEventData data) { if (menu.Placement) menu.Placement.ClearCardHint(); }
|
||||
|
||||
public void OnBeginDrag(PointerEventData data)
|
||||
{
|
||||
@@ -81,15 +85,15 @@ namespace DepthsOfGreed.HandbookUI
|
||||
public void SetBack(bool showBack, bool instant = false)
|
||||
{
|
||||
showingBack = showBack;
|
||||
if (animation != null) StopCoroutine(animation);
|
||||
animation = null;
|
||||
if (flipRoutine != null) StopCoroutine(flipRoutine);
|
||||
flipRoutine = null;
|
||||
if (instant || !isActiveAndEnabled)
|
||||
{
|
||||
transform.localScale = originalScale;
|
||||
front.SetActive(!showBack);
|
||||
back.SetActive(showBack);
|
||||
}
|
||||
else animation = StartCoroutine(Flip(showBack));
|
||||
else flipRoutine = StartCoroutine(Flip(showBack));
|
||||
}
|
||||
|
||||
private IEnumerator Flip(bool showBack)
|
||||
@@ -109,14 +113,15 @@ namespace DepthsOfGreed.HandbookUI
|
||||
yield return null;
|
||||
}
|
||||
transform.localScale = originalScale;
|
||||
animation = null;
|
||||
flipRoutine = null;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (animation != null) StopCoroutine(animation);
|
||||
animation = null;
|
||||
if (flipRoutine != null) StopCoroutine(flipRoutine);
|
||||
flipRoutine = null;
|
||||
transform.localScale = originalScale;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user