This commit is contained in:
2026-06-05 21:24:41 +07:00
parent 91183760fb
commit 98806b862d
16 changed files with 6386 additions and 3028 deletions

View File

@@ -0,0 +1,133 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
namespace Hallucinate.AI
{
public class ConversationManager : MonoBehaviour
{
public static ConversationManager Instance { get; private set; }
[Header("Settings")]
public int maxSimultaneousConversations = 3;
public float maxConversationDuration = 120f; // 2 minutes
private List<ConversationSession> activeSessions = new List<ConversationSession>();
private void Awake()
{
if (Instance == null) Instance = this;
else Destroy(gameObject);
}
public bool CanStartConversation()
{
return activeSessions.Count < maxSimultaneousConversations;
}
public void StartConversation(EnemyAI initiator, EnemyAI responder)
{
if (!CanStartConversation()) return;
ConversationSession session = new ConversationSession(initiator, responder, maxConversationDuration);
activeSessions.Add(session);
StartCoroutine(RunConversation(session));
}
private IEnumerator RunConversation(ConversationSession session)
{
Debug.Log($"<color=cyan>[ConvManager]</color> Starting: {session.initiator.npcName} & {session.responder.npcName}");
// Phase 1: Initiator speaks
bool phase1Complete = false;
session.RequestDialogue(session.initiator, (success) => phase1Complete = true);
float startTime = Time.time;
while (!phase1Complete && Time.time < startTime + 10f) yield return null;
if (phase1Complete && !session.isInterrupted)
{
yield return new WaitForSeconds(4f); // Reading time
// Phase 2: Responder speaks
bool phase2Complete = false;
session.RequestDialogue(session.responder, (success) => phase2Complete = true);
float phase2StartTime = Time.time;
while (!phase2Complete && Time.time < phase2StartTime + 10f) yield return null;
}
yield return new WaitForSeconds(4f);
EndConversation(session);
}
public void EndConversation(ConversationSession session)
{
if (activeSessions.Contains(session))
{
session.Cleanup();
activeSessions.Remove(session);
Debug.Log($"<color=cyan>[ConvManager]</color> Ended session. Active: {activeSessions.Count}");
}
}
public void InterruptConversation(EnemyAI npc)
{
ConversationSession session = activeSessions.Find(s => s.initiator == npc || s.responder == npc);
if (session != null)
{
session.isInterrupted = true;
EndConversation(session);
}
}
}
public class ConversationSession
{
public EnemyAI initiator;
public EnemyAI responder;
public float durationLimit;
public bool isInterrupted;
public ConversationSession(EnemyAI initiator, EnemyAI responder, float limit)
{
this.initiator = initiator;
this.responder = responder;
this.durationLimit = limit;
initiator.isTalking = true;
responder.isTalking = true;
// Set references for Gizmos and Facing
initiator.SetTalkingPartner(responder);
responder.SetTalkingPartner(initiator);
}
public void RequestDialogue(EnemyAI speaker, Action<bool> callback)
{
if (isInterrupted) { callback?.Invoke(false); return; }
EnemyAI listener = (speaker == initiator) ? responder : initiator;
// Face each other
speaker.FaceTarget(listener.transform.position);
listener.FaceTarget(speaker.transform.position);
string prompt = $"You are {speaker.npcName} talking to {listener.npcName}. Previous context: None. " +
"Keep it natural and short.";
GeminiService.Instance.GetResponse(speaker.persona, prompt, (json) => {
if (isInterrupted) { callback?.Invoke(false); return; }
speaker.ProcessDialogueResult(json);
callback?.Invoke(true);
});
}
public void Cleanup()
{
if (initiator != null) initiator.isTalking = false;
if (responder != null) responder.isTalking = false;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ebf63e5e8f429234b89a746833c4ca4e

View File

@@ -1,8 +1,13 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
using System.Linq;
using Random = UnityEngine.Random;
[Serializable]
public class DialogueResult { public string text; public float speedMod; public float suspicionMod; }
// Quy trình ưu tiên: Né đòn --> Bắn hạ (Artifact) --> Đuổi theo (Vector) --> Điều tra --> Nói chuyện --> Đi tuần
[RequireComponent(typeof(NavMeshAgent))]
@@ -42,13 +47,19 @@ public class EnemyAI : MonoBehaviour
[Header("Conversation Settings")]
public string npcName = "Guard";
[TextArea] public string persona = "You are a grumpy guard protecting gold.";
public float talkRange = 10f;
public float talkCooldown = 15f;
[TextArea] public string persona = "You are a bored security guard. You love coffee and hate night shifts.";
public float talkRange = 12f;
public float talkCooldown = 60f;
private float lastTalkTime;
public bool isTalking; // Public để debug
private EnemyAI talkingPartner;
private Hallucinate.UI.ChatBubble chatBubble;
[Header("Suspicion Settings")]
public float suspicionLevel = 0f;
public float investigationThreshold = 30f;
public float alertNeighborsThreshold = 70f;
public float alertRange = 20f;
public Node rootNode;
@@ -59,7 +70,6 @@ public class EnemyAI : MonoBehaviour
fov = GetComponent<FieldOfView>();
chatBubble = GetComponentInChildren<Hallucinate.UI.ChatBubble>(true);
// Rigidbody setup cho Unity 6
rb.isKinematic = true;
rb.freezeRotation = true;
@@ -69,18 +79,7 @@ public class EnemyAI : MonoBehaviour
if (playerObj != null) player = playerObj.transform;
}
/*
// Tạm thời comment đoạn này để tránh lỗi Tag chưa định nghĩa
if (patrolWaypoints == null || patrolWaypoints.Length == 0)
{
patrolWaypoints = GameObject.FindGameObjectsWithTag("PatrolPoint")
.Select(go => go.transform).ToArray();
}
*/
InitTree();
Debug.Log($"<color=white>[AI {npcName}] Init complete. Waypoints: {patrolWaypoints.Length}</color>");
}
void InitTree()
@@ -107,12 +106,10 @@ public class EnemyAI : MonoBehaviour
{
if (player == null) return;
// An toàn cho NavMeshAgent
if (!agent.isOnNavMesh)
{
Debug.LogWarning($"[AI {npcName}] NPC is NOT on NavMesh!");
return;
}
if (!agent.isOnNavMesh) return;
// Decay suspicion
suspicionLevel = Mathf.Max(0, suspicionLevel - Time.deltaTime * 0.5f);
if (!isTalking && !isDodging && agent.isStopped)
agent.isStopped = false;
@@ -139,13 +136,21 @@ public class EnemyAI : MonoBehaviour
private NodeState CheckCanSeePlayer()
{
bool canSee = fov != null && fov.canSeePlayer;
if (canSee) StopConversation();
if (canSee) { StopConversation(); AlertNeighbors(); suspicionLevel = 100; }
return canSee ? NodeState.Success : NodeState.Failure;
}
private NodeState CheckHasInvestigateTarget()
{
return (fov != null && fov.lastKnownPlayerPosition != Vector3.zero) ? NodeState.Success : NodeState.Failure;
if (fov != null && fov.lastKnownPlayerPosition != Vector3.zero)
{
if (suspicionLevel > investigationThreshold)
{
// Randomly decide to check or stay on patrol
if (Random.value < (suspicionLevel / 100f)) return NodeState.Success;
}
}
return NodeState.Failure;
}
private NodeState CheckCanTalkToNPC()
@@ -154,7 +159,14 @@ public class EnemyAI : MonoBehaviour
if (Time.time < lastTalkTime + talkCooldown) return NodeState.Failure;
if (isTalking) return NodeState.Success;
// Quét tìm NPC
if (Hallucinate.AI.ConversationManager.Instance == null)
{
Debug.LogError($"[AI {npcName}] ConversationManager Instance is NULL!");
return NodeState.Failure;
}
if (!Hallucinate.AI.ConversationManager.Instance.CanStartConversation()) return NodeState.Failure;
Collider[] hitColliders = Physics.OverlapSphere(transform.position, talkRange);
foreach (var hit in hitColliders)
{
@@ -163,10 +175,12 @@ public class EnemyAI : MonoBehaviour
EnemyAI other = hit.GetComponentInParent<EnemyAI>();
if (other != null && !other.isTalking && Time.time >= other.lastTalkTime + talkCooldown)
{
// Chỉ ID nhỏ hơn gọi để tránh trùng
if (gameObject.GetInstanceID() < other.gameObject.GetInstanceID())
// Kiểm tra khoảng cách thực tế giữa 2 NPC
float dist = Vector3.Distance(transform.position, other.transform.position);
if (dist <= talkRange && gameObject.GetInstanceID() < other.gameObject.GetInstanceID())
{
talkingPartner = other;
Debug.Log($"<color=green>[AI {npcName}]</color> Found partner: {other.npcName}. Starting conversation.");
Hallucinate.AI.ConversationManager.Instance.StartConversation(this, other);
return NodeState.Success;
}
}
@@ -178,50 +192,75 @@ public class EnemyAI : MonoBehaviour
#region ACTIONS
public void HearNoise(Vector3 location, float volume)
{
suspicionLevel += volume * 15f;
if (fov != null) fov.lastKnownPlayerPosition = location;
if (suspicionLevel >= alertNeighborsThreshold) AlertNeighbors();
StopConversation();
Debug.Log($"<color=orange>[AI {npcName}]</color> Heard noise! Suspicion: {suspicionLevel}");
}
public void AlertNeighbors()
{
Collider[] hitColliders = Physics.OverlapSphere(transform.position, alertRange);
foreach (var hit in hitColliders)
{
EnemyAI neighbor = hit.GetComponentInParent<EnemyAI>();
if (neighbor != null && neighbor != this)
{
neighbor.suspicionLevel = Mathf.Max(neighbor.suspicionLevel, 50f);
if (fov != null && neighbor.fov != null) neighbor.fov.lastKnownPlayerPosition = fov.lastKnownPlayerPosition;
}
}
}
private NodeState ActionTalk()
{
if (talkingPartner == null) return NodeState.Failure;
if (!isTalking)
if (isTalking)
{
isTalking = true;
agent.isStopped = true;
FaceTarget(talkingPartner.transform.position);
Debug.Log($"<color=yellow>[AI {npcName}] Talking to {talkingPartner.npcName}</color>");
string prompt = $"You are {npcName}. Speak 1 short sentence in English to your colleague {talkingPartner.npcName} about the shift.";
Hallucinate.AI.GeminiService.Instance.GetResponse(persona, prompt, (response) => {
if (chatBubble != null) chatBubble.Show(response);
Invoke(nameof(EndConversation), 5f);
});
talkingPartner.OnPartnerTalked(this);
// Nếu bạn diễn đi quá xa trong khi đang nói, ngắt hội thoại
if (talkingPartner != null)
{
if (Vector3.Distance(transform.position, talkingPartner.transform.position) > talkRange + 2f)
{
Debug.Log($"[AI {npcName}] Partner moved too far. Ending conversation.");
StopConversation();
return NodeState.Failure;
}
}
return NodeState.Running;
}
return NodeState.Running;
return NodeState.Failure;
}
public void OnPartnerTalked(EnemyAI partner)
public void ProcessDialogueResult(string json)
{
isTalking = true;
talkingPartner = partner;
agent.isStopped = true;
FaceTarget(partner.transform.position);
Invoke(nameof(EndConversation), 6f);
}
private void EndConversation()
{
isTalking = false;
lastTalkTime = Time.time;
if (agent != null && agent.isOnNavMesh) agent.isStopped = false;
talkingPartner = null;
try
{
DialogueResult result = JsonUtility.FromJson<DialogueResult>(json);
if (chatBubble != null) chatBubble.Show(result.text);
// Apply minor stat mods
moveSpeed += result.speedMod;
suspicionLevel = Mathf.Clamp(suspicionLevel + result.suspicionMod, 0, 100);
lastTalkTime = Time.time;
Debug.Log($"<color=green>[AI {npcName}]</color> Conv result: {result.text} | SpeedMod: {result.speedMod}");
}
catch { if (chatBubble != null) chatBubble.Show(json); }
}
private void StopConversation()
{
if (!isTalking) return;
CancelInvoke(nameof(EndConversation));
EndConversation();
if (chatBubble != null) chatBubble.Show("Wait, what's that?!", 2f);
if (isTalking && Hallucinate.AI.ConversationManager.Instance != null)
{
Hallucinate.AI.ConversationManager.Instance.InterruptConversation(this);
if (chatBubble != null) chatBubble.Show("Wait, what was that?!", 2f);
}
}
private NodeState ActionPatrol()
@@ -229,7 +268,7 @@ public class EnemyAI : MonoBehaviour
if (patrolWaypoints == null || patrolWaypoints.Length == 0) return NodeState.Failure;
agent.isStopped = false;
agent.speed = moveSpeed * 0.5f;
agent.speed = moveSpeed * (suspicionLevel > 20 ? 0.7f : 0.5f); // Walk faster if suspicious
var target = patrolWaypoints[currentWaypointIndex];
agent.SetDestination(target.position);
@@ -259,10 +298,16 @@ public class EnemyAI : MonoBehaviour
agent.isStopped = false;
agent.speed = moveSpeed * 0.7f;
agent.SetDestination(fov.lastKnownPlayerPosition);
if (Vector3.Distance(transform.position, fov.lastKnownPlayerPosition) < 1.5f)
{
fov.lastKnownPlayerPosition = Vector3.zero;
return NodeState.Success;
currentWaitTime += Time.deltaTime;
if (currentWaitTime > 3f) // Look around for 3 seconds
{
fov.lastKnownPlayerPosition = Vector3.zero;
suspicionLevel *= 0.5f; // Decrease suspicion after check
return NodeState.Success;
}
}
return NodeState.Running;
}
@@ -300,7 +345,12 @@ public class EnemyAI : MonoBehaviour
isDodging = false;
}
private void FaceTarget(Vector3 pos)
public void SetTalkingPartner(EnemyAI partner)
{
talkingPartner = partner;
}
public void FaceTarget(Vector3 pos)
{
Vector3 dir = (pos - transform.position);
dir.y = 0;

View File

@@ -21,9 +21,21 @@ namespace Hallucinate.AI
public class GeminiService : MonoBehaviour
{
public static GeminiService Instance { get; private set; }
private int activeRequests = 0;
private const int MAX_CONCURRENT_REQUESTS = 5;
[SerializeField] private string apiKey = "AQ.Ab8RN6I2hU_p8yHiPNNHtWzYBiLugbPP22gC6lzTWaYEWj4v0g"; // Replace with your key
[SerializeField] private string geminiURL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-flash-latest:generateContent";
[SerializeField] private string[] apiKeys = { "YOUR_KEY_1", "YOUR_KEY_2" };
private int currentKeyIndex = 0;
[SerializeField] private string geminiURL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent";
private float nextRequestTime = 0f;
private string[] fallbackDialogues = {
"{ \"text\": \"Nice weather, isn't it?\", \"speedMod\": 0.0, \"suspicionMod\": -5.0 }",
"{ \"text\": \"Did you hear something? Probably just a rat.\", \"speedMod\": 0.0, \"suspicionMod\": 2.0 }",
"{ \"text\": \"I'm so tired of this shift.\", \"speedMod\": -0.1, \"suspicionMod\": 0.0 }",
"{ \"text\": \"Don't forget the coffee break later.\", \"speedMod\": 0.0, \"suspicionMod\": -2.0 }"
};
private void Awake()
{
@@ -31,24 +43,50 @@ namespace Hallucinate.AI
else { Destroy(gameObject); }
}
private string GetNextKey()
{
if (apiKeys == null || apiKeys.Length == 0) return "";
string key = apiKeys[currentKeyIndex];
currentKeyIndex = (currentKeyIndex + 1) % apiKeys.Length;
return key;
}
public void GetResponse(string persona, string prompt, Action<string> onComplete)
{
if (Time.time < nextRequestTime)
{
Debug.LogWarning("[Gemini] API is cooling down. Using fallback.");
onComplete?.Invoke(fallbackDialogues[UnityEngine.Random.Range(0, fallbackDialogues.Length)]);
return;
}
if (activeRequests >= MAX_CONCURRENT_REQUESTS)
{
onComplete?.Invoke(fallbackDialogues[UnityEngine.Random.Range(0, fallbackDialogues.Length)]);
return;
}
StartCoroutine(PostRequest(persona, prompt, onComplete));
}
private IEnumerator PostRequest(string persona, string prompt, Action<string> onComplete)
{
activeRequests++;
string jsonInstruction = " Respond ONLY with a JSON object: { 'text': 'your dialogue', 'speedMod': 0.0, 'suspicionMod': 0.0 }.";
string escapedPersona = persona.Replace("\"", "\\\"");
string escapedPrompt = prompt.Replace("\"", "\\\"");
var jsonBody = $@"{{
""systemInstruction"": {{""parts"": [{{ ""text"": ""{persona}"" }}]}},
""contents"": [{{""parts"": [{{ ""text"": ""{prompt}"" }}]}}],
""systemInstruction"": {{""parts"": [{{ ""text"": ""{escapedPersona} {jsonInstruction}"" }}]}},
""contents"": [{{""parts"": [{{ ""text"": ""{escapedPrompt}"" }}]}}],
""generationConfig"": {{
""maxOutputTokens"": 60,
""temperature"": 0.7
""maxOutputTokens"": 100,
""temperature"": 0.7,
""responseMimeType"": ""application/json""
}}
}}";
var requestURL = $"{geminiURL}?key={apiKey}";
var requestURL = $"{geminiURL}?key={GetNextKey()}";
using (var request = new UnityWebRequest(requestURL, "POST"))
{
@@ -61,34 +99,25 @@ namespace Hallucinate.AI
if (request.result == UnityWebRequest.Result.Success)
{
string rawResponse = request.downloadHandler.text;
try
var response = JsonUtility.FromJson<GeminiResponse>(request.downloadHandler.text);
if (response?.candidates?.Length > 0 && response.candidates[0].content?.parts?.Length > 0)
{
var response = JsonUtility.FromJson<GeminiResponse>(rawResponse);
if (response != null &&
response.candidates != null &&
response.candidates.Length > 0 &&
response.candidates[0].content != null &&
response.candidates[0].content.parts != null &&
response.candidates[0].content.parts.Length > 0)
{
onComplete?.Invoke(response.candidates[0].content.parts[0].text);
}
else
{
Debug.LogWarning($"[Gemini] Response structure invalid or blocked. Raw: {rawResponse}");
}
}
catch (Exception e)
{
Debug.LogError($"[Gemini] JSON Parse Error: {e.Message}\nRaw Response: {rawResponse}");
onComplete?.Invoke(response.candidates[0].content.parts[0].text);
}
}
else
{
Debug.LogError($"[Gemini] API Error: {request.error}");
if (request.responseCode == 429)
{
nextRequestTime = Time.time + 60f; // Lock API for 1 minute
Debug.LogWarning("Quota Exceeded. API locked for 60s. Using fallback.");
}
onComplete?.Invoke(fallbackDialogues[UnityEngine.Random.Range(0, fallbackDialogues.Length)]);
}
}
activeRequests--;
}
}
}

