2026-10-09 04:28:39 +00:00
// ─────────────────────────────────────────────────────────────────────────────
// 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 ( ) ;
2026-10-18 15:09:05 +00:00
// #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 ) ; }
2026-10-09 04:28:39 +00:00
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" ) : "" ) ;
}
}
}