U3A HUD-Grundlagen: UI Toolkit, Sichtmodell, menschlicher Sitz
Beginn des Ports des G3-HUD (Referenz luneraria/g3, 1b540f2). - Entscheidung UI Toolkit statt uGUI nach kurzer Bewertung und einer Probe: Panel rendert im Batchmodus in eine 1920x1080-Textur (Skalierung wie Godot canvas_items/expand, 1600x960), gefaste Verlaufsplatten, alle 50 Symbole, Schriften aus den variablen Fonts der Referenz. - Sichtmodell portiert (LuneHudModel, LuneLaneThreat, LuneStats) mit den Privatsphäre-Regeln der Referenz: Gold, Entwurf, Rückgängig und neue Buchungen nur für den eigenen Sitz; Lanes, Gebäude, Boss und Wellen aus öffentlichem Zustand. - Menschlicher Sitz in LuneMatchRunner/LuneMatchDriver: Bots planen die übrigen fünf Sitze, die Planung wartet auf Bereit; Tempo 1x/2x/4x und Pause nur im Kampf. Bot-Partien unverändert. - Theme-Grundlagen: Farben und Zahlenformate, Platten (forged_style.gd), Vektorsymbole (lune_glyphs.gd) mit Painter2D, statische SDF-Atlanten aus den benannten Instanzen von Cantarell und Cinzel (OFL). - Tests: EditMode 23/23 (12 neue: gleiches öffentliches Bild für alle Sitze, fremde Entwürfe privat, private Boss-Lane-Wahl, Lane-Bedrohung, Bosszustände, nächste Welle, Bereit-Sperre, Formate, menschlicher Sitz); PlayMode 3/3, Fingerabdrücke der Bot-Partien unverändert (DCE95C96556F3A1A, 7B8E43AEFC0D60C5). - Dokumentation: U3-UI (Bewertung, U3A), U5-VISUAL (Vorgaben des Projektinhabers: stilisierter Realismus für die ganze Welt, erst in U5). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
4744a47114
commit
4dee9b18d2
59 changed files with 20890 additions and 24 deletions
18
Assets/Luneraria/Settings/LuneUi.asset
Normal file
18
Assets/Luneraria/Settings/LuneUi.asset
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: f80a232d1902841a6b0949361a252976, type: 3}
|
||||
m_Name: LuneUi
|
||||
m_EditorClassIdentifier: Luneraria.UI::Luneraria.UI.LuneUiAssets
|
||||
Text: {fileID: 11400000, guid: 9ed8c741ab987452c8ecd52e5252d418, type: 2}
|
||||
TextBold: {fileID: 11400000, guid: fe04d22bb582e4cd99959615019c6244, type: 2}
|
||||
Display: {fileID: 11400000, guid: 84c5b4f661d34482e8f3a990d043245e, type: 2}
|
||||
Theme: {fileID: -4733365628477956816, guid: 38a51cb4adf3e41879e4510c6a0d7ef6, type: 3}
|
||||
8
Assets/Luneraria/Settings/LuneUi.asset.meta
Normal file
8
Assets/Luneraria/Settings/LuneUi.asset.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 1422b9d009fb1472f89313f983a20781
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Luneraria/Tests/UI.meta
Normal file
8
Assets/Luneraria/Tests/UI.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 00dc6e7d377f8485592a9211a2ab975c
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Luneraria/Tests/UI/EditMode.meta
Normal file
8
Assets/Luneraria/Tests/UI/EditMode.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: ed2da158559184dc5955652374cf0334
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
309
Assets/Luneraria/Tests/UI/EditMode/HudModelTests.cs
Normal file
309
Assets/Luneraria/Tests/UI/EditMode/HudModelTests.cs
Normal file
|
|
@ -0,0 +1,309 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Luneraria.Core;
|
||||
using Luneraria.View;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace Luneraria.UI.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// U3: the HUD's view model (LuneHudModel, LuneLaneThreat, LuneStats) against the rules of the
|
||||
/// G3 reference (lune_hud_model.gd, lune_lane_threat.gd, verify_g3 parts B, D and E at model
|
||||
/// level): privacy of gold, drafts and bookings, the same public picture for every seat, lane
|
||||
/// threat levels, boss states, the coming wave, number formats, and the human seat of the host
|
||||
/// loop (planning waits for Ready; bot matches unchanged).
|
||||
/// </summary>
|
||||
public sealed class HudModelTests
|
||||
{
|
||||
const int Human = 1;
|
||||
|
||||
static LuneMatch Fresh(int seed = 4, int human = Human)
|
||||
{
|
||||
var match = new LuneMatch();
|
||||
match.Setup(new MatchOptions { Seed = seed, HumanSlot = human });
|
||||
for (int slot = 0; slot < 6; slot++)
|
||||
{
|
||||
if (slot != human)
|
||||
LuneBot.Plan(match, slot, "");
|
||||
}
|
||||
return match;
|
||||
}
|
||||
|
||||
static string Digest(LuneMatch match, int viewer)
|
||||
{
|
||||
return new LuneHudModel(match, viewer).PublicDigest();
|
||||
}
|
||||
|
||||
static void AssertSameForAll(LuneMatch match, string moment)
|
||||
{
|
||||
string reference = Digest(match, 0);
|
||||
for (int viewer = 1; viewer < 6; viewer++)
|
||||
Assert.AreEqual(reference, Digest(match, viewer), moment + ": public picture of seat " + viewer);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- privacy (verify_g3 part E)
|
||||
[Test]
|
||||
public void EverySeatSeesTheSamePublicPicture()
|
||||
{
|
||||
LuneMatch match = Fresh();
|
||||
AssertSameForAll(match, "planning round 1");
|
||||
match.SetReady(Human);
|
||||
match.BeginCombat();
|
||||
for (int step = 0; step < 400 && match.Phase == Phase.Combat; step++)
|
||||
{
|
||||
match.StepCombat();
|
||||
if (step % 100 == 0)
|
||||
AssertSameForAll(match, "combat step " + step);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OtherSeatsPlansStayPrivateUntilCombat()
|
||||
{
|
||||
LuneMatch match = Fresh();
|
||||
match.SetReady(Human);
|
||||
match.BeginCombat();
|
||||
while (match.Phase == Phase.Combat)
|
||||
match.StepCombat();
|
||||
Assert.AreEqual(Phase.Planning, match.Phase, "the first wave must not end the match");
|
||||
for (int slot = 0; slot < 6; slot++)
|
||||
{
|
||||
if (slot != Human)
|
||||
LuneBot.Plan(match, slot, "");
|
||||
}
|
||||
var viewer = new LuneHudModel(match, Human);
|
||||
string before = viewer.PublicDigest();
|
||||
string membersBefore = string.Join("|", viewer.Members(1));
|
||||
// An enemy recruits a hero and books an attack champion on Mid: both stay private.
|
||||
int enemy = Enumerable.Range(3, 3).First(slot => match.Players[slot].Draft.NewBookings.Count < LuneContent.MaxBookings);
|
||||
match.Players[enemy].Draft.Gold += 2000;
|
||||
if (match.Players[enemy].Draft.Hero == null)
|
||||
Assert.IsTrue(match.Submit(enemy, Command.HeroBuy(LuneContent.Heroes.Keys[0])).Ok, "hero recruit");
|
||||
string championId = LuneContent.Champions.Keys[0];
|
||||
CommandResult booking = match.Submit(enemy, Command.ChampionBook(championId, "mid"));
|
||||
Assert.IsTrue(booking.Ok, "champion booking: " + booking.Message);
|
||||
Assert.AreEqual(before, viewer.PublicDigest(), "an enemy draft changed the public picture (buildings, lanes, seats, boss, wave)");
|
||||
Assert.AreEqual(membersBefore, string.Join("|", viewer.Members(1)), "an enemy draft hero is visible before combat");
|
||||
Assert.AreEqual(match.GoldOf(Human), viewer.OwnGold, "own gold");
|
||||
// The enemy sees its own draft: gold, booking and hero.
|
||||
var enemyView = new LuneHudModel(match, enemy);
|
||||
Assert.AreEqual(match.Players[enemy].Draft.Gold, enemyView.OwnGold, "enemy's own gold");
|
||||
Assert.IsTrue(enemyView.OwnBookings().Any(b => b.Fresh && b.ChampionId == championId), "own fresh booking");
|
||||
Assert.IsNotNull(enemyView.OwnHero, "own draft hero");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BossLaneChoiceIsPrivateToTheChooser()
|
||||
{
|
||||
LuneMatch match = Fresh();
|
||||
match.RoundNumber = 4;
|
||||
match.Boss.Claim = new BossClaim { Team = 1, WonRound = 3, DeployRound = 5, ChooserSlot = 4, Lane = "top" };
|
||||
match.Players[4].Draft.BossLane = "bot";
|
||||
BossInfoCheck(new LuneHudModel(match, 4).Boss(), "deploy_next", "bot", true, true);
|
||||
BossInfoCheck(new LuneHudModel(match, Human).Boss(), "deploy_next", "top", false, false);
|
||||
AssertSameForAll(match, "boss claim with a private lane choice");
|
||||
}
|
||||
|
||||
static void BossInfoCheck(LuneHudModel.BossInfo boss, string state, string lane, bool own, bool chooser)
|
||||
{
|
||||
Assert.AreEqual(state, boss.State);
|
||||
Assert.AreEqual(lane, boss.Lane);
|
||||
Assert.AreEqual(own, boss.Own);
|
||||
Assert.AreEqual(chooser, boss.Chooser);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- lane threat
|
||||
static LuneLaneThreat.Result Threat(LuneMatch match, int team, string lane)
|
||||
{
|
||||
return LuneLaneThreat.Evaluate(match, team, lane);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void LaneThreatFollowsTowerHealth()
|
||||
{
|
||||
LuneMatch match = Fresh();
|
||||
foreach (string lane in LuneMap.Lanes)
|
||||
{
|
||||
for (int team = 0; team < 2; team++)
|
||||
Assert.AreEqual(LuneLaneThreat.Calm, Threat(match, team, lane).Level, lane + "/" + team + " at the start");
|
||||
}
|
||||
double outer = match.Map.BuildingDef("blau_mid_outer").MaxHp;
|
||||
double inner = match.Map.BuildingDef("blau_mid_inner").MaxHp;
|
||||
match.BuildingHp["blau_mid_outer"] = outer * .8;
|
||||
LuneLaneThreat.Result result = Threat(match, 0, "mid");
|
||||
Assert.AreEqual(LuneLaneThreat.Contested, result.Level);
|
||||
CollectionAssert.AreEqual(new[] { "Grenzwacht bei 80 %" }, result.Reasons);
|
||||
Assert.AreEqual(LuneLaneThreat.Calm, Threat(match, 1, "mid").Level, "the enemy's view of its own half");
|
||||
match.BuildingHp["blau_mid_outer"] = outer * .4;
|
||||
Assert.AreEqual(LuneLaneThreat.Pressure, Threat(match, 0, "mid").Level);
|
||||
match.BuildingHp["blau_mid_outer"] = 0.0;
|
||||
result = Threat(match, 0, "mid");
|
||||
Assert.AreEqual(LuneLaneThreat.Pressure, result.Level);
|
||||
CollectionAssert.AreEqual(new[] { "Grenzwacht gefallen" }, result.Reasons);
|
||||
match.BuildingHp["blau_mid_inner"] = inner * .7;
|
||||
CollectionAssert.AreEqual(new[] { "Grenzwacht gefallen", "Bastion bei 70 %" }, Threat(match, 0, "mid").Reasons);
|
||||
match.BuildingHp["blau_mid_inner"] = inner * .4;
|
||||
Assert.AreEqual(LuneLaneThreat.Critical, Threat(match, 0, "mid").Level);
|
||||
match.BuildingHp["blau_mid_inner"] = 0.0;
|
||||
result = Threat(match, 0, "mid");
|
||||
Assert.AreEqual(LuneLaneThreat.Critical, result.Level);
|
||||
Assert.AreEqual("KRITISCH", result.Name);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AnnouncedDangersRaiseTheThreat()
|
||||
{
|
||||
LuneMatch match = Fresh();
|
||||
string championId = LuneContent.Champions.Keys[0];
|
||||
match.Players[4].Bookings.Add(new Booking { Uid = "test-b1", ChampionId = championId, Lane = "mid", DeployRound = match.RoundNumber, Cost = 0 });
|
||||
LuneLaneThreat.Result result = Threat(match, 0, "mid");
|
||||
Assert.AreEqual(LuneLaneThreat.Contested, result.Level);
|
||||
CollectionAssert.AreEqual(new[] { "Angriffschampion " + LuneContent.Champions[championId].Name + " angekündigt" }, result.Reasons);
|
||||
Assert.AreEqual(LuneLaneThreat.Calm, Threat(match, 1, "mid").Level, "an own champion is no threat");
|
||||
match.Boss.Claim = new BossClaim { Team = 1, WonRound = 1, DeployRound = match.RoundNumber, ChooserSlot = 4, Lane = "top" };
|
||||
Assert.AreEqual(LuneLaneThreat.Pressure, Threat(match, 0, "top").Level, "enemy boss with the Grenzwacht standing");
|
||||
match.BuildingHp["blau_top_outer"] = 0.0;
|
||||
Assert.AreEqual(LuneLaneThreat.Critical, Threat(match, 0, "top").Level, "enemy boss after the Grenzwacht fell");
|
||||
var model = new LuneHudModel(match, Human);
|
||||
Assert.AreEqual(1, model.Lane("mid").Dangers.Count, "danger listed in the lane");
|
||||
LuneHudModel.WaveInfo wave = model.NextWave();
|
||||
Assert.AreEqual(1, wave.Champions.Count);
|
||||
Assert.IsFalse(wave.Champions[0].Own);
|
||||
Assert.IsNotNull(wave.Boss);
|
||||
Assert.AreEqual("top", wave.Boss.Lane);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BattlefieldCountsTheLaneUnits()
|
||||
{
|
||||
LuneMatch match = Fresh();
|
||||
match.SetReady(Human);
|
||||
match.BeginCombat();
|
||||
for (int step = 0; step < 300; step++)
|
||||
match.StepCombat();
|
||||
var model = new LuneHudModel(match, Human);
|
||||
foreach (string lane in LuneMap.Lanes)
|
||||
{
|
||||
int own = match.Combat.Units.Count(u => u.Alive && u.IsMobile() && u.Zone == "lane" && u.Lane == lane && u.Team == 0);
|
||||
int enemy = match.Combat.Units.Count(u => u.Alive && u.IsMobile() && u.Zone == "lane" && u.Lane == lane && u.Team == 1);
|
||||
LuneHudModel.LaneInfo info = model.Lane(lane);
|
||||
Assert.IsTrue(info.HasCounts);
|
||||
Assert.AreEqual(own, info.OwnCount, lane + " own units");
|
||||
Assert.AreEqual(enemy, info.EnemyCount, lane + " enemy units");
|
||||
Assert.AreEqual(4, info.Towers.Length);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- boss and waves
|
||||
[Test]
|
||||
public void BossStatesFromPublicState()
|
||||
{
|
||||
LuneMatch match = Fresh();
|
||||
Assert.AreEqual("available", new LuneHudModel(match, Human).Boss().State, "round 1");
|
||||
match.RoundNumber = 2;
|
||||
match.Boss.NextAvailableRound = 5;
|
||||
LuneHudModel.BossInfo boss = new LuneHudModel(match, Human).Boss();
|
||||
Assert.AreEqual("cooldown", boss.State);
|
||||
Assert.AreEqual(3, boss.RoundsLeft);
|
||||
match.Boss.Claim = new BossClaim { Team = 0, WonRound = 2, DeployRound = 3, ChooserSlot = 0, Lane = "bot" };
|
||||
boss = new LuneHudModel(match, Human).Boss();
|
||||
Assert.AreEqual("deploy_next", boss.State);
|
||||
Assert.IsTrue(boss.Own);
|
||||
Assert.AreEqual(LuneContent.BossBountyFor(3), boss.Bounty);
|
||||
Assert.IsFalse(new LuneHudModel(match, 4).Boss().Own, "the enemy's view");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NextWaveIsTheRulesWave()
|
||||
{
|
||||
LuneMatch match = Fresh();
|
||||
for (int round = 1; round <= 24; round++)
|
||||
{
|
||||
match.RoundNumber = round;
|
||||
LuneHudModel.WaveInfo wave = new LuneHudModel(match, Human).NextWave();
|
||||
List<UnitDef> rules = LuneWaves.Build(match.Balance, round);
|
||||
Assert.AreEqual(round, wave.Round);
|
||||
Assert.AreEqual(rules.Count, wave.Count);
|
||||
Assert.AreEqual(rules.Count, wave.Monsters.Sum(m => m.Count), "round " + round);
|
||||
List<string> before = round > 1 ? LuneWaves.UnlockedTypes(match.Balance, round - 1) : new List<string>();
|
||||
foreach (LuneHudModel.MonsterGroup group in wave.Monsters)
|
||||
Assert.AreEqual(round > 1 && !before.Contains(group.Id), group.New, "round " + round + " " + group.Id);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ReadyLocksThePlan()
|
||||
{
|
||||
LuneMatch match = Fresh();
|
||||
var model = new LuneHudModel(match, Human);
|
||||
Assert.IsFalse(model.Locked);
|
||||
Assert.IsFalse(model.CanUndo, "nothing to undo yet");
|
||||
Assert.IsTrue(match.Submit(Human, Command.Buy(match.BaseIds(match.Players[Human].Bund)[0])).Ok);
|
||||
Assert.IsTrue(model.CanUndo);
|
||||
match.SetReady(Human);
|
||||
Assert.IsTrue(model.Locked);
|
||||
Assert.IsFalse(model.CanUndo, "undo after Ready");
|
||||
model.ReadyCount(out int ready, out int humans);
|
||||
Assert.AreEqual(1, humans);
|
||||
Assert.AreEqual(1, ready);
|
||||
Assert.AreEqual("self", model.StatusOf(Human));
|
||||
Assert.AreEqual("bot", model.StatusOf(0));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- formats
|
||||
[Test]
|
||||
public void NumbersAreFormattedAsInGodot()
|
||||
{
|
||||
Assert.AreEqual("12.000", LuneUiTheme.Number(12000));
|
||||
Assert.AreEqual("999", LuneUiTheme.Number(999));
|
||||
Assert.AreEqual("-1.500", LuneUiTheme.Number(-1500));
|
||||
Assert.AreEqual("1.234.568", LuneUiTheme.Number(1234567.5));
|
||||
Assert.AreEqual("1,25", LuneUiTheme.Decimal(1.25, 2));
|
||||
Assert.AreEqual("2,0", LuneUiTheme.Decimal(2.0));
|
||||
Assert.AreEqual("Leicht", LuneStats.StrongAgainst(0));
|
||||
Assert.AreEqual("Schwer", LuneStats.WeakAgainst(0));
|
||||
Assert.AreEqual(1, LuneStats.StageOf("g01"));
|
||||
Assert.AreEqual(2, LuneStats.StageOf("w04"));
|
||||
Assert.AreEqual("Grenzwacht", LuneHudModel.BuildingTitle("blau_mid_outer"));
|
||||
Assert.AreEqual("Kernwacht", LuneHudModel.BuildingTitle("rot_base_2"));
|
||||
Assert.AreEqual("MID · Bastion Rot", LuneHudModel.BuildingPlace("rot_mid_inner"));
|
||||
Assert.AreEqual("Basis Blau · Kernwacht 1", LuneHudModel.BuildingPlace("blau_base_1"));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- the human seat of the host loop
|
||||
[Test]
|
||||
public void HumanSeatPlanningWaitsForReady()
|
||||
{
|
||||
var runner = new LuneMatchRunner(new MatchOptions { Seed = 4, HumanSlot = Human });
|
||||
Assert.AreEqual(Human, runner.HumanSlot);
|
||||
for (int frame = 0; frame < 600; frame++)
|
||||
runner.Advance(1.0 / 60.0);
|
||||
Assert.AreEqual(LuneMatchRunner.Stage.Planning, runner.Current, "planning waits for the human");
|
||||
Assert.IsFalse(runner.Match.Players[Human].Ready);
|
||||
Assert.IsEmpty(runner.Match.Players[Human].Draft.Roster, "the bots never plan the human seat");
|
||||
for (int slot = 0; slot < 6; slot++)
|
||||
{
|
||||
if (slot != Human)
|
||||
Assert.IsTrue(runner.Match.Players[slot].Ready, "bot seat " + slot + " ready");
|
||||
}
|
||||
Assert.IsTrue(runner.Ready());
|
||||
Assert.AreEqual(LuneMatchRunner.Stage.Combat, runner.Current, "Ready starts the combat when everyone is ready");
|
||||
int frames = 0;
|
||||
while (runner.Current == LuneMatchRunner.Stage.Combat && frames++ < 100000)
|
||||
runner.Advance(1.0 / 60.0);
|
||||
runner.Advance(1.0 / 60.0);
|
||||
Assert.AreEqual(LuneMatchRunner.Stage.Planning, runner.Current, "planning follows the settlement at once");
|
||||
Assert.IsFalse(runner.Match.Players[Human].Ready, "the human plans the next round again");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BotMatchStillStartsByItself()
|
||||
{
|
||||
var runner = new LuneMatchRunner(new MatchOptions { Seed = 4, HumanSlot = -1 });
|
||||
Assert.AreEqual(-1, runner.HumanSlot);
|
||||
Assert.IsFalse(runner.Ready(), "no human seat");
|
||||
runner.Advance(runner.PlanningSeconds + .01);
|
||||
Assert.AreEqual(LuneMatchRunner.Stage.Combat, runner.Current);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Luneraria/Tests/UI/EditMode/HudModelTests.cs.meta
Normal file
2
Assets/Luneraria/Tests/UI/EditMode/HudModelTests.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8191e33402ba940d79d36bdcb4877cad
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"name": "Luneraria.UI.Tests.EditMode",
|
||||
"rootNamespace": "Luneraria.UI.Tests",
|
||||
"references": [
|
||||
"Luneraria.Core",
|
||||
"Luneraria.View",
|
||||
"Luneraria.UI",
|
||||
"UnityEngine.TestRunner",
|
||||
"UnityEditor.TestRunner"
|
||||
],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": true,
|
||||
"precompiledReferences": [
|
||||
"nunit.framework.dll"
|
||||
],
|
||||
"autoReferenced": false,
|
||||
"defineConstraints": [
|
||||
"UNITY_INCLUDE_TESTS"
|
||||
],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: c0d1b8364f3ec4c339506546b4c36993
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Luneraria/Tests/UI/PlayMode.meta
Normal file
8
Assets/Luneraria/Tests/UI/PlayMode.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 394ebec8a17f64895857fc387b8c62bf
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"name": "Luneraria.UI.Tests.PlayMode",
|
||||
"rootNamespace": "Luneraria.UI.Tests",
|
||||
"references": [
|
||||
"Luneraria.Core",
|
||||
"Luneraria.View",
|
||||
"Luneraria.UI",
|
||||
"UnityEngine.TestRunner"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": true,
|
||||
"precompiledReferences": [
|
||||
"nunit.framework.dll"
|
||||
],
|
||||
"autoReferenced": false,
|
||||
"defineConstraints": [
|
||||
"UNITY_INCLUDE_TESTS"
|
||||
],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: f68d704577bda4e20a2b4f22e92841a2
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
133
Assets/Luneraria/Tests/UI/PlayMode/UiToolkitProbeTests.cs
Normal file
133
Assets/Luneraria/Tests/UI/PlayMode/UiToolkitProbeTests.cs
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
using System.Collections;
|
||||
using System.IO;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TestTools;
|
||||
using UnityEngine.TextCore.Text;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace Luneraria.UI.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// U3A decision probe: a UI Toolkit panel renders into a texture in batch mode, with the plate
|
||||
/// drawing, all glyphs and the three font weights (Regular, Regular with a 0.25 px outline for
|
||||
/// the G3 weight 560, Bold). Writes tools/out/ui-spike.png (docs/port/screenshots/u3/00).
|
||||
/// </summary>
|
||||
public sealed class UiToolkitProbeTests
|
||||
{
|
||||
sealed class Plate : VisualElement
|
||||
{
|
||||
readonly LunePlateStyle _style;
|
||||
public Plate(LunePlateStyle style)
|
||||
{
|
||||
_style = style;
|
||||
style.ApplyPadding(this);
|
||||
generateVisualContent += context => LunePlates.Draw(context, new Rect(0, 0, layout.width, layout.height), _style);
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Glyph : VisualElement
|
||||
{
|
||||
public Glyph(string name, Color color, float edge)
|
||||
{
|
||||
this.style.width = edge;
|
||||
this.style.height = edge;
|
||||
generateVisualContent += context => LuneGlyphs.Draw(context.painter2D, name, new Rect(0, 0, layout.width, layout.height), color);
|
||||
}
|
||||
}
|
||||
|
||||
static Label Text(string value, FontAsset font, float size, Color color, float outline = 0f, float spacing = 0f)
|
||||
{
|
||||
var label = new Label(value);
|
||||
label.style.unityFontDefinition = FontDefinition.FromSDFFont(font);
|
||||
label.style.fontSize = size;
|
||||
label.style.color = color;
|
||||
label.style.letterSpacing = spacing;
|
||||
label.style.marginLeft = 0;
|
||||
label.style.paddingLeft = 0;
|
||||
if (outline > 0f)
|
||||
{
|
||||
label.style.unityTextOutlineWidth = outline;
|
||||
label.style.unityTextOutlineColor = color;
|
||||
}
|
||||
label.style.textShadow = new TextShadow { offset = new Vector2(0f, 1f), blurRadius = 1f, color = new Color(0f, 0f, 0f, .55f) };
|
||||
return label;
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator PanelRendersIntoTexture()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
var assets = UnityEditor.AssetDatabase.LoadAssetAtPath<LuneUiAssets>("Assets/Luneraria/Settings/LuneUi.asset");
|
||||
#else
|
||||
LuneUiAssets assets = null;
|
||||
#endif
|
||||
Assert.IsNotNull(assets);
|
||||
var target = new RenderTexture(1920, 1080, 0, RenderTextureFormat.ARGB32);
|
||||
var settings = ScriptableObject.CreateInstance<PanelSettings>();
|
||||
settings.themeStyleSheet = assets.Theme;
|
||||
settings.targetTexture = target;
|
||||
settings.scaleMode = PanelScaleMode.ScaleWithScreenSize;
|
||||
settings.referenceResolution = new Vector2Int(1600, 960);
|
||||
settings.screenMatchMode = PanelScreenMatchMode.Expand;
|
||||
settings.clearColor = true;
|
||||
settings.colorClearValue = new Color(.28f, .36f, .22f, 1f);
|
||||
var host = new GameObject("UiSpike");
|
||||
var document = host.AddComponent<UIDocument>();
|
||||
document.panelSettings = settings;
|
||||
yield return null;
|
||||
VisualElement root = document.rootVisualElement;
|
||||
var panel = new Plate(LunePlateStyle.Hud(LuneUiTheme.SlateTop, LuneUiTheme.SlateBottom, LuneUiTheme.Edge, 1f, 6f, 12f, 10f));
|
||||
panel.style.position = Position.Absolute;
|
||||
panel.style.left = 12;
|
||||
panel.style.top = 12;
|
||||
panel.style.width = 760;
|
||||
root.Add(panel);
|
||||
panel.Add(Text("WELLE 21", assets.Display, 30, LuneUiTheme.Hex("f4e6c4"), 0f, 3f));
|
||||
panel.Add(Text("DEIN ANCIENT · BLAU", assets.TextBold, 14, LuneUiTheme.BronzeLight, 0f, 2f));
|
||||
panel.Add(Text("Regular 400: Einheiten mit „Grotte“ entsenden. 12.000 / 12.000", assets.Text, 15, LuneUiTheme.Text));
|
||||
panel.Add(Text("Regular + 0,25 Kontur: Einheiten mit „Grotte“ entsenden. 12.000 / 12.000", assets.Text, 15, LuneUiTheme.Text, .25f));
|
||||
panel.Add(Text("Regular + 0,45 Kontur: Einheiten mit „Grotte“ entsenden. 12.000 / 12.000", assets.Text, 15, LuneUiTheme.Text, .45f));
|
||||
panel.Add(Text("Bold 700: Einheiten mit „Grotte“ entsenden. 12.000 / 12.000", assets.TextBold, 15, LuneUiTheme.Text));
|
||||
panel.Add(Text("235", assets.TextBold, 24, LuneUiTheme.Gold));
|
||||
var glyphs = new VisualElement();
|
||||
glyphs.style.flexDirection = FlexDirection.Row;
|
||||
glyphs.style.flexWrap = Wrap.Wrap;
|
||||
glyphs.style.marginTop = 8;
|
||||
panel.Add(glyphs);
|
||||
foreach (string name in LuneGlyphs.Names)
|
||||
{
|
||||
var glyph = new Glyph(name, name.StartsWith("emblem_1") || name == "shape_1" ? LuneUiTheme.Team[1] : name.StartsWith("emblem_0") || name == "shape_0" ? LuneUiTheme.Team[0] : LuneUiTheme.Text, 28f);
|
||||
glyph.style.marginRight = 6;
|
||||
glyph.style.marginBottom = 6;
|
||||
glyphs.Add(glyph);
|
||||
}
|
||||
var buttons = new VisualElement();
|
||||
buttons.style.flexDirection = FlexDirection.Row;
|
||||
panel.Add(buttons);
|
||||
var primary = new Plate(LunePlateStyle.Menu(LuneUiTheme.Hex("e8c47e"), LuneUiTheme.Hex("b98a4a"), LuneUiTheme.Hex("fff0cc"), 1.5f, 9f, 30f, 15f));
|
||||
primary.Add(Text("BEREIT", assets.Display, 22, LuneUiTheme.Ink, 0f, 2f));
|
||||
buttons.Add(primary);
|
||||
var card = new Plate(LunePlateStyle.Hud(LuneUiTheme.Hex("19222c"), LuneUiTheme.Hex("10161d"), LuneUiTheme.Hex("34424f"), 1f, 7f, 6f, 6f));
|
||||
card.style.width = 116;
|
||||
card.style.height = 132;
|
||||
card.style.marginLeft = 10;
|
||||
buttons.Add(card);
|
||||
for (int frame = 0; frame < 6; frame++)
|
||||
yield return null;
|
||||
RenderTexture.active = target;
|
||||
var image = new Texture2D(1920, 1080, TextureFormat.RGBA32, false);
|
||||
image.ReadPixels(new Rect(0, 0, 1920, 1080), 0, 0);
|
||||
image.Apply();
|
||||
RenderTexture.active = null;
|
||||
string file = Path.Combine(Path.GetDirectoryName(Application.dataPath), "tools/out/ui-spike.png");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(file));
|
||||
File.WriteAllBytes(file, image.EncodeToPNG());
|
||||
Color corner = image.GetPixel(1900, 20);
|
||||
Color panelPixel = image.GetPixel(100, 1080 - 100);
|
||||
Debug.Log("LUNE_SPIKE corner " + corner + " panel " + panelPixel + " layout " + panel.layout);
|
||||
Object.Destroy(host);
|
||||
Assert.Greater(panel.layout.height, 50f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 2414c96b20eed4bd4b2ec981899351d1
|
||||
8
Assets/Luneraria/UI.meta
Normal file
8
Assets/Luneraria/UI.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8274853ff66f74957a3faf62bbcdc61f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Luneraria/UI/Editor.meta
Normal file
8
Assets/Luneraria/UI/Editor.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 87a5adcffce054fd5899eeae7e84e74e
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
92
Assets/Luneraria/UI/Editor/LuneUiAssetsBuilder.cs
Normal file
92
Assets/Luneraria/UI/Editor/LuneUiAssetsBuilder.cs
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Luneraria.UI;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TextCore.LowLevel;
|
||||
using UnityEngine.TextCore.Text;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace Luneraria.EditorTools
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds Settings/LuneUi.asset (U3): static SDF font atlases from the variable fonts of the
|
||||
/// reference (assets/fonts, OFL). Unity's text engine has no weight axis; FreeType loads the
|
||||
/// named instances through the face index (named instance << 16): Cantarell Regular (3) and
|
||||
/// Bold (4), Cinzel Bold (2). The atlases hold every character the HUD texts use, so the player
|
||||
/// never needs the font files.
|
||||
/// Unity -batchmode -projectPath . -executeMethod Luneraria.EditorTools.LuneUiAssetsBuilder.Build -logFile -
|
||||
/// </summary>
|
||||
public static class LuneUiAssetsBuilder
|
||||
{
|
||||
public const string AssetPath = "Assets/Luneraria/Settings/LuneUi.asset";
|
||||
const string Fonts = "Assets/Luneraria/UI/Fonts/";
|
||||
|
||||
public static void Build()
|
||||
{
|
||||
var assets = AssetDatabase.LoadAssetAtPath<LuneUiAssets>(AssetPath);
|
||||
if (assets == null)
|
||||
{
|
||||
assets = ScriptableObject.CreateInstance<LuneUiAssets>();
|
||||
AssetDatabase.CreateAsset(assets, AssetPath);
|
||||
}
|
||||
assets.Text = Font("Cantarell-VF.otf", 3, "Cantarell-Regular");
|
||||
assets.TextBold = Font("Cantarell-VF.otf", 4, "Cantarell-Bold");
|
||||
assets.Display = Font("Cinzel-VF.ttf", 2, "Cinzel-Bold");
|
||||
assets.Theme = AssetDatabase.LoadAssetAtPath<ThemeStyleSheet>("Assets/Luneraria/UI/LuneRuntimeTheme.tss");
|
||||
EditorUtility.SetDirty(assets);
|
||||
AssetDatabase.SaveAssets();
|
||||
Debug.Log("LUNE_UI built " + AssetPath);
|
||||
if (Application.isBatchMode)
|
||||
EditorApplication.Exit(0);
|
||||
}
|
||||
|
||||
/// <summary>Printable ASCII, Latin-1 and the punctuation and symbols of the HUD texts.</summary>
|
||||
public static string Characters()
|
||||
{
|
||||
var text = new StringBuilder();
|
||||
for (char c = (char)32; c < 127; c++)
|
||||
text.Append(c);
|
||||
for (char c = (char)160; c <= 255; c++)
|
||||
text.Append(c);
|
||||
text.Append("–—‘’‚“”„•…‰‹›€→←↑↓−×★≈≤≥∞·ŒœŠšŽžŸ");
|
||||
return new string(text.ToString().Distinct().ToArray());
|
||||
}
|
||||
|
||||
static FontAsset Font(string file, int namedInstance, string name)
|
||||
{
|
||||
string path = Fonts + name + ".asset";
|
||||
if (AssetDatabase.LoadAssetAtPath<FontAsset>(path) != null)
|
||||
AssetDatabase.DeleteAsset(path);
|
||||
string source = Path.GetFullPath(Fonts + file);
|
||||
FontAsset font = FontAsset.CreateFontAsset(source, namedInstance << 16, 64, 8, GlyphRenderMode.SDFAA, 1024, 1024);
|
||||
if (font == null)
|
||||
throw new System.InvalidOperationException("Font asset failed: " + file + " instance " + namedInstance);
|
||||
font.name = name;
|
||||
AssetDatabase.CreateAsset(font, path);
|
||||
if (!font.TryAddCharacters(Characters(), out string missing))
|
||||
Debug.LogWarning("LUNE_UI " + name + " misses: " + missing);
|
||||
for (int index = 0; index < font.atlasTextures.Length; index++)
|
||||
{
|
||||
Texture2D atlas = font.atlasTextures[index];
|
||||
if (atlas == null)
|
||||
continue;
|
||||
atlas.name = name + " Atlas " + index;
|
||||
AssetDatabase.AddObjectToAsset(atlas, font);
|
||||
}
|
||||
font.material.name = name + " Material";
|
||||
AssetDatabase.AddObjectToAsset(font.material, font);
|
||||
font.atlasPopulationMode = AtlasPopulationMode.Static;
|
||||
// A static atlas needs no source at runtime: keep no machine path and never clear it in builds.
|
||||
var serialized = new SerializedObject(font);
|
||||
serialized.FindProperty("m_SourceFontFilePath").stringValue = Fonts + file;
|
||||
serialized.FindProperty("m_ClearDynamicDataOnBuild").boolValue = false;
|
||||
serialized.ApplyModifiedPropertiesWithoutUndo();
|
||||
EditorUtility.SetDirty(font);
|
||||
AssetDatabase.SaveAssets();
|
||||
Debug.Log(string.Format("LUNE_UI font {0}: {1} / {2}, {3} glyphs, {4} atlas(es)", name, font.faceInfo.familyName, font.faceInfo.styleName, font.glyphTable.Count, font.atlasTextures.Length));
|
||||
return font;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Luneraria/UI/Editor/LuneUiAssetsBuilder.cs.meta
Normal file
2
Assets/Luneraria/UI/Editor/LuneUiAssetsBuilder.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: f929a7592cc0947bc89a95e9f73b5416
|
||||
18
Assets/Luneraria/UI/Editor/Luneraria.UI.Editor.asmdef
Normal file
18
Assets/Luneraria/UI/Editor/Luneraria.UI.Editor.asmdef
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "Luneraria.UI.Editor",
|
||||
"rootNamespace": "Luneraria.EditorTools",
|
||||
"references": [
|
||||
"Luneraria.Core",
|
||||
"Luneraria.View",
|
||||
"Luneraria.UI"
|
||||
],
|
||||
"includePlatforms": ["Editor"],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 554f57ef9b1504ba296369a7d999eaa9
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Luneraria/UI/Fonts.meta
Normal file
8
Assets/Luneraria/UI/Fonts.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: dd70591b054624795bd726102254f947
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
6007
Assets/Luneraria/UI/Fonts/Cantarell-Bold.asset
Normal file
6007
Assets/Luneraria/UI/Fonts/Cantarell-Bold.asset
Normal file
File diff suppressed because one or more lines are too long
8
Assets/Luneraria/UI/Fonts/Cantarell-Bold.asset.meta
Normal file
8
Assets/Luneraria/UI/Fonts/Cantarell-Bold.asset.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: fe04d22bb582e4cd99959615019c6244
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
6027
Assets/Luneraria/UI/Fonts/Cantarell-Regular.asset
Normal file
6027
Assets/Luneraria/UI/Fonts/Cantarell-Regular.asset
Normal file
File diff suppressed because one or more lines are too long
8
Assets/Luneraria/UI/Fonts/Cantarell-Regular.asset.meta
Normal file
8
Assets/Luneraria/UI/Fonts/Cantarell-Regular.asset.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 9ed8c741ab987452c8ecd52e5252d418
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Assets/Luneraria/UI/Fonts/Cantarell-VF.otf
Normal file
BIN
Assets/Luneraria/UI/Fonts/Cantarell-VF.otf
Normal file
Binary file not shown.
21
Assets/Luneraria/UI/Fonts/Cantarell-VF.otf.meta
Normal file
21
Assets/Luneraria/UI/Fonts/Cantarell-VF.otf.meta
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 4c6a010ea5ef044029a2777643a87712
|
||||
TrueTypeFontImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 4
|
||||
fontSize: 16
|
||||
forceTextureCase: -2
|
||||
characterSpacing: 0
|
||||
characterPadding: 1
|
||||
includeFontData: 1
|
||||
fontNames:
|
||||
- Cantarell
|
||||
fallbackFontReferences: []
|
||||
customCharacters:
|
||||
fontRenderingMode: 0
|
||||
ascentCalculationMode: 1
|
||||
useLegacyBoundsCalculation: 0
|
||||
shouldRoundAdvanceValue: 1
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
5890
Assets/Luneraria/UI/Fonts/Cinzel-Bold.asset
Normal file
5890
Assets/Luneraria/UI/Fonts/Cinzel-Bold.asset
Normal file
File diff suppressed because one or more lines are too long
8
Assets/Luneraria/UI/Fonts/Cinzel-Bold.asset.meta
Normal file
8
Assets/Luneraria/UI/Fonts/Cinzel-Bold.asset.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 84c5b4f661d34482e8f3a990d043245e
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Assets/Luneraria/UI/Fonts/Cinzel-VF.ttf
Normal file
BIN
Assets/Luneraria/UI/Fonts/Cinzel-VF.ttf
Normal file
Binary file not shown.
21
Assets/Luneraria/UI/Fonts/Cinzel-VF.ttf.meta
Normal file
21
Assets/Luneraria/UI/Fonts/Cinzel-VF.ttf.meta
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 1f3e8bb8eba104f40995e4dd72febe2c
|
||||
TrueTypeFontImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 4
|
||||
fontSize: 16
|
||||
forceTextureCase: -2
|
||||
characterSpacing: 0
|
||||
characterPadding: 1
|
||||
includeFontData: 1
|
||||
fontNames:
|
||||
- Cinzel
|
||||
fallbackFontReferences: []
|
||||
customCharacters:
|
||||
fontRenderingMode: 0
|
||||
ascentCalculationMode: 1
|
||||
useLegacyBoundsCalculation: 0
|
||||
shouldRoundAdvanceValue: 1
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
111
Assets/Luneraria/UI/Fonts/OFL-Cantarell.txt
Normal file
111
Assets/Luneraria/UI/Fonts/OFL-Cantarell.txt
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
Copyright (c) 2009--2019, The Cantarell Authors
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
Scripts contain parts of the source code of glyphsLib:
|
||||
|
||||
# Copyright 2015 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
7
Assets/Luneraria/UI/Fonts/OFL-Cantarell.txt.meta
Normal file
7
Assets/Luneraria/UI/Fonts/OFL-Cantarell.txt.meta
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: a09e621236d0a4dfe8983c87f70b74ad
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
93
Assets/Luneraria/UI/Fonts/OFL-Cinzel.txt
Normal file
93
Assets/Luneraria/UI/Fonts/OFL-Cinzel.txt
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
Copyright 2020 The Cinzel Project Authors (https://github.com/NDISCOVER/Cinzel)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
7
Assets/Luneraria/UI/Fonts/OFL-Cinzel.txt.meta
Normal file
7
Assets/Luneraria/UI/Fonts/OFL-Cinzel.txt.meta
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 85500d806221d439abe4e6c35dd58afe
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
1
Assets/Luneraria/UI/LuneRuntimeTheme.tss
Normal file
1
Assets/Luneraria/UI/LuneRuntimeTheme.tss
Normal file
|
|
@ -0,0 +1 @@
|
|||
@import url("unity-theme://default");
|
||||
12
Assets/Luneraria/UI/LuneRuntimeTheme.tss.meta
Normal file
12
Assets/Luneraria/UI/LuneRuntimeTheme.tss.meta
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 38a51cb4adf3e41879e4510c6a0d7ef6
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 12388, guid: 0000000000000000e000000000000000, type: 0}
|
||||
disableValidation: 0
|
||||
unsupportedSelectorAction: 0
|
||||
19
Assets/Luneraria/UI/LuneUiAssets.cs
Normal file
19
Assets/Luneraria/UI/LuneUiAssets.cs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
using UnityEngine;
|
||||
using UnityEngine.TextCore.Text;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace Luneraria.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Fonts and panel theme of the HUD (Settings/LuneUi.asset). The fonts are static SDF atlases
|
||||
/// built from named instances of the G3 variable fonts (LuneUiAssetsBuilder): Cantarell
|
||||
/// Regular and Bold for HUD text, Cinzel Bold for titles.
|
||||
/// </summary>
|
||||
public sealed class LuneUiAssets : ScriptableObject
|
||||
{
|
||||
public FontAsset Text;
|
||||
public FontAsset TextBold;
|
||||
public FontAsset Display;
|
||||
public ThemeStyleSheet Theme;
|
||||
}
|
||||
}
|
||||
2
Assets/Luneraria/UI/LuneUiAssets.cs.meta
Normal file
2
Assets/Luneraria/UI/LuneUiAssets.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: f80a232d1902841a6b0949361a252976
|
||||
17
Assets/Luneraria/UI/Luneraria.UI.asmdef
Normal file
17
Assets/Luneraria/UI/Luneraria.UI.asmdef
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "Luneraria.UI",
|
||||
"rootNamespace": "Luneraria.UI",
|
||||
"references": [
|
||||
"Luneraria.Core",
|
||||
"Luneraria.View"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
7
Assets/Luneraria/UI/Luneraria.UI.asmdef.meta
Normal file
7
Assets/Luneraria/UI/Luneraria.UI.asmdef.meta
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 53c6ebebe49a9417aa18995e9480970b
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Luneraria/UI/Model.meta
Normal file
8
Assets/Luneraria/UI/Model.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: c623eddd7d5194c9db6d6890e87a22d5
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
567
Assets/Luneraria/UI/Model/LuneHudModel.cs
Normal file
567
Assets/Luneraria/UI/Model/LuneHudModel.cs
Normal file
|
|
@ -0,0 +1,567 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using Luneraria.Core;
|
||||
|
||||
namespace Luneraria.UI
|
||||
{
|
||||
/// <summary>Network state the HUD shows (role, connection); implemented by the session in U4.</summary>
|
||||
public interface ILuneHudSession
|
||||
{
|
||||
bool IsNetworked { get; }
|
||||
bool IsAuthority { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// What one seat may see, prepared for display (port of scripts/luneraria/ui/lune_hud_model.gd).
|
||||
/// Every HUD element reads through this model, so the privacy rules live in one place:
|
||||
/// - gold, draft, undo and uncommitted bookings exist only for the viewer's own seat;
|
||||
/// - of other players the model exposes names, lanes, readiness, human/bot/offline and the
|
||||
/// committed hero (public since the last combat), never their gold or current plans;
|
||||
/// - lanes, buildings, the boss and the waves come from public state only.
|
||||
/// The model never changes the match; commands still go through the game controller.
|
||||
/// </summary>
|
||||
public sealed class LuneHudModel
|
||||
{
|
||||
public static readonly Dictionary<string, string> TowerNames = new Dictionary<string, string>
|
||||
{ { "outer", "Grenzwacht" }, { "inner", "Bastion" }, { "base", "Kernwacht" }, { "core", "Ancient" } };
|
||||
public static readonly Dictionary<string, string> TierText = new Dictionary<string, string>
|
||||
{ { "outer", "Außenturm" }, { "inner", "Innenturm" }, { "base", "Basisturm" }, { "core", "Ancient" } };
|
||||
|
||||
public readonly LuneMatch State;
|
||||
public readonly int Slot;
|
||||
/// <summary>Optional network session; only read for the display (role, connection).</summary>
|
||||
public ILuneHudSession Session;
|
||||
readonly HashSet<int> _wasHuman = new HashSet<int>();
|
||||
|
||||
public LuneHudModel(LuneMatch state, int viewer, ILuneHudSession session = null)
|
||||
{
|
||||
State = state;
|
||||
Slot = System.Math.Max(0, viewer);
|
||||
Session = session;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- phase and own seat
|
||||
public bool Planning { get { return State.Phase == Phase.Planning; } }
|
||||
public bool Fighting { get { return State.Phase == Phase.Combat && State.Combat != null; } }
|
||||
public bool Over { get { return State.Phase == Phase.Over; } }
|
||||
public Player Me { get { return State.Players[Slot]; } }
|
||||
public int Team { get { return Me.Team; } }
|
||||
public int Enemy { get { return 1 - Team; } }
|
||||
public int OwnGold { get { return State.GoldOf(Slot); } }
|
||||
/// <summary>Base income paid at the end of the current round's combat (rules: LuneBalance.RoundIncome).</summary>
|
||||
public int Income { get { return State.Balance.RoundIncome(State.RoundNumber); } }
|
||||
/// <summary>After Ready the plan is binding (the host refuses later commands).</summary>
|
||||
public bool Locked { get { return Planning && Me.Ready; } }
|
||||
public List<RosterEntry> OwnRoster { get { return Planning ? Me.Draft.Roster : Me.Roster; } }
|
||||
/// <summary>The own hero: the draft in planning, the committed hero otherwise; null without a hero.</summary>
|
||||
public HeroState OwnHero { get { return Planning ? Me.Draft.Hero : Me.Hero; } }
|
||||
public bool CanUndo { get { return Planning && !Locked && Me.Undo.Count > 0; } }
|
||||
public int SupplyCap { get { return State.Balance.SupplyCap; } }
|
||||
public bool Networked { get { return Session != null && Session.IsNetworked; } }
|
||||
public bool IsGuest { get { return Networked && !Session.IsAuthority; } }
|
||||
|
||||
public sealed class BookingView
|
||||
{
|
||||
public string Uid = "";
|
||||
public string ChampionId = "";
|
||||
public string Lane = "";
|
||||
public int DeployRound;
|
||||
public int Cost;
|
||||
/// <summary>Booked in this planning (still refundable).</summary>
|
||||
public bool Fresh;
|
||||
|
||||
public override string ToString() { return Uid + ":" + ChampionId + ":" + Lane + ":" + DeployRound + ":" + Fresh; }
|
||||
}
|
||||
|
||||
public List<BookingView> OwnBookings()
|
||||
{
|
||||
var result = new List<BookingView>();
|
||||
if (Planning)
|
||||
{
|
||||
foreach (Booking booking in Me.Draft.NewBookings)
|
||||
result.Add(new BookingView { Uid = booking.Uid, ChampionId = booking.ChampionId, Lane = booking.Lane, Cost = booking.Cost, DeployRound = State.RoundNumber + 1, Fresh = true });
|
||||
}
|
||||
foreach (Booking booking in Me.Bookings)
|
||||
{
|
||||
if (booking.DeployRound >= State.RoundNumber)
|
||||
result.Add(new BookingView { Uid = booking.Uid, ChampionId = booking.ChampionId, Lane = booking.Lane, Cost = booking.Cost, DeployRound = booking.DeployRound });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- units and heroes
|
||||
public string RoleOf(string typeId) { return State.RoleOf(typeId); }
|
||||
|
||||
public sealed class TroopInfo
|
||||
{
|
||||
public UnitDef Def;
|
||||
public string Role = "";
|
||||
public int Stage;
|
||||
}
|
||||
|
||||
/// <summary>Catalogue definition of a troop with its role and stage.</summary>
|
||||
public TroopInfo Troop(string typeId)
|
||||
{
|
||||
return new TroopInfo { Def = DataCatalog.GetUnit(typeId), Role = RoleOf(typeId), Stage = LuneStats.StageOf(typeId) };
|
||||
}
|
||||
|
||||
/// <summary>Live combat unit for a persistent uid of the viewer (or null).</summary>
|
||||
public LuneUnit LiveUnit(string uid)
|
||||
{
|
||||
if (!Fighting || string.IsNullOrEmpty(uid))
|
||||
return null;
|
||||
foreach (LuneUnit unit in State.Combat.Units)
|
||||
{
|
||||
if (unit.PersistentUid == uid)
|
||||
return unit;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public UnitDef HeroDefinition(HeroState hero)
|
||||
{
|
||||
UnitDef definition = LuneContent.HeroDefinition(hero);
|
||||
definition.Role = ContentData.HeroRoles.GetValueOrDefault(hero.HeroId, "front");
|
||||
return definition;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- buildings
|
||||
public sealed class BuildingInfo
|
||||
{
|
||||
public string Id = "";
|
||||
public int Team;
|
||||
public string Tier = "outer";
|
||||
public string Lane = "";
|
||||
public bool Alive;
|
||||
public double Hp;
|
||||
public double MaxHp = 1.0;
|
||||
public double Fraction;
|
||||
public bool Attackable;
|
||||
public BuildingInfo[] Base;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Id + ":" + Alive + ":" + LuneUiTheme.Round(Hp) + ":" + Attackable;
|
||||
}
|
||||
}
|
||||
|
||||
public BuildingInfo Building(string id)
|
||||
{
|
||||
BuildingDef definition = State.Map.BuildingDef(id);
|
||||
double maxHp = definition != null ? definition.MaxHp : 1.0;
|
||||
double hp = State.BuildingHp.GetValueOrDefault(id, 0.0);
|
||||
bool alive = hp > 0.0;
|
||||
if (Fighting && State.Combat.Buildings.TryGetValue(id, out LuneUnit unit))
|
||||
{
|
||||
alive = unit.Alive;
|
||||
hp = alive ? unit.Hp : 0.0;
|
||||
}
|
||||
return new BuildingInfo
|
||||
{
|
||||
Id = id, Team = definition != null ? definition.Team : 0, Tier = definition != null ? definition.Tier : "outer", Lane = definition != null ? definition.Lane : "",
|
||||
Alive = alive, Hp = hp, MaxHp = maxHp, Fraction = GdMath.Clamp(hp / GdMath.Max(1.0, maxHp), 0.0, 1.0),
|
||||
Attackable = alive && definition != null && LuneMap.IsAttackable(definition, AliveMap()),
|
||||
};
|
||||
}
|
||||
|
||||
Dictionary<string, bool> AliveMap()
|
||||
{
|
||||
if (Fighting)
|
||||
return State.Combat.AliveMap();
|
||||
var result = new Dictionary<string, bool>();
|
||||
foreach (KeyValuePair<string, double> pair in State.BuildingHp)
|
||||
result[pair.Key] = pair.Value > 0.0;
|
||||
return result;
|
||||
}
|
||||
|
||||
public static string BuildingTitle(string id)
|
||||
{
|
||||
string[] parts = id.Split('_');
|
||||
if (parts[1] == "ancient")
|
||||
return "Ancient";
|
||||
if (parts[1] == "base")
|
||||
return "Kernwacht";
|
||||
return TowerNames.TryGetValue(parts[2], out string name) ? name : id;
|
||||
}
|
||||
|
||||
public static string BuildingPlace(string id)
|
||||
{
|
||||
string[] parts = id.Split('_');
|
||||
string team = parts[0] == "blau" ? "Blau" : "Rot";
|
||||
if (parts[1] == "ancient")
|
||||
return "Ancient " + team;
|
||||
if (parts[1] == "base")
|
||||
return string.Format("Basis {0} · Kernwacht {1}", team, parts[2]);
|
||||
return string.Format("{0} · {1} {2}", LuneMap.LaneNames[parts[1]].ToUpperInvariant(), BuildingTitle(id), team);
|
||||
}
|
||||
|
||||
/// <summary>A team's Ancient with its two base towers (Kernwacht) in Base.</summary>
|
||||
public BuildingInfo Ancient(int team)
|
||||
{
|
||||
string prefix = team == 0 ? "blau" : "rot";
|
||||
BuildingInfo core = Building(prefix + "_ancient");
|
||||
core.Base = new[] { Building(prefix + "_base_1"), Building(prefix + "_base_2") };
|
||||
return core;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- players
|
||||
public string PlayerName(int seat)
|
||||
{
|
||||
return seat == Slot ? "Du" : State.Players[seat].Name;
|
||||
}
|
||||
|
||||
/// <summary>Seat status for display: self, human, bot, or offline (was human, a bot plays the seat).</summary>
|
||||
public string StatusOf(int seat)
|
||||
{
|
||||
Player player = State.Players[seat];
|
||||
if (player.IsHuman)
|
||||
_wasHuman.Add(seat);
|
||||
if (seat == Slot)
|
||||
return "self";
|
||||
if (player.IsHuman)
|
||||
return "human";
|
||||
return _wasHuman.Contains(seat) ? "offline" : "bot";
|
||||
}
|
||||
|
||||
public sealed class MemberInfo
|
||||
{
|
||||
public int Slot;
|
||||
public string Name = "";
|
||||
public string Lane = "";
|
||||
public bool IsSelf;
|
||||
public bool Ready;
|
||||
public string Status = "";
|
||||
public string Bund = "";
|
||||
public string HeroId = "";
|
||||
public int HeroLevel;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Slot + ":" + Name + ":" + Lane + ":" + IsSelf + ":" + Ready + ":" + Status + ":" + Bund + ":" + HeroId + ":" + HeroLevel;
|
||||
}
|
||||
}
|
||||
|
||||
public List<MemberInfo> Members(int team)
|
||||
{
|
||||
var result = new List<MemberInfo>();
|
||||
for (int seat = team * 3; seat < team * 3 + 3; seat++)
|
||||
{
|
||||
Player player = State.Players[seat];
|
||||
// Own seat: current draft. Everyone else: the committed hero, public since the last combat.
|
||||
HeroState hero = seat == Slot ? OwnHero : player.Hero;
|
||||
result.Add(new MemberInfo
|
||||
{
|
||||
Slot = seat, Name = PlayerName(seat), Lane = player.Lane, IsSelf = seat == Slot, Ready = player.Ready, Status = StatusOf(seat), Bund = player.Bund,
|
||||
HeroId = hero != null ? hero.HeroId : "", HeroLevel = hero != null ? LuneContent.LevelFor(hero.Xp) : 0,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>Ready humans and humans in the match.</summary>
|
||||
public void ReadyCount(out int ready, out int humans)
|
||||
{
|
||||
ready = 0;
|
||||
humans = 0;
|
||||
foreach (Player player in State.Players)
|
||||
{
|
||||
if (!player.IsHuman)
|
||||
continue;
|
||||
humans++;
|
||||
if (player.Ready)
|
||||
ready++;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- lanes
|
||||
public sealed class HeroBrief
|
||||
{
|
||||
public string Model = "";
|
||||
public string Name = "";
|
||||
public int Level;
|
||||
|
||||
public override string ToString() { return Model + ":" + Level; }
|
||||
}
|
||||
|
||||
public sealed class LaneInfo
|
||||
{
|
||||
public string Lane = "";
|
||||
public string Title = "";
|
||||
public string OwnPlayer = "";
|
||||
public string EnemyPlayer = "";
|
||||
public bool OwnIsMe;
|
||||
/// <summary>Own inner, own outer, enemy outer, enemy inner: the chain from the own base to the enemy's.</summary>
|
||||
public BuildingInfo[] Towers;
|
||||
public LuneLaneThreat.Result Threat;
|
||||
public readonly List<Announcement> Dangers = new List<Announcement>();
|
||||
public HeroBrief OwnHero;
|
||||
public HeroBrief EnemyHero;
|
||||
/// <summary>Unit counts exist only in combat.</summary>
|
||||
public bool HasCounts;
|
||||
public int OwnCount;
|
||||
public int EnemyCount;
|
||||
}
|
||||
|
||||
public LaneInfo Lane(string lane)
|
||||
{
|
||||
int ownSeat = State.SlotOf(Team, lane);
|
||||
int enemySeat = State.SlotOf(Enemy, lane);
|
||||
string own = Team == 0 ? "blau" : "rot";
|
||||
string enemy = Team == 0 ? "rot" : "blau";
|
||||
var result = new LaneInfo
|
||||
{
|
||||
Lane = lane, Title = LuneMap.LaneNames[lane].ToUpperInvariant(), OwnPlayer = PlayerName(ownSeat), EnemyPlayer = PlayerName(enemySeat), OwnIsMe = ownSeat == Slot,
|
||||
Towers = new[] { Building(own + "_" + lane + "_inner"), Building(own + "_" + lane + "_outer"), Building(enemy + "_" + lane + "_outer"), Building(enemy + "_" + lane + "_inner") },
|
||||
Threat = LuneLaneThreat.Evaluate(State, Team, lane), OwnHero = HeroOf(ownSeat), EnemyHero = HeroOf(enemySeat),
|
||||
};
|
||||
foreach (Announcement entry in State.Announcements())
|
||||
{
|
||||
if (entry.Lane == lane)
|
||||
result.Dangers.Add(entry);
|
||||
}
|
||||
if (Fighting)
|
||||
{
|
||||
LuneLaneThreat.Battle battle = LuneLaneThreat.Battlefield(State, Team, lane);
|
||||
result.HasCounts = true;
|
||||
result.OwnCount = battle.OwnCount;
|
||||
result.EnemyCount = battle.EnemyCount;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>A seat's hero for display: the own draft for the viewer, the committed (public) hero for everyone else; null without a hero.</summary>
|
||||
public HeroBrief HeroOf(int seat)
|
||||
{
|
||||
HeroState hero = seat == Slot ? OwnHero : State.Players[seat].Hero;
|
||||
if (hero == null)
|
||||
return null;
|
||||
HeroSpec spec = LuneContent.Heroes[hero.HeroId];
|
||||
return new HeroBrief { Model = spec.Model, Name = spec.Name, Level = LuneContent.LevelFor(hero.Xp) };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- boss grotto
|
||||
public sealed class BossInfo
|
||||
{
|
||||
/// <summary>available, contested, conquered, failed, deploy_next, deploying, defeated or cooldown.</summary>
|
||||
public string State = "cooldown";
|
||||
public int Team = -1;
|
||||
public string Lane = "";
|
||||
public int DeployRound;
|
||||
public int WonRound;
|
||||
public int Bounty;
|
||||
public int RoundsLeft;
|
||||
public bool Own;
|
||||
public bool Chooser;
|
||||
public int NextRound;
|
||||
public bool HasHp;
|
||||
public double Hp;
|
||||
public double MaxHp = 1.0;
|
||||
|
||||
/// <summary>The public part (without the viewer's own/chooser flags).</summary>
|
||||
public string PublicText()
|
||||
{
|
||||
return string.Format(CultureInfo.InvariantCulture, "{0}:{1}:{2}:{3}:{4}:{5}:{6}:{7}:{8}", State, Team, Lane, DeployRound, WonRound, Bounty, RoundsLeft, NextRound,
|
||||
HasHp ? LuneUiTheme.Round(Hp) + "/" + LuneUiTheme.Round(MaxHp) : "-");
|
||||
}
|
||||
|
||||
public override string ToString() { return PublicText() + ":" + Own + ":" + Chooser; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Public boss state: available, contested, conquered, failed, deploy_next, deploying,
|
||||
/// defeated, cooldown. On a guest the grotto fight is read from the mirrored units. The
|
||||
/// chooser sees the lane of the own draft; everyone else the public claim lane.
|
||||
/// </summary>
|
||||
public BossInfo Boss()
|
||||
{
|
||||
return Boss(false);
|
||||
}
|
||||
|
||||
BossInfo Boss(bool publicOnly)
|
||||
{
|
||||
BossClaim claim = State.Boss.Claim;
|
||||
int round = State.RoundNumber;
|
||||
var result = new BossInfo { RoundsLeft = System.Math.Max(0, State.Boss.NextAvailableRound - round), NextRound = State.Boss.NextAvailableRound };
|
||||
if (Fighting)
|
||||
{
|
||||
foreach (LuneUnit unit in State.Combat.Units)
|
||||
{
|
||||
if (unit.Kind != "boss")
|
||||
continue;
|
||||
if (unit.Zone == "grotto")
|
||||
{
|
||||
if (unit.Alive)
|
||||
{
|
||||
result.State = "contested";
|
||||
return result;
|
||||
}
|
||||
int winner = -1;
|
||||
foreach (LuneUnit other in State.Combat.Units)
|
||||
{
|
||||
if (other.Alive && other.Zone == "grotto" && other.Team <= 1)
|
||||
winner = other.Team;
|
||||
}
|
||||
result.State = winner >= 0 ? "conquered" : "failed";
|
||||
result.Team = winner;
|
||||
result.Own = winner == Team;
|
||||
result.DeployRound = round + 1;
|
||||
return result;
|
||||
}
|
||||
if (unit.Zone == "lane")
|
||||
{
|
||||
result.State = unit.Alive ? "deploying" : "defeated";
|
||||
result.Team = unit.Team;
|
||||
result.Own = unit.Team == Team;
|
||||
result.Lane = unit.Lane;
|
||||
result.DeployRound = round;
|
||||
result.Bounty = LuneContent.BossBountyFor(round);
|
||||
result.HasHp = true;
|
||||
result.Hp = unit.Hp;
|
||||
result.MaxHp = unit.MaxHp;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (claim != null && claim.DeployRound >= round)
|
||||
{
|
||||
result.State = Planning ? "deploy_next" : "deploying";
|
||||
result.Team = claim.Team;
|
||||
result.Own = claim.Team == Team;
|
||||
result.WonRound = claim.WonRound;
|
||||
result.DeployRound = claim.DeployRound;
|
||||
result.Bounty = LuneContent.BossBountyFor(claim.DeployRound);
|
||||
result.Chooser = claim.ChooserSlot == Slot;
|
||||
// The chooser's lane change is private until combat; others see the public claim lane.
|
||||
string chosen = !publicOnly && result.Chooser && Planning ? Me.Draft.BossLane ?? "" : "";
|
||||
result.Lane = chosen.Length > 0 ? chosen : claim.Lane;
|
||||
return result;
|
||||
}
|
||||
if (State.GrottoAvailable())
|
||||
result.State = "available";
|
||||
return result;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- waves
|
||||
public sealed class MonsterGroup
|
||||
{
|
||||
public string Id = "";
|
||||
public string Name = "";
|
||||
public int Count;
|
||||
public double Hp;
|
||||
public double Damage;
|
||||
public double Range;
|
||||
public bool New;
|
||||
public int Armor;
|
||||
public int Attack;
|
||||
|
||||
public override string ToString() { return Id + "x" + Count + (New ? "+" : ""); }
|
||||
}
|
||||
|
||||
public sealed class ChampionNotice
|
||||
{
|
||||
public int Team;
|
||||
public string Lane = "";
|
||||
public string ChampionId = "";
|
||||
public string Name = "";
|
||||
public bool Own;
|
||||
|
||||
public override string ToString() { return Team + ":" + Lane + ":" + ChampionId; }
|
||||
}
|
||||
|
||||
public sealed class BossNotice
|
||||
{
|
||||
public int Team;
|
||||
public string Lane = "";
|
||||
public int Bounty;
|
||||
public bool Own;
|
||||
|
||||
public override string ToString() { return Team + ":" + Lane + ":" + Bounty; }
|
||||
}
|
||||
|
||||
public sealed class WaveInfo
|
||||
{
|
||||
public int Round;
|
||||
public readonly List<MonsterGroup> Monsters = new List<MonsterGroup>();
|
||||
public int Count;
|
||||
public readonly List<ChampionNotice> Champions = new List<ChampionNotice>();
|
||||
public BossNotice Boss;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Round + "|" + string.Join(",", Monsters) + "|" + Count + "|" + string.Join(",", Champions) + "|" + (Boss != null ? Boss.ToString() : "-");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The wave of the coming combat (planning) or the one after (combat). Monsters are public
|
||||
/// and identical for both sides; champions and the boss only once they are announced.
|
||||
/// </summary>
|
||||
public WaveInfo NextWave()
|
||||
{
|
||||
int round = Fighting ? State.RoundNumber + 1 : State.RoundNumber;
|
||||
List<UnitDef> wave = LuneWaves.Build(State.Balance, round);
|
||||
List<string> before = round > 1 ? LuneWaves.UnlockedTypes(State.Balance, round - 1) : new List<string>();
|
||||
var result = new WaveInfo { Round = round, Count = wave.Count };
|
||||
var groups = new Dictionary<string, MonsterGroup>();
|
||||
foreach (UnitDef monster in wave)
|
||||
{
|
||||
if (!groups.TryGetValue(monster.Id, out MonsterGroup group))
|
||||
{
|
||||
group = new MonsterGroup
|
||||
{
|
||||
Id = monster.Id, Name = monster.DisplayName, Hp = monster.MaxHp, Damage = monster.Damage, Range = monster.AttackRange,
|
||||
New = round > 1 && !before.Contains(monster.Id), Armor = monster.ArmorType, Attack = monster.AttackType,
|
||||
};
|
||||
groups[monster.Id] = group;
|
||||
result.Monsters.Add(group);
|
||||
}
|
||||
group.Count++;
|
||||
}
|
||||
if (!Fighting)
|
||||
{
|
||||
foreach (Announcement entry in State.Announcements())
|
||||
{
|
||||
if (entry.Kind == "boss")
|
||||
result.Boss = new BossNotice { Team = entry.Team, Lane = entry.Lane, Bounty = entry.Bounty, Own = entry.Team == Team };
|
||||
else
|
||||
result.Champions.Add(new ChampionNotice { Team = entry.Team, Lane = entry.Lane, ChampionId = entry.ChampionId, Name = LuneContent.Champions[entry.ChampionId].Name, Own = entry.Team == Team });
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- network test support
|
||||
/// <summary>
|
||||
/// Everything the HUD shows that must be identical for every seat (host, guest, bot views),
|
||||
/// as one string. The boss uses the public claim lane: the chooser's draft lane is private.
|
||||
/// </summary>
|
||||
public string PublicDigest()
|
||||
{
|
||||
var text = new StringBuilder();
|
||||
text.Append("round ").Append(State.RoundNumber).Append(" phase ").Append(State.Phase).Append('\n');
|
||||
foreach (BuildingDef definition in State.Map.Buildings)
|
||||
{
|
||||
BuildingInfo entry = Building(definition.Id);
|
||||
text.Append(definition.Id).Append('=').Append(entry.Alive).Append('/').Append(LuneUiTheme.Round(entry.Hp)).Append(' ');
|
||||
}
|
||||
text.Append('\n');
|
||||
foreach (string name in LuneMap.Lanes)
|
||||
{
|
||||
for (int side = 0; side < 2; side++)
|
||||
{
|
||||
LuneLaneThreat.Result threat = LuneLaneThreat.Evaluate(State, side, name);
|
||||
text.Append(name).Append('/').Append(side).Append('=').Append(threat.Level).Append(' ').Append(string.Join(";", threat.Reasons)).Append('\n');
|
||||
}
|
||||
}
|
||||
foreach (Player player in State.Players)
|
||||
{
|
||||
text.Append(player.Slot).Append(':').Append(player.Lane).Append(':').Append(player.Ready).Append(':').Append(player.IsHuman).Append(':')
|
||||
.Append(player.Hero != null ? player.Hero.HeroId + "@" + player.Hero.Xp : "-").Append(' ');
|
||||
}
|
||||
text.Append('\n').Append("boss ").Append(Boss(true).PublicText()).Append('\n');
|
||||
text.Append("wave ").Append(NextWave()).Append('\n');
|
||||
return text.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Luneraria/UI/Model/LuneHudModel.cs.meta
Normal file
2
Assets/Luneraria/UI/Model/LuneHudModel.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 3221278a1afde4ef49945d1280a93322
|
||||
192
Assets/Luneraria/UI/Model/LuneLaneThreat.cs
Normal file
192
Assets/Luneraria/UI/Model/LuneLaneThreat.cs
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
using System.Collections.Generic;
|
||||
using Luneraria.Core;
|
||||
|
||||
namespace Luneraria.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Deterministic lane threat for one team (port of scripts/luneraria/ui/lune_lane_threat.gd),
|
||||
/// read only from public state: building HP, the units on the battlefield (positions and HP,
|
||||
/// which every client also sees) and the public announcements of this round. It never reads
|
||||
/// drafts, gold, unannounced bookings or bot notes, so host and guest compute the same level.
|
||||
/// Positions are projected onto the lane, because a network guest has positions but no lane
|
||||
/// progress.
|
||||
///
|
||||
/// RUHIG nothing below applies
|
||||
/// UMKÄMPFT a lane tower is below 85 %, an enemy attack champion is announced here, or
|
||||
/// enemy units stand in the own half of the lane
|
||||
/// UNTER DRUCK the Grenzwacht fell or is below 50 %, enemies attack the Grenzwacht, the enemy
|
||||
/// has more than 1.5 times the own unit HP in the own half, or the enemy boss
|
||||
/// is deployed on this lane
|
||||
/// KRITISCH both lane towers fell (base open), the Bastion is below 50 % after the
|
||||
/// Grenzwacht fell, enemies attack the Bastion, or the enemy boss comes while the
|
||||
/// Grenzwacht is already down
|
||||
/// </summary>
|
||||
public static class LuneLaneThreat
|
||||
{
|
||||
public const int Calm = 0;
|
||||
public const int Contested = 1;
|
||||
public const int Pressure = 2;
|
||||
public const int Critical = 3;
|
||||
public static readonly string[] Names = { "RUHIG", "UMKÄMPFT", "UNTER DRUCK", "KRITISCH" };
|
||||
public static readonly string[] Glyphs = { "check", "melee", "warning", "warning" };
|
||||
|
||||
public sealed class Result
|
||||
{
|
||||
public int Level;
|
||||
public string Name = "";
|
||||
public readonly List<string> Reasons = new List<string>();
|
||||
}
|
||||
|
||||
public sealed class Battle
|
||||
{
|
||||
public double Length;
|
||||
public int OwnCount;
|
||||
public int EnemyCount;
|
||||
public double OwnHp;
|
||||
public double EnemyHp;
|
||||
/// <summary>Deepest enemy position measured from the own base along the lane (infinity without enemies).</summary>
|
||||
public double EnemyDepth = double.PositiveInfinity;
|
||||
public double OwnFront;
|
||||
}
|
||||
|
||||
static void Building(LuneMatch state, string id, out bool alive, out double fraction)
|
||||
{
|
||||
double maxHp = state.Map.BuildingDef(id)?.MaxHp ?? 1.0;
|
||||
if (state.Phase == Phase.Combat && state.Combat != null && state.Combat.Buildings.TryGetValue(id, out LuneUnit unit))
|
||||
{
|
||||
alive = unit.Alive;
|
||||
fraction = alive ? GdMath.Clamp(unit.Hp / GdMath.Max(1.0, maxHp), 0.0, 1.0) : 0.0;
|
||||
return;
|
||||
}
|
||||
double hp = state.BuildingHp.GetValueOrDefault(id, 0.0);
|
||||
alive = hp > 0.0;
|
||||
fraction = GdMath.Clamp(hp / GdMath.Max(1.0, maxHp), 0.0, 1.0);
|
||||
}
|
||||
|
||||
static string Percent(double value)
|
||||
{
|
||||
return LuneUiTheme.Round(value * 100.0) + " %";
|
||||
}
|
||||
|
||||
public static Result Evaluate(LuneMatch state, int team, string lane)
|
||||
{
|
||||
var result = new Result();
|
||||
int level = Calm;
|
||||
string prefix = team == 0 ? "blau" : "rot";
|
||||
int enemy = 1 - team;
|
||||
Building(state, prefix + "_" + lane + "_outer", out bool outerAlive, out double outer);
|
||||
Building(state, prefix + "_" + lane + "_inner", out bool innerAlive, out double inner);
|
||||
if (!outerAlive && !innerAlive)
|
||||
{
|
||||
level = Critical;
|
||||
result.Reasons.Add("Grenzwacht und Bastion gefallen – der Weg zur Basis ist offen");
|
||||
}
|
||||
else if (!outerAlive)
|
||||
{
|
||||
level = Pressure;
|
||||
result.Reasons.Add("Grenzwacht gefallen");
|
||||
if (inner < .5)
|
||||
{
|
||||
level = Critical;
|
||||
result.Reasons.Add("Bastion bei " + Percent(inner));
|
||||
}
|
||||
else if (inner < .85)
|
||||
{
|
||||
result.Reasons.Add("Bastion bei " + Percent(inner));
|
||||
}
|
||||
}
|
||||
else if (outer < .5)
|
||||
{
|
||||
level = Pressure;
|
||||
result.Reasons.Add("Grenzwacht bei " + Percent(outer));
|
||||
}
|
||||
else if (outer < .85)
|
||||
{
|
||||
level = Contested;
|
||||
result.Reasons.Add("Grenzwacht bei " + Percent(outer));
|
||||
}
|
||||
foreach (Announcement entry in state.Announcements())
|
||||
{
|
||||
if (entry.Team != enemy || entry.Lane != lane)
|
||||
continue;
|
||||
if (entry.Kind == "boss")
|
||||
{
|
||||
level = System.Math.Max(level, !outerAlive ? Critical : Pressure);
|
||||
result.Reasons.Add("Gegnerische Schlundkrone greift hier an");
|
||||
}
|
||||
else
|
||||
{
|
||||
level = System.Math.Max(level, Contested);
|
||||
result.Reasons.Add("Angriffschampion " + LuneContent.Champions[entry.ChampionId].Name + " angekündigt");
|
||||
}
|
||||
}
|
||||
if (state.Phase == Phase.Combat && state.Combat != null)
|
||||
{
|
||||
Battle battle = Battlefield(state, team, lane);
|
||||
double length = battle.Length;
|
||||
if (battle.EnemyCount > 0)
|
||||
{
|
||||
double deepest = battle.EnemyDepth;
|
||||
double innerAt = length * state.Balance.InnerTowerFraction;
|
||||
double outerAt = length * state.Balance.OuterTowerFraction;
|
||||
double reach = state.Balance.TowerRange * .8;
|
||||
if (deepest <= innerAt + reach)
|
||||
{
|
||||
level = Critical;
|
||||
result.Reasons.Add("Gegner greifen die Bastion oder die Basis an");
|
||||
}
|
||||
else if (deepest <= outerAt + reach && outerAlive)
|
||||
{
|
||||
level = System.Math.Max(level, Pressure);
|
||||
result.Reasons.Add("Gegner greifen die Grenzwacht an");
|
||||
}
|
||||
else if (deepest <= length * .5)
|
||||
{
|
||||
level = System.Math.Max(level, Contested);
|
||||
result.Reasons.Add("Gegner in der eigenen Lanehälfte");
|
||||
}
|
||||
if (battle.EnemyHp > battle.OwnHp * 1.5 && deepest <= length * .5)
|
||||
{
|
||||
level = System.Math.Max(level, Pressure);
|
||||
result.Reasons.Add(string.Format("Übermacht: {0} gegen {1} Einheiten", battle.EnemyCount, battle.OwnCount));
|
||||
}
|
||||
}
|
||||
}
|
||||
result.Level = level;
|
||||
result.Name = Names[level];
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Units of both sides on one lane: counts, HP sums and the deepest enemy position measured
|
||||
/// from the own base along the lane (from positions only).
|
||||
/// </summary>
|
||||
public static Battle Battlefield(LuneMatch state, int team, string lane)
|
||||
{
|
||||
LanePath path = state.Map.LanePaths[lane];
|
||||
var result = new Battle { Length = path.Length };
|
||||
if (state.Combat == null)
|
||||
return result;
|
||||
foreach (LuneUnit unit in state.Combat.Units)
|
||||
{
|
||||
if (!unit.Alive || !unit.IsMobile() || unit.Zone != "lane" || unit.Lane != lane || unit.Team > 1)
|
||||
continue;
|
||||
double s = path.Project(unit.Position);
|
||||
double fromOwnBase = team == 0 ? s : path.Length - s;
|
||||
if (unit.Team == team)
|
||||
{
|
||||
result.OwnCount++;
|
||||
result.OwnHp += unit.Hp;
|
||||
result.OwnFront = GdMath.Max(result.OwnFront, fromOwnBase);
|
||||
}
|
||||
else
|
||||
{
|
||||
result.EnemyCount++;
|
||||
result.EnemyHp += unit.Hp;
|
||||
result.EnemyDepth = GdMath.Min(result.EnemyDepth, fromOwnBase);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Luneraria/UI/Model/LuneLaneThreat.cs.meta
Normal file
2
Assets/Luneraria/UI/Model/LuneLaneThreat.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 201a11a896ae1465781ac57b8f54ffbf
|
||||
181
Assets/Luneraria/UI/Model/LuneStats.cs
Normal file
181
Assets/Luneraria/UI/Model/LuneStats.cs
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
using System.Collections.Generic;
|
||||
using Luneraria.Core;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Luneraria.UI
|
||||
{
|
||||
/// <summary>Tooltip content (Godot: the dictionary with title, subtitle, stats, text, note, warn).</summary>
|
||||
public sealed class LuneTip
|
||||
{
|
||||
public string Title = "";
|
||||
public string Subtitle = "";
|
||||
public string Glyph = "";
|
||||
public Color? GlyphColor;
|
||||
/// <summary>Rows [glyph, name, value].</summary>
|
||||
public readonly List<string[]> Stats = new List<string[]>();
|
||||
public string Text = "";
|
||||
public string Note = "";
|
||||
public string Warn = "";
|
||||
|
||||
public bool IsEmpty { get { return Title.Length == 0 && Subtitle.Length == 0 && Stats.Count == 0 && Text.Length == 0 && Note.Length == 0 && Warn.Length == 0; } }
|
||||
|
||||
public LuneTip Stat(string glyph, string name, string value)
|
||||
{
|
||||
Stats.Add(new[] { glyph, name, value });
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>Everything the tooltip shows, as one string (tests and change detection).</summary>
|
||||
public override string ToString()
|
||||
{
|
||||
var parts = new List<string> { Title, Subtitle };
|
||||
foreach (string[] row in Stats)
|
||||
parts.Add(row[1] + ": " + row[2]);
|
||||
parts.Add(Text);
|
||||
parts.Add(Note);
|
||||
parts.Add(Warn);
|
||||
return string.Join("\n", parts.FindAll(p => p.Length > 0));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Display figures from the real unit definitions and combat rules (port of
|
||||
/// scripts/luneraria/ui/lune_stats.gd): damage per hit, one attack per second times the attack
|
||||
/// tempo bonus, range, armour and damage type with the damage matrix (LuneUnit.DamageMatrix).
|
||||
/// Nothing is invented; values are only formatted.
|
||||
/// </summary>
|
||||
public static class LuneStats
|
||||
{
|
||||
static readonly Dictionary<string, string> RoleNames = new Dictionary<string, string>
|
||||
{ { "front", "Tank" }, { "melee", "Nahkampf" }, { "ranged", "Fernkampf" }, { "splash", "Flächenschaden" }, { "support", "Support" } };
|
||||
static readonly Dictionary<string, string> RoleGlyphs = new Dictionary<string, string>
|
||||
{ { "front", "tank" }, { "melee", "melee" }, { "ranged", "ranged" }, { "splash", "splash" }, { "support", "support" } };
|
||||
|
||||
public static string RoleName(string role)
|
||||
{
|
||||
return RoleNames.TryGetValue(role ?? "", out string name) ? name : role;
|
||||
}
|
||||
|
||||
public static string RoleGlyph(string role)
|
||||
{
|
||||
return RoleGlyphs.TryGetValue(role ?? "", out string glyph) ? glyph : "melee";
|
||||
}
|
||||
|
||||
static int Find(int attackType, double factor)
|
||||
{
|
||||
int row = GdMath.Clamp(attackType, 0, 2);
|
||||
for (int armor = 0; armor < 3; armor++)
|
||||
{
|
||||
if (LuneUnit.DamageMatrix[row, armor] == factor)
|
||||
return armor;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>Armour class the damage type hits for 130 % (DamageMatrix).</summary>
|
||||
public static string StrongAgainst(int attackType)
|
||||
{
|
||||
return ContentData.ArmorNames[Find(attackType, 1.30)];
|
||||
}
|
||||
|
||||
/// <summary>Armour class the damage type hits for 75 % (DamageMatrix).</summary>
|
||||
public static string WeakAgainst(int attackType)
|
||||
{
|
||||
return ContentData.ArmorNames[Find(attackType, 0.75)];
|
||||
}
|
||||
|
||||
public static double HitDamage(UnitDef definition)
|
||||
{
|
||||
return definition.Damage * (1.0 + definition.DamageBonus);
|
||||
}
|
||||
|
||||
public static double AttacksPerSecond(UnitDef definition)
|
||||
{
|
||||
return 1.0 + GdMath.Clamp(definition.AttackSpeedBonus, -.25, .5);
|
||||
}
|
||||
|
||||
public static string ArmorName(int armorType)
|
||||
{
|
||||
return ContentData.ArmorNames[GdMath.Clamp(armorType, 0, 2)];
|
||||
}
|
||||
|
||||
public static string DamageName(int attackType)
|
||||
{
|
||||
return ContentData.DamageNames[GdMath.Clamp(attackType, 0, 2)];
|
||||
}
|
||||
|
||||
/// <summary>Stat rows [glyph, name, value] for tooltips and detail panels.</summary>
|
||||
public static List<string[]> Rows(UnitDef definition, bool withMove = false)
|
||||
{
|
||||
int attackType = definition.AttackType;
|
||||
double reduction = definition.ReductionBonus;
|
||||
var result = new List<string[]>
|
||||
{
|
||||
new[] { "heart", "Leben", LuneUiTheme.Number(definition.MaxHp) },
|
||||
new[] { "sword", "Schaden/Treffer", LuneUiTheme.Decimal(HitDamage(definition)) },
|
||||
new[] { "tempo", "Angriffstempo", LuneUiTheme.Decimal(AttacksPerSecond(definition), 2) + " /s" },
|
||||
new[] { "range", "Reichweite", LuneUiTheme.Decimal(definition.AttackRange) + " m" },
|
||||
new[] { "armor", "Rüstung", ArmorName(definition.ArmorType) + (reduction > 0.0 ? " · −" + LuneUiTheme.Round(reduction * 100.0) + " % Schaden" : "") },
|
||||
new[] { "splash", "Angriffsart", DamageName(attackType) + " · stark gegen " + StrongAgainst(attackType) },
|
||||
};
|
||||
if (withMove)
|
||||
result.Add(new[] { "move", "Lauftempo", LuneUiTheme.Decimal(definition.Speed) + " m/s" });
|
||||
if (definition.Regen > 0.0)
|
||||
result.Add(new[] { "heart", "Regeneration", LuneUiTheme.Decimal(definition.Regen * 100.0) + " % Leben/s" });
|
||||
if (definition.ItemSplash > 0.0)
|
||||
result.Add(new[] { "splash", "Splitter", LuneUiTheme.Round(definition.ItemSplash * 100.0) + " % an bis zu 2 Gegner" });
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>Compact pairs [glyph, "Name Wert"] for narrow panels (two per row).</summary>
|
||||
public static List<string[]> ShortRows(UnitDef definition)
|
||||
{
|
||||
int attackType = GdMath.Clamp(definition.AttackType, 0, 2);
|
||||
return new List<string[]>
|
||||
{
|
||||
new[] { "heart", "Leben " + LuneUiTheme.Number(definition.MaxHp) },
|
||||
new[] { "sword", "Schaden " + LuneUiTheme.Decimal(HitDamage(definition)) },
|
||||
new[] { "tempo", "Tempo " + LuneUiTheme.Decimal(AttacksPerSecond(definition), 2) + "/s" },
|
||||
new[] { "range", "Reichweite " + LuneUiTheme.Decimal(definition.AttackRange) + " m" },
|
||||
new[] { "armor", "Rüstung " + ArmorName(definition.ArmorType) },
|
||||
new[] { "splash", DamageName(attackType) + " › " + StrongAgainst(attackType) },
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>"Schaden pro Sekunde" from hit damage and tempo, without abilities or type modifiers.</summary>
|
||||
public static double Dps(UnitDef definition)
|
||||
{
|
||||
return HitDamage(definition) * AttacksPerSecond(definition);
|
||||
}
|
||||
|
||||
/// <summary>Stage of a troop id: even numbers are the upgraded stage (g02, w04 …).</summary>
|
||||
public static int StageOf(string typeId)
|
||||
{
|
||||
int number = int.Parse(typeId.Substring(1));
|
||||
return number % 2 == 0 ? 2 : 1;
|
||||
}
|
||||
|
||||
/// <summary>Tooltip content for a troop type (shop cards, army chips).</summary>
|
||||
public static LuneTip TroopTip(string typeId, string role, string extra = "")
|
||||
{
|
||||
UnitDef data = DataCatalog.GetUnit(typeId);
|
||||
string upgrade = data.UpgradeId ?? "";
|
||||
string note;
|
||||
if (upgrade.Length > 0)
|
||||
{
|
||||
UnitDef next = DataCatalog.GetUnit(upgrade);
|
||||
note = string.Format("Aufwertung: {0} für {1} Gold (Leben {2} → {3}, Schaden {4} → {5})", next.DisplayName, next.Cost,
|
||||
LuneUiTheme.Number(data.MaxHp), LuneUiTheme.Number(next.MaxHp), LuneUiTheme.Decimal(data.Damage), LuneUiTheme.Decimal(next.Damage));
|
||||
}
|
||||
else
|
||||
{
|
||||
note = "Höchste Stufe.";
|
||||
}
|
||||
var tip = new LuneTip { Title = data.DisplayName, Glyph = RoleGlyph(role),
|
||||
Subtitle = string.Format("{0} · Stufe {1} · {2} Gold", RoleName(role), StageOf(typeId) == 2 ? "II" : "I", data.Cost),
|
||||
Text = data.AbilityDescription ?? "", Note = note, Warn = extra ?? "" };
|
||||
tip.Stats.AddRange(Rows(data));
|
||||
return tip;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Luneraria/UI/Model/LuneStats.cs.meta
Normal file
2
Assets/Luneraria/UI/Model/LuneStats.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 743293fdb90d74ec9afb6629a70928a7
|
||||
8
Assets/Luneraria/UI/Theme.meta
Normal file
8
Assets/Luneraria/UI/Theme.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: f83fce8e4470d4335a25f56c932be60e
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
482
Assets/Luneraria/UI/Theme/LuneGlyphs.cs
Normal file
482
Assets/Luneraria/UI/Theme/LuneGlyphs.cs
Normal file
|
|
@ -0,0 +1,482 @@
|
|||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace Luneraria.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Vector symbols of the HUD (port of scripts/luneraria/ui/theme/lune_glyphs.gd), drawn from a
|
||||
/// unit square so they stay sharp at every UI scale. Team emblems follow the G2 world: Blau =
|
||||
/// Mondsichel (round shapes), Rot = Sonnenscheibe (rayed, angular), the grotto = hexagon.
|
||||
/// </summary>
|
||||
public static class LuneGlyphs
|
||||
{
|
||||
public static readonly string[] Names = { "coin", "emblem_0", "emblem_1", "emblem_2", "shape_0", "shape_1", "shape_2",
|
||||
"tank", "melee", "ranged", "splash", "support", "siege", "crown", "champion", "hourglass", "tower",
|
||||
"ancient", "warning", "check", "close", "bot", "offline", "clock", "heart", "tempo", "range", "armor",
|
||||
"move", "star", "lock", "up", "lane_top", "lane_mid", "lane_bot", "grotto", "pause", "play", "menu",
|
||||
"gear", "human", "sword", "basaltschild", "glutklinge", "rankenmantel", "funkenfokus", "quellamulett",
|
||||
"kristallsplitter", "hero", "skull" };
|
||||
|
||||
const float Tau = Mathf.PI * 2f;
|
||||
static readonly Dictionary<string, Vector2[]> Cache = new Dictionary<string, Vector2[]>();
|
||||
|
||||
/// <summary>Draws one symbol into rect (unit square centred, aspect kept). `accent` is an optional second colour for inner details.</summary>
|
||||
public static void Draw(Painter2D painter, string glyph, Rect rect, Color color, Color accent = default)
|
||||
{
|
||||
if (string.IsNullOrEmpty(glyph))
|
||||
return;
|
||||
var f = new Frame(rect);
|
||||
Color dark = accent.a > 0f ? accent : new Color(color.r * .45f, color.g * .45f, color.b * .45f, color.a);
|
||||
switch (glyph)
|
||||
{
|
||||
case "coin":
|
||||
{
|
||||
Vector2 c = f.P(.5f, .5f);
|
||||
float r = f.S * .42f;
|
||||
Disc(painter, c, r, color);
|
||||
ArcLine(painter, c, r, 0f, Tau, LuneUiTheme.Darkened(color, .35f), Mathf.Max(1f, r * .12f));
|
||||
ArcLine(painter, c, r * .62f, 0f, Tau, LuneUiTheme.Darkened(color, .28f), Mathf.Max(1f, r * .1f));
|
||||
ArcLine(painter, c, r * .8f, Mathf.PI * 1.1f, Mathf.PI * 1.45f, new Color(1f, 1f, 1f, .55f * color.a), Mathf.Max(1f, r * .1f));
|
||||
break;
|
||||
}
|
||||
case "emblem_0":
|
||||
Fill(painter, f, Crescent(), color);
|
||||
break;
|
||||
case "emblem_1":
|
||||
Disc(painter, f.P(.5f, .5f), f.S * .2f, color);
|
||||
for (int index = 0; index < 8; index++)
|
||||
{
|
||||
float angle = Tau * index / 8f - Mathf.PI * .5f;
|
||||
Vector2 tip = new Vector2(.5f, .5f) + Dir(angle) * .47f;
|
||||
Vector2 left = new Vector2(.5f, .5f) + Dir(angle - .22f) * .27f;
|
||||
Vector2 right = new Vector2(.5f, .5f) + Dir(angle + .22f) * .27f;
|
||||
Fill(painter, f, new[] { left, tip, right }, color);
|
||||
}
|
||||
break;
|
||||
case "emblem_2":
|
||||
{
|
||||
List<Vector2> hexagon = Circle(6, new Vector2(.5f, .5f), .42f, -Mathf.PI * .5f);
|
||||
hexagon.Add(hexagon[0]);
|
||||
Line(painter, f, hexagon.ToArray(), color, .08f);
|
||||
Fill(painter, f, new[] { V(.5f, .3f), V(.64f, .5f), V(.5f, .7f), V(.36f, .5f) }, color);
|
||||
break;
|
||||
}
|
||||
case "shape_0":
|
||||
Disc(painter, f.P(.5f, .5f), f.S * .4f, color);
|
||||
break;
|
||||
case "shape_1":
|
||||
Fill(painter, f, new[] { V(.5f, .06f), V(.94f, .5f), V(.5f, .94f), V(.06f, .5f) }, color);
|
||||
break;
|
||||
case "shape_2":
|
||||
Fill(painter, f, Circle(6, new Vector2(.5f, .5f), .44f, -Mathf.PI * .5f).ToArray(), color);
|
||||
break;
|
||||
case "tank":
|
||||
case "armor":
|
||||
{
|
||||
var shield = new List<Vector2> { V(.2f, .16f), V(.8f, .16f), V(.8f, .48f), V(.72f, .66f), V(.5f, .87f), V(.28f, .66f), V(.2f, .48f) };
|
||||
if (glyph == "tank")
|
||||
{
|
||||
Fill(painter, f, shield.ToArray(), color);
|
||||
Line(painter, f, new[] { V(.5f, .24f), V(.5f, .74f) }, dark, .07f);
|
||||
}
|
||||
else
|
||||
{
|
||||
shield.Add(shield[0]);
|
||||
Line(painter, f, shield.ToArray(), color, .09f);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "melee":
|
||||
case "sword":
|
||||
{
|
||||
Vector2[] blade = { V(.5f, .06f), V(.56f, .14f), V(.56f, .62f), V(.44f, .62f), V(.44f, .14f) };
|
||||
Fill(painter, f, Rot(blade, Mathf.PI * .25f), color);
|
||||
Fill(painter, f, Rot(new[] { V(.3f, .62f), V(.7f, .62f), V(.7f, .69f), V(.3f, .69f) }, Mathf.PI * .25f), color);
|
||||
Fill(painter, f, Rot(new[] { V(.46f, .69f), V(.54f, .69f), V(.54f, .88f), V(.46f, .88f) }, Mathf.PI * .25f), color);
|
||||
break;
|
||||
}
|
||||
case "ranged":
|
||||
{
|
||||
var bow = new Vector2[13];
|
||||
for (int index = 0; index < 13; index++)
|
||||
{
|
||||
float angle = Mathf.PI * .62f + Mathf.PI * .76f * index / 12f;
|
||||
bow[index] = new Vector2(.78f, .5f) + Dir(angle) * .5f;
|
||||
}
|
||||
Line(painter, f, Rot(bow, -Mathf.PI * .25f), color, .08f);
|
||||
Line(painter, f, Rot(new[] { bow[0], bow[12] }, -Mathf.PI * .25f), color, .03f);
|
||||
Line(painter, f, Rot(new[] { V(.14f, .5f), V(.82f, .5f) }, -Mathf.PI * .25f), color, .06f);
|
||||
Fill(painter, f, Rot(new[] { V(.9f, .5f), V(.76f, .41f), V(.76f, .59f) }, -Mathf.PI * .25f), color);
|
||||
break;
|
||||
}
|
||||
case "splash":
|
||||
Fill(painter, f, Star(8, .46f, .2f), color);
|
||||
Disc(painter, f.P(.5f, .5f), f.S * .1f, dark);
|
||||
break;
|
||||
case "support":
|
||||
Fill(painter, f, new[] { V(.4f, .14f), V(.6f, .14f), V(.6f, .4f), V(.86f, .4f), V(.86f, .6f), V(.6f, .6f),
|
||||
V(.6f, .86f), V(.4f, .86f), V(.4f, .6f), V(.14f, .6f), V(.14f, .4f), V(.4f, .4f) }, color);
|
||||
break;
|
||||
case "siege":
|
||||
Fill(painter, f, Rot(new[] { V(.18f, .18f), V(.74f, .18f), V(.74f, .42f), V(.18f, .42f) }, -Mathf.PI * .2f), color);
|
||||
Fill(painter, f, Rot(new[] { V(.42f, .4f), V(.52f, .4f), V(.52f, .92f), V(.42f, .92f) }, -Mathf.PI * .2f), color);
|
||||
break;
|
||||
case "crown":
|
||||
case "hero":
|
||||
Fill(painter, f, new[] { V(.12f, .74f), V(.12f, .3f), V(.32f, .5f), V(.5f, .2f), V(.68f, .5f), V(.88f, .3f), V(.88f, .74f) }, color);
|
||||
Fill(painter, f, new[] { V(.12f, .78f), V(.88f, .78f), V(.88f, .88f), V(.12f, .88f) }, color);
|
||||
if (glyph == "crown")
|
||||
Disc(painter, f.P(.5f, .6f), f.S * .06f, dark);
|
||||
break;
|
||||
case "skull":
|
||||
Fill(painter, f, Circle(20, new Vector2(.5f, .44f), .34f).ToArray(), color);
|
||||
Fill(painter, f, new[] { V(.32f, .6f), V(.68f, .6f), V(.64f, .86f), V(.36f, .86f) }, color);
|
||||
Disc(painter, f.P(.37f, .45f), f.S * .09f, dark);
|
||||
Disc(painter, f.P(.63f, .45f), f.S * .09f, dark);
|
||||
break;
|
||||
case "champion":
|
||||
Fill(painter, f, new[] { V(.5f, .06f), V(.82f, .46f), V(.61f, .46f), V(.61f, .9f), V(.39f, .9f), V(.39f, .46f), V(.18f, .46f) }, color);
|
||||
break;
|
||||
case "hourglass":
|
||||
Fill(painter, f, new[] { V(.24f, .12f), V(.76f, .12f), V(.56f, .5f), V(.44f, .5f) }, color);
|
||||
Fill(painter, f, new[] { V(.44f, .5f), V(.56f, .5f), V(.76f, .88f), V(.24f, .88f) }, color);
|
||||
Line(painter, f, new[] { V(.18f, .1f), V(.82f, .1f) }, color, .06f);
|
||||
Line(painter, f, new[] { V(.18f, .9f), V(.82f, .9f) }, color, .06f);
|
||||
break;
|
||||
case "tower":
|
||||
Fill(painter, f, new[] { V(.28f, .92f), V(.28f, .36f), V(.22f, .36f), V(.22f, .14f), V(.33f, .14f), V(.33f, .23f),
|
||||
V(.44f, .23f), V(.44f, .14f), V(.56f, .14f), V(.56f, .23f), V(.67f, .23f), V(.67f, .14f),
|
||||
V(.78f, .14f), V(.78f, .36f), V(.72f, .36f), V(.72f, .92f) }, color);
|
||||
Fill(painter, f, new[] { V(.44f, .92f), V(.44f, .72f), V(.5f, .66f), V(.56f, .72f), V(.56f, .92f) }, dark);
|
||||
break;
|
||||
case "ancient":
|
||||
Fill(painter, f, new[] { V(.5f, .05f), V(.76f, .4f), V(.6f, .95f), V(.4f, .95f), V(.24f, .4f) }, color);
|
||||
Line(painter, f, new[] { V(.5f, .05f), V(.5f, .95f) }, dark, .04f);
|
||||
Line(painter, f, new[] { V(.24f, .4f), V(.5f, .52f), V(.76f, .4f) }, dark, .04f);
|
||||
break;
|
||||
case "warning":
|
||||
Fill(painter, f, new[] { V(.5f, .08f), V(.94f, .88f), V(.06f, .88f) }, color);
|
||||
Fill(painter, f, new[] { V(.46f, .34f), V(.54f, .34f), V(.53f, .64f), V(.47f, .64f) }, dark);
|
||||
Disc(painter, f.P(.5f, .75f), f.S * .05f, dark);
|
||||
break;
|
||||
case "check":
|
||||
Line(painter, f, new[] { V(.16f, .52f), V(.4f, .76f), V(.86f, .24f) }, color, .13f);
|
||||
break;
|
||||
case "close":
|
||||
Line(painter, f, new[] { V(.2f, .2f), V(.8f, .8f) }, color, .12f);
|
||||
Line(painter, f, new[] { V(.8f, .2f), V(.2f, .8f) }, color, .12f);
|
||||
break;
|
||||
case "bot":
|
||||
case "gear":
|
||||
{
|
||||
Vector2 c = f.P(.5f, .5f);
|
||||
for (int index = 0; index < 8; index++)
|
||||
Fill(painter, f, Rot(new[] { V(.43f, .06f), V(.57f, .06f), V(.57f, .24f), V(.43f, .24f) }, Tau * index / 8f), color);
|
||||
Disc(painter, c, f.S * .3f, color);
|
||||
Disc(painter, c, f.S * .12f, dark);
|
||||
break;
|
||||
}
|
||||
case "human":
|
||||
Disc(painter, f.P(.5f, .3f), f.S * .17f, color);
|
||||
Fill(painter, f, new[] { V(.2f, .9f), V(.24f, .62f), V(.38f, .52f), V(.62f, .52f), V(.76f, .62f), V(.8f, .9f) }, color);
|
||||
break;
|
||||
case "offline":
|
||||
ArcLine(painter, f.P(.5f, .5f), f.S * .38f, 0f, Tau, color, Mathf.Max(1f, f.S * .1f));
|
||||
Line(painter, f, new[] { V(.22f, .78f), V(.78f, .22f) }, color, .1f);
|
||||
break;
|
||||
case "clock":
|
||||
ArcLine(painter, f.P(.5f, .5f), f.S * .4f, 0f, Tau, color, Mathf.Max(1f, f.S * .09f));
|
||||
Line(painter, f, new[] { V(.5f, .24f), V(.5f, .5f), V(.68f, .62f) }, color, .08f);
|
||||
break;
|
||||
case "heart":
|
||||
Fill(painter, f, Heart(), color);
|
||||
break;
|
||||
case "tempo":
|
||||
Fill(painter, f, new[] { V(.6f, .04f), V(.22f, .56f), V(.46f, .56f), V(.38f, .96f), V(.78f, .42f), V(.54f, .42f), V(.66f, .04f) }, color);
|
||||
break;
|
||||
case "range":
|
||||
{
|
||||
Vector2 c = f.P(.5f, .5f);
|
||||
ArcLine(painter, c, f.S * .38f, 0f, Tau, color, Mathf.Max(1f, f.S * .08f));
|
||||
ArcLine(painter, c, f.S * .18f, 0f, Tau, color, Mathf.Max(1f, f.S * .08f));
|
||||
Disc(painter, c, f.S * .06f, color);
|
||||
Line(painter, f, new[] { V(.5f, .02f), V(.5f, .16f) }, color, .07f);
|
||||
Line(painter, f, new[] { V(.5f, .84f), V(.5f, .98f) }, color, .07f);
|
||||
Line(painter, f, new[] { V(.02f, .5f), V(.16f, .5f) }, color, .07f);
|
||||
Line(painter, f, new[] { V(.84f, .5f), V(.98f, .5f) }, color, .07f);
|
||||
break;
|
||||
}
|
||||
case "move":
|
||||
Line(painter, f, new[] { V(.18f, .2f), V(.46f, .5f), V(.18f, .8f) }, color, .11f);
|
||||
Line(painter, f, new[] { V(.52f, .2f), V(.8f, .5f), V(.52f, .8f) }, color, .11f);
|
||||
break;
|
||||
case "star":
|
||||
Fill(painter, f, Star(5, .47f, .2f), color);
|
||||
break;
|
||||
case "lock":
|
||||
Fill(painter, f, new[] { V(.22f, .46f), V(.78f, .46f), V(.78f, .9f), V(.22f, .9f) }, color);
|
||||
ArcLine(painter, f.P(.5f, .46f), f.S * .19f, Mathf.PI, Tau, color, Mathf.Max(1f, f.S * .09f));
|
||||
Disc(painter, f.P(.5f, .64f), f.S * .06f, dark);
|
||||
break;
|
||||
case "up":
|
||||
Fill(painter, f, new[] { V(.5f, .08f), V(.86f, .48f), V(.62f, .48f), V(.62f, .9f), V(.38f, .9f), V(.38f, .48f), V(.14f, .48f) }, color);
|
||||
break;
|
||||
case "lane_top":
|
||||
case "lane_mid":
|
||||
case "lane_bot":
|
||||
{
|
||||
Line(painter, f, new[] { V(.12f, .12f), V(.88f, .12f), V(.88f, .88f), V(.12f, .88f), V(.12f, .12f) }, LuneUiTheme.WithAlpha(color, color.a * .3f), .05f);
|
||||
Vector2[] path = glyph == "lane_top" ? new[] { V(.2f, .8f), V(.2f, .2f), V(.8f, .2f) }
|
||||
: glyph == "lane_mid" ? new[] { V(.2f, .8f), V(.8f, .2f) } : new[] { V(.2f, .8f), V(.8f, .8f), V(.8f, .2f) };
|
||||
Line(painter, f, path, color, .12f);
|
||||
break;
|
||||
}
|
||||
case "grotto":
|
||||
{
|
||||
var arch = new List<Vector2> { V(.08f, .9f) };
|
||||
for (int index = 0; index < 13; index++)
|
||||
{
|
||||
float angle = Mathf.PI + Mathf.PI * index / 12f;
|
||||
arch.Add(new Vector2(.5f, .62f) + new Vector2(Mathf.Cos(angle) * .42f, Mathf.Sin(angle) * .5f));
|
||||
}
|
||||
arch.Add(V(.92f, .9f));
|
||||
Fill(painter, f, arch.ToArray(), color);
|
||||
var mouth = new List<Vector2> { V(.3f, .9f) };
|
||||
for (int index = 0; index < 9; index++)
|
||||
{
|
||||
float angle = Mathf.PI + Mathf.PI * index / 8f;
|
||||
mouth.Add(new Vector2(.5f, .72f) + new Vector2(Mathf.Cos(angle) * .2f, Mathf.Sin(angle) * .26f));
|
||||
}
|
||||
mouth.Add(V(.7f, .9f));
|
||||
Fill(painter, f, mouth.ToArray(), dark);
|
||||
break;
|
||||
}
|
||||
case "pause":
|
||||
Fill(painter, f, new[] { V(.24f, .16f), V(.42f, .16f), V(.42f, .84f), V(.24f, .84f) }, color);
|
||||
Fill(painter, f, new[] { V(.58f, .16f), V(.76f, .16f), V(.76f, .84f), V(.58f, .84f) }, color);
|
||||
break;
|
||||
case "play":
|
||||
Fill(painter, f, new[] { V(.26f, .14f), V(.84f, .5f), V(.26f, .86f) }, color);
|
||||
break;
|
||||
case "menu":
|
||||
foreach (float y in new[] { .24f, .5f, .76f })
|
||||
Line(painter, f, new[] { V(.16f, y), V(.84f, y) }, color, .1f);
|
||||
break;
|
||||
case "basaltschild":
|
||||
{
|
||||
Fill(painter, f, Circle(6, new Vector2(.5f, .5f), .44f, -Mathf.PI * .5f).ToArray(), color);
|
||||
List<Vector2> inner = Circle(6, new Vector2(.5f, .5f), .27f, -Mathf.PI * .5f);
|
||||
inner.Add(inner[0]);
|
||||
Line(painter, f, inner.ToArray(), dark, .07f);
|
||||
break;
|
||||
}
|
||||
case "glutklinge":
|
||||
{
|
||||
Vector2[] blade = { V(.5f, .06f), V(.57f, .15f), V(.57f, .66f), V(.43f, .66f), V(.43f, .15f) };
|
||||
Fill(painter, f, Rot(blade, Mathf.PI * .25f), color);
|
||||
Fill(painter, f, Rot(new[] { V(.3f, .66f), V(.7f, .66f), V(.7f, .73f), V(.3f, .73f) }, Mathf.PI * .25f), color);
|
||||
Fill(painter, f, new[] { V(.2f, .5f), V(.3f, .3f), V(.36f, .42f), V(.42f, .22f), V(.5f, .44f), V(.4f, .6f) }, accent.a > 0f ? dark : LuneUiTheme.Hex("ffb070"));
|
||||
break;
|
||||
}
|
||||
case "rankenmantel":
|
||||
Fill(painter, f, new[] { V(.5f, .1f), V(.66f, .18f), V(.8f, .9f), V(.5f, .8f), V(.2f, .9f), V(.34f, .18f) }, color);
|
||||
Line(painter, f, new[] { V(.5f, .22f), V(.42f, .4f), V(.56f, .56f), V(.46f, .74f) }, dark, .05f);
|
||||
break;
|
||||
case "funkenfokus":
|
||||
ArcLine(painter, f.P(.5f, .5f), f.S * .42f, 0f, Tau, color, Mathf.Max(1f, f.S * .08f));
|
||||
Fill(painter, f, new[] { V(.56f, .16f), V(.3f, .54f), V(.47f, .54f), V(.42f, .84f), V(.7f, .44f), V(.53f, .44f), V(.62f, .16f) }, color);
|
||||
break;
|
||||
case "quellamulett":
|
||||
ArcLine(painter, f.P(.5f, .2f), f.S * .14f, Mathf.PI * .9f, Mathf.PI * 2.1f, color, Mathf.Max(1f, f.S * .06f));
|
||||
Fill(painter, f, Drop(), color);
|
||||
Disc(painter, f.P(.44f, .66f), f.S * .06f, new Color(1f, 1f, 1f, .45f * color.a));
|
||||
break;
|
||||
case "kristallsplitter":
|
||||
Shard(painter, f, V(.5f, .5f), 0f, .44f, color);
|
||||
Shard(painter, f, V(.3f, .62f), -.55f, .3f, color);
|
||||
Shard(painter, f, V(.7f, .62f), .55f, .3f, color);
|
||||
break;
|
||||
default:
|
||||
Disc(painter, f.P(.5f, .5f), f.S * .3f, color);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void Shard(Painter2D painter, Frame f, Vector2 center, float angle, float length, Color color)
|
||||
{
|
||||
Vector2[] shard = { center + new Vector2(0f, -length), center + new Vector2(.08f, 0f), center + new Vector2(0f, length * .45f), center + new Vector2(-.08f, 0f) };
|
||||
Fill(painter, f, Rot(shard, angle, center), color);
|
||||
}
|
||||
|
||||
/// <summary>The unit square fitted into a rect (centred, aspect kept).</summary>
|
||||
struct Frame
|
||||
{
|
||||
public readonly float S;
|
||||
public readonly Vector2 Origin;
|
||||
|
||||
public Frame(Rect rect)
|
||||
{
|
||||
S = Mathf.Min(rect.width, rect.height);
|
||||
Origin = rect.position + (rect.size - new Vector2(S, S)) * .5f;
|
||||
}
|
||||
|
||||
public Vector2 P(float x, float y) { return Origin + new Vector2(x, y) * S; }
|
||||
public Vector2 P(Vector2 p) { return Origin + p * S; }
|
||||
}
|
||||
|
||||
static Vector2 V(float x, float y) { return new Vector2(x, y); }
|
||||
|
||||
static Vector2 Dir(float angle) { return new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)); }
|
||||
|
||||
static Vector2[] Rot(Vector2[] points, float angle) { return Rot(points, angle, new Vector2(.5f, .5f)); }
|
||||
|
||||
static Vector2[] Rot(Vector2[] points, float angle, Vector2 pivot)
|
||||
{
|
||||
var result = new Vector2[points.Length];
|
||||
float c = Mathf.Cos(angle);
|
||||
float s = Mathf.Sin(angle);
|
||||
for (int index = 0; index < points.Length; index++)
|
||||
{
|
||||
Vector2 d = points[index] - pivot;
|
||||
result[index] = pivot + new Vector2(d.x * c - d.y * s, d.x * s + d.y * c);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static void Fill(Painter2D painter, Frame f, Vector2[] points, Color color)
|
||||
{
|
||||
if (points.Length < 3)
|
||||
return;
|
||||
painter.fillColor = color;
|
||||
painter.BeginPath();
|
||||
painter.MoveTo(f.P(points[0]));
|
||||
for (int index = 1; index < points.Length; index++)
|
||||
painter.LineTo(f.P(points[index]));
|
||||
painter.ClosePath();
|
||||
painter.Fill(FillRule.NonZero);
|
||||
}
|
||||
|
||||
static void Line(Painter2D painter, Frame f, Vector2[] points, Color color, float width)
|
||||
{
|
||||
painter.strokeColor = color;
|
||||
painter.lineWidth = Mathf.Max(1f, width * f.S);
|
||||
painter.lineJoin = LineJoin.Miter;
|
||||
painter.lineCap = LineCap.Butt;
|
||||
painter.BeginPath();
|
||||
painter.MoveTo(f.P(points[0]));
|
||||
for (int index = 1; index < points.Length; index++)
|
||||
painter.LineTo(f.P(points[index]));
|
||||
painter.Stroke();
|
||||
}
|
||||
|
||||
static void Disc(Painter2D painter, Vector2 center, float radius, Color color)
|
||||
{
|
||||
painter.fillColor = color;
|
||||
painter.BeginPath();
|
||||
painter.Arc(center, radius, Angle.Degrees(0f), Angle.Degrees(360f));
|
||||
painter.ClosePath();
|
||||
painter.Fill();
|
||||
}
|
||||
|
||||
/// <summary>Godot draw_arc: start to end angle (radians, clockwise on screen).</summary>
|
||||
static void ArcLine(Painter2D painter, Vector2 center, float radius, float start, float end, Color color, float width)
|
||||
{
|
||||
painter.strokeColor = color;
|
||||
painter.lineWidth = width;
|
||||
painter.lineCap = LineCap.Butt;
|
||||
painter.BeginPath();
|
||||
painter.Arc(center, radius, Angle.Radians(start), Angle.Radians(end));
|
||||
if (end - start >= Tau - .001f)
|
||||
painter.ClosePath();
|
||||
painter.Stroke();
|
||||
}
|
||||
|
||||
static List<Vector2> Circle(int points, Vector2 center, float radius, float start = 0f)
|
||||
{
|
||||
var result = new List<Vector2>(points);
|
||||
for (int index = 0; index < points; index++)
|
||||
result.Add(center + Dir(start + Tau * index / points) * radius);
|
||||
return result;
|
||||
}
|
||||
|
||||
static Vector2[] Star(int points, float outer, float inner, float start = -Mathf.PI * .5f)
|
||||
{
|
||||
var result = new Vector2[points * 2];
|
||||
for (int index = 0; index < points * 2; index++)
|
||||
result[index] = new Vector2(.5f, .5f) + Dir(start + Mathf.PI * index / points) * (index % 2 == 0 ? outer : inner);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>Crescent (Blau): the outer disc minus an offset disc, as Geometry2D.clip_polygons gave in Godot.</summary>
|
||||
static Vector2[] Crescent()
|
||||
{
|
||||
if (Cache.TryGetValue("crescent", out Vector2[] cached))
|
||||
return cached;
|
||||
var outerCenter = new Vector2(.5f, .5f);
|
||||
var innerCenter = new Vector2(.66f, .4f);
|
||||
const float outerRadius = .42f;
|
||||
const float innerRadius = .33f;
|
||||
List<Vector2> outer = Circle(48, outerCenter, outerRadius);
|
||||
List<Vector2> inner = Circle(48, innerCenter, innerRadius);
|
||||
// The outer arc outside the inner disc, then the inner arc inside the outer disc, backwards.
|
||||
int startOuter = 0;
|
||||
while (!(Inside(outer[startOuter], innerCenter, innerRadius) && !Inside(outer[(startOuter + 1) % 48], innerCenter, innerRadius)))
|
||||
startOuter++;
|
||||
var result = new List<Vector2>();
|
||||
for (int step = 1; step <= 48; step++)
|
||||
{
|
||||
Vector2 point = outer[(startOuter + step) % 48];
|
||||
if (Inside(point, innerCenter, innerRadius))
|
||||
break;
|
||||
result.Add(point);
|
||||
}
|
||||
int startInner = 0;
|
||||
while (!(Inside(inner[startInner], outerCenter, outerRadius) && !Inside(inner[(startInner + 1) % 48], outerCenter, outerRadius)))
|
||||
startInner++;
|
||||
for (int step = 0; step < 48; step++)
|
||||
{
|
||||
Vector2 point = inner[((startInner - step) % 48 + 48) % 48];
|
||||
if (!Inside(point, outerCenter, outerRadius))
|
||||
break;
|
||||
result.Add(point);
|
||||
}
|
||||
cached = result.ToArray();
|
||||
Cache["crescent"] = cached;
|
||||
return cached;
|
||||
}
|
||||
|
||||
static bool Inside(Vector2 point, Vector2 center, float radius)
|
||||
{
|
||||
return (point - center).sqrMagnitude < radius * radius;
|
||||
}
|
||||
|
||||
static Vector2[] Heart()
|
||||
{
|
||||
if (Cache.TryGetValue("heart", out Vector2[] cached))
|
||||
return cached;
|
||||
cached = new Vector2[40];
|
||||
for (int index = 0; index < 40; index++)
|
||||
{
|
||||
float t = Tau * index / 40f;
|
||||
float x = 16f * Mathf.Pow(Mathf.Sin(t), 3);
|
||||
float y = 13f * Mathf.Cos(t) - 5f * Mathf.Cos(2f * t) - 2f * Mathf.Cos(3f * t) - Mathf.Cos(4f * t);
|
||||
cached[index] = new Vector2(.5f + x / 38f, .47f - y / 38f);
|
||||
}
|
||||
Cache["heart"] = cached;
|
||||
return cached;
|
||||
}
|
||||
|
||||
static Vector2[] Drop()
|
||||
{
|
||||
var result = new Vector2[18];
|
||||
result[0] = new Vector2(.5f, .16f);
|
||||
for (int index = 0; index < 17; index++)
|
||||
{
|
||||
float angle = -Mathf.PI * .15f + Mathf.PI * 1.3f * index / 16f;
|
||||
result[1 + index] = new Vector2(.5f, .64f) + Dir(angle) * .22f;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Luneraria/UI/Theme/LuneGlyphs.cs.meta
Normal file
2
Assets/Luneraria/UI/Theme/LuneGlyphs.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: d2c6054b8320f4bd498dc306758262e2
|
||||
198
Assets/Luneraria/UI/Theme/LunePlates.cs
Normal file
198
Assets/Luneraria/UI/Theme/LunePlates.cs
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace Luneraria.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// A vector-drawn plate (port of scripts/ui/theme/forged_style.gd): chamfered corners, a
|
||||
/// vertical fill gradient, an edge line, an inset line, an optional top highlight and a soft
|
||||
/// shadow. Drawn from the element's size, so it stays crisp at every UI scale.
|
||||
/// </summary>
|
||||
public sealed class LunePlateStyle
|
||||
{
|
||||
public Color FillTop = LuneUiTheme.Hex("1b2027");
|
||||
public Color FillBottom = LuneUiTheme.Hex("12161b");
|
||||
public Color Edge = LuneUiTheme.Hex("8a6a3c");
|
||||
public float EdgeWidth = 1.5f;
|
||||
public Color Inner = new Color(1f, 1f, 1f, .05f);
|
||||
public float InnerInset = 4f;
|
||||
public float Chamfer = 7f;
|
||||
public Color Highlight = Color.clear;
|
||||
public Color Shadow = Color.clear;
|
||||
public float ShadowSize;
|
||||
/// <summary>Grows the drawing outside the element (focus rings).</summary>
|
||||
public float Grow;
|
||||
public bool DrawFill = true;
|
||||
/// <summary>Content margins (Godot content_margin_*), applied as padding.</summary>
|
||||
public float PadX = 12f;
|
||||
public float PadY = 12f;
|
||||
|
||||
/// <summary>SaumwachtTheme.plate (inner .045) plus LuneUiTheme.plate (inner .035, inset 3, margins).</summary>
|
||||
public static LunePlateStyle Hud(Color top, Color bottom, Color edge, float edgeWidth = 1f, float cut = 6f, float padX = 12f, float padY = 10f)
|
||||
{
|
||||
return new LunePlateStyle { FillTop = top, FillBottom = bottom, Edge = edge, EdgeWidth = edgeWidth, Chamfer = cut, Inner = new Color(1f, 1f, 1f, .035f), InnerInset = 3f, PadX = padX, PadY = padY };
|
||||
}
|
||||
|
||||
/// <summary>SaumwachtTheme.plate with margins: menu plates (inner .045, inset 4).</summary>
|
||||
public static LunePlateStyle Menu(Color top, Color bottom, Color edge, float edgeWidth = 1.5f, float cut = 7f, float padX = 12f, float padY = 12f)
|
||||
{
|
||||
return new LunePlateStyle { FillTop = top, FillBottom = bottom, Edge = edge, EdgeWidth = edgeWidth, Chamfer = cut, Inner = new Color(1f, 1f, 1f, .045f), InnerInset = 4f, PadX = padX, PadY = padY };
|
||||
}
|
||||
|
||||
/// <summary>Keyboard focus ring outside the element (SaumwachtTheme.focus_ring).</summary>
|
||||
public static LunePlateStyle FocusRing(float cut = 7f)
|
||||
{
|
||||
return new LunePlateStyle { DrawFill = false, Edge = LuneUiTheme.Focus, EdgeWidth = 2.5f, Inner = LuneUiTheme.WithAlpha(LuneUiTheme.Focus, .55f), InnerInset = 4.5f,
|
||||
Chamfer = cut + 4f, Grow = 4f, Shadow = LuneUiTheme.WithAlpha(LuneUiTheme.Focus, .22f), ShadowSize = 5f, PadX = 0f, PadY = 0f };
|
||||
}
|
||||
|
||||
public void ApplyPadding(VisualElement element)
|
||||
{
|
||||
element.style.paddingLeft = PadX;
|
||||
element.style.paddingRight = PadX;
|
||||
element.style.paddingTop = PadY;
|
||||
element.style.paddingBottom = PadY;
|
||||
}
|
||||
|
||||
public LunePlateStyle WithShadow(Color shadow, float size)
|
||||
{
|
||||
var copy = (LunePlateStyle)MemberwiseClone();
|
||||
copy.Shadow = shadow;
|
||||
copy.ShadowSize = size;
|
||||
return copy;
|
||||
}
|
||||
|
||||
public LunePlateStyle WithInner(Color inner)
|
||||
{
|
||||
var copy = (LunePlateStyle)MemberwiseClone();
|
||||
copy.Inner = inner;
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
|
||||
public static class LunePlates
|
||||
{
|
||||
static readonly Vector2[] Points = new Vector2[8];
|
||||
|
||||
/// <summary>The eight corners of a chamfered rectangle, clockwise from the top left cut.</summary>
|
||||
public static Vector2[] PlatePoints(Rect rect, float cut, Vector2[] into = null)
|
||||
{
|
||||
Vector2[] result = into ?? new Vector2[8];
|
||||
float c = Mathf.Min(cut, Mathf.Min(rect.width, rect.height) * .5f);
|
||||
Vector2 p = rect.min;
|
||||
Vector2 e = rect.max;
|
||||
result[0] = new Vector2(p.x + c, p.y);
|
||||
result[1] = new Vector2(e.x - c, p.y);
|
||||
result[2] = new Vector2(e.x, p.y + c);
|
||||
result[3] = new Vector2(e.x, e.y - c);
|
||||
result[4] = new Vector2(e.x - c, e.y);
|
||||
result[5] = new Vector2(p.x + c, e.y);
|
||||
result[6] = new Vector2(p.x, e.y - c);
|
||||
result[7] = new Vector2(p.x, p.y + c);
|
||||
return result;
|
||||
}
|
||||
|
||||
static Rect Grow(Rect rect, float amount)
|
||||
{
|
||||
return new Rect(rect.x - amount, rect.y - amount, rect.width + amount * 2f, rect.height + amount * 2f);
|
||||
}
|
||||
|
||||
public static void Draw(MeshGenerationContext context, Rect rect, LunePlateStyle style)
|
||||
{
|
||||
Rect r = Grow(rect, style.Grow);
|
||||
if (r.width < 2f || r.height < 2f)
|
||||
return;
|
||||
Painter2D painter = context.painter2D;
|
||||
if (style.Shadow.a > 0f && style.ShadowSize > 0f)
|
||||
{
|
||||
for (int layer = 0; layer < 3; layer++)
|
||||
{
|
||||
float spread = style.ShadowSize * (layer + 1) / 3f;
|
||||
var soft = new Color(style.Shadow.r, style.Shadow.g, style.Shadow.b, style.Shadow.a / 3f);
|
||||
if (style.DrawFill)
|
||||
{
|
||||
Rect offset = new Rect(r.x, r.y + spread * .5f, r.width, r.height);
|
||||
Polygon(painter, PlatePoints(Grow(offset, spread), style.Chamfer + spread, Points), soft);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Rings only glow outwards; the element underneath stays untinted.
|
||||
Outline(painter, PlatePoints(Grow(r, spread), style.Chamfer + spread, Points), soft, style.ShadowSize / 3f + 1f);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (style.DrawFill)
|
||||
Gradient(context, PlatePoints(r, style.Chamfer, Points), r, style.FillTop, style.FillBottom);
|
||||
if (style.Highlight.a > 0f)
|
||||
{
|
||||
float y = r.y + style.EdgeWidth + 1f;
|
||||
Line(painter, new Vector2(r.x + style.Chamfer, y), new Vector2(r.xMax - style.Chamfer, y), style.Highlight, 1f);
|
||||
}
|
||||
if (style.Inner.a > 0f && style.InnerInset > 0f)
|
||||
Outline(painter, PlatePoints(Grow(r, -style.InnerInset), Mathf.Max(1f, style.Chamfer - style.InnerInset * .45f), Points), style.Inner, 1f);
|
||||
if (style.Edge.a > 0f && style.EdgeWidth > 0f)
|
||||
Outline(painter, PlatePoints(Grow(r, -style.EdgeWidth * .5f), style.Chamfer, Points), style.Edge, style.EdgeWidth);
|
||||
}
|
||||
|
||||
/// <summary>Convex polygon with a vertical colour gradient (Godot: polygon with per-vertex colours).</summary>
|
||||
public static void Gradient(MeshGenerationContext context, Vector2[] points, Rect bounds, Color top, Color bottom)
|
||||
{
|
||||
int count = points.Length;
|
||||
MeshWriteData mesh = context.Allocate(count + 1, count * 3);
|
||||
Vector2 center = Vector2.zero;
|
||||
foreach (Vector2 point in points)
|
||||
center += point;
|
||||
center /= count;
|
||||
mesh.SetNextVertex(new Vertex { position = new Vector3(center.x, center.y, Vertex.nearZ), tint = Color.Lerp(top, bottom, Fraction(center.y, bounds)) });
|
||||
foreach (Vector2 point in points)
|
||||
mesh.SetNextVertex(new Vertex { position = new Vector3(point.x, point.y, Vertex.nearZ), tint = Color.Lerp(top, bottom, Fraction(point.y, bounds)) });
|
||||
// Clockwise triangles in screen space (y down).
|
||||
for (int index = 0; index < count; index++)
|
||||
{
|
||||
mesh.SetNextIndex(0);
|
||||
mesh.SetNextIndex((ushort)(1 + index));
|
||||
mesh.SetNextIndex((ushort)(1 + (index + 1) % count));
|
||||
}
|
||||
}
|
||||
|
||||
static float Fraction(float y, Rect bounds)
|
||||
{
|
||||
return Mathf.Clamp01((y - bounds.y) / Mathf.Max(1f, bounds.height));
|
||||
}
|
||||
|
||||
public static void Polygon(Painter2D painter, Vector2[] points, Color color)
|
||||
{
|
||||
painter.fillColor = color;
|
||||
painter.BeginPath();
|
||||
painter.MoveTo(points[0]);
|
||||
for (int index = 1; index < points.Length; index++)
|
||||
painter.LineTo(points[index]);
|
||||
painter.ClosePath();
|
||||
painter.Fill();
|
||||
}
|
||||
|
||||
public static void Outline(Painter2D painter, Vector2[] points, Color color, float width)
|
||||
{
|
||||
painter.strokeColor = color;
|
||||
painter.lineWidth = width;
|
||||
painter.lineJoin = LineJoin.Miter;
|
||||
painter.BeginPath();
|
||||
painter.MoveTo(points[0]);
|
||||
for (int index = 1; index < points.Length; index++)
|
||||
painter.LineTo(points[index]);
|
||||
painter.ClosePath();
|
||||
painter.Stroke();
|
||||
}
|
||||
|
||||
public static void Line(Painter2D painter, Vector2 from, Vector2 to, Color color, float width)
|
||||
{
|
||||
painter.strokeColor = color;
|
||||
painter.lineWidth = width;
|
||||
painter.lineCap = LineCap.Butt;
|
||||
painter.BeginPath();
|
||||
painter.MoveTo(from);
|
||||
painter.LineTo(to);
|
||||
painter.Stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Luneraria/UI/Theme/LunePlates.cs.meta
Normal file
2
Assets/Luneraria/UI/Theme/LunePlates.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7c1d1aab2221b4145b24e47b3401e048
|
||||
107
Assets/Luneraria/UI/Theme/LuneUiTheme.cs
Normal file
107
Assets/Luneraria/UI/Theme/LuneUiTheme.cs
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Luneraria.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Colours and number formats of the Champions of Luneraria HUD (port of
|
||||
/// scripts/luneraria/ui/theme/lune_ui_theme.gd and the colours of the Saumwacht menu theme it
|
||||
/// extends): dark blue-anthracite plates, bronze and gold only on what the player can act on,
|
||||
/// quiet team accents. Colours are sRGB, as in Godot.
|
||||
/// </summary>
|
||||
public static class LuneUiTheme
|
||||
{
|
||||
public static readonly Color SlateTop = new Color(.075f, .1f, .135f, .9f);
|
||||
public static readonly Color SlateBottom = new Color(.05f, .068f, .094f, .93f);
|
||||
public static readonly Color Edge = Hex("2b3848");
|
||||
public static readonly Color EdgeLight = Hex("455870");
|
||||
public static readonly Color Text = Hex("ebe5d8");
|
||||
public static readonly Color Bright = Hex("f6eedc");
|
||||
public static readonly Color Muted = Hex("9aa4b0");
|
||||
public static readonly Color Faint = Hex("6a7480");
|
||||
public static readonly Color Gold = Hex("f2c96b");
|
||||
// Saumwacht menu theme (saumwacht_theme.gd).
|
||||
public static readonly Color Bronze = Hex("b7874a");
|
||||
public static readonly Color BronzeLight = Hex("e4bd7c");
|
||||
public static readonly Color BronzeDeep = Hex("6b4f2e");
|
||||
public static readonly Color Ink = Hex("1a120a");
|
||||
public static readonly Color Focus = Hex("f6e2b0");
|
||||
public static readonly Color Parchment = Hex("efe6d2");
|
||||
/// <summary>Team accents: Blau (cool, crescent moon), Rot (warm, sun disc), neutral grotto (violet).</summary>
|
||||
public static readonly Color[] Team = { Hex("6cb4ff"), Hex("ff7b5e"), Hex("b78cff") };
|
||||
public static readonly Color[] TeamDark = { Hex("1c3f6a"), Hex("722a1f"), Hex("45305f") };
|
||||
public static readonly string[] TeamNames = { "Blau", "Rot", "Grotte" };
|
||||
public static readonly string[] TeamEmblems = { "Mondsichel", "Sonnenscheibe", "Grotte" };
|
||||
/// <summary>Threat levels RUHIG, UMKÄMPFT, UNTER DRUCK, KRITISCH.</summary>
|
||||
public static readonly Color[] Threat = { Hex("7cc9a6"), Hex("e5c35e"), Hex("ff9448"), Hex("ff584a") };
|
||||
public static readonly Color Champion = Hex("e2a15e");
|
||||
public static readonly Color Hero = Hex("f2c96b");
|
||||
public static readonly Color Good = Hex("8fdcb4");
|
||||
public static readonly Color Warn = Hex("f0c060");
|
||||
public static readonly Color Danger = Hex("ff6a55");
|
||||
public static readonly Color Info = Hex("a9c6e8");
|
||||
/// <summary>Lower bound for any HUD text: 14 logical px stay above 10 px at 1280×720.</summary>
|
||||
public const int MinFont = 14;
|
||||
|
||||
public static Color Hex(string hex)
|
||||
{
|
||||
if (!ColorUtility.TryParseHtmlString("#" + hex, out Color color))
|
||||
throw new ArgumentException("Bad colour " + hex);
|
||||
return color;
|
||||
}
|
||||
|
||||
/// <summary>Godot Color.darkened: rgb times (1 - amount), alpha kept.</summary>
|
||||
public static Color Darkened(Color color, float amount)
|
||||
{
|
||||
return new Color(color.r * (1f - amount), color.g * (1f - amount), color.b * (1f - amount), color.a);
|
||||
}
|
||||
|
||||
/// <summary>Godot Color.lightened: rgb towards white by amount, alpha kept.</summary>
|
||||
public static Color Lightened(Color color, float amount)
|
||||
{
|
||||
return new Color(color.r + (1f - color.r) * amount, color.g + (1f - color.g) * amount, color.b + (1f - color.b) * amount, color.a);
|
||||
}
|
||||
|
||||
public static Color WithAlpha(Color color, float alpha)
|
||||
{
|
||||
return new Color(color.r, color.g, color.b, alpha);
|
||||
}
|
||||
|
||||
public static Color TeamColor(int team)
|
||||
{
|
||||
return Team[Mathf.Clamp(team, 0, 2)];
|
||||
}
|
||||
|
||||
public static string TeamName(int team)
|
||||
{
|
||||
return TeamNames[Mathf.Clamp(team, 0, 2)];
|
||||
}
|
||||
|
||||
/// <summary>Whole numbers with German thousands separators (12.000), rounded like Godot (half away from zero).</summary>
|
||||
public static string Number(double value)
|
||||
{
|
||||
long whole = (long)Math.Round(value, MidpointRounding.AwayFromZero);
|
||||
string digits = Math.Abs(whole).ToString(CultureInfo.InvariantCulture);
|
||||
string result = "";
|
||||
while (digits.Length > 3)
|
||||
{
|
||||
result = "." + digits.Substring(digits.Length - 3) + result;
|
||||
digits = digits.Substring(0, digits.Length - 3);
|
||||
}
|
||||
return (whole < 0 ? "-" : "") + digits + result;
|
||||
}
|
||||
|
||||
/// <summary>Fixed decimals with a decimal comma (Godot: "%.1f" % value with "." replaced).</summary>
|
||||
public static string Decimal(double value, int places = 1)
|
||||
{
|
||||
return value.ToString("F" + places, CultureInfo.InvariantCulture).Replace(".", ",");
|
||||
}
|
||||
|
||||
/// <summary>Godot round(): half away from zero.</summary>
|
||||
public static int Round(double value)
|
||||
{
|
||||
return (int)Math.Round(value, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Luneraria/UI/Theme/LuneUiTheme.cs.meta
Normal file
2
Assets/Luneraria/UI/Theme/LuneUiTheme.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 10ef80825d4ba4b23904f45aa397b86e
|
||||
|
|
@ -4,18 +4,27 @@ using UnityEngine;
|
|||
namespace Luneraria.View
|
||||
{
|
||||
/// <summary>
|
||||
/// Hosts a bot match in the scene (U2C/U2D). Update only hands frame time (times Speed) to the
|
||||
/// runner's fixed clock; it never decides anything in combat. Seed, start round and extra gold
|
||||
/// are the public fixtures of the golden scenarios.
|
||||
/// Hosts a match in the scene (U2C/U2D; human seat and tempo since U3). Update only hands frame
|
||||
/// time (times the speed) to the runner's fixed clock; it never decides anything in combat.
|
||||
/// Seed, start round and extra gold are the public fixtures of the golden scenarios. With
|
||||
/// HumanSlot >= 0 the HUD plans that seat and planning waits for its Ready; tempo steps
|
||||
/// (1×, 2×, 4×) and pause act only in combat, as in Godot (lune_game.gd).
|
||||
/// </summary>
|
||||
public sealed class LuneMatchDriver : MonoBehaviour
|
||||
{
|
||||
/// <summary>Tempo steps of the HUD's tempo button.</summary>
|
||||
public static readonly int[] SpeedSteps = { 1, 2, 4 };
|
||||
|
||||
public int Seed = 2;
|
||||
public int StartRound = 1;
|
||||
public int ExtraGold;
|
||||
public string Profile = "";
|
||||
/// <summary>Match speed: simulated seconds per real second.</summary>
|
||||
/// <summary>The seat a human plays through the HUD; -1 for a bot match (U2 tests, benchmark).</summary>
|
||||
public int HumanSlot = -1;
|
||||
/// <summary>Match speed: simulated seconds per real second (tests run faster than 1).</summary>
|
||||
public float Speed = 1f;
|
||||
/// <summary>Index into SpeedSteps (the player's tempo, on top of Speed).</summary>
|
||||
public int SpeedIndex;
|
||||
public bool Paused;
|
||||
public bool RecordTrace = true;
|
||||
|
||||
|
|
@ -24,11 +33,30 @@ namespace Luneraria.View
|
|||
public double LastAdvanceMs { get; private set; }
|
||||
readonly System.Diagnostics.Stopwatch _clock = new System.Diagnostics.Stopwatch();
|
||||
|
||||
public int Tempo { get { return SpeedSteps[Mathf.Clamp(SpeedIndex, 0, SpeedSteps.Length - 1)]; } }
|
||||
|
||||
public void Restart()
|
||||
{
|
||||
Runner = new LuneMatchRunner(new MatchOptions { Seed = Seed, HumanSlot = -1 }, StartRound, ExtraGold, Profile);
|
||||
Runner = new LuneMatchRunner(new MatchOptions { Seed = Seed, HumanSlot = HumanSlot }, StartRound, ExtraGold, Profile);
|
||||
if (RecordTrace)
|
||||
Runner.Trace = new LuneTrace();
|
||||
SpeedIndex = 0;
|
||||
Paused = false;
|
||||
}
|
||||
|
||||
/// <summary>Next tempo step (1× → 2× → 4× → 1×).</summary>
|
||||
public void CycleSpeed()
|
||||
{
|
||||
SpeedIndex = (SpeedIndex + 1) % SpeedSteps.Length;
|
||||
}
|
||||
|
||||
/// <summary>Pause exists only in combat; planning waits for the player anyway.</summary>
|
||||
public bool TogglePause()
|
||||
{
|
||||
if (Runner == null || Runner.Current != LuneMatchRunner.Stage.Combat)
|
||||
return false;
|
||||
Paused = !Paused;
|
||||
return true;
|
||||
}
|
||||
|
||||
void Awake()
|
||||
|
|
@ -40,8 +68,9 @@ namespace Luneraria.View
|
|||
void Update()
|
||||
{
|
||||
_clock.Restart();
|
||||
if (!Paused)
|
||||
Runner.Advance(Time.deltaTime * Speed);
|
||||
// Pause holds only the combat steps (Godot: a paused game still plans and settles).
|
||||
if (!(Paused && Runner.Current == LuneMatchRunner.Stage.Combat))
|
||||
Runner.Advance(Time.deltaTime * Speed * Tempo);
|
||||
LastAdvanceMs = _clock.Elapsed.TotalMilliseconds;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,13 @@ using Luneraria.Core;
|
|||
namespace Luneraria.View
|
||||
{
|
||||
/// <summary>
|
||||
/// Runs a bot match on the host (U2C/U2D) against wall-clock time: planning (the six bots plan
|
||||
/// through the command interface), combat in fixed steps, a pause after the settlement, the
|
||||
/// next round, until an Ancient falls. Only the core decides; time only paces the calls, so
|
||||
/// the sequence of core calls is the one of LuneTrace.RunCore at every frame rate.
|
||||
/// Runs a match on the host (U2C/U2D, human seat since U3) against wall-clock time: planning
|
||||
/// (the bots plan through the command interface), combat in fixed steps, a pause after the
|
||||
/// settlement, the next round, until an Ancient falls. Only the core decides; time only paces
|
||||
/// the calls, so a bot match calls the core in the order of LuneTrace.RunCore at every frame
|
||||
/// rate. With a human seat (HumanSlot >= 0) the bots plan the other five seats and planning
|
||||
/// waits for the human's Ready, as offline in Godot (lune_game.gd); there is no pause after
|
||||
/// the settlement.
|
||||
/// </summary>
|
||||
public sealed class LuneMatchRunner
|
||||
{
|
||||
|
|
@ -16,6 +19,8 @@ namespace Luneraria.View
|
|||
public readonly LuneMatch Match;
|
||||
public readonly LuneClock Clock;
|
||||
public readonly string Profile;
|
||||
/// <summary>The seat a human plans through the HUD; -1 for a bot match.</summary>
|
||||
public readonly int HumanSlot;
|
||||
public LuneTrace Trace;
|
||||
public double PlanningSeconds = 1.0;
|
||||
public double SettledSeconds = 2.0;
|
||||
|
|
@ -34,6 +39,9 @@ namespace Luneraria.View
|
|||
Match = NewMatch(options, startRound, extraGold);
|
||||
Clock = new LuneClock(Match.Balance.SimStep);
|
||||
Profile = profile;
|
||||
HumanSlot = options.HumanSlot >= 0 && options.HumanSlot < 6 ? options.HumanSlot : -1;
|
||||
if (HumanSlot >= 0)
|
||||
SettledSeconds = 0.0;
|
||||
EnterPlanning();
|
||||
}
|
||||
|
||||
|
|
@ -55,8 +63,11 @@ namespace Luneraria.View
|
|||
Current = Stage.Planning;
|
||||
_stageTime = 0.0;
|
||||
for (int slot = 0; slot < 6; slot++)
|
||||
{
|
||||
if (slot != HumanSlot)
|
||||
LuneBot.Plan(Match, slot, Profile);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Advances by simulated seconds (frame time times match speed).</summary>
|
||||
public void Advance(double seconds)
|
||||
|
|
@ -66,13 +77,9 @@ namespace Luneraria.View
|
|||
{
|
||||
case Stage.Planning:
|
||||
_stageTime += seconds;
|
||||
if (_stageTime >= PlanningSeconds)
|
||||
{
|
||||
Match.BeginCombat();
|
||||
Clock.Reset();
|
||||
Current = Stage.Combat;
|
||||
CombatStarted?.Invoke(Match.Combat);
|
||||
}
|
||||
// A bot match starts after a short look at the plans; a human starts it with Ready.
|
||||
if (HumanSlot < 0 ? _stageTime >= PlanningSeconds : Match.AllReady())
|
||||
StartCombat();
|
||||
break;
|
||||
case Stage.Combat:
|
||||
StepsThisFrame = Clock.Advance(seconds, StepOnce);
|
||||
|
|
@ -87,6 +94,28 @@ namespace Luneraria.View
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The human's Ready (Godot _on_ready): the plan becomes binding; once every seat is ready
|
||||
/// the combat starts at once.
|
||||
/// </summary>
|
||||
public bool Ready()
|
||||
{
|
||||
if (Current != Stage.Planning || HumanSlot < 0 || Match.Phase != Phase.Planning)
|
||||
return false;
|
||||
Match.SetReady(HumanSlot);
|
||||
if (Match.AllReady())
|
||||
StartCombat();
|
||||
return true;
|
||||
}
|
||||
|
||||
void StartCombat()
|
||||
{
|
||||
Match.BeginCombat();
|
||||
Clock.Reset();
|
||||
Current = Stage.Combat;
|
||||
CombatStarted?.Invoke(Match.Combat);
|
||||
}
|
||||
|
||||
bool StepOnce()
|
||||
{
|
||||
Match.StepCombat();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
# U3 · G3-HUD portieren
|
||||
|
||||
Stand: 30. September 2026 · **nicht begonnen**.
|
||||
Stand: 1. Oktober 2026.
|
||||
|
||||
| Teil | Inhalt | Zustand |
|
||||
| --- | --- | --- |
|
||||
| U3A | Entscheidung UI Toolkit, Sichtmodell mit Privatsphäre, menschlicher Sitz, Theme-Grundlagen (Farben, Platten, Symbole, Schriften) | **fertig** |
|
||||
| U3B | Komponenten, Porträts, Tooltip, alle HUD-Bereiche, Optionen, Partieende | in Arbeit |
|
||||
| U3C | Eingabe und Befehle: Tasten, Kamera-Ziele, Auswahl im Feld, Bereit-Sperre, Planungsvorschau | offen |
|
||||
| U3D | Prüfung (Teile A–H von `verify_g3`), Spieltest über die echte Oberfläche, Bilder, Leistung | offen |
|
||||
|
||||
Funktionale Referenz ist das G3-HUD aus Godot (`../saumwacht-godot/docs/luneraria/G3-HUD.md`, Commit `1b540f2`). Es wird übertragen, nicht neu erfunden.
|
||||
|
||||
|
|
@ -14,8 +21,78 @@ Funktionale Referenz ist das G3-HUD aus Godot (`../saumwacht-godot/docs/lunerari
|
|||
- Minimap, Bereit-Zustand, Tooltips, Ereignisse.
|
||||
- Datenzugang über ein Sichtmodell mit denselben Privatsphäre-Regeln wie `LuneHudModel`.
|
||||
|
||||
## Offene Entscheidung
|
||||
|
||||
UI Toolkit oder uGUI: vor Beginn eine kurze technische Bewertung, dann **ein** System als Hauptlösung.
|
||||
|
||||
Die geplanten Helden (`PlannedHeroes`) mit Command-Menü für Ultimates sind im Layout vorzusehen.
|
||||
|
||||
Der Bestand in Godot umfasst rund 5.100 Zeilen:
|
||||
- `lune_hud.gd` (1.058 Zeilen),
|
||||
- `lune_hud_model.gd` (333),
|
||||
- 22 Komponenten,
|
||||
- Theme (`lune_ui_theme.gd`, `forged_style.gd`) und Vektorsymbole (`lune_glyphs.gd`),
|
||||
- Minimap, Porträt-Cache, Optionen,
|
||||
- die Verdrahtung in `lune_game.gd` (Befehle, Tasten, Kamera, Auswahl).
|
||||
|
||||
Das Hauptmenü (`lune_menu.gd`, mit Netzwerk-Host/Join) ist der Startbildschirm des Spiels und gehört nicht zum HUD.
|
||||
|
||||
## Entscheidung: UI Toolkit
|
||||
|
||||
Vor Beginn war eine kurze technische Bewertung gefordert, dann **ein** System als Hauptlösung. Gewählt ist **UI Toolkit**; uGUI kommt im HUD nicht vor.
|
||||
|
||||
| Kriterium | UI Toolkit | uGUI |
|
||||
| --- | --- | --- |
|
||||
| Aufbau wie in Godot (Komponenten im Code, Container) | `VisualElement` und Flexbox entsprechen Godots Box-Containern; Komponenten sind C#-Klassen | GameObjects mit RectTransform und Layout-Gruppen; jede Zeile ein Objekt, Layout-Gruppen teuer beim Neuaufbau |
|
||||
| Eigenes Zeichnen (Godot `_draw`: Platten, 50 Symbole, Balken, Minimap) | `generateVisualContent` mit `Painter2D` (Pfade, Bögen, konkave Flächen) und eigenen Meshes (Verlauf) | eigene `Graphic`-Klassen; konkave Flächen müsste man selbst triangulieren |
|
||||
| Skalierung wie Godot (`canvas_items`, `expand`, Basis 1600×960) | `PanelSettings`: Scale With Screen Size, Expand, 1600×960 ergibt bei 16:9 dieselbe logische Fläche 1706×960 | `CanvasScaler` gleichwertig |
|
||||
| Prüfbarkeit im Batchmodus | Panel rendert in eine RenderTexture beliebiger Größe; Elemente per Name und Pick abfragbar | Canvas über eine Kamera rendern; Abfragen über GameObjects |
|
||||
| Tooltips, Weltelemente | Laufzeit-Tooltip selbst bauen (Godot hatte ohnehin einen eigenen); Weltbalken sind schon Meshes | eingebaut nur im Editor; Weltraum-Canvas reif |
|
||||
| Empfehlung in Unity 6 | empfohlenes System für neue Laufzeit-Oberflächen | weiter unterstützt, älterer Ansatz |
|
||||
|
||||
**Probe vor der Entscheidung** (`Tests/UI/PlayMode/UiToolkitProbeTests`, Bild `screenshots/u3/00-ui-toolkit-probe.png`):
|
||||
- Ein Panel rendert im Batchmodus in eine 1920×1080-Textur.
|
||||
- Die Position stimmt mit Godot überein: 12 logische Pixel ergeben 13,5 Pixel bei 1920×1080.
|
||||
- Die Platten mit Fasen, Verlauf, Innen- und Kantenlinie entstehen korrekt.
|
||||
- Alle 50 Symbole zeichnen sich, auch die konkaven (Mondsichel, Turm, Kreuz).
|
||||
- Die Schriften kommen aus Godots variablen Fonts.
|
||||
|
||||
## U3A · Grundlagen
|
||||
|
||||
### Sichtmodell und Privatsphäre
|
||||
|
||||
Portiert nach `Assets/Luneraria/UI/Model/`, Verhalten wie in Godot:
|
||||
|
||||
| Datei | Inhalt |
|
||||
| --- | --- |
|
||||
| `LuneHudModel.cs` | einziger Datenzugang des HUD: Phase, eigener Sitz (Gold, Entwurf, Rückgängig, Buchungen), Gebäude, Ancients, Mitspieler, Lanes, Boss, nächste Welle, öffentlicher Digest |
|
||||
| `LuneLaneThreat.cs` | Lane-Bedrohung RUHIG · UMKÄMPFT · UNTER DRUCK · KRITISCH, nur aus öffentlichem Zustand (Turm-HP, Positionen, Ankündigungen) |
|
||||
| `LuneStats.cs` | Wertzeilen und Tooltip-Inhalte aus den echten Definitionen (Schaden je Treffer, Tempo, Reichweite, Rüstungsmatrix) |
|
||||
|
||||
Die Privatsphäre-Regeln gelten unverändert:
|
||||
- Gold, Entwurf, Rückgängig und neue Buchungen gibt es nur für den eigenen Sitz.
|
||||
- Von anderen sieht man Name, Lane, Bereitschaft, Mensch/Bot/offline und den bestätigten Helden.
|
||||
- Lanes, Gebäude, Boss und Wellen kommen aus öffentlichem Zustand.
|
||||
|
||||
Eine Abweichung: Im öffentlichen Digest (Testhilfe) steht beim Boss die öffentliche Claim-Lane. Godot nahm dort auch die private Lane-Wahl des Wählenden auf, sodass der Digest für ihn vom Bild der anderen abweichen konnte.
|
||||
|
||||
### Menschlicher Sitz
|
||||
|
||||
- `LuneMatchRunner` und `LuneMatchDriver` haben einen menschlichen Sitz bekommen (`HumanSlot`).
|
||||
- Die Bots planen nur die anderen fünf Sitze. Die Planung wartet auf „Bereit“ des Menschen, wie offline in Godot (`lune_game.gd`), und nach der Abrechnung beginnt sofort die nächste Planung.
|
||||
- Tempo (1×, 2×, 4×) und Pause wirken nur im Kampf.
|
||||
- Bot-Partien (`HumanSlot = -1`) laufen unverändert. Die Bildraten-Tests prüfen das mit denselben Fingerabdrücken.
|
||||
|
||||
### Theme, Symbole, Schriften
|
||||
|
||||
- `Theme/LuneUiTheme.cs`: alle Farben aus `lune_ui_theme.gd` und dem Saumwacht-Menütheme, Zahlenformate (12.000, Dezimalkomma).
|
||||
- `Theme/LunePlates.cs`: die Platten aus `forged_style.gd` (Fase, Verlauf, Innenlinie, Kante, Schatten, Fokusring).
|
||||
- `Theme/LuneGlyphs.cs`: alle 50 Symbole aus `lune_glyphs.gd` mit `Painter2D`.
|
||||
- Schriften: Cantarell und Cinzel als statische SDF-Atlanten (`LuneUiAssetsBuilder`, `Settings/LuneUi.asset`, Lizenz OFL).
|
||||
- Unitys Textsystem hat keine Gewichtsachse. FreeType lädt aber die benannten Instanzen der variablen Fonts: Cantarell Regular und Bold, Cinzel Bold.
|
||||
- Godots Gewicht 700 und darüber (Versalien, Werte, Titel) passt damit genau.
|
||||
- Das Gewicht 560 der Fließtexte hat keine benannte Instanz. Es entsteht aus Regular mit einer 0,25-px-Kontur in der Textfarbe; im Vergleichsbild wirkt das wie Medium.
|
||||
- In Cantarell fehlt „★“ (Godot nahm eine Ersatzschrift). Das HUD zeichnet den Stern als Symbol.
|
||||
|
||||
### Prüfung U3A
|
||||
|
||||
| Test | Ergebnis |
|
||||
| --- | --- |
|
||||
| EditMode gesamt | 23/23, davon 12 neue: öffentliches Bild für alle sechs Sitze gleich (Planung und Kampf), fremde Entwürfe (Held, Champion-Buchung) bis zum Kampf unsichtbar, private Boss-Lane-Wahl, Lane-Bedrohung bei Turm-HP und Ankündigungen, Einheitenzahlen je Lane, Bosszustände, nächste Welle gleich der Regelwelle (Runden 1–24), Bereit-Sperre, Zahlenformate, menschlicher Sitz, Bot-Partie unverändert |
|
||||
| PlayMode-Probe | UI Toolkit rendert im Batchmodus (siehe oben) |
|
||||
|
|
|
|||
|
|
@ -2,6 +2,14 @@
|
|||
|
||||
Stand: 30. September 2026 · **nicht begonnen**. U5 ist die eigentliche Begründung für den Unity-Wechsel.
|
||||
|
||||
## Vorgaben des Projektinhabers (1. Oktober 2026)
|
||||
|
||||
- **Reihenfolge unverändert:** erst U3 (HUD) und U4 (Netzwerk), dann U5. Die Modelle werden nicht vorgezogen.
|
||||
- **Stil:** stilisierter Realismus wie Dota 2 und aktuelle MOBAs. Dazu gehören PBR-Oberflächen, ausgeformte Details und glaubwürdige Proportionen. Die Silhouetten bleiben klar und aus der Draufsicht auch bei über 100 Einheiten lesbar.
|
||||
- **Umfang:** Einheiten, Gebäude und Umgebung, also die ganze Welt, nicht nur eine repräsentative Gruppe.
|
||||
- **Weg:** Die Quelle bleiben Blender-Generatoren im Repository mit reproduzierbarem FBX-Export und den U2A-Prüfungen. Dazu kommen gebackene PBR-Texturen (Albedo, Normal, Rauigkeit/Metall, AO).
|
||||
- **Grenze:** Figuren mit handmodellierter Anatomie, Gesichtern und Haaren entstehen nicht prozedural. Sie brauchen Sculpts von Künstlern oder lizenzierte Asset-Bibliotheken. Downloads dafür gibt es nur nach Freigabe und Lizenzprüfung.
|
||||
|
||||
## Vertical Slice
|
||||
|
||||
Eine repräsentative Mid-Lane:
|
||||
|
|
|
|||
BIN
docs/port/screenshots/u3/00-ui-toolkit-probe.png
Normal file
BIN
docs/port/screenshots/u3/00-ui-toolkit-probe.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 146 KiB |
Loading…
Reference in a new issue