Project_WL/Assets/WL/IslandMob/WLIslandAvatar.cs

347 lines
18 KiB
C#

// ─────────────────────────────────────────────────────────────────────────────
// WLIslandAvatar.cs — 섬의 기본 아바타 = 힘민지 전투 PC · FI 농부 = 보이지 않는 그림자 (#822 · PD 역제안)
//
// ■ 원리
// · 전투 모드(기본): `MyValue.MyPC` 를 켜서 FI 농부 자리에 세우고, FI 조이스틱(SimpleInput "Horizontal/Vertical")을
// `PCActor.Run_byJoystick` 에 주입한다(힘민지 FloatingJoystick 과 같은 호출 순서 · 카메라는 FI vcam 이 고정 회전이라 월드 축 그대로).
// FI 농부는 **매 LateUpdate 에 PC 위치로 옮긴다** — 콜라이더·태그 "Player"·PlayerController 가 그대로라
// 발판·상인·확장 가격판·밭·낚시터·과일나무 트리거와 FI 카메라 Follow 가 전부 그대로 돈다. 농부의 치비 모델만 끈다.
// · 농부 모드: FI 가 농사 중(IsWorking) · 조작 잠김(낚시·나무 흔들기 = IsControllable=false) · 밭 가까이 → PC 를 끄고
// 농부 치비를 켠다(섬 기존 동작 그대로). 조건이 풀리면 PC 를 농부 자리에 다시 세운다.
// · 섬 확장 때 FI NavMesh 가 재베이크된다 → 주기적으로 PC 가 NavMesh 밖이면 농부 자리로 되돌린다.
// ■ 원본 수정 0줄 · 기존 `HideWlPc` 는 이 컴포넌트가 켜져 있으면 쓰지 않는다(WLIslandBridge 1줄 분기).
// ─────────────────────────────────────────────────────────────────────────────
using System.Collections;
using System.Collections.Generic;
using CryingSnow.FarmingIsland;
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.SceneManagement;
using WL.Island;
namespace WL.IslandMob
{
public sealed class WLIslandAvatar : MonoBehaviour
{
public enum Mode { Waiting, Combat, Farmer }
public static WLIslandAvatar Instance { get; private set; }
public static bool Enabled { get { var c = WLIslandMobSettings.Instance; return c != null && c.enabled_ != 0 && c.combatAvatarEnabled != 0; } }
public static string LastLog = "";
public static int ModeSwitches, NavRecoveries;
public Mode CurrentMode { get; private set; } = Mode.Waiting;
public bool PcMoving { get; private set; }
WLIslandMobSettings _cfg;
WLIslandSettings _islandCfg;
PlayerController _fi;
PCActor _pc;
GameObject _fiVisual;
readonly List<Farm> _farms = new List<Farm>(8);
float _farmScanTimer, _navTimer, _farmerExitTimer;
public static WLIslandAvatar Begin(WLIslandSettings islandCfg, Scene scene)
{
if (Instance != null) return Instance;
var cfg = WLIslandMobSettings.Instance;
if (cfg == null || cfg.enabled_ == 0 || cfg.combatAvatarEnabled == 0) return null;
var go = new GameObject("~WL_IslandAvatar");
if (scene.IsValid()) SceneManager.MoveGameObjectToScene(go, scene);
Instance = go.AddComponent<WLIslandAvatar>();
Instance._cfg = cfg;
Instance._islandCfg = islandCfg;
Instance.StartCoroutine(Instance.Co_Setup());
return Instance;
}
void OnEnable() { WL.Combat.Survival.DeathFlow.Died.Add(OnPcDied); }
void OnDisable() { WL.Combat.Survival.DeathFlow.Died.Remove(OnPcDied); }
public static int AutoRevives;
/// <summary>섬에서 PC 가 죽으면 부활 팝업(던전용) 대신 제자리 자동 부활 — 「포기/로비로」 가 섬을 재로드하는 경로를 막는다.</summary>
void OnPcDied(in WL.Combat.Survival.DeathEvent e)
{
if (_cfg == null || _cfg.autoReviveOnIsland == 0) return;
if (WL.Combat.Survival.DeathFlow.Revive()) { AutoRevives++; Log("섬 자동 부활 #" + AutoRevives); }
}
void OnDestroy()
{
if (Instance == this) Instance = null;
// 섬 씬이 내려갈 때(던전 입장) 농부 모델은 원래대로(안 보여도 무해) · PC 는 던전 준비가 다시 다룬다
if (_fiVisual != null) _fiVisual.SetActive(true);
if (_pc != null && PcMoving) { try { _pc.Set_JoystickStatus(0); _pc.Set_AutoBraking(true); } catch { } }
}
IEnumerator Co_Setup()
{
float t = 0f;
while (t < _cfg.pcWaitSeconds)
{
_fi = Object.FindFirstObjectByType<PlayerController>();
if (_fi != null && MyValue.bMyPC && MyValue.MyPC != null) break;
t += Time.unscaledDeltaTime;
yield return null;
}
if (_fi == null || !MyValue.bMyPC || MyValue.MyPC == null)
{
LastLog = "아바타 준비 실패 — FI 플레이어 " + (_fi != null) + " · PC " + (MyValue.bMyPC && MyValue.MyPC != null);
Log(LastLog);
yield break;
}
_pc = MyValue.MyPC;
var vis = _fi.transform.Find(WLIslandSceneSetup.PcVisualName);
_fiVisual = vis != null ? vis.gameObject : null;
RescanFarms();
EnterCombat("시작");
}
void Log(string s) { if (_cfg != null && _cfg.verboseLog != 0) Debug.Log("[WL-822] " + s); }
// ── 모드 전환 ───────────────────────────────────────────────────
// ── 전투 형태 (섬 = BattleMapConfig e_MapType Lobby → 원본 PCActor.Set_Obj 가 로비 애니메이터(공격 없음·무기 없음)를 끼운다) ──
public static int CombatFormApplied;
public string LastFormLog = "";
float _formTimer;
/// <summary>
/// 섬 맵이 「로비」 유형이라 PC 가 로비 형태(pcanim_lobby · 무기 없음)로 세팅된다 → 공격 상태가 없어 때리지 못한다(2026-10-09 PD 실플레이 · 로그 「GotoState: State could not be found」).
/// 표·UI 모드는 그대로 두고, `InGameInfo.m_GameMode` 를 **Set_Obj 호출 동안만** Stage 로 바꿔 원본 경로로 직업 애니메이터·무기·스케일을 끼운 뒤 되돌린다. 자동전투도 켠다(AutoCombatOnEnter 는 로비를 건너뛴다).
/// </summary>
void ApplyCombatForm(string why)
{
var info = InGameInfo.Ins;
if (info == null || _pc == null) return;
EnsureIngameSingletons();
var f = typeof(InGameInfo).GetField("m_GameMode", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
object prev = f != null ? f.GetValue(info) : null;
bool flipped = false;
try
{
if (f != null && prev is eGameMode && (eGameMode)prev == eGameMode.Lobby) { f.SetValue(info, eGameMode.Stage); flipped = true; }
_pc.Set_Obj(); // 직업 애니메이터(비동기 로드) + Change_Weapon + 스케일
}
catch (System.Exception ex) { LastFormLog = "전투 형태 실패 — " + ex.Message; Log(LastFormLog); }
finally { if (flipped) f.SetValue(info, prev); }
try { if (!WL.Combat.Auto.AutoCombatOnEnter.IsAutoOn) _pc.Set_Auto(); } catch (System.Exception ex) { Log("자동전투 ON 실패 — " + ex.Message); }
CombatFormApplied++;
LastFormLog = "전투 형태 적용(" + why + ") #" + CombatFormApplied + " flipped=" + flipped + " auto=" + WL.Combat.Auto.AutoCombatOnEnter.IsAutoOn;
Log(LastFormLog);
}
/// <summary>
/// 🔴 섬은 로비 UI 상태라 전투 HUD 가지(`IngameUIs/MonsterSimpleInfo`)가 한 번도 켜지지 않아 싱글턴 `Ins` 가 null 이다.
/// 원본 `MobActor.Set_Die:444` 가 `MonsterSimpleInfo.Ins.DeadTarget` 을 부르므로 **처치마다 NRE** → `base.Set_Die` 가 끊겨
/// 시체 정리(After_Die)·리젠 예약(actDie)이 영영 안 돈다(2026-10-09 실측: killmobcount 0 · 죽은 몹이 활성 상태로 남음 · 던전 815d2 와 같은 뿌리).
/// 가지를 켜지 않고(HUD 가 보이면 안 된다) **참조만** 채운다 — DeadTarget/Set 은 꺼진 오브젝트에서도 안전하다.
/// </summary>
public static int SingletonFixes;
static void EnsureIngameSingletons()
{
try
{
if (MonsterSimpleInfo.Ins == null)
{
var found = Object.FindFirstObjectByType<MonsterSimpleInfo>(FindObjectsInactive.Include);
if (found != null) { MonsterSimpleInfo.Ins = found; MonsterSimpleInfo.isIns = true; SingletonFixes++; }
}
}
catch (System.Exception ex) { Debug.LogWarning("[WL-822] 싱글턴 참조 보정 실패 — " + ex.Message); }
}
bool PcInLobbyForm()
{
var anim = _pc != null ? _pc.GetComponent<Animator>() : null;
var rac = anim != null ? anim.runtimeAnimatorController : null;
return rac != null && rac.name.IndexOf("lobby", System.StringComparison.OrdinalIgnoreCase) >= 0;
}
public string AnimatorName { get { var anim = _pc != null ? _pc.GetComponent<Animator>() : null; return anim != null && anim.runtimeAnimatorController != null ? anim.runtimeAnimatorController.name : "null"; } }
void EnterCombat(string why)
{
if (_pc == null || _fi == null) return;
if (!_pc.gameObject.activeSelf) _pc.gameObject.SetActive(true);
ApplyCombatForm(why);
WarpPcToFi();
// #893 PD 「일정 범위 내 적을 인식하고 공격」 — 섬 진입이 켠 IsStop(원본 All_Stop)이 남아 조이스틱을 한 번 쓰기 전까지
// MyActor.Update 가 첫 줄에서 빠져 적을 고르지 않았다(실측 IsStop True · 1.96 m 앞 몹 타깃 없음) → 원본 Stop(false) 로 푼다.
try { _pc.Stop(false); } catch (System.Exception ex) { Log("정지 해제 실패 — " + ex.Message); }
if (_fiVisual != null) _fiVisual.SetActive(false);
CurrentMode = Mode.Combat;
ModeSwitches++;
LastLog = "전투 모드(" + why + ") — PC @" + _pc.transform.position.ToString("F1");
Log(LastLog);
}
void EnterFarmer(string why)
{
if (_pc == null || _fi == null) return;
StopPcMove();
if (_pc.gameObject.activeSelf) _pc.gameObject.SetActive(false);
if (_fiVisual != null) _fiVisual.SetActive(true);
CurrentMode = Mode.Farmer;
ModeSwitches++;
LastLog = "농부 모드(" + why + ")";
Log(LastLog);
}
public string LastWarpLog = "";
void WarpPcToFi()
{
Vector3 p = _fi.transform.position;
NavMeshHit hit;
bool onMesh = NavMesh.SamplePosition(p, out hit, _cfg.pcNavSnapRadius, NavMesh.AllAreas);
if (onMesh) p = hit.position;
Vector3 before = _pc.transform.position;
bool dead = false;
try { dead = _pc.IsDead(); } catch { }
try { _pc.Set_Warp(p); } catch (System.Exception ex) { Log("PC 워프 실패 — " + ex.Message); }
var agent = _pc.GetComponent<NavMeshAgent>();
// Set_Warp 가 IsDead(비활성 계층 포함)로 조용히 빠지면 에이전트 Warp 를 직접 쓴다(마지막 수단 · transform 대입은 NavMeshAgent 가 되돌린다)
if ((_pc.transform.position - p).sqrMagnitude > 0.25f && agent != null)
{
agent.enabled = true;
if (!agent.Warp(p)) _pc.transform.position = p;
}
_pc.transform.rotation = Quaternion.Euler(0f, _fi.transform.eulerAngles.y, 0f);
LastWarpLog = "warp " + before.ToString("F1") + " → " + _pc.transform.position.ToString("F1") + " (목표 " + p.ToString("F1") + " navHit=" + onMesh +
" dead=" + dead + " activeInHierarchy=" + _pc.gameObject.activeInHierarchy + " agentOnMesh=" + (agent != null && agent.isOnNavMesh) + ")";
Log(LastWarpLog);
}
// ── 매 프레임 ───────────────────────────────────────────────────
void Update()
{
if (CurrentMode == Mode.Waiting || _pc == null || _fi == null) return;
_farmScanTimer += Time.unscaledDeltaTime;
if (_farmScanTimer >= _cfg.rescanSeconds) { _farmScanTimer = 0f; RescanFarms(); }
bool farmerWanted = FarmerWanted();
if (CurrentMode == Mode.Combat)
{
if (farmerWanted) { EnterFarmer(FarmerReason()); return; }
DrivePc();
CheckPcNav();
// 원본 ReSet(맵 로드·부활 등)이 로비 형태로 되돌리면 다시 전투 형태로
_formTimer += Time.unscaledDeltaTime;
if (_formTimer >= 1f) { _formTimer = 0f; if (PcInLobbyForm()) ApplyCombatForm("로비 형태 감지"); }
}
else // Farmer
{
if (farmerWanted) _farmerExitTimer = 0f;
else
{
_farmerExitTimer += Time.unscaledDeltaTime;
if (_farmerExitTimer >= _cfg.farmerExitDelay) { _farmerExitTimer = 0f; EnterCombat("농사 끝"); }
}
}
}
void LateUpdate()
{
if (CurrentMode != Mode.Combat || _pc == null || _fi == null) return;
// 농부 그림자 = PC 자리(트리거·카메라 Follow 용) — CharacterController 가 켜져 있어도 transform 직접 대입은 허용된다
var p = _pc.transform.position;
_fi.transform.position = p;
_fi.transform.rotation = Quaternion.Euler(0f, _pc.transform.eulerAngles.y, 0f);
}
string _farmerReason = "";
string FarmerReason() { return _farmerReason; }
bool FarmerWanted()
{
if (_fi.IsWorking) { _farmerReason = "농사 중"; return true; }
if (!_fi.IsControllable) { _farmerReason = "조작 잠김(낚시·나무)"; return true; }
if (_fi.FishingRod != null && _fi.FishingRod.activeSelf) { _farmerReason = "낚시"; return true; }
if (_cfg.farmerModeRadius > 0f && NearFarm(_cfg.farmerModeRadius)) { _farmerReason = "밭 근처"; return true; }
return false;
}
void RescanFarms()
{
_farms.Clear();
var arr = Object.FindObjectsByType<Farm>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
for (int i = 0; i < arr.Length; i++) if (arr[i] != null) _farms.Add(arr[i]);
}
bool NearFarm(float radius)
{
Vector3 p = CurrentMode == Mode.Combat ? _pc.transform.position : _fi.transform.position;
float r2 = radius * radius;
for (int i = 0; i < _farms.Count; i++)
{
var f = _farms[i];
if (f == null || !FarmVisible(f)) continue; // 플로우가 숨긴 밭(렌더러 off)은 농작물이 없는 것으로 본다
var col = f.GetComponent<Collider>();
Vector3 c = col != null ? col.ClosestPoint(p) : f.transform.position;
c.y = p.y;
if ((c - p).sqrMagnitude <= r2) return true;
}
return false;
}
static readonly List<Renderer> s_rend = new List<Renderer>(16);
static bool FarmVisible(Farm f)
{
if (!f.gameObject.activeInHierarchy) return false;
s_rend.Clear();
f.GetComponentsInChildren<Renderer>(false, s_rend);
for (int i = 0; i < s_rend.Count; i++) if (s_rend[i] != null && s_rend[i].enabled) return true;
return false;
}
// ── 조이스틱 → PC (힘민지 FloatingJoystick 과 같은 호출 순서) ───
void DrivePc()
{
float h = 0f, v = 0f;
try { h = SimpleInput.GetAxis("Horizontal"); v = SimpleInput.GetAxis("Vertical"); } catch { }
var dir = new Vector3(h, 0f, v);
bool want = dir.sqrMagnitude >= _cfg.moveDeadzone * _cfg.moveDeadzone;
bool can = false;
try { can = _pc.CanMove(); } catch { can = false; }
if (want && can)
{
if (!PcMoving) { _pc.Set_AutoBraking(false); _pc.Set_JoystickStatus(1); PcMoving = true; }
_pc.Run_byJoystick(dir.normalized);
}
else if (PcMoving) StopPcMove();
}
void StopPcMove()
{
if (!PcMoving) return;
PcMoving = false;
try { _pc.Set_JoystickStatus(0); _pc.Set_AutoBraking(true); } catch { }
}
// ── NavMesh 이탈 복구(섬 확장 재베이크) ─────────────────────────
void CheckPcNav()
{
_navTimer += Time.unscaledDeltaTime;
if (_navTimer < _cfg.pcNavCheckSeconds) return;
_navTimer = 0f;
var agent = _pc.GetComponent<NavMeshAgent>();
if (agent == null || !agent.enabled) return;
if (agent.isOnNavMesh) return;
NavMeshHit hit;
if (NavMesh.SamplePosition(_pc.transform.position, out hit, _cfg.pcNavSnapRadius, NavMesh.AllAreas))
{
try { _pc.Set_Warp(hit.position); NavRecoveries++; Log("PC NavMesh 복구 → " + hit.position.ToString("F1")); } catch { }
}
}
public string Describe()
{
return "mode=" + CurrentMode + " reason=" + _farmerReason + " animator=" + AnimatorName + " singletonFix=" + SingletonFixes + " msiIns=" + (MonsterSimpleInfo.Ins != null) + " form=[" + LastFormLog + "] lastWarp=[" + LastWarpLog + "] pcActive=" + (_pc != null && _pc.gameObject.activeSelf) + " fiVisual=" + (_fiVisual != null && _fiVisual.activeSelf) +
" moving=" + PcMoving + " farms=" + _farms.Count + " switches=" + ModeSwitches + " navRecover=" + NavRecoveries +
(_pc != null ? " pc@" + _pc.transform.position.ToString("F1") : "") + (_fi != null ? " fi@" + _fi.transform.position.ToString("F1") : "");
}
}
}