View File

@@ -17,7 +17,12 @@ public class GeminiTest : MonoBehaviour
string testPrompt = "Chào bạn, nếu bạn nhận được tin nhắn này, hãy trả lời: 'Kết nối Gemini thành công!'";
GeminiService.Instance.GetResponse(testPersona, testPrompt, (response) => {
Debug.Log($"<color=green>[Gemini Test] Phản hồi từ API:</color> {response}");
string finalMsg = response;
try {
DialogueResult result = JsonUtility.FromJson<DialogueResult>(response);
finalMsg = result.text;
} catch { }
Debug.Log($"<color=green>[Gemini Test] Phản hồi từ API:</color> {finalMsg}");
});
}
}

View File

@@ -67,12 +67,17 @@ public class GerminiNPC : MonoBehaviour
string prompt = $"Ta muốn bán cho ông món đồ này: {playerHeldItem}";
Hallucinate.AI.GeminiService.Instance.GetResponse(npcPersona, prompt, (response) => {
Debug.Log($"<color=green>Tom:</color> {response}");
string finalMsg = response;
try {
DialogueResult result = JsonUtility.FromJson<DialogueResult>(response);
finalMsg = result.text;
} catch { }
Debug.Log($"<color=green>Tom:</color> {finalMsg}");
AudioManager.Instance?.Play(responseSound, position: transform.position);
// Nếu có ChatBubble gắn kèm thì hiển thị luôn
var bubble = GetComponentInChildren<Hallucinate.UI.ChatBubble>(true);
if (bubble != null) bubble.Show(response);
if (bubble != null) bubble.Show(finalMsg);
});
yield break;

