diff --git a/Explorer/Assets/AddressableAssetsData/AddressableAssetSettings.asset b/Explorer/Assets/AddressableAssetsData/AddressableAssetSettings.asset
index 680abb81674..2aebaa37c4c 100644
--- a/Explorer/Assets/AddressableAssetsData/AddressableAssetSettings.asset
+++ b/Explorer/Assets/AddressableAssetsData/AddressableAssetSettings.asset
@@ -15,7 +15,7 @@ MonoBehaviour:
m_DefaultGroup: 47f803e1e9c5079449bd106df98a0b7d
m_currentHash:
serializedVersion: 2
- Hash: 8173ea4fb741d0d3cbbc7d6ca2d76db7
+ Hash: 69405828b03b7e3bfc18552c607113d2
m_OptimizeCatalogSize: 0
m_BuildRemoteCatalog: 0
m_CatalogRequestsTimeout: 0
diff --git a/Explorer/Assets/AddressableAssetsData/AssetGroups/Essentials.asset b/Explorer/Assets/AddressableAssetsData/AssetGroups/Essentials.asset
index c7cc20f73c6..5360bc2d9c1 100644
--- a/Explorer/Assets/AddressableAssetsData/AssetGroups/Essentials.asset
+++ b/Explorer/Assets/AddressableAssetsData/AssetGroups/Essentials.asset
@@ -170,6 +170,11 @@ MonoBehaviour:
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
+ - m_GUID: 69832a4fb177e7645af56a1f27d0dce2
+ m_Address: Assets/DCL/PluginSystem/Global/AvatarFaceAnimationSettings.asset
+ m_ReadOnly: 0
+ m_SerializedLabels: []
+ FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 6a252383173104e17acac54eb364afc4
m_Address: Assets/DCL/AvatarRendering/AvatarShape/Assets/PointAtMarker.prefab
m_ReadOnly: 0
@@ -355,6 +360,11 @@ MonoBehaviour:
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
+ - m_GUID: f13fd0befee6d48479806b957b3ca94f
+ m_Address: Assets/DCL/PluginSystem/Global/AvatarFaceExpressionConfig.asset
+ m_ReadOnly: 0
+ m_SerializedLabels: []
+ FlaggedDuringContentUpdateRestriction: 0
- m_GUID: f3a58c428a71b443e883f8fac00cfbbd
m_Address: Assets/DCL/AvatarRendering/AvatarShape/Assets/EmoteMaskCatalog.asset
m_ReadOnly: 0
diff --git a/Explorer/Assets/AddressableAssetsData/AssetGroups/UI.asset b/Explorer/Assets/AddressableAssetsData/AssetGroups/UI.asset
index 499752308d7..cac598bff8a 100644
--- a/Explorer/Assets/AddressableAssetsData/AssetGroups/UI.asset
+++ b/Explorer/Assets/AddressableAssetsData/AssetGroups/UI.asset
@@ -365,6 +365,11 @@ MonoBehaviour:
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
+ - m_GUID: 5c07800f394bd8945936cf8e55cdd020
+ m_Address: FacialExpressionsWheelHUD.prefab
+ m_ReadOnly: 0
+ m_SerializedLabels: []
+ FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 5deacee45f1ebfe4394a97755a51fcca
m_Address: Assets/Textures/ExplorePanel/BackpackHoverUnhoverSpriteAtlas.spriteatlasv2
m_ReadOnly: 0
diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Assets/AvatarFaceAnimationSettings.cs b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Assets/AvatarFaceAnimationSettings.cs
new file mode 100644
index 00000000000..e7dc3825b82
--- /dev/null
+++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Assets/AvatarFaceAnimationSettings.cs
@@ -0,0 +1,17 @@
+using UnityEngine;
+
+namespace DCL.AvatarRendering.AvatarShape.Assets
+{
+ [CreateAssetMenu(fileName = "AvatarFaceAnimationSettings", menuName = "DCL/Avatar/Face Animation Settings")]
+ public class AvatarFaceAnimationSettings : ScriptableObject
+ {
+ [field: Header("Blink")]
+ [field: SerializeField] public float MinBlinkInterval { get; private set; } = 2.0f;
+ [field: SerializeField] public float MaxBlinkInterval { get; private set; } = 8.0f;
+ [field: SerializeField] public float BlinkFrameDuration { get; private set; } = 0.05f;
+
+ [field: Header("Mouth Pose")]
+ [field: SerializeField] public float MouthPoseDuration { get; private set; } = 0.08f;
+ [field: SerializeField] public float VowelMouthPoseDuration { get; private set; } = 0.12f;
+ }
+}
\ No newline at end of file
diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Assets/AvatarFaceAnimationSettings.cs.meta b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Assets/AvatarFaceAnimationSettings.cs.meta
new file mode 100644
index 00000000000..7545cfa5555
--- /dev/null
+++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Assets/AvatarFaceAnimationSettings.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 3fa44f695c71c9d44a49c7ddfbf82738
\ No newline at end of file
diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/AvatarFaceExpressionConfig.cs b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/AvatarFaceExpressionConfig.cs
new file mode 100644
index 00000000000..6590cfbce93
--- /dev/null
+++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/AvatarFaceExpressionConfig.cs
@@ -0,0 +1,17 @@
+using System;
+using UnityEngine;
+
+namespace DCL.AvatarRendering.AvatarShape
+{
+ ///
+ /// ScriptableObject holding all named face expressions available to avatars.
+ /// Configure via Assets > Create > DCL > Avatar > Face Expression Config.
+ /// Expressions are the base layer of the face, underneath blink and mouth-pose animations
+ /// which temporarily override eyes and mouth respectively.
+ ///
+ [CreateAssetMenu(fileName = "AvatarFaceExpressionConfig", menuName = "DCL/Avatar/Face Expression Config")]
+ public class AvatarFaceExpressionConfig : ScriptableObject
+ {
+ public AvatarFaceExpressionDefinition[] Expressions = Array.Empty();
+ }
+}
\ No newline at end of file
diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/AvatarFaceExpressionConfig.cs.meta b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/AvatarFaceExpressionConfig.cs.meta
new file mode 100644
index 00000000000..3ea032e9960
--- /dev/null
+++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/AvatarFaceExpressionConfig.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 3fc70577efb0ebd4fb2f7ed9436f8213
\ No newline at end of file
diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/AvatarFaceExpressionDefinition.cs b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/AvatarFaceExpressionDefinition.cs
new file mode 100644
index 00000000000..e6f979f3754
--- /dev/null
+++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/AvatarFaceExpressionDefinition.cs
@@ -0,0 +1,28 @@
+using System;
+using UnityEngine;
+
+namespace DCL.AvatarRendering.AvatarShape
+{
+ ///
+ /// A named face pose combining specific atlas slice indices for eyebrows, eyes, and mouth.
+ /// Eyebrows atlas: 0 Idle, 1 Up, 2 Down, 3 Angry, 4 Sad, 5 Surprised, 6-15 Unused.
+ /// Eyes atlas: 0 Idle, 1 HalfClosed, 2 Closed, 3 WideOpen, 4-7 Look directions, 8-15 Unused.
+ /// Mouth atlas: 0-11 Mouth poses, 12 Sad, 13 Happy, 14 Smile, 15 Worried.
+ ///
+ [Serializable]
+ public struct AvatarFaceExpressionDefinition
+ {
+ public string Name;
+
+ public Sprite Icon;
+
+ [Range(0, 15)]
+ public int EyebrowsIndex;
+
+ [Range(0, 15)]
+ public int EyesIndex;
+
+ [Range(0, 15)]
+ public int MouthIndex;
+ }
+}
\ No newline at end of file
diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/AvatarFaceExpressionDefinition.cs.meta b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/AvatarFaceExpressionDefinition.cs.meta
new file mode 100644
index 00000000000..62445162a80
--- /dev/null
+++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/AvatarFaceExpressionDefinition.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 22d16ab00265faa4c860c5aa05735669
\ No newline at end of file
diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/AvatarFaceMaterialUtils.cs b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/AvatarFaceMaterialUtils.cs
new file mode 100644
index 00000000000..b7cd1eb3acf
--- /dev/null
+++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/AvatarFaceMaterialUtils.cs
@@ -0,0 +1,99 @@
+using DCL.AvatarRendering.AvatarShape.Components;
+using DCL.AvatarRendering.AvatarShape.Rendering.TextureArray;
+using DCL.AvatarRendering.Loading.Assets;
+using UnityEngine;
+
+namespace DCL.AvatarRendering.AvatarShape
+{
+ ///
+ /// Material/MaterialPropertyBlock side-effects for face animation. The avatar facial
+ /// expression system delegates all expression-index binding here so it stays focused
+ /// on state transitions.
+ ///
+ public static class AvatarFaceMaterialUtils
+ {
+ public static void ApplyEyebrowsFrame(ref AvatarFaceComponent face, int eyebrowsIndex)
+ {
+ if (face.EyebrowsRenderer == null) return;
+ if (face.CurrentEyebrowsIndex == eyebrowsIndex) return;
+
+ face.CurrentEyebrowsIndex = eyebrowsIndex;
+ SetExpressionIndex(face.EyebrowsRenderer, eyebrowsIndex);
+ }
+
+ public static void ApplyEyeFrame(ref AvatarFaceComponent face, int eyeIndex)
+ {
+ if (face.EyeRenderer == null) return;
+ if (face.CurrentEyeIndex == eyeIndex) return;
+
+ face.CurrentEyeIndex = eyeIndex;
+ SetExpressionIndex(face.EyeRenderer, eyeIndex);
+ }
+
+ public static void ApplyMouthPose(ref AvatarFaceComponent face, int mouthPoseIndex)
+ {
+ if (face.MouthRenderer == null) return;
+ if (face.CurrentMouthPoseIndex == mouthPoseIndex) return;
+
+ face.CurrentMouthPoseIndex = mouthPoseIndex;
+ SetExpressionIndex(face.MouthRenderer, mouthPoseIndex);
+ }
+
+ public static void StartBlink(ref AvatarFaceComponent face)
+ {
+ face.IsBlinking = true;
+ face.BlinkFrameIndex = 0;
+ face.BlinkFrameTimer = 0f;
+ ApplyEyeFrame(ref face, AvatarFacialExpressionConstants.BLINK_SEQUENCE[0]);
+ }
+
+ public static void EndBlink(ref AvatarFaceComponent face, float minBlinkInterval, float maxBlinkInterval)
+ {
+ face.IsBlinking = false;
+ face.TimeSinceLastBlink = 0f;
+ face.NextBlinkTime = Random.Range(minBlinkInterval, maxBlinkInterval);
+ ApplyEyeFrame(ref face, face.EyesExpressionIndex);
+ }
+
+ public static void StopMouthAnimation(ref AvatarFaceComponent face)
+ {
+ face.AnimatingText = null;
+ ApplyMouthPose(ref face, face.MouthExpressionIndex);
+ }
+
+ ///
+ /// Searches the avatar's instantiated wearables for the first renderer whose name ends
+ /// with . Returns null when none of the wearables expose one
+ /// (e.g. wearable lacks a Mask_* mesh).
+ ///
+ public static Renderer? FindRendererWithSuffix(in AvatarShapeComponent avatarShape, string suffix)
+ {
+ for (var i = 0; i < avatarShape.InstantiatedWearables.Count; i++)
+ {
+ CachedAttachment wearable = avatarShape.InstantiatedWearables[i];
+
+ for (var j = 0; j < wearable.Renderers.Count; j++)
+ {
+ Renderer renderer = wearable.Renderers[j];
+
+ if (renderer.name.EndsWith(suffix))
+ return renderer;
+ }
+ }
+
+ return null;
+ }
+
+ // Sentinel index (<0) disables atlas slicing in the shader so non-atlas wearables sample
+ // their full base map; >=0 picks one cell of the 4x4 atlas grid. _ExpressionIndex lives
+ // in UnityPerMaterial CBuffer for SRP batcher, so MPB can't override it; each face
+ // renderer already has its own pooled material instance, so a direct write is safe.
+ private static void SetExpressionIndex(Renderer renderer, int index)
+ {
+ Material material = renderer.sharedMaterial;
+
+ if (material != null)
+ material.SetInteger(TextureArrayConstants.EXPRESSION_INDEX_SHADER, index);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/AvatarFaceMaterialUtils.cs.meta b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/AvatarFaceMaterialUtils.cs.meta
new file mode 100644
index 00000000000..dfa343d23d8
--- /dev/null
+++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/AvatarFaceMaterialUtils.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 8d9334e9f4aad364ebab5fde98cacc37
\ No newline at end of file
diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/AvatarFacialExpressionConstants.cs b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/AvatarFacialExpressionConstants.cs
new file mode 100644
index 00000000000..68fce9e8e05
--- /dev/null
+++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/AvatarFacialExpressionConstants.cs
@@ -0,0 +1,80 @@
+namespace DCL.AvatarRendering.AvatarShape
+{
+ ///
+ /// Atlas slice indices, mouth-pose mapping, and pause-character classification used by
+ /// the avatar facial expression pipeline.
+ ///
+ ///
+ /// Atlases are 1024×1024, 4×4 grid of 256px cells (top-to-bottom, left-to-right).
+ ///
+ /// Eyebrows: 0 Idle, 1 Up, 2 Down, 3 Angry, 4 Sad, 5 Surprised, 6-15 Unused.
+ /// Eyes: 0 Idle, 1 HalfClosed, 2 Closed, 3 WideOpen, 4-7 Look*, 8-15 Unused.
+ /// Mouth: 0 Idle, 1 a/e/i, 2 b/m/p, 3 f/v, 4 d/th, 5 u, 6 c/g/h/k/n/s/t/x/y/z,
+ /// 7 o, 8 l, 9 r, 10 ch/j/sh, 11 w/q, 12 Sad, 13 Happy, 14 Smile, 15 Worried.
+ ///
+ public static class AvatarFacialExpressionConstants
+ {
+ public const int NO_EYEBROWS_OVERRIDE = -1;
+ public const int NO_EYE_OVERRIDE = -1;
+ public const int NO_MOUTH_POSE = -1;
+
+ public const int EYE_HALF_CLOSED = 1;
+ public const int EYE_CLOSED = 2;
+
+ public const float UPPERCASE_DURATION_MULTIPLIER = 2f;
+
+ /// Mouth-pose-rich text looped while an avatar is actively speaking in voice chat.
+ public const string VOICE_CHAT_LOOP_TEXT = "el murcielago hindu comia feliz cardillo y kiwi";
+
+ /// One blink: HalfClosed → Closed → HalfClosed → restore expression.
+ public static readonly int[] BLINK_SEQUENCE = { EYE_HALF_CLOSED, EYE_CLOSED, EYE_HALF_CLOSED };
+
+ public static bool IsVowel(char c)
+ {
+ c = char.ToLowerInvariant(c);
+ return c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u';
+ }
+
+ ///
+ /// Pause characters hold the expression mouth (not idle) instead of an articulated pose.
+ ///
+ public static bool IsPauseChar(char c) =>
+ c == ',' || c == '.' || c == ';' || c == ':' || c == '!' || c == '?' || c == ' ' || c == '\n';
+
+ ///
+ /// Maps a character at to a mouth pose slice. Digraphs
+ /// (th, ch, sh) detected by peeking. Pause chars and any unmapped char return
+ /// so the caller falls back to the expression mouth.
+ ///
+ public static int MapCharToMouthPose(string text, int index)
+ {
+ char c = char.ToLowerInvariant(text[index]);
+
+ if (IsPauseChar(c))
+ return NO_MOUTH_POSE;
+
+ char next = index + 1 < text.Length ? char.ToLowerInvariant(text[index + 1]) : '\0';
+
+ switch (c)
+ {
+ case 'a': case 'e': case 'i': return 1;
+ case 'b': case 'm': case 'p': return 2;
+ case 'f': case 'v': return 3;
+ case 't': return next == 'h' ? 4 : 6;
+ case 'u': return 5;
+ case 'd': return 4;
+ case 'g': case 'h':
+ case 'k': case 'n': case 'x':
+ case 'y': case 'z': return 6;
+ case 'c': return next == 'h' ? 10 : 6;
+ case 's': return next == 'h' ? 10 : 6;
+ case 'o': return 7;
+ case 'l': return 8;
+ case 'r': return 9;
+ case 'j': return 10;
+ case 'w': case 'q': return 11;
+ default: return NO_MOUTH_POSE;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/AvatarFacialExpressionConstants.cs.meta b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/AvatarFacialExpressionConstants.cs.meta
new file mode 100644
index 00000000000..e4a9918d375
--- /dev/null
+++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/AvatarFacialExpressionConstants.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 897324802def3d34b85dc05e3019c32c
\ No newline at end of file
diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Components/AvatarFaceComponent.cs b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Components/AvatarFaceComponent.cs
new file mode 100644
index 00000000000..a94220ffbf2
--- /dev/null
+++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Components/AvatarFaceComponent.cs
@@ -0,0 +1,60 @@
+using UnityEngine;
+
+namespace DCL.AvatarRendering.AvatarShape.Components
+{
+ ///
+ /// Per-avatar facial animation state. One component holds all three layers because they are
+ /// always added/removed together and the pipeline reads them together each frame:
+ /// base expression → blink overrides eyes → chat/voice overrides mouth → pause restores
+ /// mouth to (not idle).
+ ///
+ public struct AvatarFaceComponent
+ {
+ // Renderers — null while setup is pending or after avatar re-instantiation.
+ public Renderer EyebrowsRenderer;
+ public Renderer EyeRenderer;
+ public Renderer MouthRenderer;
+
+ // Cached at the time we last wrote _ExpressionIndex.
+ // The skinning material pool can swap the material under us during a wearable swap while
+ // returning the SAME renderer instance from the wearable cache — the new material starts at
+ // the shader-default sentinel, so a renderer-ref-only diff would happily skip the write and
+ // the channel would render the full atlas. AvatarFacialExpressionSystem diffs these against
+ // renderer.sharedMaterial each frame to detect that case.
+ public Material EyebrowsMaterial;
+ public Material EyeMaterial;
+ public Material MouthMaterial;
+
+ // Per-channel atlas capability of the currently worn wearable. Stable across animation;
+ // only changes when a wearable swap rebinds the face renderers.
+ public bool EyebrowsHasExpressionAtlas;
+ public bool EyesHasExpressionAtlas;
+ public bool MouthHasExpressionAtlas;
+
+ // Resting atlas cell per channel (0..N when capability bool is true, -1 otherwise).
+ // Eyes/mouth are restored to these when blink / mouth animation ends.
+ public int EyebrowsExpressionIndex;
+ public int EyesExpressionIndex;
+ public int MouthExpressionIndex;
+
+ // Currently applied MaterialPropertyBlock slice indices. -1 means no override (material default).
+ public int CurrentEyebrowsIndex;
+ public int CurrentEyeIndex;
+ public int CurrentMouthPoseIndex;
+
+ // Blink state.
+ public bool IsBlinking;
+ public float TimeSinceLastBlink;
+ public float NextBlinkTime;
+ public int BlinkFrameIndex;
+ public float BlinkFrameTimer;
+
+ // Mouth-pose animation state. AnimatingText is null when idle.
+ public string? AnimatingText;
+ public int CharacterIndex;
+ public float CharacterTimer;
+
+ /// True when expression indices changed and need to be pushed to renderers this frame.
+ public bool IsDirty;
+ }
+}
\ No newline at end of file
diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Components/AvatarFaceComponent.cs.meta b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Components/AvatarFaceComponent.cs.meta
new file mode 100644
index 00000000000..2ae4fd305f4
--- /dev/null
+++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Components/AvatarFaceComponent.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 136cbc65d4cd923489a70626f8a022b3
\ No newline at end of file
diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Components/AvatarMouthInputComponent.cs b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Components/AvatarMouthInputComponent.cs
new file mode 100644
index 00000000000..afe1450decb
--- /dev/null
+++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Components/AvatarMouthInputComponent.cs
@@ -0,0 +1,21 @@
+namespace DCL.AvatarRendering.AvatarShape.Components
+{
+ ///
+ /// Bridge component written by external services (chat + voice chat) to drive mouth animation.
+ /// AvatarFacialExpressionSystem reads and clears it each frame.
+ ///
+ /// Two independent writers update different fields, so writers must use a partial-update
+ /// pattern (Has + Get or Add) instead of AddOrSet to avoid clobbering each other.
+ ///
+ ///
+ /// - / — set by chat service when a message arrives.
+ /// - — set by voice handler when speaking state changes.
+ ///
+ ///
+ public struct AvatarMouthInputComponent
+ {
+ public string? PendingMessage;
+ public bool MessageIsDirty;
+ public bool IsVoiceChatSpeaking;
+ }
+}
\ No newline at end of file
diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Components/AvatarMouthInputComponent.cs.meta b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Components/AvatarMouthInputComponent.cs.meta
new file mode 100644
index 00000000000..b61498b43be
--- /dev/null
+++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Components/AvatarMouthInputComponent.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 68754f419783b9a4a8c72cda3bc21f51
\ No newline at end of file
diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Components/AvatarShapeComponent.cs b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Components/AvatarShapeComponent.cs
index 7c435ad2dd2..571a09d40a0 100644
--- a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Components/AvatarShapeComponent.cs
+++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Components/AvatarShapeComponent.cs
@@ -28,6 +28,11 @@ public struct AvatarShapeComponent
public bool ShowOnlyWearables;
+ // Per-channel atlas capability stamped by AvatarInstantiatorSystem from the worn facial-feature wearables.
+ public bool EyebrowsHasExpressionAtlas;
+ public bool EyesHasExpressionAtlas;
+ public bool MouthHasExpressionAtlas;
+
public AvatarShapeComponent(string name, string id, BodyShape bodyShape, WearablePromise wearablePromise,
Color skinColor, Color hairColor, Color eyesColor, bool showOnlyWearables = false)
{
@@ -45,6 +50,9 @@ public AvatarShapeComponent(string name, string id, BodyShape bodyShape, Wearabl
HiddenByModifierArea = false;
IsPreview = false;
ShowOnlyWearables = showOnlyWearables;
+ EyebrowsHasExpressionAtlas = false;
+ EyesHasExpressionAtlas = false;
+ MouthHasExpressionAtlas = false;
}
public void CreateOutlineCompatibilityList()
@@ -72,6 +80,9 @@ public AvatarShapeComponent(string name, string id) : this()
InstantiatedWearables = new List();
OutlineCompatibleRenderers = new List();
IsVisible = true;
+ EyebrowsHasExpressionAtlas = false;
+ EyesHasExpressionAtlas = false;
+ MouthHasExpressionAtlas = false;
}
}
}
diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Components/TriggerFacialExpressionIntent.cs b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Components/TriggerFacialExpressionIntent.cs
new file mode 100644
index 00000000000..7da6e6133e6
--- /dev/null
+++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Components/TriggerFacialExpressionIntent.cs
@@ -0,0 +1,15 @@
+namespace DCL.AvatarRendering.AvatarShape.Components
+{
+ ///
+ /// One-frame intent placed on the local player to apply a face pose. The shape mirrors the
+ /// network payload (3 atlas indices) so the wheel/shortcut writers don't need the
+ /// AvatarFaceExpressionDefinition table — they translate slot → indices upstream.
+ /// Consumed by .
+ ///
+ public struct TriggerFacialExpressionIntent
+ {
+ public byte EyebrowsIndex;
+ public byte EyesIndex;
+ public byte MouthIndex;
+ }
+}
\ No newline at end of file
diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Components/TriggerFacialExpressionIntent.cs.meta b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Components/TriggerFacialExpressionIntent.cs.meta
new file mode 100644
index 00000000000..442b200cf9d
--- /dev/null
+++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Components/TriggerFacialExpressionIntent.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 036fb31d787c86d42b9e3e98a73dfde2
\ No newline at end of file
diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/FacialExpression.meta b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/FacialExpression.meta
new file mode 100644
index 00000000000..0d448ba3b42
--- /dev/null
+++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/FacialExpression.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 3f53c2a9e5ca54f4a8390ec6b0150b20
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/FacialExpression/DCL.AvatarShape.FacialExpression.asmref b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/FacialExpression/DCL.AvatarShape.FacialExpression.asmref
new file mode 100644
index 00000000000..b24e4ef9e3d
--- /dev/null
+++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/FacialExpression/DCL.AvatarShape.FacialExpression.asmref
@@ -0,0 +1,3 @@
+{
+ "reference": "GUID:fc4fd35fb877e904d8cedee73b2256f6"
+}
\ No newline at end of file
diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/FacialExpression/DCL.AvatarShape.FacialExpression.asmref.meta b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/FacialExpression/DCL.AvatarShape.FacialExpression.asmref.meta
new file mode 100644
index 00000000000..7bc92274cea
--- /dev/null
+++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/FacialExpression/DCL.AvatarShape.FacialExpression.asmref.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 215d2bc374736a04797c1e8b34c90852
+AssemblyDefinitionReferenceImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/FacialExpression/FacialExpressionsWheelShortcutHandler.cs b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/FacialExpression/FacialExpressionsWheelShortcutHandler.cs
new file mode 100644
index 00000000000..f69f9de109a
--- /dev/null
+++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/FacialExpression/FacialExpressionsWheelShortcutHandler.cs
@@ -0,0 +1,75 @@
+using System;
+using UnityEngine;
+using Utility;
+using InputAction = UnityEngine.InputSystem.InputAction;
+
+namespace DCL.AvatarRendering.AvatarShape.FacialExpression
+{
+ public enum FacialExpressionTriggerSource
+ {
+ /// Click on a wheel slot, press [0-9], or use a cycler while the wheel is open.
+ WHEEL_SLOT,
+
+ /// Press Y+[0-9] while the wheel is closed.
+ SHORTCUT,
+ }
+
+ ///
+ /// Owns the Y key (facial expressions shortcut) release handling. Receives expression-played
+ /// notifications via ; when Y is released, applies
+ /// ignore-next-release and time-based lock, then publishes
+ /// .
+ ///
+ public class FacialExpressionsWheelShortcutHandler : IDisposable
+ {
+ private const float QUICK_APPLY_LOCK_TIME = 0.5f;
+
+ private readonly IEventBus eventBus;
+ private readonly DCLInput dclInput;
+
+ private bool ignoreNextRelease;
+ private float lockUntilTime;
+
+ public FacialExpressionsWheelShortcutHandler(IEventBus eventBus)
+ {
+ this.eventBus = eventBus;
+ dclInput = DCLInput.Instance;
+ dclInput.Shortcuts.FaceExpression.canceled += OnShortcutReleased;
+ }
+
+ public void Dispose() =>
+ dclInput.Shortcuts.FaceExpression.canceled -= OnShortcutReleased;
+
+ public void NotifyExpressionPlayed(FacialExpressionTriggerSource source)
+ {
+ switch (source)
+ {
+ case FacialExpressionTriggerSource.SHORTCUT:
+ ignoreNextRelease = true;
+ break;
+ case FacialExpressionTriggerSource.WHEEL_SLOT:
+ lockUntilTime = UnityEngine.Time.time + QUICK_APPLY_LOCK_TIME;
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(source), source, null);
+ }
+ }
+
+ private void OnShortcutReleased(InputAction.CallbackContext _)
+ {
+ if (ignoreNextRelease)
+ {
+ ignoreNextRelease = false;
+ return;
+ }
+
+ if (UnityEngine.Time.time < lockUntilTime)
+ {
+ lockUntilTime = 0f;
+ return;
+ }
+
+ eventBus.Publish(new RequestToggleFacialExpressionsWheelEvent());
+ }
+ }
+}
diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/FacialExpression/FacialExpressionsWheelShortcutHandler.cs.meta b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/FacialExpression/FacialExpressionsWheelShortcutHandler.cs.meta
new file mode 100644
index 00000000000..f9e949486cf
--- /dev/null
+++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/FacialExpression/FacialExpressionsWheelShortcutHandler.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: cd675c62e648e134986bf9cec8fb0068
\ No newline at end of file
diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/FacialExpressionApplier.cs b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/FacialExpressionApplier.cs
new file mode 100644
index 00000000000..b5084d0b172
--- /dev/null
+++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/FacialExpressionApplier.cs
@@ -0,0 +1,18 @@
+using Arch.Core;
+using DCL.AvatarRendering.AvatarShape.Components;
+
+namespace DCL.AvatarRendering.AvatarShape.FacialExpression
+{
+ public static class FacialExpressionApplier
+ {
+ public static void Apply(World world, Entity playerEntity, byte eyebrowsIndex, byte eyesIndex, byte mouthIndex)
+ {
+ world.AddOrGet(playerEntity, new TriggerFacialExpressionIntent
+ {
+ EyebrowsIndex = eyebrowsIndex,
+ EyesIndex = eyesIndex,
+ MouthIndex = mouthIndex,
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/FacialExpressionApplier.cs.meta b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/FacialExpressionApplier.cs.meta
new file mode 100644
index 00000000000..11a8186c5e3
--- /dev/null
+++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/FacialExpressionApplier.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 98b4b0b30cd532f49a1941398015bbe6
\ No newline at end of file
diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Helpers/AvatarInstantiationPolymorphicBehaviour.cs b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Helpers/AvatarInstantiationPolymorphicBehaviour.cs
index a8bb4719083..70ccb6e0a29 100644
--- a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Helpers/AvatarInstantiationPolymorphicBehaviour.cs
+++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Helpers/AvatarInstantiationPolymorphicBehaviour.cs
@@ -37,7 +37,8 @@ public static class AvatarInstantiationPolymorphicBehaviour
case WearableType.FacialFeature:
var texturesSet = facialFeaturesTextures.Value[category];
- texturesSet[TextureArrayConstants.MAINTEX_ORIGINAL_TEXTURE] = ((AttachmentTextureAsset)mainAsset).Texture;
+ var mainTex = ((AttachmentTextureAsset)mainAsset).Texture;
+ texturesSet[TextureArrayConstants.MAINTEX_ORIGINAL_TEXTURE] = mainTex;
// Mask is optional
var maskAssetRes = originalAssets[WearablePolymorphicBehaviour.MASK_ASSET_INDEX];
@@ -45,6 +46,10 @@ public static class AvatarInstantiationPolymorphicBehaviour
if (maskAssetRes is { Asset: not null })
texturesSet[TextureArrayConstants.MASK_ORIGINAL_TEXTURE_ID] = ((AttachmentTextureAsset)maskAssetRes.Value.Asset!).Texture;
+ // WearablePromise.Result may be gone by the time the face system runs; stamp atlas capability here.
+ if (resultWearable.HasFacialExpressionsTexture)
+ facialFeaturesTextures.MarkHasExpressions(category);
+
return null;
default:
{
diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Rendering/TextureArray/TextureArrayConstants.cs b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Rendering/TextureArray/TextureArrayConstants.cs
index fc16cc19c1d..67911ce8bac 100644
--- a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Rendering/TextureArray/TextureArrayConstants.cs
+++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Rendering/TextureArray/TextureArrayConstants.cs
@@ -40,6 +40,7 @@ public static class TextureArrayConstants
public static readonly int MAINTEX_ORIGINAL_TEXTURE = WearableTextureConstants.MAINTEX_ORIGINAL_TEXTURE;
public static readonly int MAINTEX_ARR_SHADER_INDEX = Shader.PropertyToID("_MainTexArr_ID");
public static readonly int MAINTEX_ARR_TEX_SHADER = Shader.PropertyToID("_MainTexArr");
+ public static readonly int EXPRESSION_INDEX_SHADER = Shader.PropertyToID("_ExpressionIndex");
public static readonly int BASE_MAP_ORIGINAL_TEXTURE = Shader.PropertyToID("_BaseMap");
public static readonly int BASE_MAP_TEX_ARR_INDEX = Shader.PropertyToID("_BaseMapArr_ID");
diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/RequestToggleFacialExpressionsWheelEvent.cs b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/RequestToggleFacialExpressionsWheelEvent.cs
new file mode 100644
index 00000000000..308d0abff0c
--- /dev/null
+++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/RequestToggleFacialExpressionsWheelEvent.cs
@@ -0,0 +1,9 @@
+namespace DCL.AvatarRendering.AvatarShape.FacialExpression
+{
+ ///
+ /// Published on the generic event bus when requesting to toggle the facial expressions wheel.
+ ///
+ public struct RequestToggleFacialExpressionsWheelEvent
+ {
+ }
+}
\ No newline at end of file
diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/RequestToggleFacialExpressionsWheelEvent.cs.meta b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/RequestToggleFacialExpressionsWheelEvent.cs.meta
new file mode 100644
index 00000000000..5df6b498062
--- /dev/null
+++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/RequestToggleFacialExpressionsWheelEvent.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: d227284a5d4b06b45bf3ad25a32752f7
\ No newline at end of file
diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/AvatarInstantiatorSystem.cs b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/AvatarInstantiatorSystem.cs
index ed165f9b23e..1514dfe23d9 100644
--- a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/AvatarInstantiatorSystem.cs
+++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/AvatarInstantiatorSystem.cs
@@ -19,6 +19,7 @@
using ECS.Abstract;
using ECS.LifeCycle.Components;
using ECS.StreamableLoading.Common;
+using Runtime.Wearables;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Pool;
@@ -253,6 +254,11 @@ private AvatarCustomSkinningComponent InstantiateAvatar(ref AvatarShapeComponent
bodyShape = instance;
}
+ // Freeze per-channel atlas capability for AvatarFacialExpressionSystem.
+ avatarShapeComponent.EyebrowsHasExpressionAtlas = facialFeatureTextures.HasExpressions(WearableCategories.Categories.EYEBROWS);
+ avatarShapeComponent.EyesHasExpressionAtlas = facialFeatureTextures.HasExpressions(WearableCategories.Categories.EYES);
+ avatarShapeComponent.MouthHasExpressionAtlas = facialFeatureTextures.HasExpressions(WearableCategories.Categories.MOUTH);
+
WearableComponentsUtils.HideBodyShape(bodyShape, wearablesToHide, usedCategories);
HashSetPool.Release(usedCategories);
diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/FacialExpression.meta b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/FacialExpression.meta
new file mode 100644
index 00000000000..d86abbf56c3
--- /dev/null
+++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/FacialExpression.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 91abfaa82595cb24bb956be994dd19a7
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/FacialExpression/ApplyFacialExpressionIntentSystem.cs b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/FacialExpression/ApplyFacialExpressionIntentSystem.cs
new file mode 100644
index 00000000000..d08aaf8b59c
--- /dev/null
+++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/FacialExpression/ApplyFacialExpressionIntentSystem.cs
@@ -0,0 +1,67 @@
+using Arch.Core;
+using Arch.System;
+using Arch.SystemGroups;
+using DCL.AvatarRendering.AvatarShape.Components;
+using DCL.Character.Components;
+using DCL.Diagnostics;
+using DCL.Input;
+using DCL.Multiplayer.FacialExpression;
+using ECS.Abstract;
+using ECS.LifeCycle.Components;
+
+namespace DCL.AvatarRendering.AvatarShape
+{
+ ///
+ /// Consumes on the local player and applies the
+ /// three atlas indices to (drives rendering) and
+ /// (drives the network send). The system
+ /// never sees the expression table — callers (wheel controller, keyboard adapter) translate
+ /// slot → indices upstream so the data shape mirrors the rfc4 payload.
+ ///
+ [UpdateInGroup(typeof(InputGroup))]
+ [LogCategory(ReportCategory.AVATAR)]
+ public partial class ApplyFacialExpressionIntentSystem : BaseUnityLoopSystem
+ {
+ internal ApplyFacialExpressionIntentSystem(World world) : base(world) { }
+
+ protected override void Update(float t)
+ {
+ SetupLocalPlayerFacialExpressionQuery(World);
+ ApplyIntentQuery(World);
+ }
+
+ ///
+ /// Adds to the player entity as soon as
+ /// is available. Runs at most once per player thanks to
+ /// the [None] filter.
+ ///
+ [Query]
+ [All(typeof(PlayerComponent), typeof(AvatarFaceComponent))]
+ [None(typeof(LocalPlayerFacialExpressionComponent), typeof(DeleteEntityIntention))]
+ private void SetupLocalPlayerFacialExpression(Entity entity)
+ {
+ World.Add(entity, new LocalPlayerFacialExpressionComponent());
+ }
+
+ [Query]
+ [All(typeof(PlayerComponent), typeof(AvatarFaceComponent))]
+ [None(typeof(DeleteEntityIntention))]
+ private void ApplyIntent(
+ Entity entity,
+ ref AvatarFaceComponent face,
+ ref LocalPlayerFacialExpressionComponent local,
+ ref TriggerFacialExpressionIntent intent)
+ {
+ face.EyebrowsExpressionIndex = intent.EyebrowsIndex;
+ face.EyesExpressionIndex = intent.EyesIndex;
+ face.MouthExpressionIndex = intent.MouthIndex;
+ face.IsDirty = true;
+
+ local.EyebrowsIndex = intent.EyebrowsIndex;
+ local.EyesIndex = intent.EyesIndex;
+ local.MouthIndex = intent.MouthIndex;
+
+ World.Remove(entity);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/FacialExpression/ApplyFacialExpressionIntentSystem.cs.meta b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/FacialExpression/ApplyFacialExpressionIntentSystem.cs.meta
new file mode 100644
index 00000000000..eb424ac0a55
--- /dev/null
+++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/FacialExpression/ApplyFacialExpressionIntentSystem.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 12507785487b2ac468a722ff3ccf78e4
\ No newline at end of file
diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/FacialExpression/AvatarFacialExpressionSystem.cs b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/FacialExpression/AvatarFacialExpressionSystem.cs
new file mode 100644
index 00000000000..0f4265d6ced
--- /dev/null
+++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/FacialExpression/AvatarFacialExpressionSystem.cs
@@ -0,0 +1,272 @@
+using Arch.Core;
+using Arch.System;
+using Arch.SystemGroups;
+using DCL.AvatarRendering.AvatarShape.Assets;
+using DCL.AvatarRendering.AvatarShape.Components;
+using ECS.Abstract;
+using ECS.LifeCycle.Components;
+using UnityEngine;
+
+namespace DCL.AvatarRendering.AvatarShape
+{
+ ///
+ /// Drives 2D facial animation for instantiated avatars: eyebrow expression (base layer),
+ /// blink (overrides eyes temporarily), and mouth pose (overrides mouth temporarily).
+ /// Pause characters in animated text restore the expression mouth instead of an articulated pose.
+ /// Material binding is delegated to and
+ /// all atlas / pose constants live in .
+ ///
+ [UpdateInGroup(typeof(AvatarGroup))]
+ [UpdateAfter(typeof(AvatarInstantiatorSystem))]
+ public partial class AvatarFacialExpressionSystem : BaseUnityLoopSystem
+ {
+ private readonly AvatarFaceAnimationSettings settings;
+
+ internal AvatarFacialExpressionSystem(
+ World world,
+ AvatarFaceAnimationSettings settings) : base(world)
+ {
+ this.settings = settings;
+ }
+
+ protected override void Update(float t)
+ {
+ SetupFaceComponentsQuery(World);
+ UpdateFaceQuery(World, t);
+ }
+
+ // ─── Setup ────────────────────────────────────────────────────────────
+
+ ///
+ /// Adds + together
+ /// so external services (chat / voice) can write inputs without checking the face
+ /// component is present, and so the per-frame query can require both.
+ ///
+ [Query]
+ [All(typeof(AvatarCustomSkinningComponent))]
+ [None(typeof(AvatarFaceComponent), typeof(DeleteEntityIntention))]
+ private void SetupFaceComponents(in Entity entity, ref AvatarShapeComponent avatarShape)
+ {
+ var face = new AvatarFaceComponent
+ {
+ EyebrowsRenderer = AvatarFaceMaterialUtils.FindRendererWithSuffix(in avatarShape, "Mask_Eyebrows"),
+ EyeRenderer = AvatarFaceMaterialUtils.FindRendererWithSuffix(in avatarShape, "Mask_Eyes"),
+ MouthRenderer = AvatarFaceMaterialUtils.FindRendererWithSuffix(in avatarShape, "Mask_Mouth"),
+ EyebrowsHasExpressionAtlas = avatarShape.EyebrowsHasExpressionAtlas,
+ EyesHasExpressionAtlas = avatarShape.EyesHasExpressionAtlas,
+ MouthHasExpressionAtlas = avatarShape.MouthHasExpressionAtlas,
+ EyebrowsExpressionIndex = avatarShape.EyebrowsHasExpressionAtlas ? 0 : AvatarFacialExpressionConstants.NO_EYEBROWS_OVERRIDE,
+ EyesExpressionIndex = avatarShape.EyesHasExpressionAtlas ? 0 : AvatarFacialExpressionConstants.NO_EYE_OVERRIDE,
+ MouthExpressionIndex = avatarShape.MouthHasExpressionAtlas ? 0 : AvatarFacialExpressionConstants.NO_MOUTH_POSE,
+ CurrentEyebrowsIndex = AvatarFacialExpressionConstants.NO_EYEBROWS_OVERRIDE,
+ CurrentEyeIndex = AvatarFacialExpressionConstants.NO_EYE_OVERRIDE,
+ CurrentMouthPoseIndex = AvatarFacialExpressionConstants.NO_MOUTH_POSE,
+ NextBlinkTime = Random.Range(settings.MinBlinkInterval, settings.MaxBlinkInterval),
+ IsDirty = true,
+ };
+
+ World.Add(entity, face, new AvatarMouthInputComponent());
+ }
+
+ // ─── Per-frame update ─────────────────────────────────────────────────
+
+ [Query]
+ [None(typeof(DeleteEntityIntention))]
+ private void UpdateFace(
+ [Data] float t,
+ ref AvatarFaceComponent face,
+ ref AvatarMouthInputComponent mouthInput,
+ ref AvatarShapeComponent avatarShape)
+ {
+ ReInitRenderersIfNeeded(ref face, in avatarShape);
+
+ if (face.IsDirty)
+ ApplyExpressionLayer(ref face);
+
+ bool visible = avatarShape.IsVisible;
+
+ // Blink and mouth animation drive atlas slice overrides via MaterialPropertyBlock.
+ // Skip whole-channel when the wearable cannot atlas-slice, leaving the static face.
+ if (face.EyesHasExpressionAtlas && face.EyeRenderer != null)
+ StepBlink(t, ref face, visible);
+
+ if (face.MouthHasExpressionAtlas && face.MouthRenderer != null)
+ StepMouthAnimation(t, ref face, ref mouthInput, visible);
+ }
+
+ // A wearable swap can return the same renderer instance from the cache but with a
+ // freshly-pooled skinning material whose _ExpressionIndex sits at the shader-default
+ // sentinel. Diff both the renderer ref AND its sharedMaterial against the cached pair so
+ // we don't miss that case; on any mismatch (or a capability bool change) reset Current*
+ // and the resting index, then flag IsDirty so ApplyExpressionLayer pushes the correct value.
+ private void ReInitRenderersIfNeeded(ref AvatarFaceComponent face, in AvatarShapeComponent avatarShape)
+ {
+ Renderer? eyebrows = AvatarFaceMaterialUtils.FindRendererWithSuffix(in avatarShape, "Mask_Eyebrows");
+ Renderer? eyes = AvatarFaceMaterialUtils.FindRendererWithSuffix(in avatarShape, "Mask_Eyes");
+ Renderer? mouth = AvatarFaceMaterialUtils.FindRendererWithSuffix(in avatarShape, "Mask_Mouth");
+
+ Material? eyebrowsMat = eyebrows != null ? eyebrows.sharedMaterial : null;
+ Material? eyesMat = eyes != null ? eyes.sharedMaterial : null;
+ Material? mouthMat = mouth != null ? mouth.sharedMaterial : null;
+
+ bool rebuilt = false;
+
+ if (eyebrows != null && (face.EyebrowsRenderer != eyebrows || face.EyebrowsMaterial != eyebrowsMat || face.EyebrowsHasExpressionAtlas != avatarShape.EyebrowsHasExpressionAtlas))
+ {
+ face.EyebrowsRenderer = eyebrows;
+ face.EyebrowsMaterial = eyebrowsMat;
+ face.EyebrowsHasExpressionAtlas = avatarShape.EyebrowsHasExpressionAtlas;
+ face.EyebrowsExpressionIndex = avatarShape.EyebrowsHasExpressionAtlas ? 0 : AvatarFacialExpressionConstants.NO_EYEBROWS_OVERRIDE;
+ face.CurrentEyebrowsIndex = AvatarFacialExpressionConstants.NO_EYEBROWS_OVERRIDE;
+ rebuilt = true;
+ }
+
+ if (eyes != null && (face.EyeRenderer != eyes || face.EyeMaterial != eyesMat || face.EyesHasExpressionAtlas != avatarShape.EyesHasExpressionAtlas))
+ {
+ face.EyeRenderer = eyes;
+ face.EyeMaterial = eyesMat;
+ face.EyesHasExpressionAtlas = avatarShape.EyesHasExpressionAtlas;
+ face.EyesExpressionIndex = avatarShape.EyesHasExpressionAtlas ? 0 : AvatarFacialExpressionConstants.NO_EYE_OVERRIDE;
+ face.CurrentEyeIndex = AvatarFacialExpressionConstants.NO_EYE_OVERRIDE;
+ face.IsBlinking = false;
+ face.BlinkFrameIndex = 0;
+ face.BlinkFrameTimer = 0f;
+ face.TimeSinceLastBlink = 0f;
+ face.NextBlinkTime = Random.Range(settings.MinBlinkInterval, settings.MaxBlinkInterval);
+ rebuilt = true;
+ }
+
+ if (mouth != null && (face.MouthRenderer != mouth || face.MouthMaterial != mouthMat || face.MouthHasExpressionAtlas != avatarShape.MouthHasExpressionAtlas))
+ {
+ face.MouthRenderer = mouth;
+ face.MouthMaterial = mouthMat;
+ face.MouthHasExpressionAtlas = avatarShape.MouthHasExpressionAtlas;
+ face.MouthExpressionIndex = avatarShape.MouthHasExpressionAtlas ? 0 : AvatarFacialExpressionConstants.NO_MOUTH_POSE;
+ face.CurrentMouthPoseIndex = AvatarFacialExpressionConstants.NO_MOUTH_POSE;
+ face.AnimatingText = null;
+ face.CharacterIndex = 0;
+ face.CharacterTimer = 0f;
+ rebuilt = true;
+ }
+
+ if (rebuilt)
+ face.IsDirty = true;
+ }
+
+ ///
+ /// Pushes the expression layer to renderers. Eyebrows always apply; eyes/mouth apply
+ /// only when no override layer is currently animating — the override layer will
+ /// restore to the new resting indices when it ends.
+ ///
+ private void ApplyExpressionLayer(ref AvatarFaceComponent face)
+ {
+ face.IsDirty = false;
+
+ // Channels whose wearable lacks an `*_expressions.png` atlas keep the wearable's static
+ // texture: skip the MaterialPropertyBlock override so we don't paint global atlas slices
+ // onto a single-frame face texture.
+ if (face.EyebrowsHasExpressionAtlas)
+ AvatarFaceMaterialUtils.ApplyEyebrowsFrame(ref face, face.EyebrowsExpressionIndex);
+
+ if (face.EyesHasExpressionAtlas && !face.IsBlinking)
+ AvatarFaceMaterialUtils.ApplyEyeFrame(ref face, face.EyesExpressionIndex);
+
+ if (face.MouthHasExpressionAtlas && face.AnimatingText == null)
+ AvatarFaceMaterialUtils.ApplyMouthPose(ref face, face.MouthExpressionIndex);
+ }
+
+ // ─── Blink layer ──────────────────────────────────────────────────────
+
+ private void StepBlink(float t, ref AvatarFaceComponent face, bool isVisible)
+ {
+ if (!isVisible || !face.EyeRenderer.enabled)
+ {
+ if (face.IsBlinking)
+ AvatarFaceMaterialUtils.EndBlink(ref face, settings.MinBlinkInterval, settings.MaxBlinkInterval);
+
+ return;
+ }
+
+ if (face.IsBlinking)
+ {
+ face.BlinkFrameTimer += t;
+
+ if (face.BlinkFrameTimer < settings.BlinkFrameDuration) return;
+
+ face.BlinkFrameTimer = 0f;
+ face.BlinkFrameIndex++;
+
+ if (face.BlinkFrameIndex >= AvatarFacialExpressionConstants.BLINK_SEQUENCE.Length)
+ AvatarFaceMaterialUtils.EndBlink(ref face, settings.MinBlinkInterval, settings.MaxBlinkInterval);
+ else
+ AvatarFaceMaterialUtils.ApplyEyeFrame(ref face, AvatarFacialExpressionConstants.BLINK_SEQUENCE[face.BlinkFrameIndex]);
+ }
+ else
+ {
+ face.TimeSinceLastBlink += t;
+
+ if (face.TimeSinceLastBlink >= face.NextBlinkTime)
+ AvatarFaceMaterialUtils.StartBlink(ref face);
+ }
+ }
+
+ // ─── Mouth animation layer ────────────────────────────────────────────
+
+ private void StepMouthAnimation(float t, ref AvatarFaceComponent face, ref AvatarMouthInputComponent input, bool isVisible)
+ {
+ if (!isVisible || !face.MouthRenderer.enabled)
+ {
+ AvatarFaceMaterialUtils.StopMouthAnimation(ref face);
+ return;
+ }
+
+ // Chat message overrides the voice-chat loop; consume it.
+ if (input.MessageIsDirty)
+ {
+ face.AnimatingText = input.PendingMessage;
+ face.CharacterIndex = 0;
+ face.CharacterTimer = 0f;
+ input.MessageIsDirty = false;
+ }
+
+ bool isSpeaking = input.IsVoiceChatSpeaking;
+
+ if (face.AnimatingText != null && face.CharacterIndex < face.AnimatingText.Length)
+ {
+ // Stop the voice loop if speaking ended mid-animation.
+ if (!isSpeaking && face.AnimatingText == AvatarFacialExpressionConstants.VOICE_CHAT_LOOP_TEXT)
+ {
+ AvatarFaceMaterialUtils.StopMouthAnimation(ref face);
+ return;
+ }
+
+ int mouthPose = AvatarFacialExpressionConstants.MapCharToMouthPose(face.AnimatingText, face.CharacterIndex);
+ AvatarFaceMaterialUtils.ApplyMouthPose(
+ ref face,
+ mouthPose == AvatarFacialExpressionConstants.NO_MOUTH_POSE ? face.MouthExpressionIndex : mouthPose);
+
+ face.CharacterTimer += t;
+
+ char currentChar = face.AnimatingText[face.CharacterIndex];
+ float baseDuration = AvatarFacialExpressionConstants.IsVowel(currentChar) ? settings.VowelMouthPoseDuration : settings.MouthPoseDuration;
+ float duration = char.IsUpper(currentChar) ? baseDuration * AvatarFacialExpressionConstants.UPPERCASE_DURATION_MULTIPLIER : baseDuration;
+
+ if (face.CharacterTimer >= duration)
+ {
+ face.CharacterTimer = 0f;
+ face.CharacterIndex++;
+ }
+ }
+ else if (isSpeaking)
+ {
+ face.AnimatingText = AvatarFacialExpressionConstants.VOICE_CHAT_LOOP_TEXT;
+ face.CharacterIndex = 0;
+ face.CharacterTimer = 0f;
+ }
+ else
+ {
+ AvatarFaceMaterialUtils.StopMouthAnimation(ref face);
+ }
+ }
+ }
+}
diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/FacialExpression/AvatarFacialExpressionSystem.cs.meta b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/FacialExpression/AvatarFacialExpressionSystem.cs.meta
new file mode 100644
index 00000000000..0eac688e196
--- /dev/null
+++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/FacialExpression/AvatarFacialExpressionSystem.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: f6950c2e6be708d47a0ab6069f08f507
\ No newline at end of file
diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/FacialExpression/MouthAnimationSystem.cs b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/FacialExpression/MouthAnimationSystem.cs
new file mode 100644
index 00000000000..97c689e4298
--- /dev/null
+++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/FacialExpression/MouthAnimationSystem.cs
@@ -0,0 +1,220 @@
+using Arch.Core;
+using Arch.System;
+using Arch.SystemGroups;
+using DCL.AvatarRendering.AvatarShape.Components;
+using DCL.Character.Components;
+using DCL.Chat.History;
+using DCL.Diagnostics;
+using DCL.Input;
+using DCL.LiveKit.Public;
+using DCL.Utilities;
+using DCL.VoiceChat;
+using ECS.Abstract;
+using ECS.LifeCycle.Components;
+using LiveKit.Rooms;
+using LiveKit.Rooms.Participants;
+using System;
+using System.Collections.Generic;
+
+namespace DCL.AvatarRendering.AvatarShape
+{
+ ///
+ /// Single source of mouth-animation input. Listens to chat (text) and voice-chat (LiveKit
+ /// active speakers + call status) and writes on the
+ /// matching avatar entity. Per ADR-317 lip-sync is local-only — events are interpreted by
+ /// each client and never propagated.
+ ///
+ /// Per-participant coalescing: many events for the same wallet within one frame collapse
+ /// to a single dict entry; remote application happens through a single query that drains
+ /// the dict by . Chat takes priority over voice —
+ /// handled downstream by AvatarFacialExpressionSystem.StepMouthAnimation which
+ /// consumes first; the voice loop
+ /// only resumes when no chat text is animating.
+ ///
+ ///
+ [UpdateInGroup(typeof(InputGroup))]
+ [LogCategory(ReportCategory.AVATAR)]
+ public partial class MouthAnimationSystem : BaseUnityLoopSystem
+ {
+ public struct PendingInput
+ {
+ public string? Message; // null when no chat update happened this frame
+ public bool? IsSpeaking; // null when no speaking change happened this frame
+ }
+
+ private readonly IChatHistory chatHistory;
+ private readonly IRoom voiceChatRoom;
+ private readonly IDisposable statusSubscription;
+
+ private PendingInput pendingSelf;
+ private readonly Dictionary pendingByWallet = new ();
+
+ private readonly HashSet activeSpeakers = new ();
+ private readonly HashSet nextActiveSpeakers = new ();
+
+ internal MouthAnimationSystem(
+ World world,
+ IChatHistory chatHistory,
+ IRoom voiceChatRoom,
+ IVoiceChatOrchestratorState voiceChatOrchestratorState) : base(world)
+ {
+ this.chatHistory = chatHistory;
+ this.voiceChatRoom = voiceChatRoom;
+
+ chatHistory.MessageAdded += OnChatMessageAdded;
+ voiceChatRoom.ActiveSpeakers.Updated += OnActiveSpeakersUpdated;
+ voiceChatRoom.Participants.UpdatesFromParticipant += OnParticipantUpdated;
+ statusSubscription = voiceChatOrchestratorState.CurrentCallStatus.Subscribe(OnCallStatusChanged);
+ }
+
+ protected override void OnDispose()
+ {
+ chatHistory.MessageAdded -= OnChatMessageAdded;
+ voiceChatRoom.ActiveSpeakers.Updated -= OnActiveSpeakersUpdated;
+ voiceChatRoom.Participants.UpdatesFromParticipant -= OnParticipantUpdated;
+ statusSubscription?.Dispose();
+ }
+
+ protected override void Update(float t)
+ {
+ if (pendingSelf.Message != null || pendingSelf.IsSpeaking.HasValue)
+ {
+ ApplyToSelfQuery(World, pendingSelf);
+ pendingSelf = default;
+ }
+
+ if (pendingByWallet.Count > 0)
+ ApplyToRemoteQuery(World);
+ }
+
+ [Query]
+ [All(typeof(PlayerComponent))]
+ private void ApplyToSelf([Data] PendingInput pending, ref AvatarMouthInputComponent input)
+ {
+ ApplyPending(pending, ref input);
+ }
+
+ ///
+ /// Iterates remote avatar entities and consumes their pending input from
+ /// by . Entries with
+ /// no matching avatar entity stay in the dict and are retried next frame (covers the
+ /// race where chat/voice events arrive before the avatar is instantiated).
+ ///
+ [Query]
+ [All(typeof(AvatarMouthInputComponent), typeof(AvatarShapeComponent))]
+ [None(typeof(PlayerComponent), typeof(DeleteEntityIntention))]
+ private void ApplyToRemote(in AvatarShapeComponent avatarShape, ref AvatarMouthInputComponent input)
+ {
+ if (string.IsNullOrEmpty(avatarShape.ID)) return;
+
+ if (!pendingByWallet.Remove(avatarShape.ID, out PendingInput pending))
+ return;
+
+ ApplyPending(pending, ref input);
+ }
+
+ private static void ApplyPending(PendingInput pending, ref AvatarMouthInputComponent input)
+ {
+ if (pending.Message != null)
+ {
+ input.PendingMessage = pending.Message;
+ input.MessageIsDirty = true;
+ }
+
+ if (pending.IsSpeaking.HasValue)
+ input.IsVoiceChatSpeaking = pending.IsSpeaking.Value;
+ }
+
+ private void OnChatMessageAdded(ChatChannel _, ChatMessage msg, int __)
+ {
+ if (msg.IsSystemMessage) return;
+
+ if (msg.IsSentByOwnUser)
+ {
+ pendingSelf.Message = msg.Message;
+ return;
+ }
+
+ if (pendingByWallet.TryGetValue(msg.SenderWalletAddress, out PendingInput existing))
+ {
+ existing.Message = msg.Message;
+ pendingByWallet[msg.SenderWalletAddress] = existing;
+ }
+ else
+ pendingByWallet[msg.SenderWalletAddress] = new PendingInput { Message = msg.Message };
+ }
+
+ private void StashSpeaking(string walletId, bool isSpeaking, bool isSelf)
+ {
+ if (isSelf)
+ {
+ pendingSelf.IsSpeaking = isSpeaking;
+ return;
+ }
+
+ if (pendingByWallet.TryGetValue(walletId, out PendingInput existing))
+ {
+ existing.IsSpeaking = isSpeaking;
+ pendingByWallet[walletId] = existing;
+ }
+ else
+ pendingByWallet[walletId] = new PendingInput { IsSpeaking = isSpeaking };
+ }
+
+ private void OnActiveSpeakersUpdated()
+ {
+ nextActiveSpeakers.Clear();
+
+ foreach (string speakerId in voiceChatRoom.ActiveSpeakers)
+ {
+ nextActiveSpeakers.Add(speakerId);
+
+ if (!activeSpeakers.Contains(speakerId))
+ EnqueueSpeaking(speakerId, true);
+ }
+
+ foreach (string oldSpeakerId in activeSpeakers)
+ if (!nextActiveSpeakers.Contains(oldSpeakerId))
+ EnqueueSpeaking(oldSpeakerId, false);
+
+ activeSpeakers.Clear();
+ activeSpeakers.UnionWith(nextActiveSpeakers);
+
+ void EnqueueSpeaking(string participantId, bool isSpeaking)
+ {
+ bool isSelf = voiceChatRoom.Participants.LocalParticipant()?.Identity == participantId;
+ StashSpeaking(participantId, isSpeaking, isSelf);
+ }
+ }
+
+ private void OnParticipantUpdated(LKParticipant participant, UpdateFromParticipant update)
+ {
+ if (update != UpdateFromParticipant.Disconnected) return;
+
+ StashSpeaking(participant.Identity, false, isSelf: false);
+ activeSpeakers.Remove(participant.Identity);
+ }
+
+ private void OnCallStatusChanged(VoiceChatStatus status)
+ {
+ switch (status)
+ {
+ case VoiceChatStatus.VOICE_CHAT_IN_CALL:
+ pendingSelf.IsSpeaking = false;
+ OnActiveSpeakersUpdated();
+ break;
+
+ case VoiceChatStatus.VOICE_CHAT_ENDING_CALL:
+ case VoiceChatStatus.DISCONNECTED:
+ case VoiceChatStatus.VOICE_CHAT_GENERIC_ERROR:
+ pendingSelf.IsSpeaking = false;
+ activeSpeakers.Clear();
+
+ foreach ((string participantId, _) in voiceChatRoom.Participants.RemoteParticipantIdentities())
+ StashSpeaking(participantId, false, isSelf: false);
+
+ break;
+ }
+ }
+ }
+}
diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/FacialExpression/MouthAnimationSystem.cs.meta b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/FacialExpression/MouthAnimationSystem.cs.meta
new file mode 100644
index 00000000000..dd7ca7f4ea0
--- /dev/null
+++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/FacialExpression/MouthAnimationSystem.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: c8f3a4abcb44e2e4aac6a4cbae534626
\ No newline at end of file
diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/FacialExpression/RemoteFacialExpressionSystem.cs b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/FacialExpression/RemoteFacialExpressionSystem.cs
new file mode 100644
index 00000000000..3963df8f234
--- /dev/null
+++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/FacialExpression/RemoteFacialExpressionSystem.cs
@@ -0,0 +1,74 @@
+using Arch.Core;
+using Arch.SystemGroups;
+using Arch.SystemGroups.DefaultSystemGroups;
+using DCL.AvatarRendering.AvatarShape.Components;
+using DCL.Diagnostics;
+using DCL.Multiplayer.FacialExpression;
+using DCL.Multiplayer.Movement.Systems;
+using DCL.Multiplayer.Profiles.Tables;
+using ECS.Abstract;
+using System.Collections.Generic;
+
+namespace DCL.AvatarRendering.AvatarShape
+{
+ ///
+ /// Drains intentions and applies them to the matching
+ /// remote avatar's . When the participant entity isn't
+ /// known yet (avatar still loading), the intention is requeued for the next frame.
+ /// Per ADR-317 face state is last-write-wins; no timestamp/order check.
+ ///
+ ///
+ /// TODO/WARNING — pending architect review. This system bypasses ECS and resolves the target
+ /// entity by walletId via instead of running a
+ /// query. Same pattern as RemoteEmotesSystem; performant but at odds with the
+ /// "systems are the sole entity manipulation entry point via queries" principle. Validate the
+ /// approach (or rework as a query-driven system) before merging.
+ ///
+ [UpdateInGroup(typeof(PresentationSystemGroup))]
+ [UpdateAfter(typeof(RemotePlayersMovementSystem))]
+ [LogCategory(ReportCategory.MULTIPLAYER_MOVEMENT)]
+ public partial class RemoteFacialExpressionSystem : BaseUnityLoopSystem
+ {
+ private readonly IReadOnlyEntityParticipantTable entityParticipantTable;
+ private readonly IFacialExpressionMessageBus bus;
+ private readonly List drained = new ();
+
+ internal RemoteFacialExpressionSystem(World world, IReadOnlyEntityParticipantTable entityParticipantTable, IFacialExpressionMessageBus bus) : base(world)
+ {
+ this.entityParticipantTable = entityParticipantTable;
+ this.bus = bus;
+ }
+
+ protected override void Update(float t)
+ {
+ bus.Drain(drained);
+
+ if (drained.Count == 0)
+ return;
+
+ foreach (RemoteFacialExpressionIntention intention in drained)
+ {
+ if (!entityParticipantTable.TryGet(intention.WalletId, out IReadOnlyEntityParticipantTable.Entry entry))
+ {
+ bus.SaveForRetry(intention);
+ continue;
+ }
+
+ ref AvatarFaceComponent face = ref World.TryGetRef(entry.Entity, out bool exists);
+
+ if (!exists)
+ {
+ bus.SaveForRetry(intention);
+ continue;
+ }
+
+ face.EyebrowsExpressionIndex = intention.EyebrowsIndex;
+ face.EyesExpressionIndex = intention.EyesIndex;
+ face.MouthExpressionIndex = intention.MouthIndex;
+ face.IsDirty = true;
+ }
+
+ drained.Clear();
+ }
+ }
+}
diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/FacialExpression/RemoteFacialExpressionSystem.cs.meta b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/FacialExpression/RemoteFacialExpressionSystem.cs.meta
new file mode 100644
index 00000000000..f92b84cff8b
--- /dev/null
+++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/FacialExpression/RemoteFacialExpressionSystem.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 19962c707cae4184ba57b5bf7e05baef
\ No newline at end of file
diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/FacialExpression/UpdateFacialExpressionInputSystem.cs b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/FacialExpression/UpdateFacialExpressionInputSystem.cs
new file mode 100644
index 00000000000..f0829091004
--- /dev/null
+++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/FacialExpression/UpdateFacialExpressionInputSystem.cs
@@ -0,0 +1,111 @@
+using Arch.Core;
+using Arch.System;
+using Arch.SystemGroups;
+using DCL.AvatarRendering.AvatarShape.Components;
+using DCL.AvatarRendering.AvatarShape.FacialExpression;
+using DCL.Character.Components;
+using DCL.Diagnostics;
+using DCL.FacialExpressionsWheel;
+using DCL.Input;
+using ECS.Abstract;
+using ECS.LifeCycle.Components;
+using System;
+using System.Collections.Generic;
+using UnityEngine.InputSystem;
+using InputAction = UnityEngine.InputSystem.InputAction;
+
+namespace DCL.AvatarRendering.AvatarShape
+{
+ ///
+ /// Listens to the FaceExpressions input map (Y+[0-9]) and writes a
+ /// on the local player. Notifies
+ /// so the upcoming Y release
+ /// doesn't toggle the wheel.
+ ///
+ [LogCategory(ReportCategory.AVATAR)]
+ [UpdateInGroup(typeof(InputGroup))]
+ public partial class UpdateFacialExpressionInputSystem : BaseUnityLoopSystem
+ {
+ private const int SLOT_COUNT = 10;
+
+ private readonly AvatarFaceExpressionConfig config;
+ private readonly FacialExpressionsWheelShortcutHandler shortcutHandler;
+ private readonly DCLInput.FaceExpressionsActions actions;
+ private readonly Dictionary slotByActionName = new ();
+
+ private int triggeredSlot = -1;
+
+ internal UpdateFacialExpressionInputSystem(
+ World world,
+ AvatarFaceExpressionConfig config,
+ FacialExpressionsWheelShortcutHandler shortcutHandler) : base(world)
+ {
+ this.config = config;
+ this.shortcutHandler = shortcutHandler;
+ actions = DCLInput.Instance.FaceExpressions;
+ ListenToSlotsInput(actions.Get());
+ }
+
+ protected override void OnDispose()
+ {
+ UnregisterSlotsInput(actions.Get());
+ }
+
+ protected override void Update(float t)
+ {
+ if (triggeredSlot < 0) return;
+
+ if (triggeredSlot < config.Expressions.Length)
+ {
+ ApplyFaceQuery(World, triggeredSlot);
+ shortcutHandler.NotifyExpressionPlayed(FacialExpressionTriggerSource.SHORTCUT);
+ }
+
+ triggeredSlot = -1;
+ }
+
+ [Query]
+ [All(typeof(PlayerComponent))]
+ [None(typeof(DeleteEntityIntention))]
+ private void ApplyFace([Data] int slot, in Entity entity)
+ {
+ ref AvatarFaceExpressionDefinition def = ref config.Expressions[slot];
+
+ World.AddOrGet(entity, new TriggerFacialExpressionIntent
+ {
+ EyebrowsIndex = (byte)def.EyebrowsIndex,
+ EyesIndex = (byte)def.EyesIndex,
+ MouthIndex = (byte)def.MouthIndex,
+ });
+ }
+
+ private void OnSlotPerformed(InputAction.CallbackContext ctx) =>
+ triggeredSlot = slotByActionName[ctx.action.name];
+
+ private void ListenToSlotsInput(InputActionMap map)
+ {
+ for (var i = 0; i < SLOT_COUNT; i++)
+ {
+ string name = FacialExpressionWheelUtils.GetSlotActionName(i);
+
+ try
+ {
+ InputAction action = map.FindAction(name);
+ action.started += OnSlotPerformed;
+ slotByActionName[name] = FacialExpressionWheelUtils.SlotIndexFromActionName(name);
+ }
+ catch (Exception e) { ReportHub.LogException(e, GetReportData()); }
+ }
+ }
+
+ private void UnregisterSlotsInput(InputActionMap map)
+ {
+ for (var i = 0; i < SLOT_COUNT; i++)
+ {
+ string name = FacialExpressionWheelUtils.GetSlotActionName(i);
+ InputAction action = map.FindAction(name);
+ action.started -= OnSlotPerformed;
+ }
+ }
+ }
+}
diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/FacialExpression/UpdateFacialExpressionInputSystem.cs.meta b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/FacialExpression/UpdateFacialExpressionInputSystem.cs.meta
new file mode 100644
index 00000000000..b537556dcdf
--- /dev/null
+++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Systems/FacialExpression/UpdateFacialExpressionInputSystem.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 398f540048a2ffa4b8ccdb75c2e9fb2a
\ No newline at end of file
diff --git a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Tests/EditMode/FakeWearable.cs b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Tests/EditMode/FakeWearable.cs
index f1aca52db00..5c7d11a4478 100644
--- a/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Tests/EditMode/FakeWearable.cs
+++ b/Explorer/Assets/DCL/AvatarRendering/AvatarShape/Tests/EditMode/FakeWearable.cs
@@ -40,6 +40,8 @@ public void SetAmount(int amount)
public WearableType Type { get; }
+ public bool HasFacialExpressionsTexture { get; set; }
+
public WearableAssets[] WearableAssetResults { get; }
public FakeWearable(
diff --git a/Explorer/Assets/DCL/AvatarRendering/Wearables/Components/IWearable.cs b/Explorer/Assets/DCL/AvatarRendering/Wearables/Components/IWearable.cs
index 6124c62fa29..e94b1eeebd4 100644
--- a/Explorer/Assets/DCL/AvatarRendering/Wearables/Components/IWearable.cs
+++ b/Explorer/Assets/DCL/AvatarRendering/Wearables/Components/IWearable.cs
@@ -17,6 +17,12 @@ public interface IWearable : IAvatarAttachment, ITrimmedWearable
{
WearableType Type { get; }
+ ///
+ /// True when a facial-feature wearable ships an `*_expressions.png` atlas. Driven by
+ /// during promise creation.
+ ///
+ bool HasFacialExpressionsTexture { get; set; }
+
///
/// Per [MALE, FEMALE]
///
diff --git a/Explorer/Assets/DCL/AvatarRendering/Wearables/Components/Wearable.cs b/Explorer/Assets/DCL/AvatarRendering/Wearables/Components/Wearable.cs
index 2d1b6128712..974a8521809 100644
--- a/Explorer/Assets/DCL/AvatarRendering/Wearables/Components/Wearable.cs
+++ b/Explorer/Assets/DCL/AvatarRendering/Wearables/Components/Wearable.cs
@@ -53,6 +53,8 @@ StreamableLoadingResult IAvatarAttachment.Model
public WearableType Type { get; private set; }
+ public bool HasFacialExpressionsTexture { get; set; }
+
public bool IsLoading { get; private set; }
public void UpdateLoadingStatus(bool isLoading)
diff --git a/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/FacialFeaturesTextures.cs b/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/FacialFeaturesTextures.cs
index eeb1cf3c6cb..c9f824ba768 100644
--- a/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/FacialFeaturesTextures.cs
+++ b/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/FacialFeaturesTextures.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using UnityEngine;
namespace DCL.AvatarRendering.Wearables.Helpers
@@ -6,15 +6,27 @@ namespace DCL.AvatarRendering.Wearables.Helpers
public readonly struct FacialFeaturesTextures
{
private readonly Dictionary> texturesByCategory;
+ private readonly HashSet categoriesWithExpressions;
public IReadOnlyDictionary> Value => texturesByCategory;
public FacialFeaturesTextures(Dictionary> value)
{
texturesByCategory = value;
+ categoriesWithExpressions = new HashSet();
}
public Texture this[string category, int originalTextureId] => Value[category][originalTextureId];
+ // Categories whose wearable carries an `*_expressions.png` atlas. Stamped during AppendToAvatar.
+ public void MarkHasExpressions(string category) =>
+ categoriesWithExpressions.Add(category);
+
+ public bool HasExpressions(string category) =>
+ categoriesWithExpressions.Contains(category);
+
+ public void ClearExpressionFlags() =>
+ categoriesWithExpressions.Clear();
+
public void CopyInto(ref FacialFeaturesTextures other)
{
var texturesByCategory = other.texturesByCategory;
@@ -30,6 +42,10 @@ public void CopyInto(ref FacialFeaturesTextures other)
foreach ((int textureId, Texture? texture) in existingTextures)
newTextures[textureId] = texture;
}
+
+ other.categoriesWithExpressions.Clear();
+ foreach (string c in categoriesWithExpressions)
+ other.categoriesWithExpressions.Add(c);
}
}
}
diff --git a/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/WearablePolymorphicBehaviour.cs b/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/WearablePolymorphicBehaviour.cs
index 3652fe02df0..9a41ac2d391 100644
--- a/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/WearablePolymorphicBehaviour.cs
+++ b/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/WearablePolymorphicBehaviour.cs
@@ -34,6 +34,13 @@ public static class WearablePolymorphicBehaviour
public const int MAIN_ASSET_INDEX = 0;
public const int MASK_ASSET_INDEX = 1;
+ // Wearables that ship a facial-expression atlas use these suffixes so the loader picks
+ // the matching main + mask pair instead of the legacy single-frame face texture.
+ // Example pair: `eyes_12_expressions.png` + `eyes_12_expressions_mask.png`.
+ private const string EXPRESSIONS_MAIN_FILE_SUFFIX = "_expressions.png";
+ private const string EXPRESSIONS_MASK_FILE_SUFFIX = "_expressions_mask.png";
+ private const string LEGACY_MASK_FILE_SUFFIX = "_mask.png";
+
///
/// Create a certain number of Asset Promises based on the type of the wearable,
/// if promises are already created does nothing and returns false
@@ -86,7 +93,11 @@ public static bool TryCreateSingleGameObjectPromise(
}
///
- /// Facial feature can consist of the main texture and the mask
+ /// Facial feature can consist of the main texture and the mask. When the wearable
+ /// ships an `*_expressions.png` atlas (4×4 expression grid), the loader substitutes it
+ /// for the legacy single-frame texture and pairs the `*_expressions_mask.png` as mask.
+ /// The DTO's mainFile pointer is unchanged; the substitution is loader-side only and
+ /// flags so renderers can atlas-slice.
///
private static bool TryCreateFacialFeaturePromises(
in GetWearablesByPointersIntention intention,
@@ -101,10 +112,77 @@ private static bool TryCreateFacialFeaturePromises(
// 0 stands for the main texture
// 1 stands for the mask
- return TryCreateMainFilePromise(typeof(Texture), intention, customStreamingSubdirectory, wearable, partitionComponent, ref wearableAssets, bodyShape, world, reportData)
+ return TryCreateFacialFeatureMainPromise(in intention, customStreamingSubdirectory, wearable, partitionComponent, ref wearableAssets, bodyShape, world, reportData)
| TryCreateMaskPromise(intention, customStreamingSubdirectory, wearable, partitionComponent, ref wearableAssets, bodyShape, world);
}
+ private static bool TryCreateFacialFeatureMainPromise(
+ in GetWearablesByPointersIntention intention,
+ URLSubdirectory customStreamingSubdirectory,
+ IWearable wearable,
+ IPartitionComponent partitionComponent,
+ ref WearableAssets wearableAssets,
+ BodyShape bodyShape,
+ World world,
+ ReportData reportData)
+ {
+ if (wearableAssets.Results[MAIN_ASSET_INDEX] != null)
+ return false;
+
+ bool hasExpressionsTexture = wearable.TryGetFileHashConditional(bodyShape,
+ static content => content.EndsWith(EXPRESSIONS_MAIN_FILE_SUFFIX, StringComparison.OrdinalIgnoreCase),
+ out string? mainHash);
+
+ // Fallback path: no `*_expressions.png` content entry, OR the wearable's declared
+ // mainFile itself is one (TryGetFileHashConditional skips the mainFile).
+ if (!hasExpressionsTexture)
+ {
+ if (!wearable.TryGetMainFileHash(bodyShape, out mainHash))
+ {
+ wearableAssets.Results[MAIN_ASSET_INDEX] =
+ new StreamableLoadingResult(reportData, new Exception("Main file hash not found"));
+ return false;
+ }
+
+ hasExpressionsTexture = TryGetMainFileKey(wearable, bodyShape, out string? mainFileKey)
+ && mainFileKey != null
+ && mainFileKey.EndsWith(EXPRESSIONS_MAIN_FILE_SUFFIX, StringComparison.OrdinalIgnoreCase);
+ }
+
+ wearable.HasFacialExpressionsTexture = hasExpressionsTexture;
+
+ CreatePromise(typeof(Texture), intention, customStreamingSubdirectory, mainHash!,
+ wearable, MAIN_ASSET_INDEX, partitionComponent, world);
+ return true;
+ }
+
+ private static bool TryGetMainFileKey(IAvatarAttachment attachment, BodyShape bodyShape, out string? key)
+ {
+ AvatarAttachmentDTO? dto = attachment.DTO;
+
+ if (dto?.Metadata.AbstractData.representations != null)
+ {
+ AvatarAttachmentDTO.Representation[] representations = dto.Metadata.AbstractData.representations;
+
+ for (var i = 0; i < representations.Length; i++)
+ {
+ AvatarAttachmentDTO.Representation representation = representations[i];
+ string[] shapes = representation.bodyShapes;
+
+ for (var j = 0; j < shapes.Length; j++)
+ {
+ if (shapes[j] != bodyShape) continue;
+
+ key = representation.mainFile;
+ return true;
+ }
+ }
+ }
+
+ key = null;
+ return false;
+ }
+
private static ref WearableAssets InitializeResultsArray(IWearable wearable, BodyShape bodyShape, int size)
{
if (wearable.IsUnisex() && wearable.HasSameModelsForAllGenders())
@@ -131,9 +209,15 @@ private static bool TryCreateMaskPromise(
if (wearableAssets.Results[MASK_ASSET_INDEX] != null)
return false;
- if (!wearable.TryGetFileHashConditional(bodyShape,
- static content => content.EndsWith("_mask.png", StringComparison.OrdinalIgnoreCase),
- out string mainFileHash))
+ // Expression-capable wearables pair their atlas with `*_expressions_mask.png`. Other
+ // wearables fall back to the legacy `*_mask.png` (excluding the expressions variant to
+ // avoid grabbing the wrong file when both pairs ship side-by-side).
+ Func contentMatch = wearable.HasFacialExpressionsTexture
+ ? static content => content.EndsWith(EXPRESSIONS_MASK_FILE_SUFFIX, StringComparison.OrdinalIgnoreCase)
+ : static content => content.EndsWith(LEGACY_MASK_FILE_SUFFIX, StringComparison.OrdinalIgnoreCase)
+ && !content.EndsWith(EXPRESSIONS_MASK_FILE_SUFFIX, StringComparison.OrdinalIgnoreCase);
+
+ if (!wearable.TryGetFileHashConditional(bodyShape, contentMatch, out string mainFileHash))
{
// If there is no mask, we don't need to create a promise for it, and it's not an exception
wearableAssets.Results[MASK_ASSET_INDEX] = new StreamableLoadingResult((AttachmentTextureAsset)null);
diff --git a/Explorer/Assets/DCL/Character/CharacterPreview/Assets/CharacterPreviewCameraSettings_Backpack.asset b/Explorer/Assets/DCL/Character/CharacterPreview/Assets/CharacterPreviewCameraSettings_Backpack.asset
index e3ff1cbf08a..3f76a305577 100644
--- a/Explorer/Assets/DCL/Character/CharacterPreview/Assets/CharacterPreviewCameraSettings_Backpack.asset
+++ b/Explorer/Assets/DCL/Character/CharacterPreview/Assets/CharacterPreviewCameraSettings_Backpack.asset
@@ -39,7 +39,7 @@ MonoBehaviour:
k__BackingField: 10
k__BackingField: {x: 27, y: 8}
k__BackingField: 6
- k__BackingField: 0.2
+ k__BackingField: 0.6
k__BackingField: 1
k__BackingField: 1
k__BackingField: 1
diff --git a/Explorer/Assets/DCL/Character/CharacterPreview/Assets/CharacterPreviewCameraSettings_FacialExpressionsWheel.asset b/Explorer/Assets/DCL/Character/CharacterPreview/Assets/CharacterPreviewCameraSettings_FacialExpressionsWheel.asset
new file mode 100644
index 00000000000..f47690af2f4
--- /dev/null
+++ b/Explorer/Assets/DCL/Character/CharacterPreview/Assets/CharacterPreviewCameraSettings_FacialExpressionsWheel.asset
@@ -0,0 +1,38 @@
+%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: d834b5e6adc04c359c4d6b299db0f5ae, type: 3}
+ m_Name: CharacterPreviewCameraSettings_FacialExpressionsWheel
+ m_EditorClassIdentifier:
+ k__BackingField:
+ k__BackingField:
+ - k__BackingField: {x: 0, y: 0.7, z: 0}
+ k__BackingField: 5
+ k__BackingField: 4
+ k__BackingField: 0
+ k__BackingField: 0.3
+ k__BackingField: -0.84
+ k__BackingField: 0.5
+ k__BackingField: 0
+ k__BackingField: 1
+ k__BackingField: 32
+ k__BackingField: 10
+ k__BackingField: {x: 40, y: 16}
+ k__BackingField: 6
+ k__BackingField: 0
+ k__BackingField: 1
+ k__BackingField: 1
+ k__BackingField: 1
+ k__BackingField:
+ - inputAction: 1
+ cursorSprite: {fileID: 21300000, guid: d34aedda08e64344091b34deada8211e, type: 3}
+ - inputAction: 0
+ cursorSprite: {fileID: 21300000, guid: a0dd824ee2daadd46a4437a4e3145c9e, type: 3}
diff --git a/Explorer/Assets/DCL/Character/CharacterPreview/Assets/CharacterPreviewCameraSettings_FacialExpressionsWheel.asset.meta b/Explorer/Assets/DCL/Character/CharacterPreview/Assets/CharacterPreviewCameraSettings_FacialExpressionsWheel.asset.meta
new file mode 100644
index 00000000000..99d5dca3087
--- /dev/null
+++ b/Explorer/Assets/DCL/Character/CharacterPreview/Assets/CharacterPreviewCameraSettings_FacialExpressionsWheel.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 0d82ea8f478e4ca4ebef878b5167a684
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Explorer/Assets/DCL/Character/CharacterPreview/Assets/CharacterPreviewCameraSettings_LoadingScreen.asset b/Explorer/Assets/DCL/Character/CharacterPreview/Assets/CharacterPreviewCameraSettings_LoadingScreen.asset
index 8a8f85bbf32..3138a40dec7 100644
--- a/Explorer/Assets/DCL/Character/CharacterPreview/Assets/CharacterPreviewCameraSettings_LoadingScreen.asset
+++ b/Explorer/Assets/DCL/Character/CharacterPreview/Assets/CharacterPreviewCameraSettings_LoadingScreen.asset
@@ -27,7 +27,7 @@ MonoBehaviour:
k__BackingField: 10
k__BackingField: {x: 40, y: 16}
k__BackingField: 6
- k__BackingField: 0.2
+ k__BackingField: 0.6
k__BackingField: 1
k__BackingField: 1
k__BackingField: 1
diff --git a/Explorer/Assets/DCL/Character/CharacterPreview/Assets/CharacterPreviewCameraSettings_Passport.asset b/Explorer/Assets/DCL/Character/CharacterPreview/Assets/CharacterPreviewCameraSettings_Passport.asset
index d3556c0c4f8..3f4dbf55924 100644
--- a/Explorer/Assets/DCL/Character/CharacterPreview/Assets/CharacterPreviewCameraSettings_Passport.asset
+++ b/Explorer/Assets/DCL/Character/CharacterPreview/Assets/CharacterPreviewCameraSettings_Passport.asset
@@ -14,7 +14,7 @@ MonoBehaviour:
m_EditorClassIdentifier:
k__BackingField:
k__BackingField:
- - k__BackingField: {x: 0, y: -0.2, z: 0}
+ - k__BackingField: {x: 0, y: 0.17, z: 0}
k__BackingField: 24
k__BackingField: 4
k__BackingField: 0
@@ -27,7 +27,7 @@ MonoBehaviour:
k__BackingField: 10
k__BackingField: {x: 40, y: 16}
k__BackingField: 6
- k__BackingField: 0.2
+ k__BackingField: 0.6
k__BackingField: 1
k__BackingField: 1
k__BackingField: 1
diff --git a/Explorer/Assets/DCL/Character/CharacterPreview/CharacterPreviewAvatarContainer.cs b/Explorer/Assets/DCL/Character/CharacterPreview/CharacterPreviewAvatarContainer.cs
index 5db03cc24f6..c0929408866 100644
--- a/Explorer/Assets/DCL/Character/CharacterPreview/CharacterPreviewAvatarContainer.cs
+++ b/Explorer/Assets/DCL/Character/CharacterPreview/CharacterPreviewAvatarContainer.cs
@@ -18,11 +18,12 @@ public class CharacterPreviewAvatarContainer : MonoBehaviour, IDisposable
private const float ANGULAR_VELOCITY_LOWER_THRES = 0.01f;
private const float FOW_LOWER_THRES = 0.01f;
private const float FOV_SPEED_COEFF = 3.5f;
- private const float FOV_SMOOTH_TIME = 0.6f;
+ private const float FOV_SMOOTH_TIME_DEFAULT = 0.6f;
private float fovTransitionStartTime;
private float fovTransitionStartValue;
private bool isFOVTransitioning;
+ private float fovSmoothDuration = FOV_SMOOTH_TIME_DEFAULT;
[field: SerializeField] internal Vector3 previewPositionInScene { get; private set; }
[field: SerializeField] internal Transform avatarParent { get; private set; }
@@ -84,11 +85,23 @@ public void SetCameraPosition(CharacterPreviewCameraPreset preset)
public void StartFOVTransition(float targetFOV)
{
TargetFOV = targetFOV;
+
+ if (fovSmoothDuration <= 0f)
+ {
+ fovTransitionStartValue = targetFOV;
+ freeLookCamera.m_Lens.FieldOfView = targetFOV;
+ isFOVTransitioning = false;
+ return;
+ }
+
fovTransitionStartTime = Time.time;
fovTransitionStartValue = freeLookCamera.m_Lens.FieldOfView;
isFOVTransitioning = true;
}
+ public void SetFOVSmoothDuration(float duration) =>
+ fovSmoothDuration = duration > 0f ? duration : 0f;
+
public void SetPreviewPlatformActive(bool isActive) =>
previewPlatform.SetActive(isActive);
@@ -155,7 +168,7 @@ private void UpdateFOV()
{
// Smooth transition for category changes
float elapsedTime = Time.time - fovTransitionStartTime;
- float normalizedTime = Mathf.Clamp01(elapsedTime / FOV_SMOOTH_TIME);
+ float normalizedTime = Mathf.Clamp01(elapsedTime / fovSmoothDuration);
float easedTime = Mathf.SmoothStep(0f, 1f, normalizedTime);
newFOV = Mathf.Lerp(fovTransitionStartValue, TargetFOV, easedTime);
diff --git a/Explorer/Assets/DCL/Character/CharacterPreview/CharacterPreviewCameraController.cs b/Explorer/Assets/DCL/Character/CharacterPreview/CharacterPreviewCameraController.cs
index 4019c03ee9c..e911ccbd3cb 100644
--- a/Explorer/Assets/DCL/Character/CharacterPreview/CharacterPreviewCameraController.cs
+++ b/Explorer/Assets/DCL/Character/CharacterPreview/CharacterPreviewCameraController.cs
@@ -26,6 +26,8 @@ public CharacterPreviewCameraController(CharacterPreviewInputEventBus characterP
characterPreviewInputEventBus.OnScrollEvent += OnScroll;
characterPreviewInputEventBus.OnChangePreviewFocusEvent += OnChangePreviewCategory;
+ characterPreviewAvatarContainer.SetFOVSmoothDuration(cameraSettings.fieldOfViewDuration);
+
OnChangePreviewCategory(AvatarWearableCategoryEnum.Body);
}
diff --git a/Explorer/Assets/DCL/Character/CharacterPreview/CharacterPreviewController.cs b/Explorer/Assets/DCL/Character/CharacterPreview/CharacterPreviewController.cs
index 85ffefc06e1..3626d172d51 100644
--- a/Explorer/Assets/DCL/Character/CharacterPreview/CharacterPreviewController.cs
+++ b/Explorer/Assets/DCL/Character/CharacterPreview/CharacterPreviewController.cs
@@ -34,6 +34,8 @@ namespace DCL.CharacterPreview
private readonly CharacterPreviewAvatarContainer characterPreviewAvatarContainer;
private readonly IComponentPool characterPreviewContainerPool;
private readonly Entity characterPreviewEntity;
+
+ public Entity PreviewEntity => characterPreviewEntity;
private readonly World globalWorld;
private readonly bool builderEmotesPreview;
@@ -163,6 +165,24 @@ public void ResetEmote()
emoteComponent.Reset();
}
+ ///
+ /// Writes the resting facial expression layer on the preview avatar. No-ops until
+ /// has been attached by the rendering pipeline
+ /// (after wearable instantiation), so safe to call before the avatar has loaded.
+ ///
+ public void TrySetFace(int eyebrowsIndex, int eyesIndex, int mouthIndex)
+ {
+ if (!globalWorld.IsAlive(characterPreviewEntity)) return;
+
+ ref AvatarFaceComponent face = ref globalWorld.TryGetRef(characterPreviewEntity, out bool exists);
+ if (!exists) return;
+
+ face.EyebrowsExpressionIndex = eyebrowsIndex;
+ face.EyesExpressionIndex = eyesIndex;
+ face.MouthExpressionIndex = mouthIndex;
+ face.IsDirty = true;
+ }
+
public bool IsPlayingEmote() =>
globalWorld.TryGet(characterPreviewEntity, out CharacterEmoteComponent emoteComponent) && emoteComponent.IsPlayingEmote;
diff --git a/Explorer/Assets/DCL/EmotesWheel/Assets/EmoteSlot.prefab b/Explorer/Assets/DCL/EmotesWheel/Assets/EmoteSlot.prefab
index 6f9b54b6b68..ae32381e514 100644
--- a/Explorer/Assets/DCL/EmotesWheel/Assets/EmoteSlot.prefab
+++ b/Explorer/Assets/DCL/EmotesWheel/Assets/EmoteSlot.prefab
@@ -34,8 +34,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
- m_AnchoredPosition: {x: 0, y: 7}
- m_SizeDelta: {x: 32, y: 32}
+ m_AnchoredPosition: {x: 0, y: 0}
+ m_SizeDelta: {x: 174, y: 160}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &69245956534039968
CanvasRenderer:
@@ -58,14 +58,89 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
- m_Color: {r: 0.8117647, g: 0.8039216, b: 0.83137256, a: 1}
+ m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 0
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
- m_Sprite: {fileID: 21300000, guid: 0dcdab27d9530440f8dd0973134f8717, type: 3}
+ m_Sprite: {fileID: 21300000, guid: 3a3dee0180f4b47859cca03776a00e7c, type: 3}
+ m_Type: 0
+ m_PreserveAspect: 0
+ m_FillCenter: 1
+ m_FillMethod: 4
+ m_FillAmount: 1
+ m_FillClockwise: 1
+ m_FillOrigin: 0
+ m_UseSpriteMesh: 0
+ m_PixelsPerUnitMultiplier: 1
+--- !u!1 &1923248890120471536
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 3331880382409646134}
+ - component: {fileID: 2249465867215692124}
+ - component: {fileID: 8829078566679261275}
+ m_Layer: 5
+ m_Name: Icon
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!224 &3331880382409646134
+RectTransform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1923248890120471536}
+ m_LocalRotation: {x: 0, y: 0, z: 0.10452846, w: 0.9945219}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 1
+ m_Children: []
+ m_Father: {fileID: 8974564133226285288}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 12}
+ m_AnchorMin: {x: 0, y: 1}
+ m_AnchorMax: {x: 0, y: 1}
+ m_AnchoredPosition: {x: 4.2, y: -18.2}
+ m_SizeDelta: {x: 18, y: 18}
+ m_Pivot: {x: 0, y: 1}
+--- !u!222 &2249465867215692124
+CanvasRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1923248890120471536}
+ m_CullTransparentMesh: 1
+--- !u!114 &8829078566679261275
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1923248890120471536}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ m_Material: {fileID: 0}
+ m_Color: {r: 1, g: 1, b: 1, a: 1}
+ m_RaycastTarget: 0
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
+ m_OnCullStateChanged:
+ m_PersistentCalls:
+ m_Calls: []
+ m_Sprite: {fileID: 21300000, guid: 9349f1fea510d448cbac392ca619d66e, type: 3}
m_Type: 0
m_PreserveAspect: 0
m_FillCenter: 1
@@ -110,7 +185,7 @@ RectTransform:
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 7}
- m_SizeDelta: {x: 50, y: 50}
+ m_SizeDelta: {x: 90, y: 90}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &8209566919986367981
CanvasRenderer:
@@ -184,8 +259,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0}
m_AnchorMax: {x: 0.5, y: 0}
- m_AnchoredPosition: {x: 0, y: 6.836853}
- m_SizeDelta: {x: 15.6108, y: 15.6108}
+ m_AnchoredPosition: {x: 0, y: 8}
+ m_SizeDelta: {x: 20, y: 20}
m_Pivot: {x: 0.5, y: 0}
--- !u!222 &3925924027178008141
CanvasRenderer:
@@ -242,8 +317,8 @@ MonoBehaviour:
m_faceColor:
serializedVersion: 2
rgba: 4294967295
- m_fontSize: 14
- m_fontSizeBase: 14
+ m_fontSize: 18
+ m_fontSizeBase: 18
m_fontWeight: 400
m_enableAutoSizing: 0
m_fontSizeMin: 10
@@ -253,6 +328,7 @@ MonoBehaviour:
m_VerticalAlignment: 512
m_textAlignment: 65535
m_characterSpacing: 0
+ m_characterHorizontalScale: 1
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
@@ -320,8 +396,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
- m_AnchoredPosition: {x: 0.0707, y: -1.0678}
- m_SizeDelta: {x: 112, y: 105}
+ m_AnchoredPosition: {x: 0, y: 0}
+ m_SizeDelta: {x: 180, y: 168}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &6732855126494316192
CanvasRenderer:
@@ -351,7 +427,7 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
- m_Sprite: {fileID: 21300000, guid: d6787e389abeb437ead1ebad0a81a461, type: 3}
+ m_Sprite: {fileID: 21300000, guid: 2092305e118884e81bb75ffbb7109788, type: 3}
m_Type: 0
m_PreserveAspect: 0
m_FillCenter: 1
@@ -396,8 +472,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
- m_AnchoredPosition: {x: 0, y: -1.0469}
- m_SizeDelta: {x: 111, y: 103}
+ m_AnchoredPosition: {x: 0, y: 0}
+ m_SizeDelta: {x: 174, y: 160}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &6807344510761186898
CanvasRenderer:
@@ -513,6 +589,7 @@ RectTransform:
- {fileID: 5343966063347327097}
- {fileID: 677941972702109446}
- {fileID: 5294823707738111947}
+ - {fileID: 3331880382409646134}
- {fileID: 4063225283202921380}
- {fileID: 1176605725860819126}
m_Father: {fileID: 1895304305689352378}
@@ -520,7 +597,7 @@ RectTransform:
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 0}
- m_SizeDelta: {x: 100, y: 100}
+ m_SizeDelta: {x: 174, y: 160}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!1 &8978849373333944373
GameObject:
@@ -650,11 +727,11 @@ PrefabInstance:
objectReference: {fileID: 0}
- target: {fileID: 1166888513132788191, guid: d565b61885fb1ef41b1582a285e748e9, type: 3}
propertyPath: m_SizeDelta.x
- value: 40
+ value: 50
objectReference: {fileID: 0}
- target: {fileID: 1166888513132788191, guid: d565b61885fb1ef41b1582a285e748e9, type: 3}
propertyPath: m_SizeDelta.y
- value: 40
+ value: 50
objectReference: {fileID: 0}
- target: {fileID: 1166888513132788191, guid: d565b61885fb1ef41b1582a285e748e9, type: 3}
propertyPath: m_LocalPosition.x
diff --git a/Explorer/Assets/DCL/EmotesWheel/Assets/EmotesWheelHUD.prefab b/Explorer/Assets/DCL/EmotesWheel/Assets/EmotesWheelHUD.prefab
index b2fd7a5feca..7f52aba4de2 100644
--- a/Explorer/Assets/DCL/EmotesWheel/Assets/EmotesWheelHUD.prefab
+++ b/Explorer/Assets/DCL/EmotesWheel/Assets/EmotesWheelHUD.prefab
@@ -34,8 +34,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
- m_AnchoredPosition: {x: 0, y: -43.872906}
- m_SizeDelta: {x: 156, y: 26.373}
+ m_AnchoredPosition: {x: 0, y: -50}
+ m_SizeDelta: {x: 190, y: 30}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &3738596849537230179
CanvasRenderer:
@@ -66,7 +66,7 @@ MonoBehaviour:
m_PersistentCalls:
m_Calls: []
m_text: Hold <#fcfcfc>[b+num] to run an emote while the wheel is
- closed
+ closed.
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 35aa85d68d15435418848a03a2db81ec, type: 2}
m_sharedMaterial: {fileID: 1701868249614554837, guid: 35aa85d68d15435418848a03a2db81ec, type: 2}
@@ -75,8 +75,8 @@ MonoBehaviour:
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
- rgba: 4293782508
- m_fontColor: {r: 0.9254902, g: 0.92156863, b: 0.92941177, a: 1}
+ rgba: 4292136399
+ m_fontColor: {r: 0.8117647, g: 0.8039216, b: 0.83137256, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
@@ -93,8 +93,8 @@ MonoBehaviour:
m_faceColor:
serializedVersion: 2
rgba: 4294967295
- m_fontSize: 10
- m_fontSizeBase: 10
+ m_fontSize: 12
+ m_fontSizeBase: 12
m_fontWeight: 400
m_enableAutoSizing: 0
m_fontSizeMin: 10
@@ -104,6 +104,7 @@ MonoBehaviour:
m_VerticalAlignment: 512
m_textAlignment: 65535
m_characterSpacing: 0
+ m_characterHorizontalScale: 1
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
@@ -167,13 +168,15 @@ RectTransform:
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
- m_Children: []
+ m_Children:
+ - {fileID: 186355610268972269}
+ - {fileID: 7706739768243273297}
m_Father: {fileID: 3368308072246989936}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
- m_AnchoredPosition: {x: 0, y: 1.0523955}
- m_SizeDelta: {x: 120, y: 124.4}
+ m_AnchoredPosition: {x: 0, y: -6}
+ m_SizeDelta: {x: 80, y: 32}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &374315793872558327
CanvasRenderer:
@@ -196,14 +199,14 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
- m_Color: {r: 1, g: 1, b: 1, a: 0}
+ m_Color: {r: 1, g: 1, b: 1, a: 0.29803923}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
- m_Sprite: {fileID: 0}
+ m_Sprite: {fileID: 21300000, guid: 1dcae7b3183f24eb39310ae8e4c092a5, type: 3}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1
@@ -212,7 +215,7 @@ MonoBehaviour:
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
- m_PixelsPerUnitMultiplier: 1
+ m_PixelsPerUnitMultiplier: 2.5
--- !u!114 &1341166584425063779
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -234,13 +237,13 @@ MonoBehaviour:
m_SelectOnRight: {fileID: 0}
m_Transition: 1
m_Colors:
- m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
- m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
- m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
- m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
- m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
+ m_NormalColor: {r: 1, g: 1, b: 1, a: 0.019607844}
+ m_HighlightedColor: {r: 1, g: 1, b: 1, a: 0.050980393}
+ m_PressedColor: {r: 1, g: 1, b: 1, a: 0.019607844}
+ m_SelectedColor: {r: 1, g: 1, b: 1, a: 0.019607844}
+ m_DisabledColor: {r: 1, g: 1, b: 1, a: 0.019607844}
m_ColorMultiplier: 1
- m_FadeDuration: 0.1
+ m_FadeDuration: 0
m_SpriteState:
m_HighlightedSprite: {fileID: 0}
m_PressedSprite: {fileID: 0}
@@ -253,146 +256,10 @@ MonoBehaviour:
m_SelectedTrigger: Selected
m_DisabledTrigger: Disabled
m_Interactable: 1
- m_TargetGraphic: {fileID: 50054618743786208}
+ m_TargetGraphic: {fileID: 3021313394658728330}
m_OnClick:
m_PersistentCalls:
m_Calls: []
---- !u!1 &1910573828672085254
-GameObject:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- serializedVersion: 6
- m_Component:
- - component: {fileID: 7155408817035108562}
- - component: {fileID: 9030177804243959839}
- - component: {fileID: 8296175027738707069}
- m_Layer: 5
- m_Name: Cutomise
- m_TagString: Untagged
- m_Icon: {fileID: 0}
- m_NavMeshLayer: 0
- m_StaticEditorFlags: 0
- m_IsActive: 1
---- !u!224 &7155408817035108562
-RectTransform:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 1910573828672085254}
- m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
- m_LocalPosition: {x: 0, y: 0, z: 0}
- m_LocalScale: {x: 1, y: 1, z: 1}
- m_ConstrainProportionsScale: 0
- m_Children: []
- m_Father: {fileID: 3368308072246989936}
- m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
- m_AnchorMin: {x: 0.5, y: 0.5}
- m_AnchorMax: {x: 0.5, y: 0.5}
- m_AnchoredPosition: {x: 0, y: -8.47291}
- m_SizeDelta: {x: 150, y: 16}
- m_Pivot: {x: 0.5, y: 0.5}
---- !u!222 &9030177804243959839
-CanvasRenderer:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 1910573828672085254}
- m_CullTransparentMesh: 0
---- !u!114 &8296175027738707069
-MonoBehaviour:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 1910573828672085254}
- m_Enabled: 1
- m_EditorHideFlags: 0
- m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
- m_Name:
- m_EditorClassIdentifier:
- m_Material: {fileID: 0}
- m_Color: {r: 1, g: 1, b: 1, a: 1}
- m_RaycastTarget: 1
- m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
- m_Maskable: 1
- m_OnCullStateChanged:
- m_PersistentCalls:
- m_Calls: []
- m_text: Customise <#fcfcfc>[E]
- m_isRightToLeft: 0
- m_fontAsset: {fileID: 11400000, guid: 35aa85d68d15435418848a03a2db81ec, type: 2}
- m_sharedMaterial: {fileID: 1701868249614554837, guid: 35aa85d68d15435418848a03a2db81ec, type: 2}
- m_fontSharedMaterials: []
- m_fontMaterial: {fileID: 0}
- m_fontMaterials: []
- m_fontColor32:
- serializedVersion: 2
- rgba: 4293782508
- m_fontColor: {r: 0.9254902, g: 0.92156863, b: 0.92941177, a: 1}
- m_enableVertexGradient: 0
- m_colorMode: 3
- m_fontColorGradient:
- topLeft: {r: 1, g: 1, b: 1, a: 1}
- topRight: {r: 1, g: 1, b: 1, a: 1}
- bottomLeft: {r: 1, g: 1, b: 1, a: 1}
- bottomRight: {r: 1, g: 1, b: 1, a: 1}
- m_fontColorGradientPreset: {fileID: 0}
- m_spriteAsset: {fileID: 0}
- m_tintAllSprites: 0
- m_StyleSheet: {fileID: 0}
- m_TextStyleHashCode: -1183493901
- m_overrideHtmlColors: 0
- m_faceColor:
- serializedVersion: 2
- rgba: 4294967295
- m_fontSize: 10
- m_fontSizeBase: 10
- m_fontWeight: 400
- m_enableAutoSizing: 0
- m_fontSizeMin: 10
- m_fontSizeMax: 12
- m_fontStyle: 0
- m_HorizontalAlignment: 2
- m_VerticalAlignment: 512
- m_textAlignment: 65535
- m_characterSpacing: 0
- m_wordSpacing: 0
- m_lineSpacing: 0
- m_lineSpacingMax: 0
- m_paragraphSpacing: 0
- m_charWidthMaxAdj: 0
- m_TextWrappingMode: 1
- m_wordWrappingRatios: 0.4
- m_overflowMode: 0
- m_linkedTextComponent: {fileID: 0}
- parentLinkedComponent: {fileID: 0}
- m_enableKerning: 1
- m_ActiveFontFeatures: 6e72656b
- m_enableExtraPadding: 0
- checkPaddingRequired: 0
- m_isRichText: 1
- m_EmojiFallbackSupport: 1
- m_parseCtrlCharacters: 1
- m_isOrthographic: 1
- m_isCullingEnabled: 0
- m_horizontalMapping: 0
- m_verticalMapping: 0
- m_uvLineOffset: 0
- m_geometrySortingOrder: 0
- m_IsTextObjectScaleStatic: 0
- m_VertexBufferAutoSizeReduction: 1
- m_useMaxVisibleDescender: 1
- m_pageToDisplay: 1
- m_margin: {x: 0, y: 0, z: 0, w: 0}
- m_isUsingLegacyAnimationComponent: 0
- m_isVolumetricText: 0
- m_hasFontAssetChanged: 0
- m_baseMaterial: {fileID: 0}
- m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!1 &2730020739912452495
GameObject:
m_ObjectHideFlags: 0
@@ -427,8 +294,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
- m_AnchoredPosition: {x: 0, y: 48.1}
- m_SizeDelta: {x: 118, y: 40}
+ m_AnchoredPosition: {x: 0, y: 60}
+ m_SizeDelta: {x: 240, y: 70}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &5735411371286715404
CanvasRenderer:
@@ -485,17 +352,18 @@ MonoBehaviour:
m_faceColor:
serializedVersion: 2
rgba: 4294967295
- m_fontSize: 14
+ m_fontSize: 18
m_fontSizeBase: 18
m_fontWeight: 400
m_enableAutoSizing: 1
m_fontSizeMin: 10
- m_fontSizeMax: 14
+ m_fontSizeMax: 18
m_fontStyle: 0
m_HorizontalAlignment: 2
- m_VerticalAlignment: 512
+ m_VerticalAlignment: 1024
m_textAlignment: 65535
m_characterSpacing: 0
+ m_characterHorizontalScale: 1
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
@@ -590,6 +458,7 @@ MonoBehaviour:
- {fileID: 5066361607108311281}
- {fileID: 8782135904965761850}
k__BackingField: {fileID: 1341166584425063779}
+ k__BackingField: {fileID: 2299642542487610113}
k__BackingField:
- {fileID: 4437412575776298108}
- {fileID: 6804220589846975022}
@@ -623,6 +492,7 @@ Canvas:
m_OverridePixelPerfect: 0
m_SortingBucketNormalizedSize: 0
m_VertexColorAlwaysGammaSpace: 0
+ m_UseReflectionProbes: 0
m_AdditionalShaderChannelsFlag: 25
m_UpdateRectTransformForStandalone: 0
m_SortingLayerID: 0
@@ -668,7 +538,7 @@ MonoBehaviour:
m_UiScaleMode: 1
m_ReferencePixelsPerUnit: 100
m_ScaleFactor: 1
- m_ReferenceResolution: {x: 1536, y: 864}
+ m_ReferenceResolution: {x: 1920, y: 1080}
m_ScreenMatchMode: 0
m_MatchWidthOrHeight: 0
m_PhysicalUnit: 3
@@ -745,7 +615,7 @@ RectTransform:
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 0}
- m_SizeDelta: {x: 400, y: 400}
+ m_SizeDelta: {x: 680, y: 680}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &5950188987537430702
CanvasRenderer:
@@ -785,7 +655,7 @@ MonoBehaviour:
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
---- !u!1 &3967867082645134450
+--- !u!1 &3932073865174807102
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
@@ -793,125 +663,50 @@ GameObject:
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- - component: {fileID: 6387641994105437130}
- - component: {fileID: 6428647394411284262}
- - component: {fileID: 6467756172435730145}
+ - component: {fileID: 4794220686679903806}
+ - component: {fileID: 8505923806040835807}
+ - component: {fileID: 2369410462101712670}
m_Layer: 5
- m_Name: Image
+ m_Name: Text
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
---- !u!224 &6387641994105437130
+--- !u!224 &4794220686679903806
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 3967867082645134450}
+ m_GameObject: {fileID: 3932073865174807102}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
- m_Father: {fileID: 2445375703582184909}
+ m_Father: {fileID: 623529305124580425}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
- m_AnchoredPosition: {x: 0, y: 0}
- m_SizeDelta: {x: -18, y: -18}
- m_Pivot: {x: 0.5, y: 0.5}
---- !u!222 &6428647394411284262
-CanvasRenderer:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 3967867082645134450}
- m_CullTransparentMesh: 0
---- !u!114 &6467756172435730145
-MonoBehaviour:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 3967867082645134450}
- m_Enabled: 1
- m_EditorHideFlags: 0
- m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
- m_Name:
- m_EditorClassIdentifier:
- m_Material: {fileID: 0}
- m_Color: {r: 1, g: 1, b: 1, a: 1}
- m_RaycastTarget: 0
- m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
- m_Maskable: 1
- m_OnCullStateChanged:
- m_PersistentCalls:
- m_Calls: []
- m_Sprite: {fileID: 21300000, guid: a4df2165befde144db1abdaf69cece34, type: 3}
- m_Type: 0
- m_PreserveAspect: 0
- m_FillCenter: 1
- m_FillMethod: 4
- m_FillAmount: 1
- m_FillClockwise: 1
- m_FillOrigin: 0
- m_UseSpriteMesh: 0
- m_PixelsPerUnitMultiplier: 1
---- !u!1 &4979140711266332923
-GameObject:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- serializedVersion: 6
- m_Component:
- - component: {fileID: 7706739768243273297}
- - component: {fileID: 4081166362944202792}
- - component: {fileID: 5782514664531182739}
- m_Layer: 5
- m_Name: Title
- m_TagString: Untagged
- m_Icon: {fileID: 0}
- m_NavMeshLayer: 0
- m_StaticEditorFlags: 0
- m_IsActive: 1
---- !u!224 &7706739768243273297
-RectTransform:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 4979140711266332923}
- m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
- m_LocalPosition: {x: 0, y: 0, z: 0}
- m_LocalScale: {x: 1, y: 1, z: 1}
- m_ConstrainProportionsScale: 1
- m_Children: []
- m_Father: {fileID: 3368308072246989936}
- m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
- m_AnchorMin: {x: 0.5, y: 0.5}
- m_AnchorMax: {x: 0.5, y: 0.5}
- m_AnchoredPosition: {x: 0, y: 6.9270954}
- m_SizeDelta: {x: 164, y: 17.4034}
+ m_AnchoredPosition: {x: -0.0001449585, y: -0.00005531311}
+ m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
---- !u!222 &4081166362944202792
+--- !u!222 &8505923806040835807
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 4979140711266332923}
+ m_GameObject: {fileID: 3932073865174807102}
m_CullTransparentMesh: 0
---- !u!114 &5782514664531182739
+--- !u!114 &2369410462101712670
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 4979140711266332923}
+ m_GameObject: {fileID: 3932073865174807102}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
@@ -925,7 +720,7 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
- m_text: Emotes
+ m_text: 'Facial Expressions [Y] '
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 96ae0a2159a39234f858ea23bdcc74ad, type: 2}
m_sharedMaterial: {fileID: 735423033564544980, guid: 96ae0a2159a39234f858ea23bdcc74ad, type: 2}
@@ -934,8 +729,8 @@ MonoBehaviour:
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
- rgba: 4294769916
- m_fontColor: {r: 0.9882353, g: 0.9882353, b: 0.9882353, a: 1}
+ rgba: 4294967295
+ m_fontColor: {r: 1, g: 1, b: 1, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
@@ -952,17 +747,18 @@ MonoBehaviour:
m_faceColor:
serializedVersion: 2
rgba: 4294967295
- m_fontSize: 12
- m_fontSizeBase: 12
+ m_fontSize: 18
+ m_fontSizeBase: 18
m_fontWeight: 400
m_enableAutoSizing: 0
m_fontSizeMin: 10
- m_fontSizeMax: 14
- m_fontStyle: 16
+ m_fontSizeMax: 12
+ m_fontStyle: 0
m_HorizontalAlignment: 2
m_VerticalAlignment: 512
m_textAlignment: 65535
m_characterSpacing: 0
+ m_characterHorizontalScale: 1
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
@@ -996,7 +792,7 @@ MonoBehaviour:
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
---- !u!1 &6941404510284847514
+--- !u!1 &3967867082645134450
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
@@ -1004,54 +800,897 @@ GameObject:
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- - component: {fileID: 2445375703582184909}
- - component: {fileID: 5505085621591519339}
- - component: {fileID: 8746213363341550890}
- - component: {fileID: 8782135904965761850}
- - component: {fileID: 6630055616766752555}
- - component: {fileID: 1479204841276357964}
+ - component: {fileID: 6387641994105437130}
+ - component: {fileID: 6428647394411284262}
+ - component: {fileID: 6467756172435730145}
m_Layer: 5
- m_Name: CloseButton
+ m_Name: Image
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
---- !u!224 &2445375703582184909
+--- !u!224 &6387641994105437130
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 6941404510284847514}
+ m_GameObject: {fileID: 3967867082645134450}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
- m_ConstrainProportionsScale: 1
- m_Children:
- - {fileID: 6387641994105437130}
- m_Father: {fileID: 3368308072246989936}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 2445375703582184909}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
- m_AnchorMin: {x: 0.5, y: 0.5}
- m_AnchorMax: {x: 0.5, y: 0.5}
- m_AnchoredPosition: {x: 166.7, y: 147}
- m_SizeDelta: {x: 28, y: 28}
- m_Pivot: {x: 0.5, y: 0.5}
+ m_AnchorMin: {x: 0, y: 0}
+ m_AnchorMax: {x: 1, y: 1}
+ m_AnchoredPosition: {x: 0, y: 0}
+ m_SizeDelta: {x: -24, y: -24}
+ m_Pivot: {x: 0.5, y: 0.5}
+--- !u!222 &6428647394411284262
+CanvasRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 3967867082645134450}
+ m_CullTransparentMesh: 0
+--- !u!114 &6467756172435730145
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 3967867082645134450}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ m_Material: {fileID: 0}
+ m_Color: {r: 1, g: 1, b: 1, a: 1}
+ m_RaycastTarget: 0
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
+ m_OnCullStateChanged:
+ m_PersistentCalls:
+ m_Calls: []
+ m_Sprite: {fileID: 21300000, guid: f49bfce399a8c41568bf017a951a357a, type: 3}
+ m_Type: 0
+ m_PreserveAspect: 0
+ m_FillCenter: 1
+ m_FillMethod: 4
+ m_FillAmount: 1
+ m_FillClockwise: 1
+ m_FillOrigin: 0
+ m_UseSpriteMesh: 0
+ m_PixelsPerUnitMultiplier: 1
+--- !u!1 &4979140711266332923
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 7706739768243273297}
+ - component: {fileID: 4081166362944202792}
+ - component: {fileID: 5782514664531182739}
+ m_Layer: 5
+ m_Name: Title
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!224 &7706739768243273297
+RectTransform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 4979140711266332923}
+ m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 1
+ m_Children: []
+ m_Father: {fileID: 6092354033751835054}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+ m_AnchorMin: {x: 0, y: 0}
+ m_AnchorMax: {x: 1, y: 1}
+ m_AnchoredPosition: {x: 0, y: 0}
+ m_SizeDelta: {x: 0, y: 0}
+ m_Pivot: {x: 0.5, y: 0.5}
+--- !u!222 &4081166362944202792
+CanvasRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 4979140711266332923}
+ m_CullTransparentMesh: 0
+--- !u!114 &5782514664531182739
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 4979140711266332923}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ m_Material: {fileID: 0}
+ m_Color: {r: 1, g: 1, b: 1, a: 1}
+ m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
+ m_OnCullStateChanged:
+ m_PersistentCalls:
+ m_Calls: []
+ m_text: Edit <#716B7C>[E]
+ m_isRightToLeft: 0
+ m_fontAsset: {fileID: 11400000, guid: 96ae0a2159a39234f858ea23bdcc74ad, type: 2}
+ m_sharedMaterial: {fileID: 735423033564544980, guid: 96ae0a2159a39234f858ea23bdcc74ad, type: 2}
+ m_fontSharedMaterials: []
+ m_fontMaterial: {fileID: 0}
+ m_fontMaterials: []
+ m_fontColor32:
+ serializedVersion: 2
+ rgba: 4294769916
+ m_fontColor: {r: 0.9882353, g: 0.9882353, b: 0.9882353, a: 1}
+ m_enableVertexGradient: 0
+ m_colorMode: 3
+ m_fontColorGradient:
+ topLeft: {r: 1, g: 1, b: 1, a: 1}
+ topRight: {r: 1, g: 1, b: 1, a: 1}
+ bottomLeft: {r: 1, g: 1, b: 1, a: 1}
+ bottomRight: {r: 1, g: 1, b: 1, a: 1}
+ m_fontColorGradientPreset: {fileID: 0}
+ m_spriteAsset: {fileID: 0}
+ m_tintAllSprites: 0
+ m_StyleSheet: {fileID: 0}
+ m_TextStyleHashCode: -1183493901
+ m_overrideHtmlColors: 0
+ m_faceColor:
+ serializedVersion: 2
+ rgba: 4294967295
+ m_fontSize: 12
+ m_fontSizeBase: 12
+ m_fontWeight: 400
+ m_enableAutoSizing: 0
+ m_fontSizeMin: 10
+ m_fontSizeMax: 14
+ m_fontStyle: 16
+ m_HorizontalAlignment: 2
+ m_VerticalAlignment: 512
+ m_textAlignment: 65535
+ m_characterSpacing: 0
+ m_characterHorizontalScale: 1
+ m_wordSpacing: 0
+ m_lineSpacing: 0
+ m_lineSpacingMax: 0
+ m_paragraphSpacing: 0
+ m_charWidthMaxAdj: 0
+ m_TextWrappingMode: 1
+ m_wordWrappingRatios: 0.4
+ m_overflowMode: 0
+ m_linkedTextComponent: {fileID: 0}
+ parentLinkedComponent: {fileID: 0}
+ m_enableKerning: 1
+ m_ActiveFontFeatures: 6e72656b
+ m_enableExtraPadding: 0
+ checkPaddingRequired: 0
+ m_isRichText: 1
+ m_EmojiFallbackSupport: 1
+ m_parseCtrlCharacters: 1
+ m_isOrthographic: 1
+ m_isCullingEnabled: 0
+ m_horizontalMapping: 0
+ m_verticalMapping: 0
+ m_uvLineOffset: 0
+ m_geometrySortingOrder: 0
+ m_IsTextObjectScaleStatic: 0
+ m_VertexBufferAutoSizeReduction: 1
+ m_useMaxVisibleDescender: 1
+ m_pageToDisplay: 1
+ m_margin: {x: 0, y: 0, z: 0, w: 0}
+ m_isUsingLegacyAnimationComponent: 0
+ m_isVolumetricText: 0
+ m_hasFontAssetChanged: 0
+ m_baseMaterial: {fileID: 0}
+ m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
+--- !u!1 &5739133538321435758
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 6023903367326146379}
+ - component: {fileID: 3926131445273152610}
+ - component: {fileID: 7088067592052326316}
+ - component: {fileID: 7106644198525987485}
+ m_Layer: 5
+ m_Name: EmotesButton
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!224 &6023903367326146379
+RectTransform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 5739133538321435758}
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 1
+ m_Children:
+ - {fileID: 3425933121361192302}
+ m_Father: {fileID: 844836543111741865}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+ m_AnchorMin: {x: 0, y: 0.5}
+ m_AnchorMax: {x: 0, y: 0.5}
+ m_AnchoredPosition: {x: 6, y: 0}
+ m_SizeDelta: {x: 120, y: 36}
+ m_Pivot: {x: 0, y: 0.5}
+--- !u!222 &3926131445273152610
+CanvasRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 5739133538321435758}
+ m_CullTransparentMesh: 1
+--- !u!114 &7088067592052326316
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 5739133538321435758}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ m_Material: {fileID: 0}
+ m_Color: {r: 1, g: 0.45490196, b: 0.22352941, a: 1}
+ m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
+ m_OnCullStateChanged:
+ m_PersistentCalls:
+ m_Calls: []
+ m_Sprite: {fileID: 21300000, guid: 12dd1efc4e826764f9b02be515a9a033, type: 3}
+ m_Type: 1
+ m_PreserveAspect: 0
+ m_FillCenter: 1
+ m_FillMethod: 4
+ m_FillAmount: 1
+ m_FillClockwise: 1
+ m_FillOrigin: 0
+ m_UseSpriteMesh: 0
+ m_PixelsPerUnitMultiplier: 2
+--- !u!114 &7106644198525987485
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 5739133538321435758}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ m_Navigation:
+ m_Mode: 3
+ m_WrapAround: 0
+ m_SelectOnUp: {fileID: 0}
+ m_SelectOnDown: {fileID: 0}
+ m_SelectOnLeft: {fileID: 0}
+ m_SelectOnRight: {fileID: 0}
+ m_Transition: 1
+ m_Colors:
+ m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
+ m_HighlightedColor: {r: 0.8867924, g: 0.8867924, b: 0.8867924, a: 1}
+ m_PressedColor: {r: 1, g: 1, b: 1, a: 1}
+ m_SelectedColor: {r: 1, g: 1, b: 1, a: 1}
+ m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
+ m_ColorMultiplier: 1
+ m_FadeDuration: 0
+ m_SpriteState:
+ m_HighlightedSprite: {fileID: 0}
+ m_PressedSprite: {fileID: 0}
+ m_SelectedSprite: {fileID: 0}
+ m_DisabledSprite: {fileID: 0}
+ m_AnimationTriggers:
+ m_NormalTrigger: Normal
+ m_HighlightedTrigger: Highlighted
+ m_PressedTrigger: Pressed
+ m_SelectedTrigger: Selected
+ m_DisabledTrigger: Disabled
+ m_Interactable: 1
+ m_TargetGraphic: {fileID: 7088067592052326316}
+ m_OnClick:
+ m_PersistentCalls:
+ m_Calls: []
+--- !u!1 &5873845419881500616
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 844836543111741865}
+ - component: {fileID: 7835879697316485249}
+ - component: {fileID: 5040959464859691122}
+ m_Layer: 5
+ m_Name: Buttons
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!224 &844836543111741865
+RectTransform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 5873845419881500616}
+ m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children:
+ - {fileID: 6023903367326146379}
+ - {fileID: 623529305124580425}
+ - {fileID: 5444716907913731492}
+ m_Father: {fileID: 3368308072246989936}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+ m_AnchorMin: {x: 0.5, y: 0.5}
+ m_AnchorMax: {x: 0.5, y: 0.5}
+ m_AnchoredPosition: {x: 0, y: 370}
+ m_SizeDelta: {x: 397, y: 48}
+ m_Pivot: {x: 0.5, y: 0.5}
+--- !u!222 &7835879697316485249
+CanvasRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 5873845419881500616}
+ m_CullTransparentMesh: 1
+--- !u!114 &5040959464859691122
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 5873845419881500616}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
+ m_Material: {fileID: 0}
+ m_Color: {r: 0, g: 0, b: 0, a: 0.9490196}
+ m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
+ m_OnCullStateChanged:
+ m_PersistentCalls:
+ m_Calls: []
+ m_Sprite: {fileID: 21300000, guid: 12dd1efc4e826764f9b02be515a9a033, type: 3}
+ m_Type: 1
+ m_PreserveAspect: 0
+ m_FillCenter: 1
+ m_FillMethod: 4
+ m_FillAmount: 1
+ m_FillClockwise: 1
+ m_FillOrigin: 0
+ m_UseSpriteMesh: 0
+ m_PixelsPerUnitMultiplier: 1.5
+--- !u!1 &6662805588403719236
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 3425933121361192302}
+ - component: {fileID: 2754684901851011965}
+ - component: {fileID: 3992472208725122564}
+ m_Layer: 5
+ m_Name: Text
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!224 &3425933121361192302
+RectTransform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 6662805588403719236}
+ m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 6023903367326146379}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+ m_AnchorMin: {x: 0, y: 0}
+ m_AnchorMax: {x: 1, y: 1}
+ m_AnchoredPosition: {x: -0.00016784668, y: -0.00005531311}
+ m_SizeDelta: {x: 0, y: 0}
+ m_Pivot: {x: 0.5, y: 0.5}
+--- !u!222 &2754684901851011965
+CanvasRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 6662805588403719236}
+ m_CullTransparentMesh: 0
+--- !u!114 &3992472208725122564
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 6662805588403719236}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ m_Material: {fileID: 0}
+ m_Color: {r: 1, g: 1, b: 1, a: 1}
+ m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
+ m_OnCullStateChanged:
+ m_PersistentCalls:
+ m_Calls: []
+ m_text: 'Emotes [B] '
+ m_isRightToLeft: 0
+ m_fontAsset: {fileID: 11400000, guid: 96ae0a2159a39234f858ea23bdcc74ad, type: 2}
+ m_sharedMaterial: {fileID: 735423033564544980, guid: 96ae0a2159a39234f858ea23bdcc74ad, type: 2}
+ m_fontSharedMaterials: []
+ m_fontMaterial: {fileID: 0}
+ m_fontMaterials: []
+ m_fontColor32:
+ serializedVersion: 2
+ rgba: 4294967295
+ m_fontColor: {r: 1, g: 1, b: 1, a: 1}
+ m_enableVertexGradient: 0
+ m_colorMode: 3
+ m_fontColorGradient:
+ topLeft: {r: 1, g: 1, b: 1, a: 1}
+ topRight: {r: 1, g: 1, b: 1, a: 1}
+ bottomLeft: {r: 1, g: 1, b: 1, a: 1}
+ bottomRight: {r: 1, g: 1, b: 1, a: 1}
+ m_fontColorGradientPreset: {fileID: 0}
+ m_spriteAsset: {fileID: 0}
+ m_tintAllSprites: 0
+ m_StyleSheet: {fileID: 0}
+ m_TextStyleHashCode: -1183493901
+ m_overrideHtmlColors: 0
+ m_faceColor:
+ serializedVersion: 2
+ rgba: 4294967295
+ m_fontSize: 18
+ m_fontSizeBase: 18
+ m_fontWeight: 400
+ m_enableAutoSizing: 0
+ m_fontSizeMin: 10
+ m_fontSizeMax: 12
+ m_fontStyle: 0
+ m_HorizontalAlignment: 2
+ m_VerticalAlignment: 512
+ m_textAlignment: 65535
+ m_characterSpacing: 0
+ m_characterHorizontalScale: 1
+ m_wordSpacing: 0
+ m_lineSpacing: 0
+ m_lineSpacingMax: 0
+ m_paragraphSpacing: 0
+ m_charWidthMaxAdj: 0
+ m_TextWrappingMode: 1
+ m_wordWrappingRatios: 0.4
+ m_overflowMode: 0
+ m_linkedTextComponent: {fileID: 0}
+ parentLinkedComponent: {fileID: 0}
+ m_enableKerning: 1
+ m_ActiveFontFeatures: 6e72656b
+ m_enableExtraPadding: 0
+ checkPaddingRequired: 0
+ m_isRichText: 1
+ m_EmojiFallbackSupport: 1
+ m_parseCtrlCharacters: 1
+ m_isOrthographic: 1
+ m_isCullingEnabled: 0
+ m_horizontalMapping: 0
+ m_verticalMapping: 0
+ m_uvLineOffset: 0
+ m_geometrySortingOrder: 0
+ m_IsTextObjectScaleStatic: 0
+ m_VertexBufferAutoSizeReduction: 1
+ m_useMaxVisibleDescender: 1
+ m_pageToDisplay: 1
+ m_margin: {x: 0, y: 0, z: 0, w: 0}
+ m_isUsingLegacyAnimationComponent: 0
+ m_isVolumetricText: 0
+ m_hasFontAssetChanged: 0
+ m_baseMaterial: {fileID: 0}
+ m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
+--- !u!1 &6941404510284847514
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 2445375703582184909}
+ - component: {fileID: 5505085621591519339}
+ - component: {fileID: 8746213363341550890}
+ - component: {fileID: 8782135904965761850}
+ - component: {fileID: 6630055616766752555}
+ - component: {fileID: 1479204841276357964}
+ m_Layer: 5
+ m_Name: CloseButton
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 0
+--- !u!224 &2445375703582184909
+RectTransform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 6941404510284847514}
+ m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 1
+ m_Children:
+ - {fileID: 6387641994105437130}
+ m_Father: {fileID: 3368308072246989936}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+ m_AnchorMin: {x: 0.5, y: 0.5}
+ m_AnchorMax: {x: 0.5, y: 0.5}
+ m_AnchoredPosition: {x: 300, y: 226}
+ m_SizeDelta: {x: 36, y: 36}
+ m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &5505085621591519339
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 6941404510284847514}
- m_CullTransparentMesh: 0
---- !u!114 &8746213363341550890
+ m_GameObject: {fileID: 6941404510284847514}
+ m_CullTransparentMesh: 0
+--- !u!114 &8746213363341550890
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 6941404510284847514}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ m_Material: {fileID: 0}
+ m_Color: {r: 1, g: 1, b: 1, a: 1}
+ m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
+ m_OnCullStateChanged:
+ m_PersistentCalls:
+ m_Calls: []
+ m_Sprite: {fileID: 21300000, guid: 12dd1efc4e826764f9b02be515a9a033, type: 3}
+ m_Type: 1
+ m_PreserveAspect: 0
+ m_FillCenter: 1
+ m_FillMethod: 4
+ m_FillAmount: 1
+ m_FillClockwise: 1
+ m_FillOrigin: 0
+ m_UseSpriteMesh: 0
+ m_PixelsPerUnitMultiplier: 2.5
+--- !u!114 &8782135904965761850
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 6941404510284847514}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ m_Navigation:
+ m_Mode: 3
+ m_WrapAround: 0
+ m_SelectOnUp: {fileID: 0}
+ m_SelectOnDown: {fileID: 0}
+ m_SelectOnLeft: {fileID: 0}
+ m_SelectOnRight: {fileID: 0}
+ m_Transition: 1
+ m_Colors:
+ m_NormalColor: {r: 0.08627451, g: 0.08235294, b: 0.09411765, a: 1}
+ m_HighlightedColor: {r: 0.2627451, g: 0.2509804, b: 0.2901961, a: 1}
+ m_PressedColor: {r: 0, g: 0, b: 0, a: 1}
+ m_SelectedColor: {r: 0.08627451, g: 0.08235294, b: 0.09411765, a: 1}
+ m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
+ m_ColorMultiplier: 1
+ m_FadeDuration: 0.1
+ m_SpriteState:
+ m_HighlightedSprite: {fileID: 0}
+ m_PressedSprite: {fileID: 0}
+ m_SelectedSprite: {fileID: 0}
+ m_DisabledSprite: {fileID: 0}
+ m_AnimationTriggers:
+ m_NormalTrigger: Normal
+ m_HighlightedTrigger: Highlighted
+ m_PressedTrigger: Pressed
+ m_SelectedTrigger: Selected
+ m_DisabledTrigger: Disabled
+ m_Interactable: 1
+ m_TargetGraphic: {fileID: 8746213363341550890}
+ m_OnClick:
+ m_PersistentCalls:
+ m_Calls: []
+--- !u!95 &6630055616766752555
+Animator:
+ serializedVersion: 7
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 6941404510284847514}
+ m_Enabled: 1
+ m_Avatar: {fileID: 0}
+ m_Controller: {fileID: 9100000, guid: c22edc5eaba53f342bc161d6cbd411de, type: 2}
+ m_CullingMode: 0
+ m_UpdateMode: 0
+ m_ApplyRootMotion: 0
+ m_LinearVelocityBlending: 0
+ m_StabilizeFeet: 0
+ m_AnimatePhysics: 0
+ m_WarningMessage:
+ m_HasTransformHierarchy: 1
+ m_AllowConstantClipSamplingOptimization: 1
+ m_KeepAnimatorStateOnDisable: 0
+ m_WriteDefaultValuesOnDisable: 0
+--- !u!114 &1479204841276357964
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 6941404510284847514}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 6a66e979ca644f3d91fd0980c5cd5f5f, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+