U3D Abnahme: Prüfung, Feld um das HUD, Bilder, Leistung
Das G3-HUD ist abgenommen; der Meilenstein U3 ist damit fertig. - Prüfung (Port von verify_g3, Teile A–H plus I): 718 Prüfungen ohne Fehler. Teil I prüft die Feld-Darstellung um das HUD (U3-Zugabe aus dem Motivvergleich): Aufstellungsflächen, Grotteneingang, ruhender Boss, Namensschilder, Eigentümer-Ring, Ancient-Balken. - Feld um das HUD: Namensschilder für Helden (mit Stufe), Angriffs- champions und den eingesetzten Boss („für Blau“), Eigentümer-Ring, Ancient-Balken bei Schaden, ruhende Schlundkrone auf dem Podest, eigener Grotteneingang, Aufstellungsflächen als eigene Objekte. - Schriften korrigiert: Die Referenz deklariert Cantarell 560/700–760 und Cinzel 700, wendet die Gewichte aber nie an (variation_opentype mit dem String-Schlüssel „wght“ wird von Godot ignoriert). G3 zeigt Regular 400, der Port ebenso — Cinzel-Regular statt Bold, keine Kontur zur Aufdickung. Laufweite aus spacing_glyph umgerechnet; dunkler Saum als Schatten statt TextCore-Kontur. - Werkzeuge: HudViewsTests (14 G3-Motive), HudPlaytest (Spieltest über die echte Oberfläche per Befehlsdateien), ui_ms_per_frame im LuneBenchmark. Layout folgt den echten Panelhöhen, Kamera startet wie Godot _ready auf der eigenen Lane. - Tests: EditMode 23/23, PlayMode 13/13, Kerntests 402/402, Verify 718/0. Fingerabdrücke der Bot-Partien unverändert (DCE95C96556F3A1A, 7B8E43AEFC0D60C5). - Bilder: 14 Motive bei 1920×1080, Auflösungen 1152×720 bis 2560×1440, Prüf-Schlüsselbilder, Spieltest-Screenshots. - Leistung (macOS-Player mit HUD, 116 Einheiten): 169 fps Übersicht, 218 fps Mid nah; HUD-Skripte 0,090 bzw. 0,078 ms je Bild. - Dokumentation: U3-UI (U3D, Schriftbefund), PORT-QA (U3), PORT-STATUS (U3 fertig, U4 nächster Schritt), KNOWN-BEHAVIOR-DIFFERENCES. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
180
Assets/Luneraria/Art/Generated/OwnerRingDiamond.asset
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!43 &4300000
|
||||
Mesh:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: OwnerRingDiamond
|
||||
serializedVersion: 12
|
||||
m_SubMeshes:
|
||||
- serializedVersion: 2
|
||||
firstByte: 0
|
||||
indexCount: 24
|
||||
topology: 0
|
||||
baseVertex: 0
|
||||
firstVertex: 0
|
||||
vertexCount: 10
|
||||
localAABB:
|
||||
m_Center: {x: -0.000000029802322, y: 0, z: -0.000000059604645}
|
||||
m_Extent: {x: 0.79195964, y: 0, z: 0.79195964}
|
||||
m_Shapes:
|
||||
vertices: []
|
||||
shapes: []
|
||||
channels: []
|
||||
fullWeights: []
|
||||
m_BindPose: []
|
||||
m_BoneNameHashes:
|
||||
m_RootBoneNameHash: 0
|
||||
m_BonesAABB: []
|
||||
m_VariableBoneCountWeights:
|
||||
m_Data:
|
||||
m_MeshCompression: 0
|
||||
m_IsReadable: 1
|
||||
m_KeepVertices: 1
|
||||
m_KeepIndices: 1
|
||||
m_IndexFormat: 0
|
||||
m_IndexBuffer: 000001000300000003000200020003000500020005000400040005000700040007000600060007000900060009000800
|
||||
m_VertexData:
|
||||
serializedVersion: 3
|
||||
m_VertexCount: 10
|
||||
m_Channels:
|
||||
- stream: 0
|
||||
offset: 0
|
||||
format: 0
|
||||
dimension: 3
|
||||
- stream: 0
|
||||
offset: 12
|
||||
format: 0
|
||||
dimension: 3
|
||||
- stream: 0
|
||||
offset: 0
|
||||
format: 0
|
||||
dimension: 0
|
||||
- stream: 0
|
||||
offset: 0
|
||||
format: 0
|
||||
dimension: 0
|
||||
- stream: 0
|
||||
offset: 0
|
||||
format: 0
|
||||
dimension: 0
|
||||
- stream: 0
|
||||
offset: 0
|
||||
format: 0
|
||||
dimension: 0
|
||||
- stream: 0
|
||||
offset: 0
|
||||
format: 0
|
||||
dimension: 0
|
||||
- stream: 0
|
||||
offset: 0
|
||||
format: 0
|
||||
dimension: 0
|
||||
- stream: 0
|
||||
offset: 0
|
||||
format: 0
|
||||
dimension: 0
|
||||
- stream: 0
|
||||
offset: 0
|
||||
format: 0
|
||||
dimension: 0
|
||||
- stream: 0
|
||||
offset: 0
|
||||
format: 0
|
||||
dimension: 0
|
||||
- stream: 0
|
||||
offset: 0
|
||||
format: 0
|
||||
dimension: 0
|
||||
- stream: 0
|
||||
offset: 0
|
||||
format: 0
|
||||
dimension: 0
|
||||
- stream: 0
|
||||
offset: 0
|
||||
format: 0
|
||||
dimension: 0
|
||||
m_DataSize: 240
|
||||
_typelessdata: f304353f00000000f304353f00000000000080bf00000000ddbd4a3f00000000ddbd4a3f00000000000080bf00000000f30435bf00000000f304353f00000000000080bf00000000ddbd4abf00000000ddbd4a3f00000000000080bf00000000f40435bf00000000f20435bf00000000000080bf00000000debd4abf00000000dcbd4abf00000000000080bf00000000f104353f00000000f50435bf00000000ffff7fbf00000000dbbd4a3f00000000dfbd4abf00000000000080bf00000000f304353f00000000f304353f00000000000080bf00000000ddbd4a3f00000000ddbd4a3f00000000000080bf00000000
|
||||
m_CompressedMesh:
|
||||
m_Vertices:
|
||||
m_NumItems: 0
|
||||
m_Range: 0
|
||||
m_Start: 0
|
||||
m_Data:
|
||||
m_BitSize: 0
|
||||
m_UV:
|
||||
m_NumItems: 0
|
||||
m_Range: 0
|
||||
m_Start: 0
|
||||
m_Data:
|
||||
m_BitSize: 0
|
||||
m_Normals:
|
||||
m_NumItems: 0
|
||||
m_Range: 0
|
||||
m_Start: 0
|
||||
m_Data:
|
||||
m_BitSize: 0
|
||||
m_Tangents:
|
||||
m_NumItems: 0
|
||||
m_Range: 0
|
||||
m_Start: 0
|
||||
m_Data:
|
||||
m_BitSize: 0
|
||||
m_Weights:
|
||||
m_NumItems: 0
|
||||
m_Data:
|
||||
m_BitSize: 0
|
||||
m_NormalSigns:
|
||||
m_NumItems: 0
|
||||
m_Data:
|
||||
m_BitSize: 0
|
||||
m_TangentSigns:
|
||||
m_NumItems: 0
|
||||
m_Data:
|
||||
m_BitSize: 0
|
||||
m_FloatColors:
|
||||
m_NumItems: 0
|
||||
m_Range: 0
|
||||
m_Start: 0
|
||||
m_Data:
|
||||
m_BitSize: 0
|
||||
m_BoneIndices:
|
||||
m_NumItems: 0
|
||||
m_Data:
|
||||
m_BitSize: 0
|
||||
m_Triangles:
|
||||
m_NumItems: 0
|
||||
m_Data:
|
||||
m_BitSize: 0
|
||||
m_UVInfo: 0
|
||||
m_LocalAABB:
|
||||
m_Center: {x: -0.000000029802322, y: 0, z: -0.000000059604645}
|
||||
m_Extent: {x: 0.79195964, y: 0, z: 0.79195964}
|
||||
m_MeshUsageFlags: 0
|
||||
m_CookingOptions: 30
|
||||
m_BakedConvexCollisionMesh:
|
||||
m_BakedTriangleCollisionMesh:
|
||||
'm_MeshMetrics[0]': 1
|
||||
'm_MeshMetrics[1]': 1
|
||||
m_MeshOptimizationFlags: 1
|
||||
m_StreamData:
|
||||
serializedVersion: 2
|
||||
offset: 0
|
||||
size: 0
|
||||
path:
|
||||
m_MeshLodInfo:
|
||||
serializedVersion: 2
|
||||
m_LodSelectionCurve:
|
||||
serializedVersion: 1
|
||||
m_LodSlope: 0
|
||||
m_LodBias: 0
|
||||
m_NumLevels: 1
|
||||
m_SubMeshes:
|
||||
- serializedVersion: 2
|
||||
m_Levels:
|
||||
- serializedVersion: 1
|
||||
m_IndexStart: 0
|
||||
m_IndexCount: 0
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e3032ccf95e8347d380459e02d512abd
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 4300000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
180
Assets/Luneraria/Art/Generated/OwnerRingRound.asset
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!43 &4300000
|
||||
Mesh:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: OwnerRingRound
|
||||
serializedVersion: 12
|
||||
m_SubMeshes:
|
||||
- serializedVersion: 2
|
||||
firstByte: 0
|
||||
indexCount: 240
|
||||
topology: 0
|
||||
baseVertex: 0
|
||||
firstVertex: 0
|
||||
vertexCount: 82
|
||||
localAABB:
|
||||
m_Center: {x: 0, y: 0, z: 0}
|
||||
m_Extent: {x: 1.12, y: 0, z: 1.12}
|
||||
m_Shapes:
|
||||
vertices: []
|
||||
shapes: []
|
||||
channels: []
|
||||
fullWeights: []
|
||||
m_BindPose: []
|
||||
m_BoneNameHashes:
|
||||
m_RootBoneNameHash: 0
|
||||
m_BonesAABB: []
|
||||
m_VariableBoneCountWeights:
|
||||
m_Data:
|
||||
m_MeshCompression: 0
|
||||
m_IsReadable: 1
|
||||
m_KeepVertices: 1
|
||||
m_KeepIndices: 1
|
||||
m_IndexFormat: 0
|
||||
m_IndexBuffer: 000001000300000003000200020003000500020005000400040005000700040007000600060007000900060009000800080009000b0008000b000a000a000b000d000a000d000c000c000d000f000c000f000e000e000f0011000e0011001000100011001300100013001200120013001500120015001400140015001700140017001600160017001900160019001800180019001b0018001b001a001a001b001d001a001d001c001c001d001f001c001f001e001e001f0021001e0021002000200021002300200023002200220023002500220025002400240025002700240027002600260027002900260029002800280029002b0028002b002a002a002b002d002a002d002c002c002d002f002c002f002e002e002f0031002e0031003000300031003300300033003200320033003500320035003400340035003700340037003600360037003900360039003800380039003b0038003b003a003a003b003d003a003d003c003c003d003f003c003f003e003e003f0041003e0041004000400041004300400043004200420043004500420045004400440045004700440047004600460047004900460049004800480049004b0048004b004a004a004b004d004a004d004c004c004d004f004c004f004e004e004f0051004e0051005000
|
||||
m_VertexData:
|
||||
serializedVersion: 3
|
||||
m_VertexCount: 82
|
||||
m_Channels:
|
||||
- stream: 0
|
||||
offset: 0
|
||||
format: 0
|
||||
dimension: 3
|
||||
- stream: 0
|
||||
offset: 12
|
||||
format: 0
|
||||
dimension: 3
|
||||
- stream: 0
|
||||
offset: 0
|
||||
format: 0
|
||||
dimension: 0
|
||||
- stream: 0
|
||||
offset: 0
|
||||
format: 0
|
||||
dimension: 0
|
||||
- stream: 0
|
||||
offset: 0
|
||||
format: 0
|
||||
dimension: 0
|
||||
- stream: 0
|
||||
offset: 0
|
||||
format: 0
|
||||
dimension: 0
|
||||
- stream: 0
|
||||
offset: 0
|
||||
format: 0
|
||||
dimension: 0
|
||||
- stream: 0
|
||||
offset: 0
|
||||
format: 0
|
||||
dimension: 0
|
||||
- stream: 0
|
||||
offset: 0
|
||||
format: 0
|
||||
dimension: 0
|
||||
- stream: 0
|
||||
offset: 0
|
||||
format: 0
|
||||
dimension: 0
|
||||
- stream: 0
|
||||
offset: 0
|
||||
format: 0
|
||||
dimension: 0
|
||||
- stream: 0
|
||||
offset: 0
|
||||
format: 0
|
||||
dimension: 0
|
||||
- stream: 0
|
||||
offset: 0
|
||||
format: 0
|
||||
dimension: 0
|
||||
- stream: 0
|
||||
offset: 0
|
||||
format: 0
|
||||
dimension: 0
|
||||
m_DataSize: 1968
|
||||
_typelessdata: 0000803f000000000000000000000000000080bf00000000295c8f3f000000000000000000000000000080bf0000000025d97c3f000000005c30203e00000000ffff7fbf0000000052988d3f000000005d69333e00000000000080bf000000007178733f000000007a379e3e00000000000080bf00000000ed57883f00000000e533b13e00000000000080bf000000000119643f000000007171e83e00000000000080bf000000002a787f3f000000000c2b023f00000000000080bf00000000bd1b4f3f000000001879163f00000000000080bf000000001bf6673f00000000a087283f00000000000080bf00000000f304353f00000000f304353f00000000ffff7fbf00000000ddbd4a3f00000000ddbd4a3f00000000000080bf000000001979163f00000000bd1b4f3f00000000000080bf00000000a187283f000000001bf6673f00000000000080bf000000007271e83e000000000119643f00000000ffff7fbf000000000d2b023f000000002a787f3f00000000000080bf0000000079379e3e000000007178733f00000000000080bf00000000e433b13e00000000ed57883f00000000000080bf000000005d30203e0000000025d97c3f00000000000080bf000000005e69333e0000000052988d3f00000000000080bf000000002ebd3bb3000000000000803f00000000ffff7fbf00000000854452b300000000295c8f3f00000000000080bf000000005a3020be0000000025d97c3f00000000000080bf000000005b6933be0000000052988d3f00000000000080bf0000000078379ebe000000007178733f00000000ffff7fbf00000000e333b1be00000000ed57883f00000000000080bf000000006e71e8be000000000219643f00000000000080bf000000000a2b02bf000000002b787f3f00000000000080bf00000000177916bf00000000bd1b4f3f00000000ffff7fbf000000009f8728bf000000001bf6673f00000000ffff7fbf00000000f30435bf00000000f304353f00000000ffff7fbf00000000ddbd4abf00000000ddbd4a3f00000000ffff7fbf00000000be1b4fbf000000001779163f00000000000080bf000000001df667bf000000009f87283f00000000000080bf00000000001964bf000000007471e83e00000000000080bf0000000029787fbf000000000e2b023f00000000000080bf00000000707873bf000000007b379e3e00000000000080bf00000000ed5788bf00000000e633b13e00000000ffff7fbf0000000025d97cbf000000005a30203e00000000000080bf0000000052988dbf000000005b69333e00000000ffff7fbf00000000000080bf000000002ebdbbb300000000ffff7fbf00000000295c8fbf000000008544d2b300000000000080bf0000000025d97cbf00000000553020be00000000000080bf0000000052988dbf00000000556933be00000000000080bf00000000717873bf0000000079379ebe00000000ffff7fbf00000000ed5788bf00000000e433b1be00000000000080bf00000000011964bf000000007371e8be00000000ffff7fbf000000002a787fbf000000000d2b02bf00000000000080bf00000000be1b4fbf00000000167916bf00000000000080bf000000001df667bf000000009e8728bf00000000000080bf00000000f40435bf00000000f20435bf00000000000080bf00000000debd4abf00000000dcbd4abf00000000000080bf000000001b7916bf00000000ba1b4fbf00000000000080bf00000000a38728bf0000000018f667bf00000000000080bf000000007771e8be00000000001964bf00000000000080bf000000000f2b02bf0000000029787fbf00000000ffff7fbf000000007d379ebe00000000707873bf00000000000080bf00000000e833b1be00000000ed5788bf00000000ffff7fbf000000005f3020be0000000024d97cbf00000000ffff7fbf00000000606933be0000000052988dbf00000000ffff7fbf000000002ede4c3200000000000080bf00000000000080bf00000000b973653200000000295c8fbf00000000000080bf000000006030203e0000000024d97cbf00000000000080bf000000006169333e0000000052988dbf00000000ffff7fbf000000007e379e3e00000000707873bf00000000000080bf00000000e933b13e00000000ed5788bf00000000000080bf000000006971e83e00000000031964bf00000000ffff7fbf00000000082b023f000000002c787fbf00000000ffff7fbf000000001579163f00000000bf1b4fbf00000000000080bf000000009d87283f000000001ef667bf00000000ffff7fbf00000000f104353f00000000f50435bf00000000000080bf00000000dbbd4a3f00000000dfbd4abf00000000ffff7fbf00000000bc1b4f3f00000000197916bf00000000000080bf000000001af6673f00000000a18728bf00000000ffff7fbf000000000119643f000000007271e8be00000000000080bf000000002a787f3f000000000d2b02bf00000000ffff7fbf000000007178733f0000000078379ebe00000000ffff7fbf00000000ed57883f00000000e333b1be00000000ffff7fbf0000000025d97c3f00000000543020be00000000ffff7fbf0000000052988d3f00000000546933be00000000000080bf000000000000803f000000002ebd3b3400000000000080bf00000000295c8f3f000000008544523400000000000080bf00000000
|
||||
m_CompressedMesh:
|
||||
m_Vertices:
|
||||
m_NumItems: 0
|
||||
m_Range: 0
|
||||
m_Start: 0
|
||||
m_Data:
|
||||
m_BitSize: 0
|
||||
m_UV:
|
||||
m_NumItems: 0
|
||||
m_Range: 0
|
||||
m_Start: 0
|
||||
m_Data:
|
||||
m_BitSize: 0
|
||||
m_Normals:
|
||||
m_NumItems: 0
|
||||
m_Range: 0
|
||||
m_Start: 0
|
||||
m_Data:
|
||||
m_BitSize: 0
|
||||
m_Tangents:
|
||||
m_NumItems: 0
|
||||
m_Range: 0
|
||||
m_Start: 0
|
||||
m_Data:
|
||||
m_BitSize: 0
|
||||
m_Weights:
|
||||
m_NumItems: 0
|
||||
m_Data:
|
||||
m_BitSize: 0
|
||||
m_NormalSigns:
|
||||
m_NumItems: 0
|
||||
m_Data:
|
||||
m_BitSize: 0
|
||||
m_TangentSigns:
|
||||
m_NumItems: 0
|
||||
m_Data:
|
||||
m_BitSize: 0
|
||||
m_FloatColors:
|
||||
m_NumItems: 0
|
||||
m_Range: 0
|
||||
m_Start: 0
|
||||
m_Data:
|
||||
m_BitSize: 0
|
||||
m_BoneIndices:
|
||||
m_NumItems: 0
|
||||
m_Data:
|
||||
m_BitSize: 0
|
||||
m_Triangles:
|
||||
m_NumItems: 0
|
||||
m_Data:
|
||||
m_BitSize: 0
|
||||
m_UVInfo: 0
|
||||
m_LocalAABB:
|
||||
m_Center: {x: 0, y: 0, z: 0}
|
||||
m_Extent: {x: 1.12, y: 0, z: 1.12}
|
||||
m_MeshUsageFlags: 0
|
||||
m_CookingOptions: 30
|
||||
m_BakedConvexCollisionMesh:
|
||||
m_BakedTriangleCollisionMesh:
|
||||
'm_MeshMetrics[0]': 1
|
||||
'm_MeshMetrics[1]': 1
|
||||
m_MeshOptimizationFlags: 1
|
||||
m_StreamData:
|
||||
serializedVersion: 2
|
||||
offset: 0
|
||||
size: 0
|
||||
path:
|
||||
m_MeshLodInfo:
|
||||
serializedVersion: 2
|
||||
m_LodSelectionCurve:
|
||||
serializedVersion: 1
|
||||
m_LodSlope: 0
|
||||
m_LodBias: 0
|
||||
m_NumLevels: 1
|
||||
m_SubMeshes:
|
||||
- serializedVersion: 2
|
||||
m_Levels:
|
||||
- serializedVersion: 1
|
||||
m_IndexStart: 0
|
||||
m_IndexCount: 0
|
||||
8
Assets/Luneraria/Art/Generated/OwnerRingRound.asset.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 1ffccb020217248f5b3ec147f53883b5
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 4300000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -36,6 +36,8 @@ namespace Luneraria.EditorTools
|
|||
library.RingRound = Keep(Ring("RingRound", 40, 0f));
|
||||
library.RingDiamond = Keep(Ring("RingDiamond", 4, 45f));
|
||||
library.RingHexagon = Keep(Ring("RingHexagon", 6, 0f));
|
||||
library.OwnerRingRound = Keep(Ring("OwnerRingRound", 40, 0f, 1f, 1.12f));
|
||||
library.OwnerRingDiamond = Keep(Ring("OwnerRingDiamond", 4, 45f, 1f, 1.12f));
|
||||
library.Quad = Keep(Quad());
|
||||
for (int team = 0; team < 3; team++)
|
||||
{
|
||||
|
|
@ -44,6 +46,11 @@ namespace Luneraria.EditorTools
|
|||
library.Bolt[team] = Marker("Bolt_" + team, LuneMaterialFactory.TeamLight[team], 3f, false);
|
||||
}
|
||||
library.HeroRing = Marker("Ring_Hero", LuneMaterialFactory.Hex("f2c96b"), 2.2f, false);
|
||||
for (int team = 0; team < 2; team++)
|
||||
{
|
||||
library.OwnerRing[team] = Marker("Ring_Owner_" + team, LuneMaterialFactory.TeamColor[team], 3f, false);
|
||||
library.AncientBarFill[team] = Marker("AncientBarFill_" + team, Color.Lerp(LuneMaterialFactory.TeamLight[team], Color.white, .15f), 1f, true);
|
||||
}
|
||||
library.BarBack = Marker("BarBack", new Color(0f, 0f, 0f, .7f), 1f, true);
|
||||
EditorUtility.SetDirty(library);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
|
@ -116,7 +123,7 @@ namespace Luneraria.EditorTools
|
|||
}
|
||||
|
||||
/// <summary>Flat ring on the ground (Godot: TorusMesh inner .42, outer .52, flattened).</summary>
|
||||
static Mesh Ring(string name, int sides, float offsetDegrees)
|
||||
static Mesh Ring(string name, int sides, float offsetDegrees, float inner = .42f, float outer = .52f)
|
||||
{
|
||||
var vertices = new List<Vector3>();
|
||||
var triangles = new List<int>();
|
||||
|
|
@ -124,8 +131,8 @@ namespace Luneraria.EditorTools
|
|||
{
|
||||
float angle = (offsetDegrees + 360f * i / sides) * Mathf.Deg2Rad;
|
||||
var direction = new Vector3(Mathf.Cos(angle), 0f, Mathf.Sin(angle));
|
||||
vertices.Add(direction * .42f);
|
||||
vertices.Add(direction * .52f);
|
||||
vertices.Add(direction * inner);
|
||||
vertices.Add(direction * outer);
|
||||
if (i < sides)
|
||||
{
|
||||
int a = i * 2;
|
||||
|
|
|
|||
33
Assets/Luneraria/Materials/Markers/AncientBarFill_0.mat
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: AncientBarFill_0
|
||||
m_Shader: {fileID: 4800000, guid: 5674755340f3b446aad7884cd0870a4f, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 1
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap: {}
|
||||
disabledShaderPasses: []
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs: []
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _Cull: 0
|
||||
- _ZTest: 8
|
||||
m_Colors:
|
||||
- _Color: {r: 0.33834773, g: 0.63246316, b: 1, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 0086d0d991b3a4d908c78c19584fa356
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
33
Assets/Luneraria/Materials/Markers/AncientBarFill_1.mat
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: AncientBarFill_1
|
||||
m_Shader: {fileID: 4800000, guid: 5674755340f3b446aad7884cd0870a4f, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 1
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap: {}
|
||||
disabledShaderPasses: []
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs: []
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _Cull: 0
|
||||
- _ZTest: 8
|
||||
m_Colors:
|
||||
- _Color: {r: 1, g: 0.49190512, b: 0.24286686, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 9606ec1caebc34df4935f43dc11b47c4
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
33
Assets/Luneraria/Materials/Markers/Ring_Owner_0.mat
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: Ring_Owner_0
|
||||
m_Shader: {fileID: 4800000, guid: 5674755340f3b446aad7884cd0870a4f, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 1
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap: {}
|
||||
disabledShaderPasses: []
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs: []
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _Cull: 0
|
||||
- _ZTest: 4
|
||||
m_Colors:
|
||||
- _Color: {r: 0.30672523, g: 1.1747177, b: 3, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
8
Assets/Luneraria/Materials/Markers/Ring_Owner_0.mat.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: f89d70a6224554988a117b329e149f14
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
33
Assets/Luneraria/Materials/Markers/Ring_Owner_1.mat
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: Ring_Owner_1
|
||||
m_Shader: {fileID: 4800000, guid: 5674755340f3b446aad7884cd0870a4f, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 1
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap: {}
|
||||
disabledShaderPasses: []
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs: []
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _Cull: 0
|
||||
- _ZTest: 4
|
||||
m_Colors:
|
||||
- _Color: {r: 3, g: 0.43238544, b: 0.2725252, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
8
Assets/Luneraria/Materials/Markers/Ring_Owner_1.mat.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 66a0290d313c549c0852cdd5ba1ac1db
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -14,5 +14,5 @@ MonoBehaviour:
|
|||
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}
|
||||
Display: {fileID: 11400000, guid: b5cdf5fef401b44509e1257c6021c15b, type: 2}
|
||||
Theme: {fileID: -4733365628477956816, guid: 38a51cb4adf3e41879e4510c6a0d7ef6, type: 3}
|
||||
|
|
|
|||
|
|
@ -245,3 +245,11 @@ MonoBehaviour:
|
|||
- {fileID: 2100000, guid: 57679ea845a534e0cb625512321e1f12, type: 2}
|
||||
- {fileID: 2100000, guid: ff98f0c178d214560b42be7959bfa214, type: 2}
|
||||
- {fileID: 2100000, guid: 325b6aa4f2372452a93a1e400cafec61, type: 2}
|
||||
OwnerRingRound: {fileID: 4300000, guid: 1ffccb020217248f5b3ec147f53883b5, type: 2}
|
||||
OwnerRingDiamond: {fileID: 4300000, guid: e3032ccf95e8347d380459e02d512abd, type: 2}
|
||||
OwnerRing:
|
||||
- {fileID: 2100000, guid: f89d70a6224554988a117b329e149f14, type: 2}
|
||||
- {fileID: 2100000, guid: 66a0290d313c549c0852cdd5ba1ac1db, type: 2}
|
||||
AncientBarFill:
|
||||
- {fileID: 2100000, guid: 0086d0d991b3a4d908c78c19584fa356, type: 2}
|
||||
- {fileID: 2100000, guid: 9606ec1caebc34df4935f43dc11b47c4, type: 2}
|
||||
|
|
|
|||
170
Assets/Luneraria/Tests/UI/PlayMode/HudPlaytest.cs
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using Luneraria.Core;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TestTools;
|
||||
|
||||
namespace Luneraria.UI.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// Interactive playtest of the real game (port of tools/luneraria/g3_playtest.gd). It never
|
||||
/// touches the match state: every action is an input a player gives, at pixel coordinates of
|
||||
/// the 1920×1080 picture (origin top left). A click reaches the HUD panel as pointer events
|
||||
/// and the field as LuneGame.ClickField, which ignores points on the HUD, because in the
|
||||
/// player both see the same mouse; game keys go through LuneGame.Key, the camera keys R, Q,
|
||||
/// E and O through the rig as in its own key handling. Pointer moves give hover (tooltips).
|
||||
/// A tester writes command batches and gets a screenshot back after each batch:
|
||||
/// /tmp/lune_play/cmd-N.txt lines: click X Y · move X Y · key NAME · wait SECONDS · shot NAME · quit
|
||||
/// /tmp/lune_play/out-N.png the screen after the batch; /tmp/lune_play/ok-N holds round and phase
|
||||
/// The log of all commands with round and phase goes to docs/port/logs/u3/playtest-log.txt.
|
||||
/// There is no main menu in the port yet, so the playtest starts in the match of the G1 scene
|
||||
/// (seed of the scene, 2; LUNE_PLAYTEST_SEED overrides). It ends with quit or after 30 idle minutes.
|
||||
/// Run: -testFilter "Luneraria.UI.Tests.HudPlaytest".
|
||||
/// </summary>
|
||||
[Explicit("Spieltest: wartet auf Befehlsdateien")]
|
||||
public sealed class HudPlaytest
|
||||
{
|
||||
const string Dir = "/tmp/lune_play/";
|
||||
const string LogFile = "docs/port/logs/u3/playtest-log.txt";
|
||||
|
||||
HudSession _s;
|
||||
readonly List<string> _log = new List<string>();
|
||||
float _started;
|
||||
bool _quit;
|
||||
|
||||
[UnityTest, Timeout(int.MaxValue)]
|
||||
public IEnumerator Play()
|
||||
{
|
||||
Directory.CreateDirectory(Dir);
|
||||
foreach (string file in Directory.GetFiles(Dir))
|
||||
File.Delete(file);
|
||||
int seed = int.TryParse(Environment.GetEnvironmentVariable("LUNE_PLAYTEST_SEED"), out int value) ? value : 2;
|
||||
_s = new HudSession();
|
||||
yield return _s.Open(1920, 1080, seed: seed);
|
||||
_started = Time.realtimeSinceStartup;
|
||||
yield return new WaitForSecondsRealtime(.5f);
|
||||
_s.Capture(Dir + "out-0.png");
|
||||
File.WriteAllText(Dir + "ok-0", Status());
|
||||
int next = 1;
|
||||
float idle = 0f;
|
||||
while (!_quit && idle < 1800f)
|
||||
{
|
||||
string path = Dir + "cmd-" + next + ".txt";
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
idle += Time.unscaledDeltaTime;
|
||||
yield return null;
|
||||
continue;
|
||||
}
|
||||
idle = 0f;
|
||||
foreach (string raw in File.ReadAllLines(path))
|
||||
{
|
||||
string line = raw.Trim();
|
||||
if (line.Length == 0 || line.StartsWith("#", StringComparison.Ordinal))
|
||||
continue;
|
||||
Write(next, line);
|
||||
yield return Command(line.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries));
|
||||
if (_quit)
|
||||
break;
|
||||
}
|
||||
yield return new WaitForSecondsRealtime(.35f);
|
||||
_s.Capture(Dir + "out-" + next + ".png");
|
||||
File.WriteAllText(Dir + "ok-" + next, Status());
|
||||
next++;
|
||||
}
|
||||
_s.Close();
|
||||
Assert.IsTrue(_quit, "the playtest ended with quit");
|
||||
}
|
||||
|
||||
string Status()
|
||||
{
|
||||
LuneMatch match = _s.Match;
|
||||
string phase = match.Phase == Phase.Planning ? "PLANUNG" : match.Phase == Phase.Combat ? "KAMPF" : "ENDE";
|
||||
return "round=" + match.RoundNumber + " phase=" + phase;
|
||||
}
|
||||
|
||||
void Write(int index, string line)
|
||||
{
|
||||
_log.Add(string.Format(CultureInfo.InvariantCulture, "{0,7:0.0} s #{1} [{2}] {3}", Time.realtimeSinceStartup - _started, index, Status(), line));
|
||||
string file = Path.Combine(HudSession.Root, LogFile);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(file));
|
||||
File.WriteAllText(file, string.Join("\n", _log) + "\n");
|
||||
}
|
||||
|
||||
IEnumerator Command(string[] parts)
|
||||
{
|
||||
switch (parts[0])
|
||||
{
|
||||
case "click":
|
||||
{
|
||||
Vector2 pixel = Pixel(parts);
|
||||
Vector2 at = _s.Hud.PixelToPanel(pixel);
|
||||
_s.MoveTo(at);
|
||||
yield return null;
|
||||
_s.Pointer(at, EventType.MouseDown);
|
||||
// The field sees the same press (LuneGame.HandleClicks); it ignores points on the HUD.
|
||||
_s.Game.ClickField(new Vector2(pixel.x, _s.Height - pixel.y));
|
||||
yield return null;
|
||||
_s.Pointer(at, EventType.MouseUp);
|
||||
yield return null;
|
||||
break;
|
||||
}
|
||||
case "move":
|
||||
_s.MoveTo(_s.Hud.PixelToPanel(Pixel(parts)));
|
||||
yield return null;
|
||||
break;
|
||||
case "key":
|
||||
Key(parts[1].ToUpperInvariant());
|
||||
yield return null;
|
||||
break;
|
||||
case "wait":
|
||||
yield return new WaitForSecondsRealtime(float.Parse(parts[1], CultureInfo.InvariantCulture));
|
||||
break;
|
||||
case "shot":
|
||||
yield return null;
|
||||
_s.Capture(Dir + parts[1] + ".png");
|
||||
break;
|
||||
case "quit":
|
||||
_quit = true;
|
||||
break;
|
||||
default:
|
||||
Debug.LogWarning("LUNE_PLAYTEST unknown command: " + string.Join(" ", parts));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static Vector2 Pixel(string[] parts)
|
||||
{
|
||||
return new Vector2(float.Parse(parts[1], CultureInfo.InvariantCulture), float.Parse(parts[2], CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
void Key(string name)
|
||||
{
|
||||
// Camera keys of the rig (LuneCameraRig.HandleInput), blocked while the options are open.
|
||||
if (name == "R" || name == "Q" || name == "E" || name == "O")
|
||||
{
|
||||
if (_s.Hud.Settings.Open)
|
||||
return;
|
||||
if (name == "R")
|
||||
_s.Rig.ResetRequested?.Invoke();
|
||||
else if (name == "O")
|
||||
_s.Rig.Overview();
|
||||
else
|
||||
_s.Rig.RotateStep(name == "Q" ? -1 : 1);
|
||||
return;
|
||||
}
|
||||
KeyCode code;
|
||||
if (name.Length == 1 && char.IsDigit(name[0]))
|
||||
code = KeyCode.Alpha0 + (name[0] - '0');
|
||||
else if (name == "ESC")
|
||||
code = KeyCode.Escape;
|
||||
else
|
||||
code = (KeyCode)Enum.Parse(typeof(KeyCode), name, true);
|
||||
_s.Game.Key(code);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Luneraria/Tests/UI/PlayMode/HudPlaytest.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 2fe7931df72014a86aa513b5942e520c
|
||||
|
|
@ -40,7 +40,7 @@ namespace Luneraria.UI.Tests
|
|||
public static string Root { get { return Path.GetDirectoryName(Application.dataPath); } }
|
||||
|
||||
/// <summary>Loads the scene and starts a match with the human seat, HUD into a width×height texture.</summary>
|
||||
public IEnumerator Open(int width = 1920, int height = 1080, int seed = 4, float uiScale = 1f, bool reducedMotion = false, int startRound = 1, int extraGold = 0)
|
||||
public IEnumerator Open(int width = 1920, int height = 1080, int seed = 4, float uiScale = 1f, bool reducedMotion = false, int startRound = 1, int extraGold = 0, string[] bunds = null)
|
||||
{
|
||||
LunePreferences.ResetSession(uiScale, reducedMotion);
|
||||
yield return HudCaptureTests.LoadScene();
|
||||
|
|
@ -53,19 +53,21 @@ namespace Luneraria.UI.Tests
|
|||
Rig = Game.Rig;
|
||||
Camera = View.ViewCamera != null ? View.ViewCamera : Camera.main;
|
||||
Target = new RenderTexture(width, height, 0, RenderTextureFormat.ARGB32);
|
||||
_sceneTarget = new RenderTexture(width, height, 24, RenderTextureFormat.ARGB32);
|
||||
// MSAA like the player (URP asset 4×); a camera target texture sets the sample count itself.
|
||||
_sceneTarget = new RenderTexture(width, height, 24, RenderTextureFormat.ARGB32) { antiAliasing = 4 };
|
||||
Camera.targetTexture = _sceneTarget;
|
||||
Game.Target = Target;
|
||||
Driver.HumanSlot = Human;
|
||||
Driver.Seed = seed;
|
||||
Driver.StartRound = startRound;
|
||||
Driver.ExtraGold = extraGold;
|
||||
Driver.Bunds = bunds;
|
||||
Driver.Speed = 1f;
|
||||
Driver.Restart();
|
||||
yield return Frames(3);
|
||||
Hud = Game.Hud;
|
||||
Assert.AreSame(Driver.Runner.Match, Hud.Match, "the HUD shows the driver's match");
|
||||
Game.ResetCamera(true);
|
||||
Game.StartCamera(true);
|
||||
yield return Frames(2);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@ namespace Luneraria.UI.Tests
|
|||
/// E Ready lock, bots, privacy, a dropped human seat;
|
||||
/// F input: minimap, lane clicks, hotkeys, field clicks, no click-through, tooltips;
|
||||
/// G events, critical alerts and reduced motion;
|
||||
/// H update rates and the portrait cache.
|
||||
/// H update rates and the portrait cache;
|
||||
/// I (U3 addition) the field: formation areas, grotto entrance, resting boss, name tags, owner ring, Ancient bar.
|
||||
/// Every check is counted; the totals go to tools/out/hud-verify-*.txt.
|
||||
/// </summary>
|
||||
public sealed class HudVerifyTests
|
||||
|
|
@ -497,5 +498,83 @@ namespace Luneraria.UI.Tests
|
|||
s.Close();
|
||||
Done("H");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- I field
|
||||
static GameObject SceneObject(string name)
|
||||
{
|
||||
return Object.FindObjectsByType<Transform>(FindObjectsInactive.Include, FindObjectsSortMode.None).Select(t => t.gameObject).FirstOrDefault(g => g.name == name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// U3 addition (from the G3 motif comparison): the field around the HUD. Formation areas and
|
||||
/// the own grotto entrance in planning, the resting Schlundkrone while the grotto is open,
|
||||
/// name tags of heroes and of a deployed boss, its owner ring, the Ancients' health bar.
|
||||
/// </summary>
|
||||
[UnityTest]
|
||||
[Timeout(900000)]
|
||||
public IEnumerator I_Field()
|
||||
{
|
||||
var s = new HudSession();
|
||||
yield return s.Open();
|
||||
yield return HudSession.Frames(2);
|
||||
List<Renderer> pads = Object.FindObjectsByType<Renderer>(FindObjectsInactive.Include, FindObjectsSortMode.None).Where(r => r.gameObject.name.StartsWith("Deployment_")).ToList();
|
||||
Check(pads.Count == 6, "six formation areas in the scene: " + pads.Count);
|
||||
Check(pads.Where(p => p.gameObject.name.StartsWith("Deployment_0_")).All(p => p.gameObject.activeInHierarchy), "own formation areas shown in planning");
|
||||
Check(pads.Where(p => p.gameObject.name.StartsWith("Deployment_1_")).All(p => !p.gameObject.activeInHierarchy), "enemy formation areas hidden");
|
||||
GameObject ownEntry = SceneObject("GrottoEntry_0");
|
||||
GameObject enemyEntry = SceneObject("GrottoEntry_1");
|
||||
Check(ownEntry != null && ownEntry.activeInHierarchy, "own grotto entrance shown while the grotto is open");
|
||||
Check(enemyEntry != null && !enemyEntry.activeInHierarchy, "enemy grotto entrance hidden");
|
||||
Check(s.View.RestingBoss != null && s.View.RestingBoss.activeInHierarchy, "the Schlundkrone rests in the open grotto");
|
||||
s.Match.Boss.NextAvailableRound = 5;
|
||||
yield return HudSession.Frames(2);
|
||||
Check(ownEntry != null && !ownEntry.activeInHierarchy, "grotto entrance hidden while the grotto is locked");
|
||||
Check(s.View.RestingBoss != null && !s.View.RestingBoss.activeInHierarchy, "no resting boss while the grotto is locked");
|
||||
s.Match.Boss.NextAvailableRound = 1;
|
||||
// The starter army stands on Mid: that area lights up.
|
||||
yield return s.Starter();
|
||||
s.Leave();
|
||||
yield return HudSession.Frames(2);
|
||||
var block = new MaterialPropertyBlock();
|
||||
Renderer mid = pads.First(p => p.gameObject.name == "Deployment_0_mid");
|
||||
mid.GetPropertyBlock(block);
|
||||
Check(block.GetFloat("_Highlight") > .5f, "the area of a lane with own units lights up");
|
||||
Renderer top = pads.First(p => p.gameObject.name == "Deployment_0_top");
|
||||
top.GetPropertyBlock(block);
|
||||
Check(block.GetFloat("_Highlight") < .5f, "an empty lane's area stays plain");
|
||||
// A recruited hero carries its name and level in the planning preview.
|
||||
s.Me.Draft.Gold += 2000;
|
||||
s.Hud.OpenHero();
|
||||
string heroId = LuneContent.Heroes.Keys[0];
|
||||
yield return s.Tap(() => s.Hud.HeroDetail.RecruitButtons[heroId]);
|
||||
s.Hud.CloseModal();
|
||||
s.Leave();
|
||||
yield return HudSession.Frames(3);
|
||||
string heroTag = LuneContent.Heroes[heroId].Name + " · Stufe 1";
|
||||
Check(s.Hud.FieldTags.Texts.Contains(heroTag), "hero tag in the planning preview: " + string.Join(", ", s.Hud.FieldTags.Texts));
|
||||
// An Ancient below full health shows its bar.
|
||||
double core = s.Match.Map.BuildingDef("blau_ancient").MaxHp;
|
||||
s.Match.BuildingHp["blau_ancient"] = core * .6;
|
||||
yield return HudSession.Frames(2);
|
||||
Transform bar = s.View.Building("blau_ancient").transform.Find("AncientBar");
|
||||
Check(bar != null && bar.gameObject.activeInHierarchy, "damaged Ancient shows its health bar");
|
||||
Transform enemyBar = s.View.Building("rot_ancient").transform.Find("AncientBar");
|
||||
Check(enemyBar == null || !enemyBar.gameObject.activeInHierarchy, "an Ancient at full health shows none");
|
||||
// Combat with the own team's boss deployed on Mid: areas hidden, boss tag and owner ring.
|
||||
s.Match.Boss.Claim = new BossClaim { Team = 0, WonRound = 0, DeployRound = s.Match.RoundNumber, ChooserSlot = HudSession.Human, Lane = "mid" };
|
||||
s.Game.Key(KeyCode.Space);
|
||||
yield return s.Fight(1.0);
|
||||
yield return HudSession.Frames(2);
|
||||
Check(pads.All(p => !p.gameObject.activeInHierarchy), "formation areas hidden in combat");
|
||||
Check(ownEntry != null && !ownEntry.activeInHierarchy, "grotto entrance hidden in combat");
|
||||
Check(s.Hud.FieldTags.Texts.Contains("SCHLUNDKRONE · für Blau"), "deployed boss says whom it fights for: " + string.Join(", ", s.Hud.FieldTags.Texts));
|
||||
Check(s.Hud.FieldTags.Texts.Contains(heroTag), "hero tag in combat");
|
||||
LuneUnitView boss = s.View.Units.FirstOrDefault(v => v.Unit.Kind == "boss" && v.Unit.Team == 0);
|
||||
Transform ring = boss != null ? boss.transform.Find("OwnerRing") : null;
|
||||
Check(ring != null && ring.gameObject.activeInHierarchy, "deployed boss wears the owner ring of its team");
|
||||
Check(s.View.RestingBoss == null || !s.View.RestingBoss.activeInHierarchy, "no resting boss while it is deployed");
|
||||
s.Close();
|
||||
Done("I");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
223
Assets/Luneraria/Tests/UI/PlayMode/HudViewsTests.cs
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Luneraria.Core;
|
||||
using Luneraria.View;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TestTools;
|
||||
|
||||
namespace Luneraria.UI.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// The fourteen G3 motifs (port of tools/luneraria/g3_views.gd) from the real game with the
|
||||
/// real HUD at one window size. Like Godot, fixtures only set public match state before or
|
||||
/// between phases (gold, round, building HP, boss claim, the armies of round 14), the starter
|
||||
/// army comes from the HUD's own button (its toast stays, as in Godot's pictures), further
|
||||
/// commands go straight to the match followed by the preview rebuild (Godot command() and
|
||||
/// _rebuild_preview()), and HUD views open through the HUD's public methods. After a click
|
||||
/// the pointer leaves the HUD, so no tooltip covers a picture. A capture tool, not part of the
|
||||
/// suite: -testFilter "Luneraria.UI.Tests.HudViewsTests" (all sizes) or one size by its test
|
||||
/// name. 1920×1080 is written as PNG (the reference set next to Godot's), the other sizes as
|
||||
/// JPEG to keep the repository small.
|
||||
/// </summary>
|
||||
[Explicit("Bildwerkzeug: nur gezielt per -testFilter")]
|
||||
public sealed class HudViewsTests
|
||||
{
|
||||
/// <summary>The Bünde of Godot's g3_views.gd (the human seat 1 plays the Wurzelbund).</summary>
|
||||
static readonly string[] Bunds = { "wurzelbund", "wurzelbund", "glutpakt", "glutpakt", "wurzelbund", "glutpakt" };
|
||||
|
||||
int _width;
|
||||
int _height;
|
||||
string _folder;
|
||||
string _extension;
|
||||
readonly List<string> _shots = new List<string>();
|
||||
|
||||
[UnityTest, Timeout(1800000)] public IEnumerator Motifs_1920x1080() { yield return Run(1920, 1080); }
|
||||
[UnityTest, Timeout(1800000)] public IEnumerator Motifs_2560x1440() { yield return Run(2560, 1440); }
|
||||
[UnityTest, Timeout(1800000)] public IEnumerator Motifs_1280x720() { yield return Run(1280, 720); }
|
||||
[UnityTest, Timeout(1800000)] public IEnumerator Motifs_2560x1080() { yield return Run(2560, 1080); }
|
||||
|
||||
IEnumerator Run(int width, int height)
|
||||
{
|
||||
_width = width;
|
||||
_height = height;
|
||||
_folder = "docs/port/screenshots/u3/motive-" + width + "x" + height + "/";
|
||||
_extension = width == 1920 && height == 1080 ? ".png" : ".jpg";
|
||||
_shots.Clear();
|
||||
yield return PlanningViews();
|
||||
yield return BossViews();
|
||||
yield return CombatViews();
|
||||
yield return AncientViews();
|
||||
Debug.Log("LUNE_VIEWS " + width + "x" + height + ": " + _shots.Count + " " + string.Join(", ", _shots));
|
||||
Assert.AreEqual(14, _shots.Count, "all fourteen motifs");
|
||||
}
|
||||
|
||||
IEnumerator Fresh(HudSession s)
|
||||
{
|
||||
yield return s.Open(_width, _height, seed: 4, bunds: Bunds);
|
||||
}
|
||||
|
||||
IEnumerator Shot(HudSession s, string label)
|
||||
{
|
||||
yield return s.Shot(_folder + label + _extension);
|
||||
_shots.Add(label);
|
||||
}
|
||||
|
||||
static IEnumerator Wait(float seconds)
|
||||
{
|
||||
yield return new WaitForSecondsRealtime(seconds);
|
||||
}
|
||||
|
||||
/// <summary>Godot command(): a command of the human seat straight to the match, then the preview.</summary>
|
||||
static void Send(HudSession s, Command command)
|
||||
{
|
||||
CommandResult result = s.Match.Submit(HudSession.Human, command);
|
||||
Assert.IsTrue(result.Ok, command.Type + ": " + result.Message);
|
||||
s.Game.RebuildPreview();
|
||||
s.Sync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Godot armies(): the given seats plan round n with plenty of gold (profile "ausgewogen"),
|
||||
/// grotto orders go to the own lane; the human keeps an ordinary amount afterwards.
|
||||
/// </summary>
|
||||
static void Armies(HudSession s, int round)
|
||||
{
|
||||
LuneMatch match = s.Match;
|
||||
match.RoundNumber = round;
|
||||
for (int slot = 0; slot < 6; slot++)
|
||||
{
|
||||
Draft draft = match.Players[slot].Draft;
|
||||
draft.Roster.Clear();
|
||||
draft.Gold = 100000;
|
||||
LuneBot.Plan(match, slot, "ausgewogen");
|
||||
foreach (RosterEntry entry in draft.Roster)
|
||||
{
|
||||
if (entry.Lane == LuneMatch.Grotto)
|
||||
entry.Lane = match.Players[slot].Lane;
|
||||
}
|
||||
if (draft.Hero != null && draft.Hero.Lane == LuneMatch.Grotto)
|
||||
draft.Hero.Lane = match.Players[slot].Lane;
|
||||
}
|
||||
match.Players[HudSession.Human].Draft.Gold = 185;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- planning
|
||||
IEnumerator PlanningViews()
|
||||
{
|
||||
var s = new HudSession();
|
||||
yield return Fresh(s);
|
||||
// A normal planning round: round 3, starter army bought, some gold left.
|
||||
s.Match.RoundNumber = 3;
|
||||
s.Me.Draft.Gold = 420;
|
||||
yield return s.Starter();
|
||||
s.Leave();
|
||||
yield return Wait(.4f);
|
||||
Assert.AreEqual(3, s.Model.OwnRoster.Count, "starter army");
|
||||
yield return Shot(s, "01-planung");
|
||||
// Hero hired and shown.
|
||||
s.Me.Draft.Gold = 900;
|
||||
Send(s, Command.HeroBuy("held_ringborke"));
|
||||
s.Hud.OpenHero();
|
||||
yield return Wait(.4f);
|
||||
yield return Shot(s, "02-held-ausgewaehlt");
|
||||
// Item choice with one item equipped.
|
||||
Send(s, Command.ItemBuy("glutklinge", s.Me.Draft.Hero.Uid));
|
||||
s.Hud.OpenItems();
|
||||
yield return Wait(.3f);
|
||||
yield return Shot(s, "03-itemauswahl");
|
||||
s.Hud.CloseModal();
|
||||
// A normal unit selected.
|
||||
s.Hud.SelectUnit(s.Model.OwnRoster.First().Uid);
|
||||
yield return Wait(.3f);
|
||||
yield return Shot(s, "04-einheit-ausgewaehlt");
|
||||
s.Hud.ClearSelection();
|
||||
// Attack champions: one booked for the next round.
|
||||
Send(s, Command.ChampionBook("ch_belagerung", "mid"));
|
||||
s.Hud.OpenChampions();
|
||||
yield return Wait(.3f);
|
||||
yield return Shot(s, "05-angriffschampion");
|
||||
s.Close();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- boss
|
||||
IEnumerator BossViews()
|
||||
{
|
||||
var s = new HudSession();
|
||||
yield return Fresh(s);
|
||||
s.Match.RoundNumber = 10;
|
||||
s.Match.Boss.NextAvailableRound = 10;
|
||||
s.Game.FocusTarget("grotto");
|
||||
s.Sync();
|
||||
yield return Wait(1f);
|
||||
yield return Shot(s, "06-boss-verfuegbar");
|
||||
// The own team conquered the boss in round 11: deployment in round 12 on Mid, planning of 12.
|
||||
s.Match.RoundNumber = 12;
|
||||
s.Match.Boss.Claim = new BossClaim { Team = 0, WonRound = 11, DeployRound = 12, ChooserSlot = HudSession.Human, Lane = "mid" };
|
||||
s.Match.Boss.NextAvailableRound = 15;
|
||||
s.Game.FocusLane("mid", true);
|
||||
s.Sync();
|
||||
yield return Wait(1f);
|
||||
yield return Shot(s, "07-boss-angekuendigt");
|
||||
s.Close();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- combat
|
||||
IEnumerator CombatViews()
|
||||
{
|
||||
var s = new HudSession();
|
||||
yield return Fresh(s);
|
||||
Armies(s, 14);
|
||||
s.Match.BuildingHp["rot_top_outer"] = 0.0;
|
||||
s.Match.BuildingHp["blau_bot_outer"] = 0.0;
|
||||
s.Game.Key(KeyCode.Space);
|
||||
yield return HudSession.Frames(1);
|
||||
Assert.AreEqual(LuneMatchRunner.Stage.Combat, s.Runner.Current, "combat of round 14");
|
||||
// Godot advance_combat(22): the clock runs to 22 s, then holds.
|
||||
yield return s.Fight(22.0);
|
||||
s.Driver.Paused = true;
|
||||
foreach (var spec in new[] { ("08-kampf-top", "top"), ("09-kampf-mid", "mid"), ("10-kampf-bottom", "bot") })
|
||||
{
|
||||
s.Game.FocusLane(spec.Item2, true);
|
||||
yield return Wait(.5f);
|
||||
yield return Shot(s, spec.Item1);
|
||||
}
|
||||
Vec2 ruin = s.Match.Map.BuildingDef("blau_bot_outer").Position;
|
||||
s.Rig.LookAt(ruin, 22f, true);
|
||||
yield return Wait(.5f);
|
||||
yield return Shot(s, "13-turm-zerstoert");
|
||||
s.Rig.LookAt(Vec2.Zero, 46f, true);
|
||||
yield return Wait(.5f);
|
||||
yield return Shot(s, "14-grosse-kampfansicht");
|
||||
s.Close();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Ancients
|
||||
IEnumerator AncientViews()
|
||||
{
|
||||
var s = new HudSession();
|
||||
yield return Fresh(s);
|
||||
s.Match.RoundNumber = 21;
|
||||
foreach (string id in new[] { "blau_mid_outer", "blau_mid_inner", "blau_base_1", "blau_base_2" })
|
||||
s.Match.BuildingHp[id] = 0.0;
|
||||
s.Match.BuildingHp["blau_ancient"] = s.Match.Map.BuildingDef("blau_ancient").MaxHp * .22;
|
||||
s.Game.FocusTarget("base");
|
||||
s.Sync();
|
||||
yield return Wait(1.2f);
|
||||
yield return Shot(s, "11-eigener-ancient-kritisch");
|
||||
s.Close();
|
||||
s = new HudSession();
|
||||
yield return Fresh(s);
|
||||
s.Match.RoundNumber = 137;
|
||||
foreach (string id in new[] { "rot_top_outer", "rot_top_inner", "rot_base_1", "rot_base_2" })
|
||||
s.Match.BuildingHp[id] = 0.0;
|
||||
s.Match.BuildingHp["rot_ancient"] = s.Match.Map.BuildingDef("rot_ancient").MaxHp * .18;
|
||||
s.Rig.LookAt(s.Match.Map.CorePosition(1), 26f, true);
|
||||
s.Sync();
|
||||
yield return Wait(1.2f);
|
||||
yield return Shot(s, "12-gegnerischer-ancient-kritisch");
|
||||
s.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Luneraria/Tests/UI/PlayMode/HudViewsTests.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 3be86884884be4cdf8fc2b15e672e1f8
|
||||
|
|
@ -12,10 +12,12 @@ 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.
|
||||
/// reference (assets/fonts, OFL). Unity's text engine has no weight axis; FreeType loads a
|
||||
/// named instance through the face index (named instance << 16), index 0 is the default
|
||||
/// instance. The HUD uses Cantarell Regular (3) and Cinzel in its default instance (Regular
|
||||
/// 400), the weights G3 really shows (see LuneFace); Cantarell Bold (4) stays for the U3A probe.
|
||||
/// The atlases hold every character the HUD texts use, so the player never needs the font
|
||||
/// files. Existing atlases are kept (no churn); delete one to rebuild it.
|
||||
/// Unity -batchmode -projectPath . -executeMethod Luneraria.EditorTools.LuneUiAssetsBuilder.Build -logFile -
|
||||
/// </summary>
|
||||
public static class LuneUiAssetsBuilder
|
||||
|
|
@ -33,7 +35,7 @@ namespace Luneraria.EditorTools
|
|||
}
|
||||
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.Display = Font("Cinzel-VF.ttf", 0, "Cinzel-Regular");
|
||||
assets.Theme = AssetDatabase.LoadAssetAtPath<ThemeStyleSheet>("Assets/Luneraria/UI/LuneRuntimeTheme.tss");
|
||||
EditorUtility.SetDirty(assets);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
|
@ -57,8 +59,9 @@ namespace Luneraria.EditorTools
|
|||
static FontAsset Font(string file, int namedInstance, string name)
|
||||
{
|
||||
string path = Fonts + name + ".asset";
|
||||
if (AssetDatabase.LoadAssetAtPath<FontAsset>(path) != null)
|
||||
AssetDatabase.DeleteAsset(path);
|
||||
FontAsset existing = AssetDatabase.LoadAssetAtPath<FontAsset>(path);
|
||||
if (existing != null)
|
||||
return existing;
|
||||
string source = Path.GetFullPath(Fonts + file);
|
||||
FontAsset font = FontAsset.CreateFontAsset(source, namedInstance << 16, 64, 8, GlyphRenderMode.SDFAA, 1024, 1024);
|
||||
if (font == null)
|
||||
|
|
|
|||
5854
Assets/Luneraria/UI/Fonts/Cinzel-Regular.asset
Normal file
|
|
@ -1,5 +1,5 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 84c5b4f661d34482e8f3a990d043245e
|
||||
guid: b5cdf5fef401b44509e1257c6021c15b
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
|
|
@ -15,7 +15,10 @@ namespace Luneraria.UI
|
|||
/// clicks and hotkeys move the camera; a left click in the field selects an own unit. During
|
||||
/// planning the own draft army stands in its formation and the own deployment areas light up.
|
||||
/// Offline, planning waits for the human; pause and tempo are local. Nothing here decides a rule.
|
||||
/// Runs after the match view (execution order 200 after 100), so name tags follow the figures
|
||||
/// of the same frame.
|
||||
/// </summary>
|
||||
[DefaultExecutionOrder(200)]
|
||||
public sealed class LuneGame : MonoBehaviour
|
||||
{
|
||||
public LuneMatchDriver Driver;
|
||||
|
|
@ -37,11 +40,16 @@ namespace Luneraria.UI
|
|||
|
||||
Camera _camera;
|
||||
LuneMatchRunner _runner;
|
||||
readonly System.Diagnostics.Stopwatch _clock = new System.Diagnostics.Stopwatch();
|
||||
double _updateMs;
|
||||
double _lateMs;
|
||||
LuneMatchRunner.Stage _lastStage = (LuneMatchRunner.Stage)(-1);
|
||||
int _laneCycle;
|
||||
bool _pausedBySettings;
|
||||
VisualElement _leave;
|
||||
readonly List<(Renderer renderer, int team, string lane)> _pads = new List<(Renderer, int, string)>();
|
||||
readonly List<(GameObject ring, int team)> _grottoRings = new List<(GameObject, int)>();
|
||||
string _deployment = "";
|
||||
MaterialPropertyBlock _block;
|
||||
|
||||
void Start()
|
||||
|
|
@ -63,7 +71,14 @@ namespace Luneraria.UI
|
|||
Rig.KeysBlocked = () => Hud != null && Hud.Settings.Open;
|
||||
Rig.ResetRequested = () => ResetCamera();
|
||||
}
|
||||
ResetCamera(true);
|
||||
StartCamera(true);
|
||||
// The benchmark counts the HUD scripts: its Tick plus this controller's frame work.
|
||||
LuneBenchmark.UiMs = () => (Hud != null ? Hud.LastTickMs : 0.0) + _updateMs + _lateMs;
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
LuneBenchmark.UiMs = null;
|
||||
}
|
||||
|
||||
/// <summary>(Re)builds the HUD for the driver's current match and wires its events.</summary>
|
||||
|
|
@ -157,7 +172,7 @@ namespace Luneraria.UI
|
|||
Driver.Restart();
|
||||
View.Preview = null;
|
||||
Bind();
|
||||
ResetCamera(true);
|
||||
StartCamera(true);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- camera targets
|
||||
|
|
@ -219,6 +234,17 @@ namespace Luneraria.UI
|
|||
return true;
|
||||
}
|
||||
|
||||
/// <summary>The first view of a match (Godot _ready): the own lane at planning distance, a bot match from above.</summary>
|
||||
public void StartCamera(bool instant = true)
|
||||
{
|
||||
if (Rig == null)
|
||||
return;
|
||||
if (Human)
|
||||
FocusLane(Match.Players[Seat].Lane, instant);
|
||||
else
|
||||
Rig.Overview(instant);
|
||||
}
|
||||
|
||||
public void ResetCamera(bool instant = false)
|
||||
{
|
||||
if (Rig == null)
|
||||
|
|
@ -228,26 +254,36 @@ namespace Luneraria.UI
|
|||
}
|
||||
|
||||
// ---------------------------------------------------------------- planning preview and deployment areas
|
||||
/// <summary>
|
||||
/// The formation areas (Deployment_team_lane) and grotto entrance rings (GrottoEntry_team) of
|
||||
/// the scene. They are built inactive, like Godot's hidden pads, so the search includes
|
||||
/// inactive objects.
|
||||
/// </summary>
|
||||
void FindPads()
|
||||
{
|
||||
_pads.Clear();
|
||||
foreach (Renderer renderer in FindObjectsByType<Renderer>(FindObjectsSortMode.None))
|
||||
_grottoRings.Clear();
|
||||
foreach (Renderer renderer in FindObjectsByType<Renderer>(FindObjectsInactive.Include, FindObjectsSortMode.None))
|
||||
{
|
||||
string name = renderer.gameObject.name;
|
||||
if (!name.StartsWith("Deployment_", StringComparison.Ordinal))
|
||||
continue;
|
||||
string[] parts = name.Split('_');
|
||||
if (parts.Length >= 3 && int.TryParse(parts[1], out int team))
|
||||
string[] parts = renderer.gameObject.name.Split('_');
|
||||
if (parts.Length >= 3 && parts[0] == "Deployment" && int.TryParse(parts[1], out int team))
|
||||
_pads.Add((renderer, team, parts[2]));
|
||||
else if (parts.Length == 2 && parts[0] == "GrottoEntry" && int.TryParse(parts[1], out int side))
|
||||
_grottoRings.Add((renderer.gameObject, side));
|
||||
}
|
||||
_deployment = "";
|
||||
}
|
||||
|
||||
/// <summary>Planning shows the own formation areas; lanes that hold own units light up (Godot show_deployment).</summary>
|
||||
/// <summary>
|
||||
/// Planning shows the own formation areas, lanes that hold own units light up, and the own
|
||||
/// grotto entrance while the grotto is open (Godot show_deployment). Checked every frame,
|
||||
/// applied only when something changed.
|
||||
/// </summary>
|
||||
void UpdateDeployment()
|
||||
{
|
||||
bool planning = Match.Phase == Phase.Planning && Human;
|
||||
var used = new Dictionary<string, int>();
|
||||
if (Human)
|
||||
if (planning)
|
||||
{
|
||||
Draft draft = Match.Players[Seat].Draft;
|
||||
foreach (RosterEntry entry in draft.Roster)
|
||||
|
|
@ -255,24 +291,36 @@ namespace Luneraria.UI
|
|||
if (draft.Hero != null)
|
||||
used[draft.Hero.Lane] = used.GetValueOrDefault(draft.Hero.Lane, 0) + 1;
|
||||
}
|
||||
bool grotto = planning && Match.GrottoAvailable();
|
||||
string signature = planning + "|" + OwnTeam + "|" + grotto + "|" + string.Join(",", LuneMap.Lanes.Select(lane => used.GetValueOrDefault(lane, 0) > 0 ? lane : ""));
|
||||
if (signature == _deployment)
|
||||
return;
|
||||
_deployment = signature;
|
||||
_block ??= new MaterialPropertyBlock();
|
||||
foreach (var pad in _pads)
|
||||
{
|
||||
if (!Human)
|
||||
continue;
|
||||
bool visible = planning && pad.team == OwnTeam;
|
||||
if (pad.renderer.enabled != visible)
|
||||
pad.renderer.enabled = visible;
|
||||
if (pad.renderer.gameObject.activeSelf != visible)
|
||||
pad.renderer.gameObject.SetActive(visible);
|
||||
if (!visible)
|
||||
continue;
|
||||
pad.renderer.GetPropertyBlock(_block);
|
||||
_block.SetFloat("_Highlight", used.GetValueOrDefault(pad.lane, 0) > 0 ? 1f : 0f);
|
||||
pad.renderer.SetPropertyBlock(_block);
|
||||
}
|
||||
foreach (var entry in _grottoRings)
|
||||
{
|
||||
bool visible = grotto && entry.team == OwnTeam;
|
||||
if (entry.ring.activeSelf != visible)
|
||||
entry.ring.SetActive(visible);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The human player's current draft army in its formation (Godot _rebuild_preview).</summary>
|
||||
void RebuildPreview()
|
||||
/// <summary>
|
||||
/// The human player's current draft army in its formation (Godot _rebuild_preview); after
|
||||
/// every own command, and by fixtures that change the draft directly (g3_views).
|
||||
/// </summary>
|
||||
public void RebuildPreview()
|
||||
{
|
||||
UpdateDeployment();
|
||||
if (Match.Phase != Phase.Planning || !Human)
|
||||
|
|
@ -308,6 +356,13 @@ namespace Luneraria.UI
|
|||
|
||||
// ---------------------------------------------------------------- frame
|
||||
void Update()
|
||||
{
|
||||
_clock.Restart();
|
||||
FrameUpdate();
|
||||
_updateMs = _clock.Elapsed.TotalMilliseconds;
|
||||
}
|
||||
|
||||
void FrameUpdate()
|
||||
{
|
||||
if (Driver.Runner != _runner)
|
||||
{
|
||||
|
|
@ -316,10 +371,21 @@ namespace Luneraria.UI
|
|||
Bind();
|
||||
}
|
||||
TrackStage();
|
||||
UpdateDeployment();
|
||||
HandleKeys();
|
||||
HandleClicks();
|
||||
if (Rig != null && _camera != null)
|
||||
}
|
||||
|
||||
/// <summary>After the match view moved the figures: name tags and the minimap's camera frame.</summary>
|
||||
void LateUpdate()
|
||||
{
|
||||
if (Hud == null || _camera == null)
|
||||
return;
|
||||
_clock.Restart();
|
||||
Hud.FieldTags.Sync(View.Units, _camera, Hud);
|
||||
if (Rig != null)
|
||||
Hud.Minimap.SetCamera(Rig.Focus, Rig.Zoom, Rig.Footprint(_camera));
|
||||
_lateMs = _clock.Elapsed.TotalMilliseconds;
|
||||
}
|
||||
|
||||
void TrackStage()
|
||||
|
|
|
|||
|
|
@ -431,12 +431,15 @@ namespace Luneraria.UI
|
|||
{
|
||||
style.width = 30f;
|
||||
style.height = 30f;
|
||||
style.paddingLeft = 5f;
|
||||
style.paddingRight = 5f;
|
||||
style.paddingTop = 5f;
|
||||
style.paddingBottom = 5f;
|
||||
// Godot: the glyph fills the slot with 5 px insets at any slot size (46 in the hero window).
|
||||
_glyph = LuneUi.Glyph("", Color.white, 20f, this);
|
||||
_glyph.style.flexGrow = 1f;
|
||||
_glyph.style.position = Position.Absolute;
|
||||
_glyph.style.left = 5f;
|
||||
_glyph.style.top = 5f;
|
||||
_glyph.style.right = 5f;
|
||||
_glyph.style.bottom = 5f;
|
||||
_glyph.style.width = StyleKeyword.Auto;
|
||||
_glyph.style.height = StyleKeyword.Auto;
|
||||
return;
|
||||
}
|
||||
style.minHeight = 74f;
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ namespace Luneraria.UI
|
|||
{
|
||||
Normal = M("e8c47e", "b98a4a", H("fff0cc"), 1.5f, 7f, 16f, 8f), Hover = M("f6d898", "cc9c56", Color.white, 1.5f, 7f, 16f, 8f),
|
||||
Pressed = M("b98a4a", "d5aa66", H("fff0cc"), 2f, 7f, 16f, 8f), Disabled = M("4a4234", "3a3328", H("6a5c46"), 1f, 7f, 16f, 8f),
|
||||
TextVariation = "HudValue", TextSize = 16f, Text = LuneUiTheme.Ink, TextHover = LuneUiTheme.Ink, TextPressed = LuneUiTheme.Ink, TextDisabled = H("9c8f78"),
|
||||
TextVariation = "HudPrimary", TextSize = 16f, Text = LuneUiTheme.Ink, TextHover = LuneUiTheme.Ink, TextPressed = LuneUiTheme.Ink, TextDisabled = H("9c8f78"),
|
||||
};
|
||||
|
||||
/// <summary>Godot PrimaryButton of the menu theme (Bereit, Neue Partie).</summary>
|
||||
|
|
@ -146,7 +146,7 @@ namespace Luneraria.UI
|
|||
{
|
||||
Normal = M("e8c47e", "b98a4a", H("fff0cc"), 1.5f, 9f, 30f, 15f), Hover = M("f6d898", "cc9c56", Color.white, 1.5f, 9f, 30f, 15f),
|
||||
Pressed = M("b98a4a", "d5aa66", H("fff0cc"), 2f, 9f, 30f, 15f), Disabled = M("5a4c38", "463a2a", H("7d6a4c"), 1.5f, 9f, 30f, 15f),
|
||||
TextVariation = "HudName", TextSize = 22f, Text = LuneUiTheme.Ink, TextHover = LuneUiTheme.Ink, TextPressed = LuneUiTheme.Ink, TextDisabled = H("2a2218"),
|
||||
TextVariation = "PrimaryButton", TextSize = 22f, Text = LuneUiTheme.Ink, TextHover = LuneUiTheme.Ink, TextPressed = LuneUiTheme.Ink, TextDisabled = H("2a2218"),
|
||||
};
|
||||
|
||||
/// <summary>Godot Button of the menu theme (secondary).</summary>
|
||||
|
|
|
|||
121
Assets/Luneraria/UI/Hud/LuneFieldTags.cs
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
using System.Collections.Generic;
|
||||
using Luneraria.Core;
|
||||
using Luneraria.View;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace Luneraria.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Name tags of the named figures in the field (port of the Label3D tags of
|
||||
/// lune_unit_view.gd): heroes with their level in hero gold, attack champions and the boss in
|
||||
/// their team's light colour. A deployed boss says whom it fights for ("SCHLUNDKRONE · für
|
||||
/// Blau"), G3's answer to the colour closeness of its crystals and Blau's armies. The tags lie
|
||||
/// in the HUD panel behind every region, centred above the figure's head, at a fixed size on
|
||||
/// screen like Godot's fixed_size tags, and never take the mouse.
|
||||
/// </summary>
|
||||
public sealed class LuneFieldTags : VisualElement
|
||||
{
|
||||
/// <summary>World team colours of lune_unit_view.gd (TEAM_COLORS) and HERO_GOLD.</summary>
|
||||
static readonly Color[] TeamColors = { LuneUiTheme.Hex("5aa8ff"), LuneUiTheme.Hex("ff6a55"), LuneUiTheme.Hex("b37cff") };
|
||||
static readonly Color HeroGold = LuneUiTheme.Hex("f2c96b");
|
||||
/// <summary>Logical size; Godot tags measure about 18 px at 1080p and a tenth wider than Cantarell at 16.</summary>
|
||||
public const float Size = 17.5f;
|
||||
|
||||
readonly Dictionary<LuneUnitView, Label> _tags = new Dictionary<LuneUnitView, Label>();
|
||||
readonly List<LuneUnitView> _gone = new List<LuneUnitView>();
|
||||
readonly HashSet<LuneUnitView> _seen = new HashSet<LuneUnitView>();
|
||||
|
||||
public LuneFieldTags()
|
||||
{
|
||||
name = "field-tags";
|
||||
pickingMode = PickingMode.Ignore;
|
||||
style.position = Position.Absolute;
|
||||
style.left = 0f;
|
||||
style.top = 0f;
|
||||
style.right = 0f;
|
||||
style.bottom = 0f;
|
||||
}
|
||||
|
||||
/// <summary>Visible tags by text (tests).</summary>
|
||||
public IEnumerable<string> Texts
|
||||
{
|
||||
get
|
||||
{
|
||||
foreach (Label label in _tags.Values)
|
||||
{
|
||||
if (LuneUi.Visible(label))
|
||||
yield return label.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static bool Named(LuneUnit unit)
|
||||
{
|
||||
return unit.Kind == "hero" || unit.Kind == "champion" || unit.Kind == "boss";
|
||||
}
|
||||
|
||||
/// <summary>Godot: hero "Name · Stufe n", deployed boss "NAME · für Blau/Rot", otherwise the name.</summary>
|
||||
public static string TextFor(LuneUnit unit)
|
||||
{
|
||||
string title = unit.Def.DisplayName;
|
||||
if (unit.Kind == "hero")
|
||||
return title + " · Stufe " + unit.Def.Level;
|
||||
if (unit.Kind == "boss" && unit.Team <= 1)
|
||||
return title.ToUpperInvariant() + " · für " + (unit.Team == 0 ? "Blau" : "Rot");
|
||||
return title;
|
||||
}
|
||||
|
||||
public static Color ColorFor(LuneUnit unit)
|
||||
{
|
||||
return unit.Kind == "hero" ? HeroGold : LuneUiTheme.Lightened(TeamColors[Mathf.Clamp(unit.Team, 0, 2)], .3f);
|
||||
}
|
||||
|
||||
/// <summary>Places a tag over every living named figure the camera sees; the others hide.</summary>
|
||||
public void Sync(IEnumerable<LuneUnitView> views, Camera camera, LuneHud hud)
|
||||
{
|
||||
_seen.Clear();
|
||||
Vector2 target = hud.TargetSize;
|
||||
foreach (LuneUnitView view in views)
|
||||
{
|
||||
LuneUnit unit = view.Unit;
|
||||
if (unit == null || !Named(unit) || !unit.Alive || !view.gameObject.activeInHierarchy)
|
||||
continue;
|
||||
Vector3 screen = camera.WorldToScreenPoint(view.TagPoint);
|
||||
if (screen.z <= 0f)
|
||||
continue;
|
||||
if (!_tags.TryGetValue(view, out Label label))
|
||||
{
|
||||
label = LuneUi.Label("", "HudText", this, Size);
|
||||
label.style.position = Position.Absolute;
|
||||
label.style.translate = new Translate(Length.Percent(-50f), Length.Percent(-50f));
|
||||
// Godot's tag has a heavy dark outline; a dense shadow keeps the strokes whole (see LuneUi.SetColor).
|
||||
label.style.textShadow = new TextShadow { offset = new Vector2(0f, 1f), blurRadius = 3f, color = new Color(0f, 0f, 0f, .95f) };
|
||||
_tags[view] = label;
|
||||
}
|
||||
string text = TextFor(unit);
|
||||
if (label.text != text)
|
||||
label.text = text;
|
||||
label.style.color = ColorFor(unit);
|
||||
Vector2 at = hud.PixelToPanel(new Vector2(screen.x, target.y - screen.y));
|
||||
label.style.left = Mathf.Round(at.x);
|
||||
label.style.top = Mathf.Round(at.y);
|
||||
LuneUi.Show(label, true);
|
||||
_seen.Add(view);
|
||||
}
|
||||
_gone.Clear();
|
||||
foreach (KeyValuePair<LuneUnitView, Label> pair in _tags)
|
||||
{
|
||||
if (pair.Key == null)
|
||||
_gone.Add(pair.Key);
|
||||
else if (!_seen.Contains(pair.Key))
|
||||
LuneUi.Show(pair.Value, false);
|
||||
}
|
||||
foreach (LuneUnitView view in _gone)
|
||||
{
|
||||
_tags[view].RemoveFromHierarchy();
|
||||
_tags.Remove(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Luneraria/UI/Hud/LuneFieldTags.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 00e663adafe8943d78136f26fd1fa8c0
|
||||
|
|
@ -414,8 +414,8 @@ namespace Luneraria.UI
|
|||
if (hero.Fresh && model.Planning)
|
||||
{
|
||||
int refund = hero.Invested + hero.Items.Sum(id => LuneContent.Items[id].Cost);
|
||||
// Godot: a plain button in the column fills its width.
|
||||
var cancel = new LuneButton("Held zurückgeben +" + refund, LuneButtons.HudButton, left);
|
||||
cancel.style.alignSelf = Align.FlexStart;
|
||||
cancel.Disabled = !canAct;
|
||||
cancel.Clicked += () => CommandRequested?.Invoke(Command.HeroCancel());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,6 +65,8 @@ namespace Luneraria.UI
|
|||
public VisualElement Root { get; private set; }
|
||||
public float UiScale { get; private set; } = 1f;
|
||||
public LuneMinimap Minimap { get; private set; }
|
||||
/// <summary>Name tags of heroes, champions and the boss in the field, behind every region.</summary>
|
||||
public LuneFieldTags FieldTags { get; private set; }
|
||||
public LuneTooltip Tooltip { get; private set; }
|
||||
public LuneHeroCard HeroCard { get; private set; }
|
||||
public LuneHeroDetail HeroDetail { get; private set; }
|
||||
|
|
@ -217,6 +219,8 @@ namespace Luneraria.UI
|
|||
_hud.style.right = 0f;
|
||||
_hud.style.bottom = 0f;
|
||||
Root.Add(_hud);
|
||||
FieldTags = new LuneFieldTags();
|
||||
_hud.Add(FieldTags);
|
||||
BuildTop();
|
||||
BuildSystem();
|
||||
BuildLeft();
|
||||
|
|
@ -234,6 +238,12 @@ namespace Luneraria.UI
|
|||
_hud.Add(Tooltip);
|
||||
_hud.RegisterCallback<PointerMoveEvent>(evt => Tooltip.Pointer = evt.position, TrickleDown.TrickleDown);
|
||||
_hud.RegisterCallback<PointerDownEvent>(evt => Tooltip.Pointer = evt.position, TrickleDown.TrickleDown);
|
||||
// Banner, status line and right column follow the real heights of the top panel, the
|
||||
// bottom bar and the system row (Godot _layout on resize); before the first layout
|
||||
// Layout() uses estimates.
|
||||
_top.RegisterCallback<GeometryChangedEvent>(_ => Layout());
|
||||
_bottom.RegisterCallback<GeometryChangedEvent>(_ => Layout());
|
||||
_system.RegisterCallback<GeometryChangedEvent>(_ => Layout());
|
||||
LunePreferences.Changed -= Fit;
|
||||
LunePreferences.Changed += Fit;
|
||||
Fit();
|
||||
|
|
@ -699,12 +709,17 @@ namespace Luneraria.UI
|
|||
}
|
||||
|
||||
// ---------------------------------------------------------------- per frame
|
||||
/// <summary>Main-thread cost of the last frame's Tick (milliseconds; benchmark).</summary>
|
||||
public double LastTickMs { get; private set; }
|
||||
readonly System.Diagnostics.Stopwatch _tickClock = new System.Diagnostics.Stopwatch();
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (Match == null)
|
||||
return;
|
||||
float delta = Time.unscaledDeltaTime;
|
||||
Tick(delta);
|
||||
_tickClock.Restart();
|
||||
Tick(Time.unscaledDeltaTime);
|
||||
LastTickMs = _tickClock.Elapsed.TotalMilliseconds;
|
||||
}
|
||||
|
||||
/// <summary>One HUD frame (Godot _process); tests call it with simulated time.</summary>
|
||||
|
|
|
|||
|
|
@ -56,10 +56,12 @@ namespace Luneraria.UI
|
|||
return image;
|
||||
}
|
||||
|
||||
/// <summary>PNG, or JPEG (quality 90) for a .jpg file.</summary>
|
||||
public static void Save(Texture2D image, string file)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(file));
|
||||
File.WriteAllBytes(file, image.EncodeToPNG());
|
||||
bool jpeg = file.EndsWith(".jpg", System.StringComparison.OrdinalIgnoreCase);
|
||||
File.WriteAllBytes(file, jpeg ? image.EncodeToJPG(90) : image.EncodeToPNG());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,14 +6,20 @@ using TextElement = UnityEngine.UIElements.TextElement;
|
|||
|
||||
namespace Luneraria.UI
|
||||
{
|
||||
/// <summary>Weights of the HUD fonts (Godot FontVariation weights of lune_ui_theme.gd).</summary>
|
||||
/// <summary>
|
||||
/// Font roles of the HUD as lune_ui_theme.gd declares them: Cantarell 560, Cantarell 700–760
|
||||
/// and Cinzel 700. Godot never applied those weights: its FontVariation used the String key
|
||||
/// "wght", which Godot ignores (measured in 4.7.2: the same advances at 400, 560 and 700; only
|
||||
/// the integer tag changes them). G3 therefore shows both fonts in their default instance,
|
||||
/// Regular 400, and so does the port. The roles stay apart so that U5 can give them weights.
|
||||
/// </summary>
|
||||
public enum LuneFace
|
||||
{
|
||||
/// <summary>Cantarell 560: Regular with a thin outline in the text colour.</summary>
|
||||
/// <summary>Cantarell (declared 560).</summary>
|
||||
Text,
|
||||
/// <summary>Cantarell 700–760.</summary>
|
||||
/// <summary>Cantarell (declared 700–760).</summary>
|
||||
Bold,
|
||||
/// <summary>Cinzel 700.</summary>
|
||||
/// <summary>Cinzel (declared 700).</summary>
|
||||
Display,
|
||||
}
|
||||
|
||||
|
|
@ -55,12 +61,14 @@ namespace Luneraria.UI
|
|||
{ "HudBadge", new LuneTextStyle(LuneFace.Bold, 14, LuneUiTheme.Text, 1f) },
|
||||
{ "HudLane", new LuneTextStyle(LuneFace.Display, 16, Title, 1f) },
|
||||
{ "HudValue", new LuneTextStyle(LuneFace.Bold, 17, LuneUiTheme.Bright) },
|
||||
{ "HudPrimary", new LuneTextStyle(LuneFace.Bold, 16, LuneUiTheme.Ink, 1f) },
|
||||
{ "HudGold", new LuneTextStyle(LuneFace.Bold, 24, LuneUiTheme.Gold, 0f, 4f) },
|
||||
{ "HudName", new LuneTextStyle(LuneFace.Display, 18, Title, 1f) },
|
||||
{ "HudTitle", new LuneTextStyle(LuneFace.Display, 24, Title, 1f, 4f) },
|
||||
{ "HudWave", new LuneTextStyle(LuneFace.Display, 30, Title, 3f, 4f) },
|
||||
// Menu theme (game-over panel, options).
|
||||
{ "TitleLabel", new LuneTextStyle(LuneFace.Display, 78, Title, 2f, 6f) },
|
||||
{ "PrimaryButton", new LuneTextStyle(LuneFace.Display, 22, LuneUiTheme.Ink, 2f) },
|
||||
{ "HeadingLabel", new LuneTextStyle(LuneFace.Display, 34, Title, 1f) },
|
||||
{ "SectionLabel", new LuneTextStyle(LuneFace.Display, 16, LuneUiTheme.BronzeLight, 3f) },
|
||||
{ "BodyLabel", new LuneTextStyle(LuneFace.Text, 20, LuneUiTheme.Parchment) },
|
||||
|
|
@ -69,14 +77,21 @@ namespace Luneraria.UI
|
|||
{ "CardFactLabel", new LuneTextStyle(LuneFace.Text, 17, LuneUiTheme.Hex("d8cfbc")) },
|
||||
};
|
||||
|
||||
/// <summary>Outline that thickens Cantarell Regular to the G3 text weight 560.</summary>
|
||||
public const float MediumOutline = .25f;
|
||||
|
||||
/// <summary>The atlas of a role: Cinzel Regular for Display, Cantarell Regular otherwise (see LuneFace).</summary>
|
||||
public static FontAsset Font(LuneFace face)
|
||||
{
|
||||
if (Assets == null)
|
||||
return null;
|
||||
return face == LuneFace.Display ? Assets.Display : face == LuneFace.Bold ? Assets.TextBold : Assets.Text;
|
||||
return face == LuneFace.Display ? Assets.Display : Assets.Text;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Godot's spacing_glyph (pixels per glyph) as UI Toolkit letter spacing, which counts in
|
||||
/// hundredths of the font size (measured: 10 widens each glyph by 0.1 em).
|
||||
/// </summary>
|
||||
public static float LetterSpacing(float pixels, float fontSize)
|
||||
{
|
||||
return fontSize > 0f ? pixels * 100f / fontSize : 0f;
|
||||
}
|
||||
|
||||
/// <summary>Applies a text style; size 0 keeps the style's size.</summary>
|
||||
|
|
@ -87,13 +102,18 @@ namespace Luneraria.UI
|
|||
FontAsset font = Font(style.Face);
|
||||
if (font != null)
|
||||
element.style.unityFontDefinition = FontDefinition.FromSDFFont(font);
|
||||
element.style.fontSize = size > 0f ? Mathf.Max(LuneUiTheme.MinFont, size) : style.Size;
|
||||
element.style.letterSpacing = style.Spacing;
|
||||
float fontSize = size > 0f ? Mathf.Max(LuneUiTheme.MinFont, size) : style.Size;
|
||||
element.style.fontSize = fontSize;
|
||||
element.style.letterSpacing = LetterSpacing(style.Spacing, fontSize);
|
||||
element.style.textShadow = new TextShadow { offset = new Vector2(0f, 1f), blurRadius = 1f, color = new Color(0f, 0f, 0f, .55f) };
|
||||
SetColor(element, style.Color, style);
|
||||
}
|
||||
|
||||
/// <summary>Text colour; keeps the weight outline (560) or the dark title outline in step.</summary>
|
||||
/// <summary>
|
||||
/// Text colour. Titles, the wave and gold get Godot's dark rim (outline_size, black .55) as
|
||||
/// a soft shadow: a TextCore outline straddles the glyph edge and eats the thin strokes of
|
||||
/// the regular faces, Godot's outline only grows outwards.
|
||||
/// </summary>
|
||||
public static void SetColor(TextElement element, Color color, LuneTextStyle style = null)
|
||||
{
|
||||
element.style.color = color;
|
||||
|
|
@ -102,20 +122,9 @@ namespace Luneraria.UI
|
|||
if (style == null)
|
||||
return;
|
||||
element.userData = style;
|
||||
element.style.unityTextOutlineWidth = 0f;
|
||||
if (style.Outline > 0f)
|
||||
{
|
||||
element.style.unityTextOutlineWidth = style.Outline * .25f;
|
||||
element.style.unityTextOutlineColor = new Color(0f, 0f, 0f, .55f);
|
||||
}
|
||||
else if (style.Face == LuneFace.Text)
|
||||
{
|
||||
element.style.unityTextOutlineWidth = MediumOutline;
|
||||
element.style.unityTextOutlineColor = color;
|
||||
}
|
||||
else
|
||||
{
|
||||
element.style.unityTextOutlineWidth = 0f;
|
||||
}
|
||||
element.style.textShadow = new TextShadow { offset = new Vector2(0f, 1f), blurRadius = 1f + style.Outline * .5f, color = new Color(0f, 0f, 0f, .7f) };
|
||||
}
|
||||
|
||||
public static Label Label(string text, string variation = "HudText", VisualElement parent = null, float size = 0f)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ namespace Luneraria.View
|
|||
/// "-luneBenchmark <out.json>": runs the match of seed 9 from round 21 (the golden fixture
|
||||
/// high_wave_21, the most units) at normal speed, VSync off, and measures two windows once the
|
||||
/// armies meet: the whole map (20 s) and a close view of the Mid lane (10 s). Then it quits.
|
||||
/// Since U3 the scene carries the HUD (watching seat 1 of the bot match); its own scripts'
|
||||
/// cost per frame comes through UiMs (layout and drawing of the panel count in the threads).
|
||||
/// </summary>
|
||||
[DefaultExecutionOrder(-100)]
|
||||
public sealed class LuneBenchmark : MonoBehaviour
|
||||
|
|
@ -30,6 +32,7 @@ namespace Luneraria.View
|
|||
public readonly List<double> FrameMs = new List<double>();
|
||||
public double Simulation;
|
||||
public double Views;
|
||||
public double Ui;
|
||||
public int Units;
|
||||
public int GcStart;
|
||||
public int GcEnd;
|
||||
|
|
@ -70,6 +73,9 @@ namespace Luneraria.View
|
|||
/// <summary>True in a benchmark run: the game controller keeps the bot match (no human seat).</summary>
|
||||
public static bool Running { get; private set; }
|
||||
|
||||
/// <summary>Main-thread cost of the HUD scripts in the last frame (set by the game controller, U3).</summary>
|
||||
public static Func<double> UiMs;
|
||||
|
||||
void Start()
|
||||
{
|
||||
Driver.HumanSlot = -1;
|
||||
|
|
@ -114,6 +120,7 @@ namespace Luneraria.View
|
|||
}
|
||||
window.Simulation += Driver.LastAdvanceMs;
|
||||
window.Views += View.LastSyncMs;
|
||||
window.Ui += UiMs != null ? UiMs() : 0.0;
|
||||
if (match.Combat != null && window.FrameMs.Count % 30 == 1)
|
||||
window.Units = Math.Max(window.Units, CountAlive(match.Combat));
|
||||
_elapsed += Time.unscaledDeltaTime;
|
||||
|
|
@ -167,9 +174,9 @@ namespace Luneraria.View
|
|||
double[] sorted = window.FrameMs.OrderBy(v => v).ToArray();
|
||||
double total = window.FrameMs.Sum();
|
||||
text.AppendFormat(CultureInfo.InvariantCulture,
|
||||
" {{\"view\": \"{0}\", \"frames\": {1}, \"fps\": {2:0.0}, \"frame_ms_avg\": {3:0.00}, \"frame_ms_median\": {4:0.00}, \"frame_ms_p95\": {5:0.00}, \"frame_ms_p99\": {6:0.00}, \"frame_ms_max\": {7:0.00}, \"frames_over_16_7_ms\": {8}, \"of_them_with_simulation_step\": {9}, \"gc_collections\": {10}, \"simulation_ms_per_frame\": {11:0.000}, \"views_ms_per_frame\": {12:0.000}, \"units_alive_max\": {13}}}{14}\n",
|
||||
" {{\"view\": \"{0}\", \"frames\": {1}, \"fps\": {2:0.0}, \"frame_ms_avg\": {3:0.00}, \"frame_ms_median\": {4:0.00}, \"frame_ms_p95\": {5:0.00}, \"frame_ms_p99\": {6:0.00}, \"frame_ms_max\": {7:0.00}, \"frames_over_16_7_ms\": {8}, \"of_them_with_simulation_step\": {9}, \"gc_collections\": {10}, \"simulation_ms_per_frame\": {11:0.000}, \"views_ms_per_frame\": {12:0.000}, \"ui_ms_per_frame\": {15:0.000}, \"units_alive_max\": {13}}}{14}\n",
|
||||
window.Label, sorted.Length, sorted.Length / (total / 1000.0), total / sorted.Length, sorted[sorted.Length / 2], sorted[(int)(sorted.Length * .95)], sorted[(int)(sorted.Length * .99)], sorted[sorted.Length - 1],
|
||||
window.SlowFrames, window.SlowWithStep, window.GcEnd - window.GcStart, window.Simulation / sorted.Length, window.Views / sorted.Length, window.Units, i < _windows.Count - 1 ? "," : "");
|
||||
window.SlowFrames, window.SlowWithStep, window.GcEnd - window.GcStart, window.Simulation / sorted.Length, window.Views / sorted.Length, window.Units, i < _windows.Count - 1 ? "," : "", window.Ui / sorted.Length);
|
||||
text.AppendFormat(CultureInfo.InvariantCulture, " // timings {0}: main thread {1}, render thread {2}, gpu {3}, present wait {4}\n", window.Label,
|
||||
Describe(window.MainThreadMs), Describe(window.RenderThreadMs), Describe(window.GpuMs), Describe(window.PresentWaitMs));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Collections.Generic;
|
||||
using Luneraria.Core;
|
||||
using UnityEngine;
|
||||
|
||||
|
|
@ -19,6 +20,8 @@ namespace Luneraria.View
|
|||
public int StartRound = 1;
|
||||
public int ExtraGold;
|
||||
public string Profile = "";
|
||||
/// <summary>Optional Bünde of the six seats (fixtures like Godot's start option "bunds"); empty seats follow the seed.</summary>
|
||||
[System.NonSerialized] public string[] Bunds;
|
||||
/// <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>
|
||||
|
|
@ -37,7 +40,8 @@ namespace Luneraria.View
|
|||
|
||||
public void Restart()
|
||||
{
|
||||
Runner = new LuneMatchRunner(new MatchOptions { Seed = Seed, HumanSlot = HumanSlot }, StartRound, ExtraGold, Profile);
|
||||
var options = new MatchOptions { Seed = Seed, HumanSlot = HumanSlot, Bunds = Bunds != null ? new List<string>(Bunds) : new List<string>() };
|
||||
Runner = new LuneMatchRunner(options, StartRound, ExtraGold, Profile);
|
||||
if (RecordTrace)
|
||||
Runner.Trace = new LuneTrace();
|
||||
SpeedIndex = 0;
|
||||
|
|
|
|||
|
|
@ -6,8 +6,9 @@ namespace Luneraria.View
|
|||
{
|
||||
/// <summary>
|
||||
/// Shows the driver's match (U2C): unit views per combat, building looks from the authoritative
|
||||
/// HP, tower bolts from the towers' attack events. Runs after the driver in the same frame and
|
||||
/// only reads the core.
|
||||
/// HP with the Ancients' health bars, tower bolts from the towers' attack events, and the
|
||||
/// Schlundkrone resting in its grotto while the grotto is open (U3, from lune_grotto_view.gd).
|
||||
/// Runs after the driver in the same frame and only reads the core.
|
||||
/// </summary>
|
||||
[DefaultExecutionOrder(100)]
|
||||
public sealed class LuneMatchView : MonoBehaviour
|
||||
|
|
@ -28,7 +29,23 @@ namespace Luneraria.View
|
|||
LuneCombat _combat;
|
||||
Transform _unitRoot;
|
||||
bool _synced;
|
||||
GameObject _restingBoss;
|
||||
readonly Dictionary<string, AncientBar> _ancientBars = new Dictionary<string, AncientBar>();
|
||||
|
||||
/// <summary>Health bar over an Ancient (lune_ancient_view.gd _build_bar: 3.4 m wide, 11.2 m up).</summary>
|
||||
sealed class AncientBar
|
||||
{
|
||||
public Transform Root;
|
||||
public Transform Fill;
|
||||
}
|
||||
|
||||
public const float AncientBarWidth = 3.4f;
|
||||
public const float AncientBarHeight = 11.2f;
|
||||
/// <summary>Godot BOSS_LOCAL and view scale of the resting boss on its dais.</summary>
|
||||
public const float RestingBossScale = 1.7f;
|
||||
|
||||
/// <summary>The resting Schlundkrone (tests); null until the grotto was first open.</summary>
|
||||
public GameObject RestingBoss { get { return _restingBoss; } }
|
||||
sealed class Bolt
|
||||
{
|
||||
public LineRenderer Line;
|
||||
|
|
@ -96,6 +113,7 @@ namespace Luneraria.View
|
|||
}
|
||||
SyncBuildings(match);
|
||||
SyncBolts(dt);
|
||||
SyncRestingBoss(match);
|
||||
_synced = true;
|
||||
LastSyncMs = _clock.Elapsed.TotalMilliseconds;
|
||||
}
|
||||
|
|
@ -112,6 +130,8 @@ namespace Luneraria.View
|
|||
if (_combat != null && _combat.Buildings.TryGetValue(definition.Id, out unit) && fighting)
|
||||
hp = unit.Hp;
|
||||
view.SetHealth((float)hp, (float)definition.MaxHp, _synced);
|
||||
if (view.Tier == "core")
|
||||
SyncAncientBar(view, (float)hp, (float)definition.MaxHp);
|
||||
if (unit != null && unit.Kind == "tower" && _combat != null)
|
||||
TowerAttack(view, unit);
|
||||
}
|
||||
|
|
@ -134,6 +154,88 @@ namespace Luneraria.View
|
|||
_bolts.Add(new Bolt { Line = line, Target = target, From = view.Muzzle });
|
||||
}
|
||||
|
||||
/// <summary>The bar shows while the Ancient is damaged and standing, always facing the camera.</summary>
|
||||
void SyncAncientBar(LuneBuildingView view, float hp, float maxHp)
|
||||
{
|
||||
float fraction = Mathf.Clamp01(hp / Mathf.Max(1f, maxHp));
|
||||
bool visible = hp > 0f && fraction < .999f;
|
||||
_ancientBars.TryGetValue(view.BuildingId, out AncientBar bar);
|
||||
if (bar == null)
|
||||
{
|
||||
if (!visible || Library == null || Library.Quad == null)
|
||||
return;
|
||||
bar = new AncientBar { Root = new GameObject("AncientBar").transform };
|
||||
bar.Root.SetParent(view.transform, false);
|
||||
bar.Root.localPosition = new Vector3(0f, AncientBarHeight, 0f);
|
||||
Transform back = Quad(bar.Root, "Back", Library.BarBack);
|
||||
back.localScale = new Vector3(AncientBarWidth + .1f, .26f, 1f);
|
||||
bar.Fill = Quad(bar.Root, "Fill", Library.AncientBarFill[Mathf.Clamp(view.Team, 0, 1)]);
|
||||
_ancientBars[view.BuildingId] = bar;
|
||||
}
|
||||
if (bar.Root.gameObject.activeSelf != visible)
|
||||
bar.Root.gameObject.SetActive(visible);
|
||||
if (!visible)
|
||||
return;
|
||||
bar.Fill.localScale = new Vector3(AncientBarWidth * Mathf.Max(.001f, fraction), .17f, 1f);
|
||||
bar.Fill.localPosition = new Vector3(-AncientBarWidth * .5f * (1f - fraction), 0f, -.001f);
|
||||
if (ViewCamera != null)
|
||||
bar.Root.rotation = ViewCamera.transform.rotation;
|
||||
}
|
||||
|
||||
Transform Quad(Transform parent, string label, Material material)
|
||||
{
|
||||
var go = new GameObject(label);
|
||||
go.transform.SetParent(parent, false);
|
||||
go.AddComponent<MeshFilter>().sharedMesh = Library.Quad;
|
||||
var renderer = go.AddComponent<MeshRenderer>();
|
||||
renderer.sharedMaterial = material;
|
||||
renderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
|
||||
renderer.receiveShadows = false;
|
||||
return go.transform;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Schlundkrone rests on its dais while the grotto is open and no boss unit stands there
|
||||
/// (lune_grotto_view.gd: state AVAILABLE without a fighter): the figure the fight uses,
|
||||
/// neutral, facing the arena and the two stairs, in its idle clip. The grotto's light
|
||||
/// states (runes, crystals, motes) are not ported yet.
|
||||
/// </summary>
|
||||
void SyncRestingBoss(LuneMatch match)
|
||||
{
|
||||
LuneUnit fighter = match.Phase == Phase.Combat && match.Combat != null ? match.Combat.GrottoBoss : null;
|
||||
bool visible = match.Boss.Claim == null && match.GrottoAvailable() && fighter == null;
|
||||
if (visible && _restingBoss == null)
|
||||
_restingBoss = MakeRestingBoss(match.Map);
|
||||
if (_restingBoss != null && _restingBoss.activeSelf != visible)
|
||||
_restingBoss.SetActive(visible);
|
||||
}
|
||||
|
||||
GameObject MakeRestingBoss(LuneMap map)
|
||||
{
|
||||
LuneUnitLibrary.UnitModel model = Library != null ? Library.Find(Library.BossModel) : null;
|
||||
if (model == null || model.Prefab == null)
|
||||
return null;
|
||||
var root = new GameObject("RestingBoss");
|
||||
root.transform.SetParent(transform, false);
|
||||
Vec2 at = map.GrottoBossPoint();
|
||||
root.transform.position = LuneSpace.World(at, (Library.Ground != null ? Library.Ground.StandHeight(at) : 0f) + .03f);
|
||||
root.transform.rotation = LuneSpace.Facing(System.Math.Atan2(LuneMap.Axis.X, LuneMap.Axis.Y));
|
||||
GameObject figure = Instantiate(model.Prefab, root.transform, false);
|
||||
figure.name = "Model";
|
||||
figure.SetActive(true);
|
||||
figure.transform.localScale = Vector3.one * RestingBossScale;
|
||||
Library.Materials.Dress(figure, 2);
|
||||
var animator = figure.GetComponentInChildren<Animator>();
|
||||
if (animator != null && model.Controller != null)
|
||||
{
|
||||
animator.runtimeAnimatorController = model.Controller;
|
||||
animator.applyRootMotion = false;
|
||||
if (animator.HasState(0, Animator.StringToHash("idle")))
|
||||
animator.Play("idle", 0, 0f);
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
void SyncBolts(float dt)
|
||||
{
|
||||
for (int i = _bolts.Count - 1; i >= 0; i--)
|
||||
|
|
|
|||
|
|
@ -39,6 +39,12 @@ namespace Luneraria.View
|
|||
public Material BarBack;
|
||||
public Material[] BarFill = new Material[3];
|
||||
public Material[] Bolt = new Material[3];
|
||||
/// <summary>Wide owner ring of a deployed boss (lune_unit_view.gd, G3): round for Blau, diamond for Rot.</summary>
|
||||
public Mesh OwnerRingRound;
|
||||
public Mesh OwnerRingDiamond;
|
||||
public Material[] OwnerRing = new Material[2];
|
||||
/// <summary>Fill of the Ancients' health bars (team light colour, lune_ancient_view.gd).</summary>
|
||||
public Material[] AncientBarFill = new Material[2];
|
||||
|
||||
Dictionary<string, UnitModel> _index;
|
||||
readonly Dictionary<GameObject, GameObject> _combined = new Dictionary<GameObject, GameObject>();
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ namespace Luneraria.View
|
|||
/// <summary>
|
||||
/// Visual twin of one LuneUnit (port of scripts/luneraria/view/lune_unit_view.gd). Reads the
|
||||
/// authoritative state and never writes back: position between the last two fixed steps,
|
||||
/// facing, clips from real combat events (attack and ability serials), team ring, health bar.
|
||||
/// facing, clips from real combat events (attack and ability serials), team ring, health bar,
|
||||
/// and for a deployed boss the wide owner ring of its team (G3: round for Blau, diamond for Rot).
|
||||
/// </summary>
|
||||
public sealed class LuneUnitView : MonoBehaviour
|
||||
{
|
||||
|
|
@ -19,6 +20,7 @@ namespace Luneraria.View
|
|||
bool _skeletal;
|
||||
float _height = 1.6f;
|
||||
Transform _ring;
|
||||
Transform _ownerRing;
|
||||
Transform _bars;
|
||||
Transform _fill;
|
||||
int _lastAttack;
|
||||
|
|
@ -60,6 +62,12 @@ namespace Luneraria.View
|
|||
unit.Kind == "hero" ? library.HeroRing : library.TeamRing[team]);
|
||||
_ring.localScale = new Vector3(scale, 1f, scale);
|
||||
_ring.localPosition = new Vector3(0f, .05f, 0f);
|
||||
if (unit.Kind == "boss" && unit.Team <= 1 && library.OwnerRingRound != null)
|
||||
{
|
||||
_ownerRing = Marker("OwnerRing", unit.Team == 0 ? library.OwnerRingRound : library.OwnerRingDiamond, library.OwnerRing[unit.Team]);
|
||||
_ownerRing.localScale = new Vector3(scale, 1f, scale);
|
||||
_ownerRing.localPosition = new Vector3(0f, .07f, 0f);
|
||||
}
|
||||
_bars = new GameObject("Bars").transform;
|
||||
_bars.SetParent(transform, false);
|
||||
_bars.localPosition = new Vector3(0f, _height + .25f, 0f);
|
||||
|
|
@ -98,6 +106,9 @@ namespace Luneraria.View
|
|||
/// <summary>Where tower bolts hit this figure (upper body).</summary>
|
||||
public Vector3 AimPoint { get { return transform.position + Vector3.up * _height * .55f; } }
|
||||
|
||||
/// <summary>Anchor of the name tag above the head (Godot tag.position.y = height + .55).</summary>
|
||||
public Vector3 TagPoint { get { return transform.position + Vector3.up * (_height + .55f); } }
|
||||
|
||||
public void Sync(float alpha, float dt, Transform camera)
|
||||
{
|
||||
_time += dt;
|
||||
|
|
@ -124,6 +135,8 @@ namespace Luneraria.View
|
|||
if (camera != null)
|
||||
_bars.rotation = camera.rotation;
|
||||
_ring.gameObject.SetActive(Unit.Alive);
|
||||
if (_ownerRing != null)
|
||||
_ownerRing.gameObject.SetActive(Unit.Alive);
|
||||
// Rigid figures have no death clip; skeletal ones stay down in their death pose.
|
||||
if (!_skeletal)
|
||||
_model.gameObject.SetActive(Unit.Alive);
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ Die Szene ändert kein Verhalten. Der Host treibt den Kern im festen Takt; die A
|
|||
| Wert | Warum kein Regelunterschied |
|
||||
| --- | --- |
|
||||
| Blickrichtung (`LuneUnit.Facing`) | Der Kern schreibt sie nur (Bewegungsrichtung ab 1 mm Schritt); keine Regel liest sie. Die Darstellung darf sie glätten. |
|
||||
| Schriftgewichte (U3D) | Das Theme der Referenz deklariert Cantarell 560/700–760 und Cinzel 700, wendet sie aber nie an: `SaumwachtTheme.font` schreibt `variation_opentype = {"wght": …}` mit dem String-Schlüssel „wght“, den Godot 4.7.2 ignoriert. G3 zeigt Regular 400, der Port ebenso. Die Rollen bleiben getrennt, damit U5 echte Gewichte vergeben kann ([U3-UI.md](U3-UI.md#theme-symbole-schriften)). |
|
||||
|
||||
## Technische Repräsentation
|
||||
|
||||
|
|
|
|||
|
|
@ -55,6 +55,18 @@ Hinweis: Die Projektdateien des .NET-Harness (`tools/dotnet/*/*.csproj`) fehlten
|
|||
| Offen | Draw Calls messen (U5); Windows-/Linux-Player (Build-Module) |
|
||||
| Commit | `luneraria/u2`: Batch-Import `3ca38a8` (Modelle direkt in Git) |
|
||||
|
||||
## U3 bis U5
|
||||
## U3 · G3-HUD portieren
|
||||
|
||||
U3 (G3-HUD portieren) ist der nächste Schritt; U4 und U5 sind noch nicht begonnen.
|
||||
| Punkt | Ergebnis |
|
||||
| --- | --- |
|
||||
| Tests | EditMode 23/23 (Kern 402 Prüfungen, Sichtmodell, Bildraten-Logik). PlayMode 13/13: Verify A–I **718 Prüfungen, 0 Fehler** (Port von `verify_g3` 715/715 plus Teil I), Szene bei echten Bildraten, Vertical Function Test, Capture, UI-Toolkit-Probe. Bildwerkzeuge und Spieltest sind `Explicit`. Protokolle `logs/u3/`, Summen `tools/out/hud-verify-*.txt` |
|
||||
| Parität | Bot-Partie Seed 2 `DCE95C96556F3A1A` (12.690 Schritte), Seed 9 ab Runde 21 `7B8E43AEFC0D60C5` (4.195 Schritte), beide unverändert gleich dem Kern |
|
||||
| Bild | `screenshots/u3/motive-1920x1080/` (14 G3-Motive), `pruefung/` (Teile B–F), `aufloesungen/` (1152×720 … 2560×1440), `r1-*.png`/`r2-*.png` (Spieltest) |
|
||||
| Leistung | macOS-Player mit HUD, 116 Einheiten: 169 fps (Übersicht) und 218 fps (Mid nah). HUD-Skripte **0,090 bzw. 0,078 ms je Bild**; Simulation 0,09/0,04 ms, Ansichten 0,31/0,33 ms. Ausreißer beim Present (p95 10–13 ms) im Rahmen der U2D-Streuung. Rohdaten `logs/u3/player-benchmark-macos.json` |
|
||||
| Bekannte Unterschiede | Godots deklarierte Schriftgewichte werden in der Referenz nie angewendet (String-Schlüssel „wght“); Port zeigt wie G3 Regular 400. Kein Hauptmenü, kein Grottenlicht. Details [U3-UI.md](U3-UI.md) und [KNOWN-BEHAVIOR-DIFFERENCES.md](KNOWN-BEHAVIOR-DIFFERENCES.md) |
|
||||
| Offen | Hauptmenü und Netzwerk (U4); Schriftgewichte, Grottenlicht und weitere Darstellung (U5); Windows-/Linux-Player (Build-Module) |
|
||||
| Commits | `luneraria/u3`: U3A `4dee9b1`, U3B/U3C `2b86c0d`, U3D (dieser Stand) |
|
||||
|
||||
## U4 und U5
|
||||
|
||||
U4 (Netzwerk) und U5 (Visuelle Neuausrichtung) sind noch nicht begonnen.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Port-Status · Champions of Luneraria in Unity
|
||||
|
||||
Stand: 1. Oktober 2026.
|
||||
Stand: 2. Oktober 2026.
|
||||
|
||||
Referenz ist der Godot-Stand `luneraria/g3` (`1b540f2`) in `../saumwacht-godot`; er wird nicht mehr verändert. Engine: Unity 6.3 LTS (6000.3.20f1), Renderer URP. HDRP ist ausgeschlossen, bis ein Vergleich mit einer echten Szene etwas anderes nahelegt.
|
||||
|
||||
|
|
@ -11,8 +11,8 @@ Zielplattformen sind Fedora 44 und Windows 11; macOS ist die Entwicklungsplattfo
|
|||
| U1 · C#-Simulation | **fertig** (Referenzmeilenstein, `luneraria/u1` = `4d6ed17`): 402/402 Prüfungen, Golden-Parität mit Godot | [U1-RULES.md](U1-RULES.md) |
|
||||
| U1.5 · Unity-Integrationsprüfung | **fertig**: Kern im Unity-Editor 402/402 (Release und Debug), Golden bitgleich nach expliziter float32-Rundung in `Vec2`, Editor-Benchmark | [U1.5-UNITY.md](U1.5-UNITY.md) |
|
||||
| U2 · Unity-Spielwelt | **fertig**: U2A Blender→FBX-Pipeline (9 Assets geprüft), U2B G1-Karte mit Materialsystem, U2C fester Takt und Ansichten (bildratenunabhängig nachgewiesen), U2D vollständige Bot-Partien sichtbar und gleich dem Kern; danach Batch-Import aller 86 Modelle, Einheiten mit eigenen Modellen | [U2-WORLD.md](U2-WORLD.md) |
|
||||
| U3 · G3-HUD portieren | nächster Schritt | [U3-UI.md](U3-UI.md) |
|
||||
| U4 · Netzwerk | nicht begonnen; M4-Umfang ist erfasst | [U4-NETWORK.md](U4-NETWORK.md) |
|
||||
| U3 · G3-HUD portieren | **fertig** (`luneraria/u3`): UI Toolkit, Sichtmodell mit Privatsphäre, menschlicher Sitz, alle HUD-Bereiche, Eingabe und Befehle, Prüfung 718/0, 14 G3-Motive, Spieltest, Leistung mit HUD | [U3-UI.md](U3-UI.md) |
|
||||
| U4 · Netzwerk | nächster Schritt; M4-Umfang ist erfasst | [U4-NETWORK.md](U4-NETWORK.md) |
|
||||
| U5 · Visuelle Neuausrichtung (Vertical Slice Mid-Lane) | nicht begonnen | [U5-VISUAL.md](U5-VISUAL.md) |
|
||||
|
||||
QA je Meilenstein: [PORT-QA.md](PORT-QA.md).
|
||||
|
|
@ -36,6 +36,7 @@ Bekannte Verhaltensunterschiede, auch die bewusst übernommenen Referenzfehler:
|
|||
- **Prozedurale Welt gebacken (U2B):** Gelände und Vegetation der G1-Karte hängen an Godots Rauschen und Zufallsgenerator; ein Godot-Werkzeug backt die Welt aus dem Referenzgenerator, Unity baut daraus die Szene. U5 bekommt eine neue Weltquelle.
|
||||
- **Koordinaten:** Unity = (−x, Höhe, y) der Kartenpunkte; FBX-Pipeline, Welt und Kamera nutzen dieselbe Umrechnung, das Bild entspricht Godots.
|
||||
- **Host-Schleife (U2C):** fester Takt mit Akkumulator; die Bildrate bestimmt nur die Zahl der Schritte je Bild. Nachweis über Fingerabdrücke der Partie.
|
||||
- **Schriftgewichte wie in G3 (U3D):** Die Referenz deklariert Gewichte, wendet sie aber nie an (String-Schlüssel „wght“ wird von Godot ignoriert). Der Port zeigt wie G3 Regular 400. Die Stilrollen bleiben getrennt, damit U5 echte Gewichte vergeben kann.
|
||||
- **Modelle direkt in Git** (1. Oktober 2026): Auch die Modelle des Batch-Imports liegen wie die U2A-Modelle im Repository, ohne Git LFS. Keine Datei ist größer als 40 MB; das Repository wächst auf etwa 240 MB.
|
||||
- **Projektort außerhalb von iCloud Drive** (1. Oktober 2026): Der Arbeitsordner `TowerDefense` zieht von `~/Documents/Projekte` nach `~/Developer`. iCloud Drive synchronisierte `~/Documents` und legte Konfliktkopien an, die einen Player unbrauchbar machten (U2-WORLD.md, Batch-Import). Der Schutz in `LunePlayerBuild` bleibt.
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ Stand: 1. Oktober 2026.
|
|||
| 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 | **fertig** |
|
||||
| U3C | Eingabe und Befehle: Tasten, Kamera-Ziele, Auswahl im Feld, Bereit-Sperre, Planungsvorschau | **fertig** |
|
||||
| U3D | Prüfung (Teile A–H von `verify_g3`), Spieltest über die echte Oberfläche, Bilder, Leistung | in Arbeit |
|
||||
| U3D | Prüfung (Teile A–H von `verify_g3`), Spieltest über die echte Oberfläche, Bilder, Leistung | **fertig** |
|
||||
|
||||
Funktionale Referenz ist das G3-HUD aus Godot (`../saumwacht-godot/docs/luneraria/G3-HUD.md`, Commit `1b540f2`). Es wird übertragen, nicht neu erfunden.
|
||||
|
||||
|
|
@ -85,10 +85,10 @@ Eine Abweichung: Im öffentlichen Digest (Testhilfe) steht beim Boss die öffent
|
|||
- `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.
|
||||
- Unitys Textsystem hat keine Gewichtsachse. FreeType lädt die benannten Instanzen der variablen Fonts über den Face-Index (Instanz ≪ 16).
|
||||
- In Cantarell fehlt „★“ (Godot nahm eine Ersatzschrift). Das HUD zeichnet den Stern als Symbol.
|
||||
- **Gewichte (U3D korrigiert):** Das Theme der Referenz deklariert Cantarell 560/700–760 und Cinzel 700, wendet sie aber nie an. `SaumwachtTheme.font` schreibt `variation_opentype = {"wght": weight}` mit dem *String*-Schlüssel „wght“; Godot 4.7.2 ignoriert den (gemessen: gleiche Fortschritte bei 400, 560 und 700; nur der Integer-Tag ändert sie). G3 zeigt beide Fonts in ihrer Grundinstanz Regular 400, und so auch der Port. Die Rollen (`LuneFace`) bleiben getrennt, damit U5 echte Gewichte vergeben kann.
|
||||
- **Laufweite und Konturen (U3D):** Godots `spacing_glyph` (Pixel je Glyph) wird als UI-Toolkit-`letterSpacing` umgerechnet (Hundertstel der Schriftgröße). Titel, Welle und Gold bekommen Godots dunklen Saum (`outline_size`) als weichen Schatten statt einer TextCore-Kontur — die Kontur liegt auf beiden Seiten der Glyphenkante und frisst die dünnen Striche der Regular-Schnitte, Godots Outline wächst nur nach außen.
|
||||
|
||||
### Prüfung U3A
|
||||
|
||||
|
|
@ -149,3 +149,98 @@ Die Szenen- und Bibliotheksbauer arbeiten jetzt idempotent:
|
|||
- Vorher erzeugte jeder Szenenbau neue interne IDs in 15 Dateien, obwohl der Inhalt gleich blieb.
|
||||
- Die Szene selbst entsteht weiter bei jedem Bau neu; Unity vergibt dabei neue Objekt-IDs.
|
||||
- Zwei Bauten hintereinander ergeben inhaltlich dieselbe Szene. Ersetzt man die IDs durch Platzhalter, sind alle 2840 Objekteinträge gleich; nur ihre Reihenfolge in der Datei ändert sich, weil Unity nach ID sortiert.
|
||||
|
||||
## U3D · Prüfung und Abnahme
|
||||
|
||||
### Prüfung (Port von `verify_g3`, Teile A–I)
|
||||
|
||||
`HudVerifyTests` zählt jede Prüfung und schreibt die Summen nach `tools/out/hud-verify-*.txt`. Die Referenz `verify_g3` der Godot-Seite zählt 715 Prüfungen; der Port hat denselben Umfang plus Teil I (Feld um das HUD, U3-Zugabe).
|
||||
|
||||
| Teil | Inhalt | Prüfungen | Fehler |
|
||||
| --- | --- | ---: | ---: |
|
||||
| A | Layout 1152×720 … 2560×1440 und Ultrawide, UI-Skalen 90–130 % | 585 | 0 |
|
||||
| B | Partiestatus: Ancients, Türme, Lanes, Wellen mit zwei und drei Ziffern, nur eigenes Gold | 21 | 0 |
|
||||
| C | Held, Items, Talente, normale Einheiten (ohne Itemslots), Lane-Befehle mit Grottenwarnung | 23 | 0 |
|
||||
| D | Angriffschampions (zeitlich), Bossgrotte in allen Zuständen, Lane des Wählenden, Banner | 16 | 0 |
|
||||
| E | Bereit-Sperre, Bots, Privatsphäre, abgebrochener Menschensitz | 17 | 0 |
|
||||
| F | Eingabe: Minimap, Lane-Klicks, Tasten, Feldklicks, kein Klick-Durchgriff, Tooltips | 18 | 0 |
|
||||
| G | Ereignisse, kritische Warnungen, Bewegung reduzieren | 14 | 0 |
|
||||
| H | Aktualisierungsraten, Porträt-Cache | 5 | 0 |
|
||||
| I | Feld (U3-Zugabe): Aufstellungsflächen, Grotteneingang, ruhender Boss, Namensschilder, Eigentümer-Ring, Ancient-Balken | 19 | 0 |
|
||||
| **Summe** | | **718** | **0** |
|
||||
|
||||
Dazu EditMode 23/23 (Kern 402 Prüfungen, Sichtmodell, Bildraten-Logik) und PlayMode 13/13 (Szene bei echten Bildraten, Vertical Function Test, Capture, Verify A–I, UI-Toolkit-Probe). Die Bildwerkzeuge `HudViewsTests` und der Spieltest `HudPlaytest` sind `Explicit` und laufen nur gezielt. Fingerabdrücke der Bot-Partien unverändert: Seed 2 `DCE95C96556F3A1A`, Seed 9 ab Runde 21 `7B8E43AEFC0D60C5`.
|
||||
|
||||
### Bilder
|
||||
|
||||
| Ordner | Inhalt |
|
||||
| --- | --- |
|
||||
| `screenshots/u3/motive-1920x1080/` | die 14 G3-Motive (`HudViewsTests`, Port von `g3_views.gd`): Planung, Held, Items, Einheit, Champion, Boss verfügbar/angekündigt, Kampf Top/Mid/Bottom, eigener und gegnerischer Ancient kritisch, Turm zerstört, große Kampfansicht |
|
||||
| `screenshots/u3/pruefung/` | Schlüsselbilder der Teile B–F (Status, Held, Items, Einheit, Champions, Boss, Tooltip) |
|
||||
| `screenshots/u3/aufloesungen/` | Kampf bei 1152×720, 1280×720, 2560×1080, 2560×1440 (Teil A) |
|
||||
| `screenshots/u3/r1-*.png`, `r2-*.png` | Spieltest über die echte Oberfläche: Lane-Tooltip, Kampfbeginn, Pause, Karten-Tooltip, Kampf Bottom |
|
||||
|
||||
Die 14 Motive liegen auch als JPEG bei 1280×720, 2560×1080 und 2560×1440 (Repository-Größe). Vergleichsbilder der Referenz: `../saumwacht-godot/docs/luneraria/screenshots/`.
|
||||
|
||||
### Spieltest über die echte Oberfläche
|
||||
|
||||
`HudPlaytest` (Port von `g3_playtest.gd`) treibt die echte Partie nur über Eingaben: Klicks an Pixelkoordinaten von 1920×1080, Tasten, Zeigerbewegungen für Tooltips. Ein Tester schreibt Befehlsstapel nach `/tmp/lune_play/cmd-N.txt` und bekommt danach einen Screenshot zurück; der Protokoll aller Befehle mit Runde und Phase steht in `logs/u3/playtest-log.txt`.
|
||||
|
||||
Geprüft in einem Lauf über drei Runden: Lane-Tooltip in der Planung, Bereit und Kampfbeginn, Pause im Kampf (P), Karten-Tooltip, Tastenkauf (4), Lane-Wechsel per F3/F4 und Kamera nach Bottom. Der Port hat noch kein Hauptmenü; der Spieltest startet deshalb in der Partie der G1-Szene.
|
||||
|
||||
### Leistung
|
||||
|
||||
**Player** (macOS, Mono, Release; Apple M5, Metal; 1920 × 1080 im Fenster, VSync aus): Seed 9 in Runde 21, Spieltempo 1×, gemessen ab 10 s Kampf mit `LuneBenchmark`. Die Szene trägt seit U3 das HUD (Sitz 1 der Bot-Partie); `ui_ms_per_frame` zählt die HUD-Skripte (Tick des Panels plus Spielcontroller).
|
||||
|
||||
| Ansicht | Einheiten | fps (Mittel) | Bildzeit Median | p95 | Sim | Views | **UI** | Hauptthread (Mittel/p95) |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | --- |
|
||||
| ganze Karte (Zoom 64) | 116 | 169 | 4,60 ms | 13,05 ms | 0,094 ms | 0,309 ms | **0,090 ms** | 2,77 / 3,94 ms |
|
||||
| Mid-Lane nah (Zoom 22) | 63 | 218 | 3,90 ms | 9,60 ms | 0,044 ms | 0,332 ms | **0,078 ms** | 2,35 / 3,06 ms |
|
||||
|
||||
Das HUD kostet unter 0,1 ms je Bild und liegt damit unter Simulation und Ansichten. Gegenüber den U2D-Läufen ohne HUD (158–160 bzw. 205–207 fps) ist die Bildrate im Rahmen der Laufstreuung; die Ausreißer beim Present (p95 10–13 ms) sind kürzer als in den früheren U2D-Spannen (18–26 ms), ohne dass sich daran etwas geändert hätte — das bleibt Laufstreuung und offene U5-Frage.
|
||||
|
||||
Rohdaten: `logs/u3/player-benchmark-macos.json`. Befehle siehe [U2-WORLD.md](U2-WORLD.md#leistung) (Build `LunePlayerBuild.BuildMac`, Lauf mit `-luneBenchmark`).
|
||||
|
||||
### Feld um das HUD (Zugabe aus dem Motivvergleich)
|
||||
|
||||
Der Vergleich der 14 Motive mit der Referenz brachte Darstellungen hervor, die zum G3-Bild gehören, aber noch nicht im Port standen:
|
||||
|
||||
- **Namensschilder** (`LuneFieldTags`): Helden mit Stufe in Heldengold, Angriffschampions und der Boss in der hellen Teamfarbe. Ein eingesetzter Boss nennt, für wen er kämpft („SCHLUNDKRONE · für Blau“) — G3s Antwort auf die Farbnähe seiner Kristalle und Blaus Armeen. Feste Bildschirmgröße wie Godots `fixed_size`-Schilder, hinter allen HUD-Bereichen, ohne Maus.
|
||||
- **Eigentümer-Ring** am eingesetzten Boss in der Teamfarbe.
|
||||
- **Ancient-Balken** über den Ancients, solange sie beschädigt stehen (3,4 m breit, 11,2 m hoch, kameragerichtet).
|
||||
- **Ruhende Schlundkrone** auf dem Podest, solange die Grotte offen ist und kein Kämpfer dort steht (`lune_grotto_view.gd`), neutral, in ihrem Idle-Clip. Die Lichtzustände der Grotte (Runen, Kristalle, Funken) sind noch nicht übertragen.
|
||||
- **Grotteneingang** der eigenen Seite als Ring, solange die Grotte offen ist.
|
||||
- **Aufstellungsflächen** als eigene Objekte (vormals nur Renderer-Sichtbarkeit), beleuchtet, wenn die Lane eigene Einheiten hat.
|
||||
|
||||
### Befunde und Korrekturen
|
||||
|
||||
| Befund | Maßnahme |
|
||||
| --- | --- |
|
||||
| Godot wendet die deklarierten Schriftgewichte nie an (String-Schlüssel „wght“ wird ignoriert) | Port zeigt wie G3 Regular 400; `Cinzel-Bold` ersetzt durch `Cinzel-Regular`; Rollen bleiben für U5 |
|
||||
| TextCore-Kontur frisst die dünnen Striche der Regular-Schnitte | Godots dunkler Saum als weicher Schatten; Laufweite aus `spacing_glyph` umgerechnet |
|
||||
| Layout der rechten Spalte und des Banners nutzte geschätzte Höhen | Folgt den echten Höhen von Top-Panel, Bottom-Leiste und Systemzeile (`GeometryChangedEvent`) |
|
||||
| Kamera startete in jeder Partie in derselben Ansicht | Start wie Godot `_ready`: eigene Lane in Planungsdistanz, Bot-Partie von oben |
|
||||
| Eigentümer-Ringe und Ancient-Balken fehlten in den Motiven | `OwnerRingRound`/`OwnerRingDiamond`, `AncientBarFill_*`, Balken und ruhender Boss in der Ansicht |
|
||||
|
||||
### Abnahme U3
|
||||
|
||||
| Vorgabe | Stand |
|
||||
| --- | --- |
|
||||
| G3-HUD übertragen (Ancients, Wellen, Gold, Lanes, Türme, Team, Einheitenkarten, Armee, Held mit Items und Talenten, Angriffschampions, nächste Welle, Bossstatus, Minimap, Bereit, Tooltips, Ereignisse, Optionen, Partieende) | erfüllt (U3A–C) |
|
||||
| Sichtmodell mit denselben Privatsphäre-Regeln wie `LuneHudModel` | erfüllt (U3A) |
|
||||
| Eingabe und Befehle wie in G3, Befehle über dieselbe Schnittstelle wie die Bots | erfüllt (U3C) |
|
||||
| Offene G3-Punkte: Champion-Etikett, Typ-Symbole, Armeewert, Helden auf der Minimap | erfüllt (U3B) |
|
||||
| Prüfung Teile A–H von `verify_g3` | erfüllt, 718 Prüfungen mit Teil I, 0 Fehler |
|
||||
| Spieltest über die echte Oberfläche | erfüllt (Runden 1–3, siehe oben) |
|
||||
| Bilder der 14 Motive und der Auflösungen | erfüllt |
|
||||
| Leistung gemessen, einschließlich HUD-Kosten | erfüllt (`ui_ms_per_frame` 0,08–0,09) |
|
||||
| Bot-Partien und Fingerabdrücke unverändert | erfüllt |
|
||||
| Bekannte Unterschiede dokumentiert | erfüllt (siehe unten und [KNOWN-BEHAVIOR-DIFFERENCES.md](KNOWN-BEHAVIOR-DIFFERENCES.md)) |
|
||||
|
||||
### Bekannte Grenzen
|
||||
|
||||
- **Kein Hauptmenü** im Port; „Menü“ beendet das Spiel nach Rückfrage. Das Menü mit Netzwerk-Host/Join gehört zu U4.
|
||||
- **Grottenlicht** (Runen, Kristalle, Funken) fehlt; nur die ruhende Schlundkrone steht dort.
|
||||
- **Geplante Helden** behalten ihren reservierten Befehlsplatz, solange keiner dieser Helden spielbar ist.
|
||||
- **Windows- und Linux-Player** weiterhin an die fehlenden Build-Module gebunden ([U1.5-UNITY.md](U1.5-UNITY.md)).
|
||||
- **Schriftgewichte** wie in G3 absichtlich Regular 400; echte Gewichte sind eine U5-Entscheidung.
|
||||
|
|
|
|||
14
docs/port/logs/u3/player-benchmark-macos.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"build": "OSXPlayer player, 6000.3.20f1, release",
|
||||
"device": "Mac17,2; Apple M5; Apple M5",
|
||||
"graphics_api": "Metal",
|
||||
"resolution": "1920x1080",
|
||||
"vsync": 0,
|
||||
"match": "seed 9 from round 21 (fixture high_wave_21), speed 1x, measured after 10 s of combat",
|
||||
"windows": [
|
||||
{"view": "overview (zoom 64)", "frames": 3384, "fps": 169.2, "frame_ms_avg": 5.91, "frame_ms_median": 4.60, "frame_ms_p95": 13.05, "frame_ms_p99": 20.17, "frame_ms_max": 24.97, "frames_over_16_7_ms": 74, "of_them_with_simulation_step": 3, "gc_collections": 28, "simulation_ms_per_frame": 0.094, "views_ms_per_frame": 0.309, "ui_ms_per_frame": 0.090, "units_alive_max": 116},
|
||||
// timings overview (zoom 64): main thread avg 2.77 / median 2.75 / p95 3.94 ms, render thread avg 3.22 / median 3.23 / p95 3.59 ms, gpu avg 0.00 / median 0.00 / p95 0.00 ms, present wait avg 3.14 / median 1.45 / p95 10.49 ms
|
||||
{"view": "mid lane close (zoom 22)", "frames": 2180, "fps": 218.0, "frame_ms_avg": 4.59, "frame_ms_median": 3.90, "frame_ms_p95": 9.60, "frame_ms_p99": 14.09, "frame_ms_max": 20.30, "frames_over_16_7_ms": 3, "of_them_with_simulation_step": 0, "gc_collections": 10, "simulation_ms_per_frame": 0.044, "views_ms_per_frame": 0.332, "ui_ms_per_frame": 0.078, "units_alive_max": 63}
|
||||
// timings mid lane close (zoom 22): main thread avg 2.35 / median 2.40 / p95 3.06 ms, render thread avg 2.88 / median 2.85 / p95 3.32 ms, gpu avg 0.00 / median 0.00 / p95 0.00 ms, present wait avg 2.24 / median 1.46 / p95 7.33 ms
|
||||
]
|
||||
}
|
||||
31
docs/port/logs/u3/playtest-log.txt
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
16.1 s #1 [round=1 phase=PLANUNG] click 993 917
|
||||
16.1 s #1 [round=1 phase=PLANUNG] wait 0.6
|
||||
16.7 s #1 [round=1 phase=PLANUNG] move 190 205
|
||||
16.7 s #1 [round=1 phase=PLANUNG] wait 0.8
|
||||
17.5 s #1 [round=1 phase=PLANUNG] shot r1-lane-tooltip
|
||||
90.0 s #2 [round=1 phase=PLANUNG] move 1000 450
|
||||
90.0 s #2 [round=1 phase=PLANUNG] key SPACE
|
||||
90.2 s #2 [round=1 phase=KAMPF] wait 1.5
|
||||
91.7 s #2 [round=1 phase=KAMPF] shot r1-kampfbeginn
|
||||
92.4 s #2 [round=1 phase=KAMPF] click 1720 33
|
||||
92.4 s #2 [round=1 phase=KAMPF] key F3
|
||||
92.4 s #2 [round=1 phase=KAMPF] wait 6
|
||||
115.9 s #3 [round=2 phase=PLANUNG] key P
|
||||
115.9 s #3 [round=2 phase=PLANUNG] wait 0.6
|
||||
116.5 s #3 [round=2 phase=PLANUNG] shot r1-pause
|
||||
117.1 s #3 [round=2 phase=PLANUNG] key P
|
||||
117.1 s #3 [round=2 phase=PLANUNG] wait 14
|
||||
150.8 s #4 [round=2 phase=PLANUNG] move 1000 450
|
||||
150.8 s #4 [round=2 phase=PLANUNG] key 4
|
||||
150.8 s #4 [round=2 phase=PLANUNG] wait 0.4
|
||||
151.2 s #4 [round=2 phase=PLANUNG] move 424 975
|
||||
151.2 s #4 [round=2 phase=PLANUNG] wait 0.8
|
||||
152.0 s #4 [round=2 phase=PLANUNG] shot r2-karte-tooltip
|
||||
171.0 s #5 [round=2 phase=PLANUNG] move 1000 450
|
||||
171.0 s #5 [round=2 phase=PLANUNG] key SPACE
|
||||
171.1 s #5 [round=2 phase=KAMPF] wait 2
|
||||
173.1 s #5 [round=2 phase=KAMPF] key F4
|
||||
173.1 s #5 [round=2 phase=KAMPF] wait 3
|
||||
176.1 s #5 [round=2 phase=KAMPF] shot r2-kampf-bottom
|
||||
176.2 s #5 [round=2 phase=KAMPF] wait 18
|
||||
210.6 s #6 [round=3 phase=PLANUNG] wait 15
|
||||
|
Before Width: | Height: | Size: 997 KiB After Width: | Height: | Size: 944 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1 MiB |
|
Before Width: | Height: | Size: 2.9 MiB After Width: | Height: | Size: 2.9 MiB |
|
Before Width: | Height: | Size: 3.6 MiB After Width: | Height: | Size: 3.6 MiB |
BIN
docs/port/screenshots/u3/motive-1920x1080/01-planung.png
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
BIN
docs/port/screenshots/u3/motive-1920x1080/03-itemauswahl.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
BIN
docs/port/screenshots/u3/motive-1920x1080/06-boss-verfuegbar.png
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 2.1 MiB |
BIN
docs/port/screenshots/u3/motive-1920x1080/08-kampf-top.png
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
BIN
docs/port/screenshots/u3/motive-1920x1080/09-kampf-mid.png
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
docs/port/screenshots/u3/motive-1920x1080/10-kampf-bottom.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
BIN
docs/port/screenshots/u3/motive-1920x1080/13-turm-zerstoert.png
Normal file
|
After Width: | Height: | Size: 2 MiB |
|
After Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 2.1 MiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 2.1 MiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 2 MiB After Width: | Height: | Size: 2.1 MiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 2.2 MiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 2.1 MiB |
BIN
docs/port/screenshots/u3/r1-kampfbeginn.png
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
docs/port/screenshots/u3/r1-lane-tooltip.png
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
docs/port/screenshots/u3/r1-pause.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
docs/port/screenshots/u3/r2-kampf-bottom.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
docs/port/screenshots/u3/r2-karte-tooltip.png
Normal file
|
After Width: | Height: | Size: 2 MiB |