View File

@@ -0,0 +1,32 @@
using UnityEngine;
namespace Hallucinate.AI
{
public class NoiseEmitter : MonoBehaviour
{
[Header("Settings")]
public float defaultNoiseRange = 10f;
public LayerMask npcLayer;
public void EmitNoise(float volumeMultiplier = 1f)
{
float range = defaultNoiseRange * volumeMultiplier;
Collider[] hitColliders = Physics.OverlapSphere(transform.position, range, npcLayer);
foreach (var hit in hitColliders)
{
EnemyAI npc = hit.GetComponentInParent<EnemyAI>();
if (npc != null)
{
npc.HearNoise(transform.position, volumeMultiplier);
}
}
}
private void OnDrawGizmosSelected()
{
Gizmos.color = new Color(1, 1, 0, 0.3f);
Gizmos.DrawWireSphere(transform.position, defaultNoiseRange);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 67d06596d1741d34594e4a68adcaf257

View File

@@ -38,6 +38,13 @@ namespace Invector
EditorGUILayout.PropertyField(serializedObject.FindProperty("_useTriggerEnter"));
serializedObject.FindProperty("debugTextureName").boolValue = EditorGUILayout.Toggle("Debug Texture Name", serializedObject.FindProperty("debugTextureName").boolValue);
GUILayout.BeginVertical("box");
GUILayout.Box("AI Noise Settings", GUILayout.ExpandWidth(true));
EditorGUILayout.PropertyField(serializedObject.FindProperty("emitAINoise"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("aiNoiseRange"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("npcLayer"));
GUILayout.EndVertical();
if (serializedObject.FindProperty("animationType").enumValueIndex == (int)AnimationType.Humanoid)
{
GUILayout.BeginHorizontal("box");

View File

@@ -20,6 +20,11 @@ namespace Invector
public bool SpawnParticle { get { return _spawnParticle; } set { _spawnParticle = value; } }
public bool SpawnStepMark { get { return _spawnStepMark; } set { _spawnStepMark = value; } }
[Header("AI Noise Settings")]
public bool emitAINoise = true;
public float aiNoiseRange = 10f;
public LayerMask npcLayer;
protected int surfaceIndex = 0;
protected Terrain terrain;
protected TerrainCollider terrainCollider;
@@ -248,6 +253,37 @@ namespace Invector
currentFootStep.spawnParticleEffect = SpawnParticle;
currentFootStep.spawnStepMarkEffect = SpawnStepMark;
SpawnSurfaceEffect(currentFootStep);
if (emitAINoise) EmitAINoise();
}
}
protected virtual void EmitAINoise()
{
float currentRange = aiNoiseRange;
float currentVolume = Volume;
// Kiểm tra trạng thái ngồi từ Animator
Animator anim = GetComponent<Animator>();
if (anim != null)
{
// Nếu đang ngồi (IsCrouching = true), giảm 50% vùng phát hiện và âm lượng
if (anim.GetBool("IsCrouching"))
{
currentRange *= 0.5f;
currentVolume *= 0.5f;
}
}
// Tìm tất cả Collider trong bán kính tiếng động thuộc Layer NPC
Collider[] hitColliders = Physics.OverlapSphere(transform.position, currentRange, npcLayer);
foreach (var hit in hitColliders)
{
var npc = hit.GetComponentInParent<EnemyAI>();
if (npc != null)
{
npc.HearNoise(transform.position, currentVolume);
}
}
}

View File

@@ -115,7 +115,7 @@ Material:
- _ZWrite: 0
m_Colors:
- _CameraFadeParams: {r: 0, g: Infinity, b: 0, a: 0}
- _Color: {r: 0.6132076, g: 0.1454958, b: 0.118592024, a: 0.1882353}
- _Color: {r: 1, g: 0.8260788, b: 0.08962262, a: 0.25}
- _Emission: {r: 0, g: 0, b: 0, a: 0}
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
- _Flip: {r: 1, g: 1, b: 1, a: 1}
@@ -123,6 +123,6 @@ Material:
- _SoftParticleFadeParams: {r: 0, g: 0, b: 0, a: 0}
- _SpecColor: {r: 0, g: 0, b: 0, a: 0}
- _Specular: {r: 1, g: 1, b: 1, a: 0}
- _TintColor: {r: 0.6132076, g: 0.1454958, b: 0.118592024, a: 0.1882353}
- _TintColor: {r: 1, g: 0.8260788, b: 0.08962262, a: 0.25}
m_BuildTextureStacks: []
m_AllowLocking: 1