// ───────────────────────────────────────────────────────────────────────────── // 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 _farms = new List(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(); 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; /// 섬에서 PC 가 죽으면 부활 팝업(던전용) 대신 제자리 자동 부활 — 「포기/로비로」 가 섬을 재로드하는 경로를 막는다. 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(); 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; /// /// 섬 맵이 「로비」 유형이라 PC 가 로비 형태(pcanim_lobby · 무기 없음)로 세팅된다 → 공격 상태가 없어 때리지 못한다(2026-10-09 PD 실플레이 · 로그 「GotoState: State could not be found」). /// 표·UI 모드는 그대로 두고, `InGameInfo.m_GameMode` 를 **Set_Obj 호출 동안만** Stage 로 바꿔 원본 경로로 직업 애니메이터·무기·스케일을 끼운 뒤 되돌린다. 자동전투도 켠다(AutoCombatOnEnter 는 로비를 건너뛴다). /// 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); } /// /// 🔴 섬은 로비 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 은 꺼진 오브젝트에서도 안전하다. /// public static int SingletonFixes; static void EnsureIngameSingletons() { try { if (MonsterSimpleInfo.Ins == null) { var found = Object.FindFirstObjectByType(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() : 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() : 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(); // 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(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(); 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 s_rend = new List(16); static bool FarmVisible(Farm f) { if (!f.gameObject.activeInHierarchy) return false; s_rend.Clear(); f.GetComponentsInChildren(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(); 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") : ""); } } }