347 lines
18 KiB
C#
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") : "");
|
|
}
|
|
}
|
|
}
|