From 25b53cb062005ea0a60fa1a60800cbffc0f14630 Mon Sep 17 00:00:00 2001 From: Luis Gonzalez Date: Sun, 7 Jun 2026 13:28:25 -0700 Subject: [PATCH] UI --- Assets/Screenshots/HudSyntyPass_Base.png | 3 + Assets/Screenshots/HudSyntyPass_Base.png.meta | 117 ++++ Assets/Screenshots/HudSyntyPass_BuildMode.png | 3 + .../HudSyntyPass_BuildMode.png.meta | 117 ++++ .../HudSyntyPass_ConveyorFacing.png | 3 + .../HudSyntyPass_ConveyorFacing.png.meta | 117 ++++ Assets/Screenshots/HudSyntyPass_LowHealth.png | 3 + .../HudSyntyPass_LowHealth.png.meta | 117 ++++ ...SciFiSoldier_Gradient_Outwards_01.png.meta | 11 +- .../PolygonScifiSpace_Cockpit_Glass_01.mat | 2 + Assets/_Project/Resources/HudTheme.asset | 45 ++ Assets/_Project/Resources/HudTheme.asset.meta | 8 + .../Scripts/Client/Presentation/HudSystem.cs | 635 +++++++++++++++--- Assets/_Project/Scripts/Client/UI/HudTheme.cs | 128 ++++ .../Scripts/Client/UI/HudTheme.cs.meta | 2 + Assets/_Project/Scripts/Client/UI/HudUi.cs | 164 ++++- .../Scripts/Client/UI/HudVisualMath.cs | 45 ++ .../Scripts/Client/UI/HudVisualMath.cs.meta | 2 + Assets/_Project/Scripts/Client/UI/MenuUi.cs | 6 + .../Tests/EditMode/HudVisualMathTests.cs | 70 ++ .../Tests/EditMode/HudVisualMathTests.cs.meta | 2 + CLAUDE.md | 2 +- .../Vault/06_Roadmap/Synty_Asset_Inventory.md | 2 +- .../2026/2026-06-07_HUD_Synty_Visual_Pass.md | 45 ++ .../_Decisions/DR-024_HUD_Synty_Skin_Theme.md | 49 ++ 25 files changed, 1573 insertions(+), 125 deletions(-) create mode 100644 Assets/Screenshots/HudSyntyPass_Base.png create mode 100644 Assets/Screenshots/HudSyntyPass_Base.png.meta create mode 100644 Assets/Screenshots/HudSyntyPass_BuildMode.png create mode 100644 Assets/Screenshots/HudSyntyPass_BuildMode.png.meta create mode 100644 Assets/Screenshots/HudSyntyPass_ConveyorFacing.png create mode 100644 Assets/Screenshots/HudSyntyPass_ConveyorFacing.png.meta create mode 100644 Assets/Screenshots/HudSyntyPass_LowHealth.png create mode 100644 Assets/Screenshots/HudSyntyPass_LowHealth.png.meta create mode 100644 Assets/_Project/Resources/HudTheme.asset create mode 100644 Assets/_Project/Resources/HudTheme.asset.meta create mode 100644 Assets/_Project/Scripts/Client/UI/HudTheme.cs create mode 100644 Assets/_Project/Scripts/Client/UI/HudTheme.cs.meta create mode 100644 Assets/_Project/Scripts/Client/UI/HudVisualMath.cs create mode 100644 Assets/_Project/Scripts/Client/UI/HudVisualMath.cs.meta create mode 100644 Assets/_Project/Tests/EditMode/HudVisualMathTests.cs create mode 100644 Assets/_Project/Tests/EditMode/HudVisualMathTests.cs.meta create mode 100644 Docs/Vault/07_Sessions/2026/2026-06-07_HUD_Synty_Visual_Pass.md create mode 100644 Docs/Vault/07_Sessions/_Decisions/DR-024_HUD_Synty_Skin_Theme.md diff --git a/Assets/Screenshots/HudSyntyPass_Base.png b/Assets/Screenshots/HudSyntyPass_Base.png new file mode 100644 index 000000000..c3d4287b9 --- /dev/null +++ b/Assets/Screenshots/HudSyntyPass_Base.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ad673f16cc1019a11a579d07704a873425f190d403f127686233b8280136d7c +size 248067 diff --git a/Assets/Screenshots/HudSyntyPass_Base.png.meta b/Assets/Screenshots/HudSyntyPass_Base.png.meta new file mode 100644 index 000000000..a62427855 --- /dev/null +++ b/Assets/Screenshots/HudSyntyPass_Base.png.meta @@ -0,0 +1,117 @@ +fileFormatVersion: 2 +guid: afca5e6e55c487f409a970ba901dad9e +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Screenshots/HudSyntyPass_BuildMode.png b/Assets/Screenshots/HudSyntyPass_BuildMode.png new file mode 100644 index 000000000..01ff57e9b --- /dev/null +++ b/Assets/Screenshots/HudSyntyPass_BuildMode.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:187eeeb188bb501b1430dcbb14af837d6f1f2e9fedbcf060b7d4ff122dab05b0 +size 253513 diff --git a/Assets/Screenshots/HudSyntyPass_BuildMode.png.meta b/Assets/Screenshots/HudSyntyPass_BuildMode.png.meta new file mode 100644 index 000000000..a58db710a --- /dev/null +++ b/Assets/Screenshots/HudSyntyPass_BuildMode.png.meta @@ -0,0 +1,117 @@ +fileFormatVersion: 2 +guid: f9a065d41a38802498de6e0afcba10f0 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Screenshots/HudSyntyPass_ConveyorFacing.png b/Assets/Screenshots/HudSyntyPass_ConveyorFacing.png new file mode 100644 index 000000000..0e2d03285 --- /dev/null +++ b/Assets/Screenshots/HudSyntyPass_ConveyorFacing.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4aadbda2cc1c6421dc578f8b77522842aa8095d46edc506dffbb3291d188e69a +size 251527 diff --git a/Assets/Screenshots/HudSyntyPass_ConveyorFacing.png.meta b/Assets/Screenshots/HudSyntyPass_ConveyorFacing.png.meta new file mode 100644 index 000000000..ba1cb4717 --- /dev/null +++ b/Assets/Screenshots/HudSyntyPass_ConveyorFacing.png.meta @@ -0,0 +1,117 @@ +fileFormatVersion: 2 +guid: ccc39a1f3a2fc4d4aaa6e9ecc6926317 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Screenshots/HudSyntyPass_LowHealth.png b/Assets/Screenshots/HudSyntyPass_LowHealth.png new file mode 100644 index 000000000..3e1a9ba23 --- /dev/null +++ b/Assets/Screenshots/HudSyntyPass_LowHealth.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e8710e3cbe3cb8ca52e75bacda8a83571133de03ab04dc02149f79c559520251 +size 234936 diff --git a/Assets/Screenshots/HudSyntyPass_LowHealth.png.meta b/Assets/Screenshots/HudSyntyPass_LowHealth.png.meta new file mode 100644 index 000000000..36bc38c54 --- /dev/null +++ b/Assets/Screenshots/HudSyntyPass_LowHealth.png.meta @@ -0,0 +1,117 @@ +fileFormatVersion: 2 +guid: 4df1a2477053b6e4fb590f9f31587ad8 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Synty/InterfaceSciFiSoldierHUD/Sprites/HUD/SPR_HUD_SciFiSoldier_Gradient_Outwards_01.png.meta b/Assets/Synty/InterfaceSciFiSoldierHUD/Sprites/HUD/SPR_HUD_SciFiSoldier_Gradient_Outwards_01.png.meta index 51dead4c1..2f0a3a304 100644 --- a/Assets/Synty/InterfaceSciFiSoldierHUD/Sprites/HUD/SPR_HUD_SciFiSoldier_Gradient_Outwards_01.png.meta +++ b/Assets/Synty/InterfaceSciFiSoldierHUD/Sprites/HUD/SPR_HUD_SciFiSoldier_Gradient_Outwards_01.png.meta @@ -43,7 +43,7 @@ TextureImporter: nPOTScale: 0 lightmap: 0 compressionQuality: 50 - spriteMode: 2 + spriteMode: 1 spriteExtrude: 0 spriteMeshType: 1 alignment: 0 @@ -67,7 +67,7 @@ TextureImporter: swizzle: 50462976 cookieLightType: 1 platformSettings: - - serializedVersion: 3 + - serializedVersion: 4 buildTarget: DefaultTexturePlatform maxTextureSize: 2048 resizeAlgorithm: 0 @@ -80,7 +80,7 @@ TextureImporter: ignorePlatformSupport: 0 androidETC2FallbackOverride: 0 forceMaximumCompressionQuality_BC6H_BC7: 0 - - serializedVersion: 3 + - serializedVersion: 4 buildTarget: Standalone maxTextureSize: 2048 resizeAlgorithm: 0 @@ -93,7 +93,7 @@ TextureImporter: ignorePlatformSupport: 0 androidETC2FallbackOverride: 0 forceMaximumCompressionQuality_BC6H_BC7: 0 - - serializedVersion: 3 + - serializedVersion: 4 buildTarget: Server maxTextureSize: 2048 resizeAlgorithm: 0 @@ -110,6 +110,7 @@ TextureImporter: serializedVersion: 2 sprites: [] outline: [] + customData: physicsShape: [] bones: [] spriteID: 5e97eb03825dee720800000000000000 @@ -119,6 +120,8 @@ TextureImporter: edges: [] weights: [] secondaryTextures: [] + spriteCustomMetadata: + entries: [] nameFileIdTable: {} mipmapLimitGroupName: pSDRemoveMatte: 0 diff --git a/Assets/Synty/PolygonSciFiSpace/Materials/Misc/PolygonScifiSpace_Cockpit_Glass_01.mat b/Assets/Synty/PolygonSciFiSpace/Materials/Misc/PolygonScifiSpace_Cockpit_Glass_01.mat index 5b158f777..fe510979c 100644 --- a/Assets/Synty/PolygonSciFiSpace/Materials/Misc/PolygonScifiSpace_Cockpit_Glass_01.mat +++ b/Assets/Synty/PolygonSciFiSpace/Materials/Misc/PolygonScifiSpace_Cockpit_Glass_01.mat @@ -76,6 +76,7 @@ Material: - _Cutoff: 0.5 - _DetailNormalMapScale: 1 - _DstBlend: 10 + - _DstBlendAlpha: 10 - _Enable_Emission: 0 - _GlossMapScale: 1 - _Glossiness: 0.608 @@ -92,6 +93,7 @@ Material: - _SmoothnessTextureChannel: 0 - _SpecularHighlights: 1 - _SrcBlend: 1 + - _SrcBlendAlpha: 1 - _Surface: 1 - _UVSec: 0 - _WorkflowMode: 1 diff --git a/Assets/_Project/Resources/HudTheme.asset b/Assets/_Project/Resources/HudTheme.asset new file mode 100644 index 000000000..379f13ada --- /dev/null +++ b/Assets/_Project/Resources/HudTheme.asset @@ -0,0 +1,45 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5b0ba3a2a48e57547b89018d8aba1134, type: 3} + m_Name: HudTheme + m_EditorClassIdentifier: ProjectM.Client::ProjectM.Client.HudTheme + DisplayFont: {fileID: 12800000, guid: 319f905d228f1954287b03ca7f765339, type: 3} + BodyFont: {fileID: 12800000, guid: 6aba851fb3214a44db734640c86770d8, type: 3} + BodyLightFont: {fileID: 12800000, guid: d3a2f510bc359a746b7dbf70ee67a4e2, type: 3} + PanelBox: {fileID: 21300000, guid: dae3c84a5c66df34dbbaa61259992522, type: 3} + BarTrack: {fileID: 21300000, guid: 68cc419df845fee45b1f03838b9db588, type: 3} + Vignette: {fileID: 21300000, guid: b884942c7c315d445aab18a35ec0ded5, type: 3} + Glow: {fileID: 21300000, guid: 3df67136bd1b3034e8a05b816d8f4925, type: 3} + PipActive: {fileID: 21300000, guid: e1ab7424b5c11bd4f99e8f3ebc195b80, type: 3} + PipInactive: {fileID: 21300000, guid: 1bbdc2e30de1b8a438a5bd8779b8906a, type: 3} + HealthIcon: {fileID: 21300000, guid: bcfc86f084f11084aa3ea0818f67122a, type: 3} + ShieldIcon: {fileID: 21300000, guid: cdb36b97147bf0a47acdab7757676fd1, type: 3} + ThreatIcon: {fileID: 21300000, guid: 01355905a4556544cab941de4a8c6286, type: 3} + GoalIcon: {fileID: 21300000, guid: bb2be08a1b4f7fb4ab0253875a76672c, type: 3} + CooldownIcon: {fileID: 21300000, guid: 0f29a99d278aaa649a4a9712799550fb, type: 3} + LocationBaseIcon: {fileID: 21300000, guid: 660c70bdd241e334fba13240a702fe5f, type: 3} + LocationExpeditionIcon: {fileID: 21300000, guid: 5089ba11ce996474eb180d6cff1861c0, type: 3} + AetherIcon: {fileID: 21300000, guid: 5ec17eed1a417b944b4494db0a88e645, type: 3} + OreIcon: {fileID: 21300000, guid: 5a8c6e8575552cb4d96a8fe09227c6e2, type: 3} + BioIcon: {fileID: 21300000, guid: ca8f53ae67b997e489ba530a7c0ea190, type: 3} + TurretIcon: {fileID: 21300000, guid: 0f29a99d278aaa649a4a9712799550fb, type: 3} + WallIcon: {fileID: 21300000, guid: 977995b1071c6c044b8725d231a428a8, type: 3} + PylonIcon: {fileID: 21300000, guid: 5ec17eed1a417b944b4494db0a88e645, type: 3} + HarvesterIcon: {fileID: 21300000, guid: 5a8c6e8575552cb4d96a8fe09227c6e2, type: 3} + FabricatorIcon: {fileID: 21300000, guid: ca43072da0d43f44fbdbf57c997414ba, type: 3} + ConveyorIcon: {fileID: 21300000, guid: ba4939cd85537454a93777a4cc9e8b38, type: 3} + KbmPlace: {fileID: 21300000, guid: 3fc063021eaa5eb4cb710e15a38ceb6c, type: 3} + KbmCancel: {fileID: 21300000, guid: 78d618b10af7f9b4db2600f77fdce06c, type: 3} + PadPlace: {fileID: 21300000, guid: 15a4f1d900c35fd4091f6970c5250a44, type: 3} + PadCancel: {fileID: 21300000, guid: e311023e5bfd1434d9701f9d89d81e40, type: 3} + PadRotate: {fileID: 21300000, guid: 28c34b18ad3c2cf4dacc46ef515a44aa, type: 3} + PadExit: {fileID: 21300000, guid: ecf39456e341c8640a5c4aec5de3f5aa, type: 3} diff --git a/Assets/_Project/Resources/HudTheme.asset.meta b/Assets/_Project/Resources/HudTheme.asset.meta new file mode 100644 index 000000000..e3cf3282b --- /dev/null +++ b/Assets/_Project/Resources/HudTheme.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 96bc82b4127f03242aa7e1c35e36a82e +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Project/Scripts/Client/Presentation/HudSystem.cs b/Assets/_Project/Scripts/Client/Presentation/HudSystem.cs index 08ff7c1d9..d1c0848f2 100644 --- a/Assets/_Project/Scripts/Client/Presentation/HudSystem.cs +++ b/Assets/_Project/Scripts/Client/Presentation/HudSystem.cs @@ -8,33 +8,72 @@ using UnityEngine.UIElements; namespace ProjectM.Client { /// - /// Client-only screen HUD, rebuilt on UI Toolkit so it reads in the same Aether-cyan visual language as the - /// menu / pause / settings screens ( / ). A managed presentation - /// SystemBase () that OBSERVES the local player ghost + the global - /// cycle / ledger / goal each frame and pushes values into a runtime UIDocument (shared PanelSettings, - /// sortingOrder 50 so it sits BEHIND the pause overlay's 100). The root is pickingMode = Ignore so the - /// HUD never eats clicks meant for the game world — only the build-palette buttons pick. Presentation only - /// (client world, no simulation). The palette buttons set (client-local UI - /// state), which BuildSendSystem turns into a ground ghost + click-to-place. The visual tree is built on the - /// first OnUpdate where the UIDocument's root exists (giving the panel a frame to initialise its PanelSettings). + /// Client-only screen HUD on UI Toolkit, skinned with the curated Synty sci-fi-soldier kit + /// () over 's Aether palette so it reads in one visual language with + /// the menu / pause / settings. A managed presentation () + /// that OBSERVES the local player ghost + the global cycle / ledger / goal each frame and pushes values into a + /// runtime UIDocument (shared PanelSettings, sortingOrder 50 so it sits behind the pause overlay's 100). The + /// root is pickingMode = Ignore so the HUD never eats world clicks — only the build-palette slots pick. + /// Layout (good-HUD spatial convention): persistent self-state hugs the corners (health bottom-left, resources + /// top-left, threat top-right, build deck bottom-center); transient mission state (phase / countdown / wave / + /// goal) lives center-top; a low-health vignette + hurt-flash + a scheme-aware build-mode control-hint bar give + /// just-in-time feedback. EVERY skinned element is null-safe: with no it falls back to + /// the flat-colour HUD. Presentation only (client world, no simulation, no rollback double-fire). /// [WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)] [UpdateInGroup(typeof(PresentationSystemGroup))] public partial class HudSystem : SystemBase { + // ---- palette (Aether language; Synty white skins are tinted into these) ---- + static readonly Color AetherCyan = new(0.30f, 0.85f, 1f); + static readonly Color OreAmber = new(1f, 0.72f, 0.35f); + static readonly Color BioGreen = new(0.55f, 0.85f, 0.45f); + static readonly Color PanelDark = new(0.08f, 0.11f, 0.15f, 0.90f); + static readonly Color PanelWarm = new(0.16f, 0.09f, 0.09f, 0.88f); + static readonly Color PipDim = new(0.25f, 0.30f, 0.36f, 0.9f); + static readonly Color BlightRed = new(0.85f, 0.10f, 0.08f); + static readonly Color ThreatWarm = new(1f, 0.62f, 0.4f); + static readonly Color SlotIdleBg = new(0.09f, 0.11f, 0.15f, 0.92f); + static readonly Color SlotSelBg = new(0.16f, 0.26f, 0.32f, 0.95f); + static readonly Color SlotIdleBorder = new(1f, 1f, 1f, 0.08f); + const int MaxPips = 12; + const float ExpeditionRegionXMin = 500f; // camera x past this = the +1000 expedition region (DR-013) + GameObject _hudGo; UIDocument _doc; bool _built; + bool _themed; // HudTheme + PanelBox present (drives sprite-tint vs flat-colour retint) - VisualElement _healthFill, _cooldownFill, _goalFill, _downed, _paletteRow; - Label _healthText, _threatText, _phaseText, _resourceText, _locationText, _goalText; + // vitals + VisualElement _healthFill, _cooldownFill, _shieldRow, _cdRow; + Label _healthText; - bool _paletteBuilt; + // threat + VisualElement _threatPanel, _threatIcon; + Label _threatNum; + + // macro: banner + location + goal + VisualElement _banner, _goalContainer, _goalPipsRow, _goalBar, _goalFill; + Label _phaseText, _cycleText, _locationText, _goalText; + readonly List _pips = new(); + + // resources + Label _aetherNum, _oreNum, _bioNum; + + // build palette + hints + VisualElement _paletteRow, _hintBar, _facingArrow; + bool _paletteBuilt, _hintBuilt, _hintConveyor; + byte _hintScheme = 255; readonly Dictionary _palette = new(); + // overlays + VisualElement _vignette, _downed; + float _prevHp, _flash; + bool _haveHp; + EntityQuery _huskQuery; - struct PaletteItem { public VisualElement Root; public Label Cost; public int CostAmount; } + struct PaletteItem { public VisualElement Root; public Label Cost; public int CostAmount; public byte CostRes; public VisualElement Glow; public VisualElement Icon; } protected override void OnCreate() { @@ -67,46 +106,84 @@ namespace ProjectM.Client _built = true; } + // Job-safety insurance (matches the sibling presentation systems + CLAUDE.md): finish any jobs writing + // the components we read on the main thread before reading them. No job writes these today, but this + // stays correct the day a Health/stats writer is parallelised. + EntityManager.CompleteDependencyBeforeRO(); + EntityManager.CompleteDependencyBeforeRO(); + EntityManager.CompleteDependencyBeforeRO(); + EntityManager.CompleteDependencyBeforeRO(); + EntityManager.CompleteDependencyBeforeRO(); + + float dt = SystemAPI.Time.DeltaTime; // wall-frame delta — correct in a presentation system bool haveTick = SystemAPI.TryGetSingleton(out var nt); int huskCount = _huskQuery.CalculateEntityCount(); - // ---- Macro loop: phase + cycle + countdown ---- + // ---- Macro: phase + cycle + countdown (center-top banner) ---- bool haveCycle = SystemAPI.TryGetSingleton(out var cyc); + bool siege = haveCycle && cyc.Phase == CyclePhase.Siege; if (haveCycle) { var endTick = new NetworkTick(cyc.PhaseEndTick); string detail; - if (cyc.Phase == CyclePhase.Siege) + if (siege) detail = "WAVE " + cyc.WaveNumber + " - " + huskCount + " HUSKS"; else if (haveTick && cyc.PhaseEndTick != 0 && endTick.IsValid && endTick.IsNewerThan(nt.ServerTick)) detail = "INCURSION IN " + (endTick.TicksSince(nt.ServerTick) / 60 + 1) + "s"; else detail = ""; + var col = PhaseColor(cyc.Phase); _phaseText.text = PhaseLabel(cyc.Phase) + (detail.Length > 0 ? " - " + detail : ""); - _phaseText.style.color = PhaseColor(cyc.Phase); + _phaseText.style.color = col; + _cycleText.text = "CYCLE " + cyc.CycleNumber; + _banner.style.borderBottomColor = col; + RetintPanel(_banner, siege ? PanelWarm : PanelDark); } else { _phaseText.text = ""; + _cycleText.text = ""; } - // ---- Location + gate hint ---- + // ---- Location + gate hint (banner sub-line) ---- var cam = Camera.main; - bool onExpedition = cam != null && cam.transform.position.x > 500f; + bool onExpedition = cam != null && cam.transform.position.x > ExpeditionRegionXMin; _locationText.text = onExpedition ? "ON EXPEDITION - return through the gate" : "AT BASE - deploy through the gate when you're ready"; _locationText.style.color = onExpedition ? new Color(1f, 0.8f, 0.4f) : new Color(0.6f, 0.85f, 1f); - // ---- Goal ---- + // ---- Goal (hex-pip meter, or a continuous bar for large targets) ---- if (SystemAPI.TryGetSingleton(out var goal)) { + _goalContainer.style.display = DisplayStyle.Flex; float gfrac = goal.Target > 0 ? Mathf.Clamp01(goal.Charge / (float)goal.Target) : 0f; - HudUi.SetFill(_goalFill, gfrac); _goalText.text = "GOAL " + goal.Charge + " / " + goal.Target; + if (goal.Target >= 1 && goal.Target <= MaxPips) + { + _goalPipsRow.style.display = DisplayStyle.Flex; + _goalBar.style.display = DisplayStyle.None; + int active = Mathf.Min(goal.Charge, goal.Target); // Charge is the integer pip count; never over-fill + for (int i = 0; i < _pips.Count; i++) + { + bool show = i < goal.Target; + _pips[i].style.display = show ? DisplayStyle.Flex : DisplayStyle.None; + if (show) SetPip(_pips[i], i < active); + } + } + else + { + _goalPipsRow.style.display = DisplayStyle.None; + _goalBar.style.display = DisplayStyle.Flex; + HudUi.SetFill(_goalFill, gfrac); + } + } + else + { + _goalContainer.style.display = DisplayStyle.None; } - // ---- Resources (Ore feeds the palette affordability) ---- + // ---- Resources (feed palette affordability) ---- int aether = 0, ore = 0, bio = 0; if (SystemAPI.TryGetSingletonEntity(out var ledgerE)) { @@ -119,10 +196,39 @@ namespace ProjectM.Client else if (en.ItemId == ResourceId.Biomass) bio = en.Count; } } - _resourceText.text = "AETHER " + aether + " ORE " + ore + " BIO " + bio; + _aetherNum.text = aether.ToString(); + _oreNum.text = ore.ToString(); + _bioNum.text = bio.ToString(); - // ---- Build palette (lazy-built once the catalog has streamed; hidden off-base) ---- - UpdatePalette(ore, onExpedition); + // ---- Threat readout (top-right) — hidden entirely at base with zero husks; its reappearance is the cue ---- + bool showThreat = siege || huskCount > 0; + _threatPanel.style.display = showThreat ? DisplayStyle.Flex : DisplayStyle.None; + if (showThreat) + { + float intensity = Mathf.Clamp01(huskCount / 30f); + Color tc = siege ? Color.Lerp(ThreatWarm, BlightRed, intensity) : ThreatWarm; + _threatNum.text = huskCount.ToString(); + _threatNum.style.color = tc; + _threatIcon.style.unityBackgroundImageTintColor = tc; + RetintPanel(_threatPanel, siege ? PanelWarm : PanelDark); + } + + // ---- Build palette + control hints (bottom-center) ---- + UpdatePalette(aether, ore, bio, onExpedition); + bool buildActive = BuildPaletteState.Active && !onExpedition && _paletteBuilt; + if (buildActive) + { + byte scheme = AimPresentation.Scheme; + bool conv = BuildPaletteState.Selected == StructureType.Conveyor; + if (!_hintBuilt || _hintScheme != scheme || _hintConveyor != conv) RebuildHints(scheme, conv); + if (conv && _facingArrow != null) + _facingArrow.style.rotate = new StyleRotate(new Rotate(new Angle(FacingDegrees(BuildPaletteState.Direction)))); + _hintBar.style.display = DisplayStyle.Flex; + } + else + { + _hintBar.style.display = DisplayStyle.None; + } // ---- Per-player vitals ---- bool found = false; @@ -153,27 +259,71 @@ namespace ProjectM.Client } _doc.rootVisualElement.style.display = (found || haveCycle) ? DisplayStyle.Flex : DisplayStyle.None; - if (!found) { _downed.style.display = DisplayStyle.None; return; } - float frac = maxHp > 0f ? Mathf.Clamp01(hp / maxHp) : 0f; - HudUi.SetFill(_healthFill, frac); - _healthFill.style.backgroundColor = shielded - ? new Color(0.45f, 0.85f, 1f) - : Color.Lerp(new Color(0.92f, 0.16f, 0.16f), new Color(0.25f, 0.9f, 0.5f), frac); - _healthText.text = Mathf.CeilToInt(Mathf.Max(0f, hp)) + " / " + Mathf.CeilToInt(maxHp) + (shielded ? " SHIELDED" : ""); + // ---- Low-health vignette + hurt flash (full-screen) ---- + _flash = HudVisualMath.DecayFlash(_flash, dt); + if (found) + { + float frac = maxHp > 0f ? Mathf.Clamp01(hp / maxHp) : 0f; + if (_haveHp && hp < _prevHp - 1f) _flash = HudVisualMath.HurtFlashKick; + _prevHp = hp; _haveHp = true; - HudUi.SetFill(_cooldownFill, cdFrac); - _threatText.text = "HUSKS " + huskCount; - _downed.style.display = dead ? DisplayStyle.Flex : DisplayStyle.None; + float vigOp = dead ? 0f : HudVisualMath.CombinedOpacity(frac, _flash); + _vignette.style.opacity = vigOp; + _vignette.style.display = vigOp > 0.001f ? DisplayStyle.Flex : DisplayStyle.None; + + HudUi.SetFill(_healthFill, frac); + _healthFill.style.backgroundColor = shielded + ? new Color(0.45f, 0.85f, 1f) + : Color.Lerp(new Color(0.92f, 0.16f, 0.16f), new Color(0.25f, 0.9f, 0.5f), frac); + _healthText.text = Mathf.CeilToInt(Mathf.Max(0f, hp)) + " / " + Mathf.CeilToInt(maxHp); + _shieldRow.style.display = shielded ? DisplayStyle.Flex : DisplayStyle.None; + + HudUi.SetFill(_cooldownFill, cdFrac); + // A READY weapon (full bar) recedes; a CHARGING one is bright — so the inverted-vs-health polarity reads. + if (_cdRow != null) _cdRow.style.opacity = cdFrac >= 1f ? 0.4f : 1f; + _downed.style.display = dead ? DisplayStyle.Flex : DisplayStyle.None; + } + else + { + _haveHp = false; _flash = 0f; + _vignette.style.display = DisplayStyle.None; + _downed.style.display = DisplayStyle.None; + } } - void UpdatePalette(int ore, bool onExpedition) + // ---- per-frame helpers ---- + + void RetintPanel(VisualElement p, Color c) + { + if (_themed) p.style.unityBackgroundImageTintColor = c; + else p.style.backgroundColor = c; + } + + void SetPip(VisualElement pip, bool active) + { + var theme = HudTheme.Get(); + var spr = active ? theme?.PipActive : theme?.PipInactive; + if (spr != null) + { + pip.style.backgroundImage = new StyleBackground(Background.FromSprite(spr)); + pip.style.unityBackgroundImageTintColor = active ? AetherCyan : PipDim; + pip.style.backgroundSize = new StyleBackgroundSize(new BackgroundSize(BackgroundSizeType.Contain)); + } + else + { + pip.style.backgroundColor = active ? AetherCyan : PipDim; + MenuUi.Round(pip, 3); + } + } + + void UpdatePalette(int aether, int ore, int bio, bool onExpedition) { if (!_paletteBuilt && SystemAPI.TryGetSingletonEntity(out var catE)) { var cat = SystemAPI.GetBuffer(catE); for (int i = 0; i < cat.Length; i++) - AddPaletteItem(cat[i].Type, cat[i].CostAmount); + AddPaletteItem(cat[i].Type, cat[i].CostAmount, cat[i].CostResourceId); _paletteBuilt = true; } if (!_paletteBuilt) { _paletteRow.style.display = DisplayStyle.None; return; } @@ -182,116 +332,419 @@ namespace ProjectM.Client foreach (var kv in _palette) { var item = kv.Value; - bool affordable = ore >= item.CostAmount; + int have = item.CostRes == ResourceId.Aether ? aether : item.CostRes == ResourceId.Biomass ? bio : ore; + bool affordable = have >= item.CostAmount; bool selected = BuildPaletteState.Selected == kv.Key; - item.Root.style.opacity = affordable ? 1f : 0.45f; + item.Root.style.opacity = affordable ? 1f : 0.5f; item.Cost.style.color = affordable ? new Color(0.7f, 0.95f, 0.8f) : new Color(1f, 0.5f, 0.4f); - MenuUi.Border(item.Root, selected ? MenuUi.Accent : new Color(1f, 1f, 1f, 0.08f), selected ? 2 : 1); - item.Root.style.backgroundColor = selected - ? new Color(0.16f, 0.26f, 0.32f, 0.95f) - : new Color(0.09f, 0.11f, 0.15f, 0.92f); + if (item.Icon != null) + item.Icon.style.unityBackgroundImageTintColor = affordable ? AetherCyan : new Color(0.5f, 0.55f, 0.6f); + MenuUi.Border(item.Root, selected ? MenuUi.Accent : SlotIdleBorder, selected ? 2 : 1); + item.Root.style.backgroundColor = selected ? SlotSelBg : SlotIdleBg; + if (item.Glow != null) item.Glow.style.opacity = selected ? 0.6f : 0f; } } - void AddPaletteItem(byte type, int cost) + void AddPaletteItem(byte type, int cost, byte costRes) { if (type == 0 || _palette.ContainsKey(type)) return; + var theme = HudTheme.Get(); + var root = new VisualElement(); - root.style.width = 92; + root.style.width = 86; root.style.marginLeft = 4; root.style.marginRight = 4; - root.style.paddingTop = 6; root.style.paddingBottom = 6; + root.style.paddingTop = 8; root.style.paddingBottom = 6; root.style.alignItems = Align.Center; - root.style.backgroundColor = new Color(0.09f, 0.11f, 0.15f, 0.92f); + root.style.backgroundColor = SlotIdleBg; root.pickingMode = PickingMode.Position; MenuUi.Round(root, 6); - MenuUi.Border(root, new Color(1f, 1f, 1f, 0.08f), 1); + MenuUi.Border(root, SlotIdleBorder, 1); - var nameLabel = HudUi.Text(StructureName(type), 13, MenuUi.TextCol, TextAnchor.MiddleCenter); - var costLabel = HudUi.Text(cost + " ORE", 12, new Color(0.7f, 0.95f, 0.8f), TextAnchor.MiddleCenter); - costLabel.style.marginTop = 2; + // selection glow: a soft Synty glow filling the slot behind everything, opacity toggled on select. + var glow = new VisualElement(); + glow.style.position = Position.Absolute; + glow.style.left = 3; glow.style.right = 3; glow.style.top = 4; glow.style.bottom = 4; + glow.pickingMode = PickingMode.Ignore; + glow.style.opacity = 0f; + if (theme != null && theme.Glow != null) + { + glow.style.backgroundImage = new StyleBackground(Background.FromSprite(theme.Glow)); + glow.style.unityBackgroundImageTintColor = AetherCyan; + glow.style.backgroundSize = new StyleBackgroundSize(new BackgroundSize(BackgroundSizeType.Cover)); + } + root.Add(glow); + + var iconEl = HudUi.Icon(theme != null ? theme.StructureIcon(type) : null, 44, AetherCyan); + root.Add(iconEl); + + var nameLabel = HudUi.Text(StructureName(type), 12, MenuUi.TextCol, TextAnchor.MiddleCenter); + nameLabel.style.marginTop = 2; root.Add(nameLabel); - root.Add(costLabel); + + var costRow = new VisualElement(); + costRow.style.flexDirection = FlexDirection.Row; + costRow.style.alignItems = Align.Center; + costRow.style.marginTop = 2; + costRow.pickingMode = PickingMode.Ignore; + var costIcon = HudUi.Icon(ResourceSprite(theme, costRes), 14, ResourceTint(costRes)); + costIcon.style.marginRight = 3; + costRow.Add(costIcon); + var costLabel = HudUi.Display(cost.ToString(), 13, new Color(0.7f, 0.95f, 0.8f), TextAnchor.MiddleCenter); + costRow.Add(costLabel); + root.Add(costRow); byte t = type; root.RegisterCallback(_ => BuildPaletteState.Select(BuildPaletteState.Selected == t ? (byte)0 : t)); _paletteRow.Add(root); - _palette[type] = new PaletteItem { Root = root, Cost = costLabel, CostAmount = cost }; + _palette[type] = new PaletteItem { Root = root, Cost = costLabel, CostAmount = cost, CostRes = costRes, Glow = glow, Icon = iconEl }; + } + + void RebuildHints(byte scheme, bool conveyor) + { + _hintBar.Clear(); + _facingArrow = null; + var theme = HudTheme.Get(); + bool pad = scheme == InputSchemeId.Gamepad; + AddHint(pad ? theme?.PadPlace : theme?.KbmPlace, pad ? "A" : "LMB", "PLACE"); + AddHint(pad ? theme?.PadCancel : theme?.KbmCancel, pad ? "B" : "RMB", "CANCEL"); + if (conveyor) + { + // Rotate hint + a LIVE facing arrow (resolves the DR-021 conveyor-facing indicator). Only conveyors + // rotate, so this chip is gated to them — the other buildables don't show a meaningless ROTATE. + var chip = MakeChip(); + chip.Add(HudUi.Glyph(pad ? theme?.PadRotate : null, pad ? "LB" : "R", 26)); + var lbl = HudUi.Text("FACING", 12, MenuUi.SubCol, TextAnchor.MiddleLeft); + lbl.style.marginLeft = 5; lbl.style.marginRight = 6; + chip.Add(lbl); + _facingArrow = HudUi.Icon(theme != null ? theme.ConveyorIcon : null, 24, AetherCyan); + chip.Add(_facingArrow); + _hintBar.Add(chip); + } + AddHint(pad ? theme?.PadExit : null, pad ? "MENU" : "ESC", "EXIT"); + _hintScheme = scheme; + _hintConveyor = conveyor; + _hintBuilt = true; + } + + VisualElement MakeChip() + { + var chip = new VisualElement(); + chip.style.flexDirection = FlexDirection.Row; + chip.style.alignItems = Align.Center; + chip.style.marginLeft = 8; chip.style.marginRight = 8; + chip.pickingMode = PickingMode.Ignore; + return chip; + } + + void AddHint(Sprite glyph, string fallback, string action) + { + var chip = MakeChip(); + chip.Add(HudUi.Glyph(glyph, fallback, 26)); + var lbl = HudUi.Text(action, 12, MenuUi.SubCol, TextAnchor.MiddleLeft); + lbl.style.marginLeft = 5; + chip.Add(lbl); + _hintBar.Add(chip); } // ---- UITK construction ---- void BuildTree(VisualElement root) { + var theme = HudTheme.Get(); + _themed = theme != null && theme.PanelBox != null; + root.style.position = Position.Absolute; root.style.left = 0; root.style.right = 0; root.style.top = 0; root.style.bottom = 0; root.pickingMode = PickingMode.Ignore; // never eat game-world clicks - // Health + cooldown (bottom-left). - var vitals = HudUi.Group(); - vitals.style.position = Position.Absolute; - vitals.style.left = 40; vitals.style.bottom = 40; - var cdBar = HudUi.Bar(440, 12, new Color(0.4f, 0.8f, 1f), out _cooldownFill); - cdBar.style.marginBottom = 6; - var hpBar = HudUi.Bar(440, 40, new Color(0.25f, 0.9f, 0.5f), out _healthFill); - _healthText = HudUi.Text("100 / 100", 22, Color.white, TextAnchor.MiddleCenter); + BuildVignette(root); + BuildVitals(root); + BuildThreat(root); + BuildMacro(root); + BuildResources(root); + BuildPaletteRow(root); + BuildHintBar(root); + BuildDowned(root); + } + + void BuildVignette(VisualElement root) + { + _vignette = new VisualElement(); + _vignette.style.position = Position.Absolute; + _vignette.style.left = 0; _vignette.style.right = 0; _vignette.style.top = 0; _vignette.style.bottom = 0; + _vignette.pickingMode = PickingMode.Ignore; + var theme = HudTheme.Get(); + if (theme != null && theme.Vignette != null) + { + _vignette.style.backgroundImage = new StyleBackground(Background.FromSprite(theme.Vignette)); + _vignette.style.unityBackgroundImageTintColor = BlightRed; + _vignette.style.backgroundSize = new StyleBackgroundSize(new BackgroundSize(BackgroundSizeType.Cover)); + } + else + { + _vignette.style.backgroundColor = new Color(BlightRed.r, BlightRed.g, BlightRed.b, 1f); + } + _vignette.style.display = DisplayStyle.None; + root.Add(_vignette); + } + + void BuildVitals(VisualElement root) + { + var panel = HudUi.Panel(PanelDark); + panel.style.position = Position.Absolute; + panel.style.left = 40; panel.style.bottom = 40; + panel.style.paddingLeft = 14; panel.style.paddingRight = 14; + panel.style.paddingTop = 12; panel.style.paddingBottom = 12; + panel.style.alignItems = Align.FlexStart; + var theme = HudTheme.Get(); + + // shield chip (shown only while the respawn shield is active) + _shieldRow = new VisualElement(); + _shieldRow.style.flexDirection = FlexDirection.Row; + _shieldRow.style.alignItems = Align.Center; + _shieldRow.style.marginBottom = 6; + _shieldRow.pickingMode = PickingMode.Ignore; + var shieldIcon = HudUi.Icon(theme != null ? theme.ShieldIcon : null, 22, AetherCyan); + shieldIcon.style.marginRight = 6; + _shieldRow.Add(shieldIcon); + _shieldRow.Add(HudUi.Text("SHIELDED", 13, new Color(0.45f, 0.85f, 1f), TextAnchor.MiddleLeft)); + _shieldRow.style.display = DisplayStyle.None; + panel.Add(_shieldRow); + + // cooldown row: weapon icon + thin bar + _cdRow = new VisualElement(); + _cdRow.style.flexDirection = FlexDirection.Row; + _cdRow.style.alignItems = Align.Center; + _cdRow.style.marginBottom = 6; + _cdRow.pickingMode = PickingMode.Ignore; + var cdIcon = HudUi.Icon(theme != null ? theme.CooldownIcon : null, 22, AetherCyan); + cdIcon.style.marginRight = 8; + _cdRow.Add(cdIcon); + var cdBar = HudUi.Bar(420, 12, new Color(0.4f, 0.8f, 1f), out _cooldownFill); + _cdRow.Add(cdBar); + panel.Add(_cdRow); + + // health row: health icon + big bar with numeric overlay + var hpRow = new VisualElement(); + hpRow.style.flexDirection = FlexDirection.Row; + hpRow.style.alignItems = Align.Center; + hpRow.pickingMode = PickingMode.Ignore; + var hpIcon = HudUi.Icon(theme != null ? theme.HealthIcon : null, 34, new Color(0.95f, 0.4f, 0.4f)); + hpIcon.style.marginRight = 8; + hpRow.Add(hpIcon); + var hpBar = HudUi.Bar(420, 40, new Color(0.25f, 0.9f, 0.5f), out _healthFill); + _healthText = HudUi.Display("100 / 100", 24, Color.white, TextAnchor.MiddleCenter); _healthText.style.position = Position.Absolute; _healthText.style.left = 0; _healthText.style.right = 0; _healthText.style.top = 0; _healthText.style.bottom = 0; hpBar.Add(_healthText); - vitals.Add(cdBar); - vitals.Add(hpBar); - root.Add(vitals); + hpRow.Add(hpBar); + panel.Add(hpRow); - // Threat (top-right). - _threatText = HudUi.Text("HUSKS 0", 30, new Color(1f, 0.62f, 0.4f), TextAnchor.UpperRight); - _threatText.style.position = Position.Absolute; - _threatText.style.right = 40; _threatText.style.top = 28; _threatText.style.width = 380; - root.Add(_threatText); + root.Add(panel); + } - // Macro stack (top-center). + void BuildThreat(VisualElement root) + { + _threatPanel = HudUi.Panel(PanelDark); + _threatPanel.style.position = Position.Absolute; + _threatPanel.style.right = 40; _threatPanel.style.top = 28; + _threatPanel.style.paddingLeft = 16; _threatPanel.style.paddingRight = 16; + _threatPanel.style.paddingTop = 8; _threatPanel.style.paddingBottom = 8; + _threatPanel.style.alignItems = Align.FlexEnd; + var theme = HudTheme.Get(); + + var row = new VisualElement(); + row.style.flexDirection = FlexDirection.Row; + row.style.alignItems = Align.Center; + row.pickingMode = PickingMode.Ignore; + _threatIcon = HudUi.Icon(theme != null ? theme.ThreatIcon : null, 36, ThreatWarm); + _threatIcon.style.marginRight = 8; + row.Add(_threatIcon); + _threatNum = HudUi.Display("0", 34, ThreatWarm, TextAnchor.MiddleRight); + row.Add(_threatNum); + _threatPanel.Add(row); + + var caption = HudUi.Text("HUSKS", 13, MenuUi.SubCol, TextAnchor.MiddleRight); + caption.style.letterSpacing = 4; + _threatPanel.Add(caption); + + _threatPanel.style.display = DisplayStyle.None; + root.Add(_threatPanel); + } + + void BuildMacro(VisualElement root) + { var macro = HudUi.Group(Align.Center); macro.style.position = Position.Absolute; - macro.style.top = 22; macro.style.left = 0; macro.style.right = 0; - _phaseText = HudUi.Text("", 30, new Color(0.55f, 0.9f, 1f), TextAnchor.MiddleCenter); - _resourceText = HudUi.Text("", 22, new Color(0.7f, 0.95f, 0.8f), TextAnchor.MiddleCenter); - _resourceText.style.marginTop = 4; - _locationText = HudUi.Text("", 18, new Color(0.6f, 0.85f, 1f), TextAnchor.MiddleCenter); - _locationText.style.marginTop = 4; - var goalBar = HudUi.Bar(360, 16, new Color(0.8f, 0.6f, 1f), out _goalFill); - goalBar.style.marginTop = 8; - _goalText = HudUi.Text("GOAL 0 / 10", 13, Color.white, TextAnchor.MiddleCenter); - _goalText.style.position = Position.Absolute; - _goalText.style.left = 0; _goalText.style.right = 0; _goalText.style.top = 0; _goalText.style.bottom = 0; - goalBar.Add(_goalText); - macro.Add(_phaseText); - macro.Add(_resourceText); - macro.Add(_locationText); - macro.Add(goalBar); - root.Add(macro); + macro.style.top = 16; macro.style.left = 0; macro.style.right = 0; + var theme = HudTheme.Get(); - // Build palette row (bottom-center). The row passes clicks through; its buttons pick. + // banner: objective icon + phase line + cycle, phase-coloured underline + _banner = HudUi.Panel(PanelDark); + _banner.style.flexDirection = FlexDirection.Row; + _banner.style.alignItems = Align.Center; + _banner.style.paddingLeft = 22; _banner.style.paddingRight = 22; + _banner.style.paddingTop = 8; _banner.style.paddingBottom = 8; + _banner.style.borderBottomWidth = 2; + _banner.style.borderBottomColor = AetherCyan; + var bIcon = HudUi.Icon(theme != null ? theme.GoalIcon : null, 26, AetherCyan); + bIcon.style.marginRight = 10; + _banner.Add(bIcon); + _phaseText = HudUi.Display("", 30, AetherCyan, TextAnchor.MiddleCenter); + _banner.Add(_phaseText); + _cycleText = HudUi.Text("", 14, MenuUi.SubCol, TextAnchor.MiddleCenter); + _cycleText.style.marginLeft = 14; + _banner.Add(_cycleText); + macro.Add(_banner); + + _locationText = HudUi.Text("", 15, new Color(0.6f, 0.85f, 1f), TextAnchor.MiddleCenter); + _locationText.style.marginTop = 5; + macro.Add(_locationText); + + // goal: hex-pip meter (or fallback bar) + numeral + _goalContainer = HudUi.Group(Align.Center); + _goalContainer.style.marginTop = 8; + + var goalLine = new VisualElement(); + goalLine.style.flexDirection = FlexDirection.Row; + goalLine.style.alignItems = Align.Center; + goalLine.pickingMode = PickingMode.Ignore; + + _goalPipsRow = new VisualElement(); + _goalPipsRow.style.flexDirection = FlexDirection.Row; + _goalPipsRow.style.alignItems = Align.Center; + _goalPipsRow.pickingMode = PickingMode.Ignore; + for (int i = 0; i < MaxPips; i++) + { + var pip = new VisualElement(); + pip.style.width = 22; pip.style.height = 22; + pip.style.marginLeft = 2; pip.style.marginRight = 2; + pip.style.flexShrink = 0; + pip.pickingMode = PickingMode.Ignore; + pip.style.display = DisplayStyle.None; + _pips.Add(pip); + _goalPipsRow.Add(pip); + } + goalLine.Add(_goalPipsRow); + + _goalText = HudUi.Display("GOAL 0 / 10", 16, AetherCyan, TextAnchor.MiddleCenter); + _goalText.style.marginLeft = 10; + goalLine.Add(_goalText); + _goalContainer.Add(goalLine); + + // fallback continuous bar (large targets) + _goalBar = HudUi.Bar(360, 16, new Color(0.8f, 0.6f, 1f), out _goalFill); + _goalBar.style.marginTop = 4; + _goalBar.style.display = DisplayStyle.None; + _goalContainer.Add(_goalBar); + + macro.Add(_goalContainer); + root.Add(macro); + } + + void BuildResources(VisualElement root) + { + var strip = HudUi.Panel(PanelDark); + strip.style.position = Position.Absolute; + strip.style.left = 40; strip.style.top = 28; + strip.style.flexDirection = FlexDirection.Row; + strip.style.alignItems = Align.Center; + strip.style.paddingLeft = 14; strip.style.paddingRight = 14; + strip.style.paddingTop = 8; strip.style.paddingBottom = 8; + var theme = HudTheme.Get(); + + strip.Add(ResourceChip(theme != null ? theme.AetherIcon : null, AetherCyan, "0", out _aetherNum, 26, 20)); + strip.Add(ResourceChip(theme != null ? theme.OreIcon : null, OreAmber, "0", out _oreNum, 30, 22)); + strip.Add(ResourceChip(theme != null ? theme.BioIcon : null, BioGreen, "0", out _bioNum, 26, 20)); + + root.Add(strip); + } + + VisualElement ResourceChip(Sprite icon, Color tint, string initial, out Label num, float iconSize, int fontSize) + { + var chip = new VisualElement(); + chip.style.flexDirection = FlexDirection.Row; + chip.style.alignItems = Align.Center; + chip.style.marginLeft = 9; chip.style.marginRight = 9; + chip.pickingMode = PickingMode.Ignore; + var ic = HudUi.Icon(icon, iconSize, tint); + ic.style.marginRight = 6; + chip.Add(ic); + num = HudUi.Display(initial, fontSize, tint, TextAnchor.MiddleLeft); + chip.Add(num); + return chip; + } + + void BuildPaletteRow(VisualElement root) + { _paletteRow = new VisualElement(); _paletteRow.style.position = Position.Absolute; _paletteRow.style.bottom = 24; _paletteRow.style.left = 0; _paletteRow.style.right = 0; _paletteRow.style.flexDirection = FlexDirection.Row; _paletteRow.style.justifyContent = Justify.Center; - _paletteRow.pickingMode = PickingMode.Ignore; + _paletteRow.pickingMode = PickingMode.Ignore; // the row passes clicks through; its slots pick root.Add(_paletteRow); + } - // Downed overlay. + void BuildHintBar(VisualElement root) + { + _hintBar = new VisualElement(); + _hintBar.style.position = Position.Absolute; + _hintBar.style.bottom = 138; _hintBar.style.left = 0; _hintBar.style.right = 0; + _hintBar.style.flexDirection = FlexDirection.Row; + _hintBar.style.justifyContent = Justify.Center; + _hintBar.pickingMode = PickingMode.Ignore; + _hintBar.style.display = DisplayStyle.None; + root.Add(_hintBar); + } + + void BuildDowned(VisualElement root) + { _downed = new VisualElement(); _downed.style.position = Position.Absolute; _downed.style.left = 0; _downed.style.right = 0; _downed.style.top = 0; _downed.style.bottom = 0; - _downed.style.backgroundColor = new Color(0.35f, 0f, 0f, 0.35f); _downed.style.alignItems = Align.Center; _downed.style.justifyContent = Justify.Center; _downed.pickingMode = PickingMode.Ignore; - _downed.Add(HudUi.Text("DOWNED - RESPAWNING", 52, new Color(1f, 0.45f, 0.4f), TextAnchor.MiddleCenter)); + var theme = HudTheme.Get(); + if (theme != null && theme.Vignette != null) + { + _downed.style.backgroundImage = new StyleBackground(Background.FromSprite(theme.Vignette)); + _downed.style.unityBackgroundImageTintColor = new Color(0.45f, 0f, 0f, 0.6f); + _downed.style.backgroundSize = new StyleBackgroundSize(new BackgroundSize(BackgroundSizeType.Cover)); + } + else + { + _downed.style.backgroundColor = new Color(0.35f, 0f, 0f, 0.35f); + } + _downed.Add(HudUi.Display("DOWNED - RESPAWNING", 52, new Color(1f, 0.45f, 0.4f), TextAnchor.MiddleCenter)); _downed.style.display = DisplayStyle.None; root.Add(_downed); } + static Color ResourceTint(byte resId) + => resId == ResourceId.Aether ? AetherCyan : resId == ResourceId.Biomass ? BioGreen : OreAmber; + + static Sprite ResourceSprite(HudTheme t, byte resId) + { + if (t == null) return null; + return resId == ResourceId.Aether ? t.AetherIcon : resId == ResourceId.Biomass ? t.BioIcon : t.OreIcon; + } + + // Conveyor facing (BuildPaletteState.Direction 0=+X,1=-X,2=+Z,3=-Z) → arrow rotation; the arrow art points up (+Z). + static float FacingDegrees(byte dir) + { + switch (dir) + { + case 0: return 90f; // +X + case 1: return 270f; // -X + case 3: return 180f; // -Z + default: return 0f; // +Z + } + } + static Color PhaseColor(byte phase) { switch (phase) diff --git a/Assets/_Project/Scripts/Client/UI/HudTheme.cs b/Assets/_Project/Scripts/Client/UI/HudTheme.cs new file mode 100644 index 000000000..cafb16892 --- /dev/null +++ b/Assets/_Project/Scripts/Client/UI/HudTheme.cs @@ -0,0 +1,128 @@ +using ProjectM.Simulation; +using UnityEngine; +using UnityEngine.TextCore.Text; +using UnityEngine.UIElements; + +namespace ProjectM.Client +{ + /// + /// Curated Synty sci-fi-soldier HUD skin — serialized + references + /// harvested from the InterfaceSciFiSoldierHUD / InterfaceCore packs and authored into + /// Assets/_Project/Resources/HudTheme.asset. Those source sprites/fonts live under + /// Assets/Synty/… (NOT a Resources folder), so a serialized asset like this is the build-safe way to + /// pull a curated subset into the player build (the dependency walker follows the refs). Loaded null-safe via + /// (mirrors ); EVERY consumer must null-check the theme + /// AND each field and fall back to the flat-color HUD, so a missing asset/ref never breaks (or magenta-s) the + /// HUD. Fonts are applied as cached SDF font definitions: a is built once per font from + /// the serialized (crisp at any size, supports outlines) and re-built per play session (the + /// static cache is reset on play-enter, like the project's other static presentation bridges). + /// + [CreateAssetMenu(menuName = "ProjectM/HUD Theme", fileName = "HudTheme")] + public class HudTheme : ScriptableObject + { + [Header("Fonts")] + public Font DisplayFont; // Orbitron-ExtraBold — numerics + phase words (the HUD's authoritative voice) + public Font BodyFont; // Exo 2.0 SemiBold — labels + public Font BodyLightFont; // Exo 2.0 Regular — captions / hints + + [Header("Panels & meters")] + public Sprite PanelBox; // 9-sliced card background (tinted per cluster) + public Sprite BarTrack; // bar fill skin + public Sprite Vignette; // radial edge gradient (low-HP / hurt flash / downed) + public Sprite Glow; // soft glow accent (selection / pulse) + public Sprite PipActive; // filled hex pip (goal meter) + public Sprite PipInactive; // empty hex pip + + [Header("Status / info icons")] + public Sprite HealthIcon; + public Sprite ShieldIcon; + public Sprite ThreatIcon; + public Sprite GoalIcon; + public Sprite CooldownIcon; + public Sprite LocationBaseIcon; + public Sprite LocationExpeditionIcon; + + [Header("Resource icons")] + public Sprite AetherIcon; + public Sprite OreIcon; + public Sprite BioIcon; + + [Header("Structure icons")] + public Sprite TurretIcon; + public Sprite WallIcon; + public Sprite PylonIcon; + public Sprite HarvesterIcon; + public Sprite FabricatorIcon; + public Sprite ConveyorIcon; + + [Header("Build-mode control glyphs")] + public Sprite KbmPlace; // LMB + public Sprite KbmCancel; // RMB + public Sprite PadPlace; // gamepad A / south + public Sprite PadCancel; // gamepad B / east + public Sprite PadRotate; // gamepad LB + public Sprite PadExit; // gamepad Menu / start + + // ---- null-safe load + cache (mirrors MenuUi.LoadPanelSettings Resources idiom) ---- + static HudTheme _cached; + static bool _tried; + + /// The loaded theme, or null if the asset is missing. Callers MUST null-check this and each field. + public static HudTheme Get() + { + if (_tried) return _cached; + _tried = true; + _cached = Resources.Load("HudTheme"); + return _cached; + } + + /// Icon for a byte (null → caller falls back to the structure name text). + public Sprite StructureIcon(byte type) + { + switch (type) + { + case StructureType.Turret: return TurretIcon; + case StructureType.Wall: return WallIcon; + case StructureType.Pylon: return PylonIcon; + case StructureType.Harvester: return HarvesterIcon; + case StructureType.Fabricator: return FabricatorIcon; + case StructureType.Conveyor: return ConveyorIcon; + default: return null; + } + } + + // ---- cached SDF font definitions (one FontAsset per font, built once, reset per play session) ---- + static FontAsset _displayFa, _bodyFa, _bodyLightFa; + static bool _displayTried, _bodyTried, _bodyLightTried; + + /// Apply the display font (Orbitron) to a style, if available. No-op otherwise (stock font). + public void ApplyDisplay(IStyle style) => Apply(style, DisplayFont, ref _displayFa, ref _displayTried); + + /// Apply the body font (Exo 2.0 SemiBold) to a style, if available. + public void ApplyBody(IStyle style) => Apply(style, BodyFont, ref _bodyFa, ref _bodyTried); + + /// Apply the light body font (Exo 2.0 Regular) to a style, if available. + public void ApplyBodyLight(IStyle style) => Apply(style, BodyLightFont, ref _bodyLightFa, ref _bodyLightTried); + + static void Apply(IStyle style, Font font, ref FontAsset fa, ref bool tried) + { + if (!tried) + { + tried = true; + if (font != null) fa = FontAsset.CreateFontAsset(font); // dynamic SDF atlas, built once per session + } + if (fa != null) + style.unityFontDefinition = new StyleFontDefinition(FontDefinition.FromSDFFont(fa)); + } + + // Fast-enter-playmode keeps statics alive across sessions; the runtime FontAssets are destroyed on play-exit, + // so a stale cache would reference a destroyed atlas. Reset everything so it re-loads/re-builds per session. + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] + static void ResetStatics() + { + _cached = null; _tried = false; + _displayFa = _bodyFa = _bodyLightFa = null; + _displayTried = _bodyTried = _bodyLightTried = false; + } + } +} diff --git a/Assets/_Project/Scripts/Client/UI/HudTheme.cs.meta b/Assets/_Project/Scripts/Client/UI/HudTheme.cs.meta new file mode 100644 index 000000000..dfe2f70ca --- /dev/null +++ b/Assets/_Project/Scripts/Client/UI/HudTheme.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5b0ba3a2a48e57547b89018d8aba1134 \ No newline at end of file diff --git a/Assets/_Project/Scripts/Client/UI/HudUi.cs b/Assets/_Project/Scripts/Client/UI/HudUi.cs index b19bdca9a..3bd2bb1fe 100644 --- a/Assets/_Project/Scripts/Client/UI/HudUi.cs +++ b/Assets/_Project/Scripts/Client/UI/HudUi.cs @@ -4,41 +4,39 @@ using UnityEngine.UIElements; namespace ProjectM.Client { /// - /// UI Toolkit factories for the in-game HUD — a thin extension of 's Aether palette so the - /// HUD reads in the same visual language as the menu / pause / settings screens. Bars are a dark rounded track - /// + a percent-width fill; labels use the shared text weights/colours. Every element is built - /// pickingMode = Ignore by default so the HUD never eats clicks meant for the game world (only the - /// interactive build-palette buttons opt back into picking). + /// UI Toolkit factories for the in-game HUD — a thin extension of 's Aether palette, + /// now skinned with the curated Synty sci-fi-soldier kit (). Panels are 9-sliced Synty + /// boxes tinted into the Aether palette; bars are a skinned track + a percent-width fill; numerics use the + /// Orbitron display font and labels the Exo 2.0 body font. EVERYTHING is null-safe: if + /// (or a given sprite/font) is missing, each factory falls back to the original flat-colour look, so the HUD + /// is shippable with or without the theme asset. Every element is pickingMode = Ignore by default so + /// the HUD never eats clicks meant for the game world (only the interactive build-palette slots opt back in). /// public static class HudUi { public static readonly Color Track = new(0f, 0f, 0f, 0.55f); - /// A dark rounded bar track with a percent-width fill child (returned via ). - public static VisualElement Bar(float width, float height, Color fillColor, out VisualElement fill) - { - var track = new VisualElement(); - track.style.width = width; - track.style.height = height; - track.style.backgroundColor = Track; - track.style.paddingLeft = 2; track.style.paddingRight = 2; - track.style.paddingTop = 2; track.style.paddingBottom = 2; - track.style.flexDirection = FlexDirection.Row; - track.pickingMode = PickingMode.Ignore; - MenuUi.Round(track, 4); + // ---- text (Orbitron display vs Exo body, theme-driven with a bold fallback) ---- - fill = new VisualElement(); - fill.style.height = Length.Percent(100); - fill.style.width = Length.Percent(100); - fill.style.backgroundColor = fillColor; - fill.pickingMode = PickingMode.Ignore; - MenuUi.Round(fill, 3); - track.Add(fill); - return track; + /// A body label (Exo 2.0 when themed) — labels, captions, hints. + public static Label Text(string text, int size, Color color, TextAnchor align) + { + var l = MakeLabel(text, size, color, align); + var theme = HudTheme.Get(); + if (theme != null) theme.ApplyBody(l.style); + return l; } - /// A bold HUD label (non-interactive). - public static Label Text(string text, int size, Color color, TextAnchor align) + /// A display label (Orbitron when themed) — numerics + phase words, the authoritative HUD voice. + public static Label Display(string text, int size, Color color, TextAnchor align) + { + var l = MakeLabel(text, size, color, align); + var theme = HudTheme.Get(); + if (theme != null) theme.ApplyDisplay(l.style); + return l; + } + + static Label MakeLabel(string text, int size, Color color, TextAnchor align) { var l = new Label(text); l.style.fontSize = size; @@ -49,13 +47,54 @@ namespace ProjectM.Client return l; } + // ---- bars ---- + + /// A dark rounded bar track with a percent-width fill child (returned via ). + public static VisualElement Bar(float width, float height, Color fillColor, out VisualElement fill) + { + var theme = HudTheme.Get(); + var trackSpr = theme != null ? theme.BarTrack : null; + + var track = new VisualElement(); + track.style.width = width; + track.style.height = height; + track.style.paddingLeft = 2; track.style.paddingRight = 2; + track.style.paddingTop = 2; track.style.paddingBottom = 2; + track.style.flexDirection = FlexDirection.Row; + track.pickingMode = PickingMode.Ignore; + + fill = new VisualElement(); + fill.style.height = Length.Percent(100); + fill.style.width = Length.Percent(100); + fill.style.backgroundColor = fillColor; + fill.pickingMode = PickingMode.Ignore; + MenuUi.Round(fill, 3); + + if (trackSpr != null) + { + // Bar_Angled ships an 80/0 border → UITK 9-slices it horizontally from the art; no style override. + track.style.backgroundImage = new StyleBackground(Background.FromSprite(trackSpr)); + track.style.unityBackgroundImageTintColor = new Color(0.04f, 0.06f, 0.09f, 0.92f); + } + else + { + track.style.backgroundColor = Track; + MenuUi.Round(track, 4); + } + + track.Add(fill); + return track; + } + /// Set a fill's width to a 0..1 fraction of its track. public static void SetFill(VisualElement fill, float frac) { if (fill != null) fill.style.width = Length.Percent(Mathf.Clamp01(frac) * 100f); } - /// A semi-transparent rounded panel for grouping a cluster of HUD elements. + // ---- panels / icons / glyphs ---- + + /// A grouping container (no background). Transparent, click-through. public static VisualElement Group(Align items = Align.FlexStart) { var g = new VisualElement(); @@ -64,5 +103,74 @@ namespace ProjectM.Client return g; } + /// + /// A 9-sliced Synty panel tinted into the Aether palette; falls back to a flat translucent rounded panel + /// when the theme/sprite is missing. multiplies the (light-grey) skin, so a dark + /// tint reads as panel-dark while preserving the printed bevels. + /// + public static VisualElement Panel(Color tint) + { + var p = new VisualElement(); + p.pickingMode = PickingMode.Ignore; + var theme = HudTheme.Get(); + var box = theme != null ? theme.PanelBox : null; + if (box != null) + { + p.style.backgroundImage = new StyleBackground(Background.FromSprite(box)); + p.style.unityBackgroundImageTintColor = tint; + // 9-slice uses the sprite's AUTHORED border (Box_Glass ships 25px); no style override → no + // "borders overridden by style slices" log, and the art's intended corners are preserved. + } + else + { + p.style.backgroundColor = tint; + MenuUi.Round(p, 8); + MenuUi.Border(p, new Color(1f, 1f, 1f, 0.08f), 1); + } + return p; + } + + /// A fixed-size icon element backed by a Synty sprite (tinted). Returns an empty element if null. + public static VisualElement Icon(Sprite sprite, float size, Color tint) + { + var e = new VisualElement(); + e.style.width = size; e.style.height = size; + e.style.flexShrink = 0; + e.pickingMode = PickingMode.Ignore; + if (sprite != null) + { + e.style.backgroundImage = new StyleBackground(Background.FromSprite(sprite)); + e.style.unityBackgroundImageTintColor = tint; + e.style.backgroundSize = new StyleBackgroundSize(new BackgroundSize(BackgroundSizeType.Contain)); + } + return e; + } + + /// True when the theme is loaded and a sprite is present (callers choose icon vs text fallback). + + + /// + /// An input-prompt chip: the Synty key/button glyph (left untinted to keep its printed face) when present, + /// else a bordered text keycap with (e.g. "R", "Esc"). + /// + public static VisualElement Glyph(Sprite sprite, string fallbackText, float size) + { + if (sprite != null) return Icon(sprite, size, Color.white); + + var cap = new VisualElement(); + cap.style.minWidth = size; cap.style.height = size; + cap.style.paddingLeft = 7; cap.style.paddingRight = 7; + cap.style.alignItems = Align.Center; cap.style.justifyContent = Justify.Center; + cap.style.backgroundColor = new Color(0.14f, 0.17f, 0.22f, 0.95f); + cap.style.flexShrink = 0; + cap.pickingMode = PickingMode.Ignore; + MenuUi.Round(cap, 4); + MenuUi.Border(cap, new Color(1f, 1f, 1f, 0.25f), 1); + cap.Add(Text(fallbackText, Mathf.Max(10, (int)(size * 0.5f)), MenuUi.TextCol, TextAnchor.MiddleCenter)); + return cap; + } + + /// Apply a uniform 9-slice (horizontal / vertical source-texture px) to a skinned element. + } } diff --git a/Assets/_Project/Scripts/Client/UI/HudVisualMath.cs b/Assets/_Project/Scripts/Client/UI/HudVisualMath.cs new file mode 100644 index 000000000..d14f34dcb --- /dev/null +++ b/Assets/_Project/Scripts/Client/UI/HudVisualMath.cs @@ -0,0 +1,45 @@ +using UnityEngine; + +namespace ProjectM.Client +{ + /// + /// Pure HUD presentation math (unit-tested, no ECS / no UnityEngine.Object). Drives the low-health screen + /// vignette + the transient hurt-flash so the damage feedback is deterministic and testable. Used by the + /// client-only observe-only in PresentationSystemGroup; the flash decay runs on the + /// wall-frame SystemAPI.Time.DeltaTime, which is correct in a presentation system (the dt-trap only + /// applies to plain simulation systems). + /// + public static class HudVisualMath + { + /// Below this health fraction the low-health vignette starts to ramp in. + public const float LowHealthThreshold = 0.35f; + + /// Maximum steady vignette opacity at 0 HP. + public const float MaxVignetteOpacity = 0.55f; + + /// Opacity boost added by a fresh hit (a hurt flash), decaying back to the steady vignette. + public const float HurtFlashKick = 0.40f; + + /// Seconds for a full hurt-flash kick to decay to zero. + public const float HurtFlashDuration = 0.40f; + + /// 0 at/above the low-health threshold, ramping to 1 as the health fraction falls to 0. + public static float VignetteIntensity(float healthFrac) + { + float f = Mathf.Clamp01(healthFrac); + if (f >= LowHealthThreshold) return 0f; + return Mathf.Clamp01((LowHealthThreshold - f) / LowHealthThreshold); + } + + /// Steady low-health vignette opacity (no flash) from the current health fraction. + public static float VignetteOpacity(float healthFrac) => VignetteIntensity(healthFrac) * MaxVignetteOpacity; + + /// Decay an active hurt-flash boost toward 0 over seconds (clamped, never negative). + public static float DecayFlash(float flash, float dt) + => Mathf.Max(0f, flash - (HurtFlashKick / HurtFlashDuration) * Mathf.Max(0f, dt)); + + /// Final overlay opacity: the steady low-health vignette plus the current flash boost, clamped to 1. + public static float CombinedOpacity(float healthFrac, float flash) + => Mathf.Clamp01(VignetteOpacity(healthFrac) + Mathf.Clamp01(flash)); + } +} diff --git a/Assets/_Project/Scripts/Client/UI/HudVisualMath.cs.meta b/Assets/_Project/Scripts/Client/UI/HudVisualMath.cs.meta new file mode 100644 index 000000000..5feadc9fe --- /dev/null +++ b/Assets/_Project/Scripts/Client/UI/HudVisualMath.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 01204e46e6631634fa1007f4b3640035 \ No newline at end of file diff --git a/Assets/_Project/Scripts/Client/UI/MenuUi.cs b/Assets/_Project/Scripts/Client/UI/MenuUi.cs index ac319fbdd..e8f7a3872 100644 --- a/Assets/_Project/Scripts/Client/UI/MenuUi.cs +++ b/Assets/_Project/Scripts/Client/UI/MenuUi.cs @@ -66,6 +66,8 @@ namespace ProjectM.Client l.style.color = Accent; l.style.unityTextAlign = TextAnchor.MiddleCenter; l.style.marginBottom = 16; + var theme = HudTheme.Get(); + if (theme != null) theme.ApplyDisplay(l.style); return l; } @@ -76,6 +78,8 @@ namespace ProjectM.Client l.style.color = SubCol; l.style.unityTextAlign = TextAnchor.MiddleCenter; l.style.marginTop = 8; l.style.marginBottom = 6; + var theme = HudTheme.Get(); + if (theme != null) theme.ApplyBodyLight(l.style); return l; } @@ -90,6 +94,8 @@ namespace ProjectM.Client b.style.unityFontStyleAndWeight = FontStyle.Bold; Round(b, 6); Border(b, new Color(0f, 0f, 0f, 0f), 0); + var theme = HudTheme.Get(); + if (theme != null) theme.ApplyBody(b.style); return b; } diff --git a/Assets/_Project/Tests/EditMode/HudVisualMathTests.cs b/Assets/_Project/Tests/EditMode/HudVisualMathTests.cs new file mode 100644 index 000000000..62db6b002 --- /dev/null +++ b/Assets/_Project/Tests/EditMode/HudVisualMathTests.cs @@ -0,0 +1,70 @@ +using NUnit.Framework; +using ProjectM.Client; + +namespace ProjectM.Tests +{ + /// + /// Pure tests for — the low-health screen vignette ramp + hurt-flash decay that + /// drive the HUD damage overlay. Verifies the curve is 0 above the threshold, 1 at empty, monotonic between, + /// and that the flash decays to zero without going negative. + /// + public class HudVisualMathTests + { + const float Eps = 1e-4f; + + [Test] + public void Vignette_ZeroAtFullHealth_AndAtThreshold() + { + Assert.AreEqual(0f, HudVisualMath.VignetteIntensity(1f), Eps); + Assert.AreEqual(0f, HudVisualMath.VignetteIntensity(HudVisualMath.LowHealthThreshold), Eps); + Assert.AreEqual(0f, HudVisualMath.VignetteIntensity(0.9f), Eps); + } + + [Test] + public void Vignette_OneAtEmpty_HalfAtMidband() + { + Assert.AreEqual(1f, HudVisualMath.VignetteIntensity(0f), Eps); + // Half of the [0, threshold] band → ~0.5 intensity. + Assert.AreEqual(0.5f, HudVisualMath.VignetteIntensity(HudVisualMath.LowHealthThreshold * 0.5f), Eps); + } + + [Test] + public void Vignette_IsMonotonicDecreasingInHealth() + { + float prev = HudVisualMath.VignetteIntensity(0f); + for (float f = 0.05f; f <= 0.4f; f += 0.05f) + { + float cur = HudVisualMath.VignetteIntensity(f); + Assert.LessOrEqual(cur, prev + Eps, "intensity must not rise as health rises"); + prev = cur; + } + } + + [Test] + public void VignetteOpacity_ScalesByMax() + { + Assert.AreEqual(HudVisualMath.MaxVignetteOpacity, HudVisualMath.VignetteOpacity(0f), Eps); + Assert.AreEqual(0f, HudVisualMath.VignetteOpacity(1f), Eps); + } + + [Test] + public void Flash_DecaysToZero_NeverNegative() + { + Assert.AreEqual(0f, HudVisualMath.DecayFlash(HudVisualMath.HurtFlashKick, HudVisualMath.HurtFlashDuration), Eps); + // Half the duration → roughly half the kick remains. + Assert.AreEqual(HudVisualMath.HurtFlashKick * 0.5f, + HudVisualMath.DecayFlash(HudVisualMath.HurtFlashKick, HudVisualMath.HurtFlashDuration * 0.5f), Eps); + Assert.AreEqual(0f, HudVisualMath.DecayFlash(0.1f, 10f), Eps, "over-long dt clamps to 0, not negative"); + } + + [Test] + public void CombinedOpacity_ClampsToOne() + { + Assert.AreEqual(1f, HudVisualMath.CombinedOpacity(0f, 1f), Eps); + Assert.AreEqual(0f, HudVisualMath.CombinedOpacity(1f, 0f), Eps); + float mid = HudVisualMath.CombinedOpacity(0.175f, 0.2f); + Assert.Greater(mid, 0f); + Assert.LessOrEqual(mid, 1f); + } + } +} diff --git a/Assets/_Project/Tests/EditMode/HudVisualMathTests.cs.meta b/Assets/_Project/Tests/EditMode/HudVisualMathTests.cs.meta new file mode 100644 index 000000000..3f98c7fd2 --- /dev/null +++ b/Assets/_Project/Tests/EditMode/HudVisualMathTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c15dadef04ec9f543a9fc9926bdf2b0f \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 251bfea4b..2dce9c32c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -89,7 +89,7 @@ Long-form originals + the milestone each came from: `Docs/Vault/_Meta/CLAUDE_Bui - **All juice/HUD = client-only managed `SystemBase` in `PresentationSystemGroup`** (once/frame, no rollback double-fire) that OBSERVES replicated state, never mutates the sim. Read ECS via `SystemAPI.Query` in `OnUpdate` + `EntityManager.CompleteDependencyBeforeRO()` — NOT a MonoBehaviour `LateUpdate` (job-safety throw). `Entity` is a stable client dict key for a ghost's lifetime — **prune the cache each frame** (a pruned enemy = a kill → death VFX); **never `DestroyEntity` a ghost from the client** (`GhostDespawnSystem` owns despawn). Hit-stop = a camera punch, **never `Time.timeScale`** (corrupts the deterministic sim). - **Asset-free presentation:** procedural `AudioClip.Create` SFX; runtime `ParticleSystem` pool (Sprites/Default + HDR start color); code-built **UI Toolkit** HUD / menus (runtime `UIDocument` + shared `RuntimePanelSettings`; see the UITK bullet below). Edit a prefab asset's component in code via `PrefabUtility.LoadPrefabContents` → modify → **`SaveAsPrefabAsset(root, path)`** → `UnloadPrefabContents`. Watch **shared-material bleed** when re-tinting. ACES tonemapping needs URP color grading mode = HDR (`m_ColorGradingMode=1`). - **Prototype glue lives in `ProjectM.Client` as MonoBehaviours:** `PrototypeCameraRig` (player-following ARPG cam), `VFXConfig` (static `Instance` + prefab fields bridging authored VFX to the managed `CombatFeedbackSystem`; keep a procedural fallback). A **static presentation bridge must reset on play-enter** via `[RuntimeInitializeOnLoadMethod(SubsystemRegistration)]` (statics survive fast-enter-playmode reloads → stale flash). -- **UITK HUD + menus ★:** `MenuUi` owns the shared palette + element factories + `PanelSettings`/`EventSystem` plumbing + the canonical `Round`/`Border` helpers; `HudUi` is a thin extension (bars/labels). `HudSystem` is a `PresentationSystemGroup` observe-only `SystemBase` owning a runtime `UIDocument` (`sortingOrder 50`, behind the pause overlay's 100); builds the tree on the first frame `rootVisualElement != null`, root `pickingMode = Ignore` so the HUD never eats world clicks (only palette buttons opt back in). **Runtime UITK needs a `PanelSettings` WITH a `themeStyleSheet`** (a `.tss` importing `unity-theme://default`) **AND** an `EventSystem` + `InputSystemUIInputModule` or buttons are silently dead. The **build palette** (lazy-built from the client `StructureCatalog`) drives click-to-place: ground-ghost preview (green/red via `BuildPreviewMath`, the client mirror of the server check), left-click → `BuildPlaceRequest` RPC, right-click/Esc cancels, `[`/`]`/R rotates; `Fire` suppressed in build mode. See [[DR-021_HUD_UITK_BuildPalette]]. +- **UITK HUD + menus ★:** `MenuUi` owns the shared palette + element factories + `PanelSettings`/`EventSystem` plumbing + the canonical `Round`/`Border` helpers; `HudUi` is a thin extension (bars/labels). `HudSystem` is a `PresentationSystemGroup` observe-only `SystemBase` owning a runtime `UIDocument` (`sortingOrder 50`, behind the pause overlay's 100); builds the tree on the first frame `rootVisualElement != null`, root `pickingMode = Ignore` so the HUD never eats world clicks (only palette buttons opt back in). **Runtime UITK needs a `PanelSettings` WITH a `themeStyleSheet`** (a `.tss` importing `unity-theme://default`) **AND** an `EventSystem` + `InputSystemUIInputModule` or buttons are silently dead. The **build palette** (lazy-built from the client `StructureCatalog`) drives click-to-place: ground-ghost preview (green/red via `BuildPreviewMath`, the client mirror of the server check), left-click → `BuildPlaceRequest` RPC, right-click/Esc cancels, `[`/`]`/R rotates; `Fire` suppressed in build mode. See [[DR-021_HUD_UITK_BuildPalette]]. **Synty skin via a build-safe `HudTheme`** (DR-024): the in-game HUD is reskinned with the InterfaceSciFiSoldierHUD/Core kit — but those sprites/fonts live under `Assets/Synty/…` (NOT Resources), so a runtime name-string `Resources.Load("Synty/…")` is **stripped from the build**; instead a curated `HudTheme : ScriptableObject` at `Assets/_Project/Resources/HudTheme.asset` holds **serialized** Sprite/Font refs (dependency-walked into the build), loaded null-safe via `HudTheme.Get()`, and **every consumer falls back to the flat look if a ref/the asset is null**. `unityBackgroundImageTintColor` MULTIPLIES (tint white skins into the Aether palette, zero asset bleed). Fonts = cached SDF `FontAsset.CreateFontAsset(Font)`+`FontDefinition.FromSDFFont`, built **once per font per session**, reset (with the theme cache) on `[RuntimeInitializeOnLoadMethod(SubsystemRegistration)]`. **Synty HUD frame/bar sprites ship authored 9-slice borders** (Box_Glass=25, Bar_Angled=80/0) — setting `unitySlice*` in code OVERRIDES them and logs a `"borders overridden by style slices"` ERROR per element; let the art border drive slicing (no `unitySlice*`), only set slices for border-0 sprites. Some Synty sprites import as Sprite **Multiple** mode (e.g. `Gradient_Outwards`) → `LoadAssetAtPath` silently returns null; verify import mode + each theme ref non-null after authoring. See [[DR-024_HUD_Synty_Skin_Theme]]. ### Art import (HDRP store packs → URP) - BefourStudios art is **HDRP-authored** → magenta under URP 17.4 + Entities Graphics. **Convert, don't switch pipelines** (HDRP breaks Entities Graphics). Re-author to stock URP/Lit via `Assets/_Project/Scripts/Editor/EnvArtTools.cs` (menu `ProjectM/Art/1. Convert Curated Env Materials`). Synty art is **URP-native — no conversion**. diff --git a/Docs/Vault/06_Roadmap/Synty_Asset_Inventory.md b/Docs/Vault/06_Roadmap/Synty_Asset_Inventory.md index 61e85ac2b..3528562d6 100644 --- a/Docs/Vault/06_Roadmap/Synty_Asset_Inventory.md +++ b/Docs/Vault/06_Roadmap/Synty_Asset_Inventory.md @@ -48,4 +48,4 @@ Catalog of the Synty Polygon packs under `Assets/Synty/`, produced by the [[DR-0 2. **Quadruped Swarmer** — PolygonDog as a separate Rukhanka rig + dog controller (its bundled Attack/Death clips enable a real death anim). 3. **Ruined-city / off-world maps** — Kaiju destroyed-city + NatureBiomes Arid_Desert for the base + expedition regions. 4. **Combat/heal/portal VFX** — wire PolygonParticleFX into `VFXConfig`. -5. **HUD polish** — bake PolygonIcons + harvest Interface* sprites/fonts into the UITK HUD ([[DR-021_HUD_UITK_BuildPalette]]). +5. **HUD polish** — ✅ DONE ([[DR-024_HUD_Synty_Skin_Theme]]): harvested Interface* sprites/fonts (status/map/inventory icons, HUD panels/bars/pips/gradients/glow, 640 input glyphs, Orbitron + Exo 2.0) into the UITK HUD via a build-safe `HudTheme` ScriptableObject. PolygonIcons (3D meshes) still un-baked — a future render-to-sprite option if more icon variety is wanted. diff --git a/Docs/Vault/07_Sessions/2026/2026-06-07_HUD_Synty_Visual_Pass.md b/Docs/Vault/07_Sessions/2026/2026-06-07_HUD_Synty_Visual_Pass.md new file mode 100644 index 000000000..955255955 --- /dev/null +++ b/Docs/Vault/07_Sessions/2026/2026-06-07_HUD_Synty_Visual_Pass.md @@ -0,0 +1,45 @@ +--- +date: 2026-06-07 +type: session +tags: [session, hud, ui-toolkit, synty, presentation, juice, build, dots] +--- + +# Session 2026-06-07 — Expansive HUD visual pass (Synty sci-fi-soldier kit → UITK) + +## Goal + +Operator (via `/dots-dev`, ultracode): *"Do a pass on the HUD using the new assets that were imported. Use everything you can and make it look good and follow good design principles (both game and visual). This should be an expansive and large pass."* + +The in-game HUD (DR-021) was code-built UITK but flat-colour + system font + text-only. The newly-imported **InterfaceSciFiSoldierHUD** + **InterfaceCore** kits (status/map/inventory icons, HUD panel/bar/pip/gradient/glow sprites, 640 input glyphs, Orbitron + Exo 2.0 fonts) are a complete on-theme HUD kit — harvest them into the existing observe-only `HudSystem`. + +## Process + +- **Verify+design Workflow (4 parallel agents):** (1) UITK runtime API correctness — empirically confirmed on the live editor: `Background.FromSprite`, `unityBackgroundImageTintColor` MULTIPLIES, code-set `unitySlice*` works even on border-0 sprites (but OVERRIDES an authored border), `FontAsset.CreateFontAsset(Font)`+`FontDefinition.FromSDFFont` is the reliable runtime SDF path; (2) build-safety — a serialized ScriptableObject-in-Resources is the build-safe way to pull curated `Assets/Synty/...` sprites into the player build (the Synty sprites are NOT in a Resources folder, so a name-string `Resources.Load` would fail); (3) exact asset manifest (real prefixed filenames verified on disk); (4) HUD layout/design spec. +- **Plan → "go" (fonts applied to shared MenuUi too) → serial implementation** through the one focused editor. +- **Adversarial review Workflow (3 lenses)** post-build → shippable, no blockers; applied the worthwhile fixes (below). + +## Done + +- **`HudTheme` ScriptableObject** (`Assets/_Project/Resources/HudTheme.asset`, 31 curated Synty Sprite/Font refs) — build-safe via serialized refs, loaded null-safe via `HudTheme.Get()` (mirrors `MenuUi.LoadPanelSettings`). Fonts applied as cached SDF `FontAsset`s built once-per-font-per-session via `ApplyDisplay/ApplyBody/ApplyBodyLight`; static cache + FontAssets reset on `[RuntimeInitializeOnLoadMethod(SubsystemRegistration)]`. **Every consumer is null-safe → falls back to the flat HUD if the asset/ref is missing.** +- **`HudVisualMath`** (pure, +6 EditMode tests) — low-health vignette intensity ramp + hurt-flash decay. +- **`HudUi` expansion** — null-safe skinned factories: `Panel` (9-sliced Synty box, tinted), `Bar` (skinned track), `Icon`, `Glyph` (input prompt or text keycap), `Text`/`Display` (Exo body / Orbitron display fonts). `MenuUi` Title/Caption/Button now apply the theme fonts → **menu/pause/settings re-fonted too** (one visual language). +- **`HudSystem` reworked** — framed, iconified, theme-driven layout: bottom-left vitals (health icon + skinned bar + Orbitron number, cooldown row that dims when ready, shield chip); top-left resource strip (Aether/Ore/Bio icons + counts); top-right threat (skull + count, **hidden at zero/calm**, warms+reddens under siege); center-top mission banner (Orbitron phase + countdown + **CYCLE N**, phase-coloured underline) + location sub-line + **hex-pip goal meter** (bar fallback for Target>12); bottom-center build palette with **per-structure icons** + per-resource cost icon/affordability + selection glow; **scheme-aware build-mode control-hint bar** (KBM/gamepad glyphs) with a **live conveyor-facing arrow** gated to conveyors; full-screen **low-health vignette + hurt-flash** + re-skinned downed overlay. +- **Resolves the [[DR-021_HUD_UITK_BuildPalette]] open items**: per-buildable icons + a build-mode hint line + the conveyor-facing indicator. → [[DR-024_HUD_Synty_Skin_Theme]]. + +## Validation (runtime, real Server+Client) + +- Compiles clean; EditMode **214/214** (incl. 6 new `HudVisualMath`). Console clean of HUD errors (only benign server-tick-batching + an unrelated FMOD audio-device note). +- **4 Play states screenshotted + verified**: base HUD, build-mode (palette icons + selection glow + hint bar), low-health (red edge vignette + depleted bar), conveyor-facing (live arrow + conveyor-gated ROTATE/FACING + CYCLE readout). Theme + all 31 refs loaded non-null; HUD tree built (8 root clusters). +- Adversarial review: client-only observe-only (no sim mutation/structural change), null-safety + picking modes correct, font cache once-per-session, hurt-flash `prevHp` resets on despawn. + +## Gotchas captured (→ CLAUDE.md + [[DR-024_HUD_Synty_Skin_Theme]]) + +- **Synty HUD frame/bar sprites ship authored 9-slice borders** (Box_Glass=25, Bar_Angled=80/0). Code-set `unitySlice*` OVERRIDES them and logs a `"borders … overridden by style slices"` ERROR per element → for bordered skins, assign the sprite + tint and let the **art border drive slicing** (no `unitySlice*`); only set slices for border-0 sprites. +- **Some Synty sprites import as Sprite *Multiple* mode** (e.g. `Gradient_Outwards`) → `LoadAssetAtPath` returns null silently. Verify import mode; convert to Single when curating a single-image sprite. Always verify each theme ref non-null after authoring (a wrong/odd path is a silent null, which the null-safe fallback hides). +- **Build-safe asset harvest** = serialized refs in a ScriptableObject-in-Resources, NEVER `Resources.Load("Synty/…")` (those live outside Resources → stripped from the build). +- `execute_code` safety-checks block `AssetDatabase.DeleteAsset` (use `safety_checks=false` or skip when the asset is new). + +## Next-session intent + +- Eyeball the HUD at a true 1920 game-view (the narrow ~945 editor window overlaps vitals↔palette — resolution artifact, fine at target res). +- Confirm the conveyor-facing arrow's compass mapping vs the world placement ghost (a 1-line `FacingDegrees` offset if the arrow art's default vector differs); optional polish: cooldown radial, throughput visuals (needs a replicated machine field — DR-020 open). diff --git a/Docs/Vault/07_Sessions/_Decisions/DR-024_HUD_Synty_Skin_Theme.md b/Docs/Vault/07_Sessions/_Decisions/DR-024_HUD_Synty_Skin_Theme.md new file mode 100644 index 000000000..97934a0de --- /dev/null +++ b/Docs/Vault/07_Sessions/_Decisions/DR-024_HUD_Synty_Skin_Theme.md @@ -0,0 +1,49 @@ +--- +id: DR-024 +title: HUD reskinned with the Synty sci-fi-soldier kit via a build-safe HudTheme ScriptableObject (icons, fonts, panels, vignette, scheme-aware hints) +status: accepted +date: 2026-06-07 +tags: +- decision +- hud +- ui-toolkit +- synty +- presentation +- juice +permalink: gamevault/07-sessions/decisions/dr-024-hud-synty-skin-theme +--- + +# DR-024 — HUD Synty skin + HudTheme (build-safe asset harvest into the code-built UITK HUD) + +## Context + +[[DR-021_HUD_UITK_BuildPalette]] put the HUD on UI Toolkit but flat-colour, system-font, text-only, and left open: per-buildable icons, a build-mode hint line, a conveyor-facing arrow. The imported **InterfaceSciFiSoldierHUD** + **InterfaceCore** packs ([[Synty_Asset_Inventory]]) are a complete on-theme HUD kit (status/map/inventory icons, HUD box/frame/bar/pip/gradient/glow sprites, 640 input-prompt glyphs, Orbitron + Exo 2.0 fonts). Operator: *"expansive, large pass … use everything you can, make it look good, good design principles."* + +## Decision + +1. **Curated, serialized `HudTheme` ScriptableObject is the build-safe asset bridge.** The Synty sprites/fonts live under `Assets/Synty/…`, NOT a `Resources/` folder, so a runtime name-string `Resources.Load("Synty/…")` would be stripped from the player build. Instead, `Assets/_Project/Resources/HudTheme.asset` holds ~31 **serialized** Sprite/Font references (the dependency walker pulls them into the build); it's loaded once, null-safe, via `HudTheme.Get()` (mirroring `MenuUi.LoadPanelSettings`). **Every consumer null-checks the theme AND each field and falls back to the flat-colour HUD** — a missing asset/ref never breaks or magenta-s the HUD, and EditMode stays green without the asset. + +2. **Tint the white kit into the Aether palette; don't adopt the kit's stock colours.** `unityBackgroundImageTintColor` MULTIPLIES the (light-grey) Synty skins, so one panel sprite (`Box_Glass_01`) tinted per-cluster (panel-dark, warm-dark under siege) reads as a coherent system. Aether-cyan = friendly/charge/selection + structure icons; Blight-orange→red = threat/low-HP/siege; Ore-amber / Bio-green for resources. Orbitron-ExtraBold (SDF) for numerics + phase words; Exo 2.0 for labels/hints — applied through `MenuUi`'s shared factories so **menu/pause/settings re-font too**. + +3. **Fonts = cached runtime SDF FontAssets.** `FontAsset.CreateFontAsset(Font)` + `FontDefinition.FromSDFFont` is the reliable runtime path (crisp at any scale, supports outlines). Built **once per font per session**, cached statically, and reset — together with the theme cache — on `[RuntimeInitializeOnLoadMethod(SubsystemRegistration)]` so a fast-enter-playmode session never references a destroyed atlas (matches the `VFXConfig`/`BuildPaletteState` static-reset rule). A tiny per-editor-session atlas leak is accepted. + +4. **Let the art's authored 9-slice border drive slicing.** The Synty HUD frame/bar sprites ship borders (Box_Glass=25, Bar_Angled=80/0). Setting `unitySlice*` in code OVERRIDES them and logs a `"borders … overridden by style slices"` error per element. So `HudUi.Panel`/`Bar` assign the sprite + tint and set NO `unitySlice*` — the art border 9-slices correctly and the console stays clean. (Code slices are only needed for border-0 sprites — verified separately.) + +5. **Layout follows HUD spatial convention; only data-backed elements are surfaced.** Persistent self-state hugs the corners (health bottom-left, resources top-left, threat top-right, build deck bottom-center); transient mission state is center-top (phase + countdown + CYCLE N + hex-pip goal). Conditional elements appear only when live (shield chip, threat panel hidden at zero/calm, control hints only in build mode) so the resting HUD is clean. No fake minimap/ammo/oxygen despite the kit shipping those sprites. + +6. **Resolves the DR-021 opens.** Per-buildable icons (from `HudTheme.StructureIcon`, cost icon + affordability now keyed off `StructureCatalogEntry.CostResourceId` so it stays correct as the catalog grows); a **scheme-aware** build-mode control-hint bar (KBM vs gamepad glyphs via `AimPresentation.Scheme`, text keycaps where no glyph exists); and a **live conveyor-facing arrow** (rotated by `BuildPaletteState.Direction`) gated to conveyors. Low-health screen **vignette + hurt-flash** (`HudVisualMath`, pure + unit-tested) is the new unmissable damage signal. + +## Consequences + +- **No netcode / asmdef / ghost-hash change.** Pure client presentation: a new managed ScriptableObject + Resources asset + the rewritten observe-only `HudSystem` (client `PresentationSystemGroup`), `HudUi`/`MenuUi` factories, and `HudVisualMath`. `HudSystem` reads ECS read-only and mutates nothing; it adds `EntityManager.CompleteDependencyBeforeRO()` for the components it reads, matching the sibling presentation systems. +- **Validated on 6.4.7** (real Server+Client): EditMode **214/214** (+6 `HudVisualMath`); 4 Play states screenshotted (base / build-mode / low-health / conveyor-facing); console clean. Screenshots: `HudSyntyPass_Base/BuildMode/LowHealth/ConveyorFacing.png`. +- New durable conventions folded into `CLAUDE.md` (build-safe HudTheme harvest; art-border-drives-slicing; verify Synty sprite import-mode). + +## Open / deferred + +- Conveyor-facing arrow's absolute compass mapping vs the world placement ghost (a 1-line `FacingDegrees` offset if the arrow art's default vector differs) — confirm in natural play. +- Narrow-window vitals↔palette overlap (resolution artifact; fine at 1920) — responsive layout deferred. +- Cooldown radial treatment; throughput-on-belt visuals (needs a replicated machine field — the [[DR-020_M7_Automation_Production_Chains]] open item). +- `HudTheme` icon picks are operator-tunable in the one asset (e.g. Wall uses the Armor glyph — no dedicated wall icon in the kit). + +Builds on [[DR-021_HUD_UITK_BuildPalette]] (the UITK HUD + palette this skins) and [[DR-019_Frontend_Menu_Settings_Saves_Build]] (shared `MenuUi`/PanelSettings). Consumes [[Synty_Asset_Inventory]]'s "HUD polish" hook. Serves the co-op base-building + game-feel [[Pillars]].