#821 — FR(FirstRising) 기존 장비 시스템 이식: Assets/WL/Equip (데이터 모델·FR json 로더·인벤/장착/강화/합성(본1+재료2)/강등·ActorStatInfo 가산·던전 드랍·더미 UI·인게임 런처·로컬 세이브) + 설정 SO · 원본 1줄 훅 3곳(ActorStatInfo.Reset · WLIslandBridge.OnStageCleared · DummyMenuButton) · PC 기본 공격력 5 사후 보정 #822 — 섬 몬스터 존(땅 1칸 1곳 · 동시 3 · 누적 10 → 3h 쿨타임 · 라벨 · 몹 HP 18) + 섬 기본 아바타 = 전투 PC(농부 = 투명 그림자 · 밭/낚시 때 농부) · 전투 형태 재적용(로비 맵 유형 우회) · MonsterSimpleInfo 참조 보정(처치 NRE·리젠 중단 해소) · 섬 자동 부활 · WLPcScaleCompensator 안정 값만 보정(연출 펀치 누적 확대 해소) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
b95f043696
commit
64a4c38443
|
|
@ -308,6 +308,7 @@ public class ActorStatInfo
|
|||
Set_JobPassive(classData); // 직업 전용 패시브 값 적용
|
||||
Set_SkillPassive(); // 스킬 패시브 값 적용
|
||||
Set_EquipItem(); // 착용 아이템 값 적용
|
||||
WL.Equip.WLEquipStats.Apply(this, m_Role); // WL-821 FR 장비(레벨표 + 등급 옵션) — PC 만 · 설정 off 면 무동작
|
||||
Set_EquipSlotEnchant(classData); // 착용 슬롯 인챈트 값 적용
|
||||
Set_PCAbility(classData.e_BattleType); // 캐릭터 특성 값 적용
|
||||
Set_PCAwaken(); // 캐릭터 각성 값 적용
|
||||
|
|
|
|||
|
|
@ -38,6 +38,17 @@ namespace WL.Character
|
|||
|
||||
float _lastWritten = float.NaN;
|
||||
|
||||
// WL-822(2026-10-09 PD 「플레이어 크기가 자꾸 커진다」): 원본이 쓰는 값은 두 가지뿐(로비 1.0 · 인게임 f_Scale)이다.
|
||||
// 그 밖의 변화(피격·레벨업 스케일 펀치 등 **일시 연출**)까지 「원본이 새로 썼다」로 보고 ×k 를 또 곱하니
|
||||
// 펀치마다 누적 확대됐다(1.05 → 1.64 → 3.96 실측). 이제 **아는 원본 값**일 때만 보정한다.
|
||||
// 구분 기준 = **안정성**: 원본 대입(1.0 / f_Scale)은 그 값이 그대로 머물고, 연출 펀치는 매 프레임(사인 곡선) 값이 바뀐다.
|
||||
// → 같은 값이 stableFrames 프레임 연속이면 원본 값으로 보고 보정, 아니면 무시.
|
||||
[Tooltip("원본 값으로 인정하기까지 같은 값이 유지돼야 하는 LateUpdate 수")]
|
||||
public int stableFrames = 3;
|
||||
float _pendingValue = float.NaN;
|
||||
int _pendingFrames;
|
||||
public static int Ignored, Applied;
|
||||
|
||||
/// <summary>지금 적용 중인 보정 배수. 스위치가 꺼져 있으면 1.</summary>
|
||||
public float Compensation
|
||||
{
|
||||
|
|
@ -61,7 +72,17 @@ namespace WL.Character
|
|||
// 자기가 마지막으로 쓴 값이면 원본이 아직 안 건드린 것 = 할 일 없음(멱등).
|
||||
if (!float.IsNaN(_lastWritten) && Mathf.Abs(cur - _lastWritten) <= 1e-5f) return;
|
||||
|
||||
// 첫 적용은 즉시(OnEnable 시점 값 = 원본 대입). 이후엔 같은 값이 stableFrames 연속일 때만 원본 대입으로 인정한다.
|
||||
if (!float.IsNaN(_lastWritten))
|
||||
{
|
||||
if (float.IsNaN(_pendingValue) || Mathf.Abs(cur - _pendingValue) > 1e-5f) { _pendingValue = cur; _pendingFrames = 1; Ignored++; return; }
|
||||
_pendingFrames++;
|
||||
if (_pendingFrames < Mathf.Max(1, stableFrames)) { Ignored++; return; }
|
||||
}
|
||||
_pendingValue = float.NaN; _pendingFrames = 0;
|
||||
|
||||
float target = cur * k;
|
||||
Applied++;
|
||||
_lastWritten = target;
|
||||
transform.localScale = new Vector3(target, target, target);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 9d16edef4fa3f734896b8bd61165d630
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 17160f7548ab6a443b330592517769c6
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: edb58ba8a3b5648468a8e17195960065
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 52265444400b870469f05703fac26ec5
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
[
|
||||
{
|
||||
"Index": 0,
|
||||
"ItemGrade": 0,
|
||||
"ResultGrade": 1,
|
||||
"RequiredGradeLevel": 0,
|
||||
"ResultGradeLevel": 0,
|
||||
"SameGroupNo": 1,
|
||||
"RequiredItemCount": 2
|
||||
},
|
||||
{
|
||||
"Index": 1000,
|
||||
"ItemGrade": 1,
|
||||
"ResultGrade": 2,
|
||||
"RequiredGradeLevel": 0,
|
||||
"ResultGradeLevel": 0,
|
||||
"SameGroupNo": 1,
|
||||
"RequiredItemCount": 2
|
||||
},
|
||||
{
|
||||
"Index": 2000,
|
||||
"ItemGrade": 2,
|
||||
"ResultGrade": 3,
|
||||
"RequiredGradeLevel": 0,
|
||||
"ResultGradeLevel": 0,
|
||||
"SameGroupNo": 1,
|
||||
"RequiredItemCount": 2
|
||||
},
|
||||
{
|
||||
"Index": 3000,
|
||||
"ItemGrade": 3,
|
||||
"ResultGrade": 3,
|
||||
"RequiredGradeLevel": 0,
|
||||
"ResultGradeLevel": 1,
|
||||
"SameGroupNo": 0,
|
||||
"RequiredItemCount": 1
|
||||
},
|
||||
{
|
||||
"Index": 3001,
|
||||
"ItemGrade": 3,
|
||||
"ResultGrade": 3,
|
||||
"RequiredGradeLevel": 0,
|
||||
"ResultGradeLevel": 2,
|
||||
"SameGroupNo": 0,
|
||||
"RequiredItemCount": 2
|
||||
},
|
||||
{
|
||||
"Index": 3002,
|
||||
"ItemGrade": 3,
|
||||
"ResultGrade": 4,
|
||||
"RequiredGradeLevel": 2,
|
||||
"ResultGradeLevel": 0,
|
||||
"SameGroupNo": 1,
|
||||
"RequiredItemCount": 1
|
||||
},
|
||||
{
|
||||
"Index": 4000,
|
||||
"ItemGrade": 4,
|
||||
"ResultGrade": 4,
|
||||
"RequiredGradeLevel": 0,
|
||||
"ResultGradeLevel": 1,
|
||||
"SameGroupNo": 0,
|
||||
"RequiredItemCount": 1
|
||||
},
|
||||
{
|
||||
"Index": 4001,
|
||||
"ItemGrade": 4,
|
||||
"ResultGrade": 4,
|
||||
"RequiredGradeLevel": 0,
|
||||
"ResultGradeLevel": 2,
|
||||
"SameGroupNo": 0,
|
||||
"RequiredItemCount": 1
|
||||
},
|
||||
{
|
||||
"Index": 4002,
|
||||
"ItemGrade": 4,
|
||||
"ResultGrade": 4,
|
||||
"RequiredGradeLevel": 0,
|
||||
"ResultGradeLevel": 3,
|
||||
"SameGroupNo": 0,
|
||||
"RequiredItemCount": 1
|
||||
},
|
||||
{
|
||||
"Index": 4003,
|
||||
"ItemGrade": 4,
|
||||
"ResultGrade": 5,
|
||||
"RequiredGradeLevel": 0,
|
||||
"ResultGradeLevel": 0,
|
||||
"SameGroupNo": 1,
|
||||
"RequiredItemCount": 2
|
||||
},
|
||||
{
|
||||
"Index": 5000,
|
||||
"ItemGrade": 5,
|
||||
"ResultGrade": 5,
|
||||
"RequiredGradeLevel": 0,
|
||||
"ResultGradeLevel": 1,
|
||||
"SameGroupNo": 0,
|
||||
"RequiredItemCount": 1
|
||||
},
|
||||
{
|
||||
"Index": 5001,
|
||||
"ItemGrade": 5,
|
||||
"ResultGrade": 5,
|
||||
"RequiredGradeLevel": 0,
|
||||
"ResultGradeLevel": 2,
|
||||
"SameGroupNo": 0,
|
||||
"RequiredItemCount": 1
|
||||
},
|
||||
{
|
||||
"Index": 5002,
|
||||
"ItemGrade": 5,
|
||||
"ResultGrade": 5,
|
||||
"RequiredGradeLevel": 0,
|
||||
"ResultGradeLevel": 3,
|
||||
"SameGroupNo": 0,
|
||||
"RequiredItemCount": 1
|
||||
},
|
||||
{
|
||||
"Index": 5003,
|
||||
"ItemGrade": 5,
|
||||
"ResultGrade": 5,
|
||||
"RequiredGradeLevel": 0,
|
||||
"ResultGradeLevel": 4,
|
||||
"SameGroupNo": 0,
|
||||
"RequiredItemCount": 1
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e0a36cd5ac4a3dd43b8782a526b0965e
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
[
|
||||
{
|
||||
"Index": 1001,
|
||||
"Name": "\uC77C\uBC18 \uC7A5\uBE44 \uC0C1\uC790 (\uB79C\uB364) ",
|
||||
"DropType": 1,
|
||||
"Rate": 10000,
|
||||
"Package_ID": 10001
|
||||
},
|
||||
{
|
||||
"Index": 1002,
|
||||
"Name": "\uC77C\uBC18 \uBAAC\uC2A4\uD130 \uB4DC\uB78D ",
|
||||
"DropType": 1,
|
||||
"Rate": 2,
|
||||
"Package_ID": 10001
|
||||
},
|
||||
{
|
||||
"Name": "\uC77C\uBC18 \uAC15\uD654\uC11D \uB4DC\uB78D",
|
||||
"DropType": 1,
|
||||
"Rate": 10,
|
||||
"Package_ID": 10003
|
||||
},
|
||||
{
|
||||
"Index": 1003,
|
||||
"Name": "\uC77C\uBC18 \uC7A5\uBE44 \uC0C1\uC790 (\uB79C\uB364) ",
|
||||
"DropType": 1,
|
||||
"Rate": 10000,
|
||||
"Package_ID": 10001
|
||||
},
|
||||
{
|
||||
"Name": "\uC77C\uBC18 \uBB34\uAE30 \uC0C1\uC790 (ALL)",
|
||||
"DropType": 1,
|
||||
"Rate": 10000,
|
||||
"Package_ID": 10002
|
||||
},
|
||||
{
|
||||
"Index": 1004,
|
||||
"Name": "\uC0C1\uC810 \uC7A5\uBE44 \uC0C1\uC790 (\uC77C\uBC18)(\uB79C\uB364)",
|
||||
"DropType": 1,
|
||||
"Rate": 10000,
|
||||
"Package_ID": 10007
|
||||
},
|
||||
{
|
||||
"Index": 1005,
|
||||
"Name": "\uC0C1\uC810 \uC7A5\uBE44 \uC0C1\uC790 (\uACE0\uAE09)(\uB79C\uB364)",
|
||||
"DropType": 1,
|
||||
"Rate": 10000,
|
||||
"Package_ID": 10008
|
||||
},
|
||||
{
|
||||
"Index": 1006,
|
||||
"Name": "\uCC55\uD130 1 \uBCF4\uC0C1 Step1",
|
||||
"DropType": 0,
|
||||
"Rate": 10000,
|
||||
"Package_ID": 10015
|
||||
},
|
||||
{
|
||||
"Index": 1007,
|
||||
"Name": "\uCC55\uD130 1 \uBCF4\uC0C1 Step2",
|
||||
"DropType": 0,
|
||||
"Rate": 10000,
|
||||
"Package_ID": 10015
|
||||
},
|
||||
{
|
||||
"Index": 1008,
|
||||
"Name": "\uCC55\uD130 1 \uBCF4\uC0C1 Step3",
|
||||
"DropType": 0,
|
||||
"Rate": 10000,
|
||||
"Package_ID": 10015
|
||||
},
|
||||
{
|
||||
"DropType": 1,
|
||||
"Rate": 10000,
|
||||
"Package_ID": 10001
|
||||
},
|
||||
{
|
||||
"Index": 1009,
|
||||
"Name": "\uC77C\uC77C \uC0C1\uC810 (\uD310\uB9E4 \uC0C1\uD488)",
|
||||
"DropType": 0,
|
||||
"Rate": 10000,
|
||||
"Package_ID": 10009
|
||||
},
|
||||
{
|
||||
"DropType": 1,
|
||||
"Rate": 10000,
|
||||
"Package_ID": 10010
|
||||
},
|
||||
{
|
||||
"DropType": 1,
|
||||
"Rate": 10000,
|
||||
"Package_ID": 10011
|
||||
},
|
||||
{
|
||||
"DropType": 1,
|
||||
"Rate": 10000,
|
||||
"Package_ID": 10012
|
||||
},
|
||||
{
|
||||
"DropType": 1,
|
||||
"Rate": 10000,
|
||||
"Package_ID": 10013
|
||||
},
|
||||
{
|
||||
"DropType": 1,
|
||||
"Rate": 10000,
|
||||
"Package_ID": 10014
|
||||
},
|
||||
{
|
||||
"Index": 2001,
|
||||
"Name": "\uCC55\uD130 1 \uC77C\uBC18 \uBAAC\uC2A4\uD130(\uACE8\uB4DC)",
|
||||
"DropType": 1,
|
||||
"Rate": 10000,
|
||||
"Package_ID": 20001
|
||||
},
|
||||
{
|
||||
"Index": 1010,
|
||||
"Name": "\uC77C\uBC18, \uB9E4\uC9C1 \uB8EC \uC0C1\uC790 (\uB79C\uB364) ",
|
||||
"DropType": 1,
|
||||
"Rate": 10000,
|
||||
"Package_ID": 10017
|
||||
},
|
||||
{
|
||||
"Index": 1011,
|
||||
"Name": "\uC0C1\uC810 \uB8EC \uC0C1\uC790 (\uACE0\uAE09)(\uB79C\uB364)",
|
||||
"DropType": 1,
|
||||
"Rate": 10000,
|
||||
"Package_ID": 10018
|
||||
},
|
||||
{
|
||||
"Index": 1012,
|
||||
"Name": "\uAC15\uD654 \uC8FC\uBB38\uC11C \uC0C1\uC790",
|
||||
"DropType": 1,
|
||||
"Rate": 10000,
|
||||
"Package_ID": 10003
|
||||
},
|
||||
{
|
||||
"Index": 1013,
|
||||
"Name": "s \uD15C \uC7A5\uBE44 \uC0C1\uC790",
|
||||
"DropType": 1,
|
||||
"Rate": 10000,
|
||||
"Package_ID": 10027
|
||||
},
|
||||
{
|
||||
"Index": 1014,
|
||||
"Name": "\uC5D8\uB9AC\uD2B8 \uC7A5\uBE44 \uC0C1\uC790",
|
||||
"DropType": 1,
|
||||
"Rate": 10000,
|
||||
"Package_ID": 10028
|
||||
},
|
||||
{
|
||||
"Index": 1015,
|
||||
"Name": "\uC5D8\uB9AC\uD2B8 \uB8EC \uC0C1\uC790",
|
||||
"DropType": 1,
|
||||
"Rate": 10000,
|
||||
"Package_ID": 10029
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: c4e7eabc6829ac241a2f58882f3a5b34
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8f58267c85d91af4e88bfa2398566922
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,752 @@
|
|||
[
|
||||
{
|
||||
"Index": 1295,
|
||||
"Desc": "\uD39C\uB9B4 \uAE30\uC0AC\uB2E8 \uBCA8\uD2B8",
|
||||
"ItemGrade": 1,
|
||||
"Ability": 18,
|
||||
"AbilityValue": 20
|
||||
},
|
||||
{
|
||||
"ItemGrade": 2,
|
||||
"Ability": 2,
|
||||
"AbilityValue": 15
|
||||
},
|
||||
{
|
||||
"ItemGrade": 3,
|
||||
"ConditionalAbility": 2,
|
||||
"ConditionValue0": 500,
|
||||
"ConditionValue1": 3
|
||||
},
|
||||
{
|
||||
"ItemGrade": 4,
|
||||
"Ability": 2,
|
||||
"AbilityValue": 10
|
||||
},
|
||||
{
|
||||
"ItemGrade": 5,
|
||||
"ConditionalAbility": 8,
|
||||
"ConditionValue0": 50,
|
||||
"ConditionValue1": 3
|
||||
},
|
||||
{
|
||||
"Index": 1085,
|
||||
"Desc": "\uD39C\uB9B4 \uAE30\uC0AC\uB2E8 \uBAA9\uAC78\uC774",
|
||||
"ItemGrade": 1,
|
||||
"Ability": 14,
|
||||
"AbilityValue": 1
|
||||
},
|
||||
{
|
||||
"ItemGrade": 2,
|
||||
"Ability": 6,
|
||||
"AbilityValue": 15
|
||||
},
|
||||
{
|
||||
"ItemGrade": 3,
|
||||
"Ability": "15\n16",
|
||||
"AbilityValue": "10\n50"
|
||||
},
|
||||
{
|
||||
"ItemGrade": 4,
|
||||
"Ability": 6,
|
||||
"AbilityValue": 10
|
||||
},
|
||||
{
|
||||
"ItemGrade": 5,
|
||||
"ConditionalAbility": 13,
|
||||
"ConditionValue0": 2
|
||||
},
|
||||
{
|
||||
"Index": 1225,
|
||||
"Desc": "\uD39C\uB9B4 \uAE30\uC0AC\uB2E8 \uAC11\uC637",
|
||||
"ItemGrade": 1,
|
||||
"Ability": 17,
|
||||
"AbilityValue": 100
|
||||
},
|
||||
{
|
||||
"ItemGrade": 2,
|
||||
"Ability": 2,
|
||||
"AbilityValue": 15
|
||||
},
|
||||
{
|
||||
"ItemGrade": 3,
|
||||
"ConditionalAbility": "10\n11",
|
||||
"ConditionValue0": "15\n15"
|
||||
},
|
||||
{
|
||||
"ItemGrade": 4,
|
||||
"Ability": 2,
|
||||
"AbilityValue": 10
|
||||
},
|
||||
{
|
||||
"ItemGrade": 5,
|
||||
"Ability": 17,
|
||||
"AbilityValue": 100,
|
||||
"ConditionalAbility": 12,
|
||||
"ConditionValue0": 5
|
||||
},
|
||||
{
|
||||
"Index": 1155,
|
||||
"Desc": "\uD39C\uB9B4 \uAE30\uC0AC\uB2E8 \uC7A5\uAC11",
|
||||
"ItemGrade": 1,
|
||||
"ConditionalAbility": 1,
|
||||
"ConditionValue0": 0,
|
||||
"ConditionValue1": 0
|
||||
},
|
||||
{
|
||||
"ItemGrade": 2,
|
||||
"Ability": 6,
|
||||
"AbilityValue": 15
|
||||
},
|
||||
{
|
||||
"ItemGrade": 3,
|
||||
"ConditionalAbility": 15,
|
||||
"ConditionValue0": 50,
|
||||
"ConditionValue1": 1
|
||||
},
|
||||
{
|
||||
"ItemGrade": 4,
|
||||
"Ability": 6,
|
||||
"AbilityValue": 10
|
||||
},
|
||||
{
|
||||
"ItemGrade": 5,
|
||||
"ConditionalAbility": 16,
|
||||
"ConditionValue0": 50,
|
||||
"ConditionValue1": 200
|
||||
},
|
||||
{
|
||||
"Index": 1365,
|
||||
"Desc": "\uD39C\uB9B4 \uAE30\uC0AC\uB2E8 \uC2E0\uBC1C",
|
||||
"ItemGrade": 1,
|
||||
"Ability": 19,
|
||||
"AbilityValue": 1
|
||||
},
|
||||
{
|
||||
"ItemGrade": 2,
|
||||
"Ability": 2,
|
||||
"AbilityValue": 15
|
||||
},
|
||||
{
|
||||
"ItemGrade": 3,
|
||||
"ConditionalAbility": 36,
|
||||
"ConditionValue0": 1,
|
||||
"ConditionValue1": 900011
|
||||
},
|
||||
{
|
||||
"ItemGrade": 4,
|
||||
"Ability": 2,
|
||||
"AbilityValue": 10
|
||||
},
|
||||
{
|
||||
"ItemGrade": 5,
|
||||
"ConditionalAbility": 36,
|
||||
"ConditionValue0": 1,
|
||||
"ConditionValue1": 900012
|
||||
},
|
||||
{
|
||||
"Index": 1309,
|
||||
"Desc": "\uD589\uC6B4\uC758 \uBCA8\uD2B8",
|
||||
"ItemGrade": 2,
|
||||
"Ability": 2,
|
||||
"AbilityValue": 10
|
||||
},
|
||||
{
|
||||
"ItemGrade": 3,
|
||||
"ConditionalAbility": 22,
|
||||
"ConditionValue0": 5
|
||||
},
|
||||
{
|
||||
"ItemGrade": 4,
|
||||
"ConditionalAbility": 22,
|
||||
"ConditionValue0": 7
|
||||
},
|
||||
{
|
||||
"ItemGrade": 5,
|
||||
"Ability": 2,
|
||||
"AbilityValue": 5
|
||||
},
|
||||
{
|
||||
"Index": 1099,
|
||||
"Desc": "\uD589\uC6B4\uC758 \uBAA9\uAC78\uC774",
|
||||
"ItemGrade": 2,
|
||||
"Ability": 6,
|
||||
"AbilityValue": 10
|
||||
},
|
||||
{
|
||||
"ItemGrade": 3,
|
||||
"ConditionalAbility": 23,
|
||||
"ConditionValue0": 1
|
||||
},
|
||||
{
|
||||
"ItemGrade": 4,
|
||||
"ConditionalAbility": 28,
|
||||
"ConditionValue0": 15
|
||||
},
|
||||
{
|
||||
"ItemGrade": 5,
|
||||
"Ability": 6,
|
||||
"AbilityValue": 5
|
||||
},
|
||||
{
|
||||
"Index": 1239,
|
||||
"Desc": "\uAD6C\uC6D0\uC758 \uAC11\uC637",
|
||||
"ItemGrade": 2,
|
||||
"Ability": 2,
|
||||
"AbilityValue": 10
|
||||
},
|
||||
{
|
||||
"ItemGrade": 3,
|
||||
"ConditionalAbility": 21,
|
||||
"ConditionValue0": 5,
|
||||
"ConditionValue1": 30
|
||||
},
|
||||
{
|
||||
"ItemGrade": 4,
|
||||
"ConditionalAbility": 24,
|
||||
"ConditionValue0": 1
|
||||
},
|
||||
{
|
||||
"ItemGrade": 5,
|
||||
"Ability": 2,
|
||||
"AbilityValue": 5
|
||||
},
|
||||
{
|
||||
"Index": 1169,
|
||||
"Desc": "\uC591\uBA74\uC758 \uC7A5\uAC11",
|
||||
"ItemGrade": 2,
|
||||
"Ability": 6,
|
||||
"AbilityValue": 10
|
||||
},
|
||||
{
|
||||
"ItemGrade": 3,
|
||||
"ConditionalAbility": 25,
|
||||
"ConditionValue0": 1,
|
||||
"ConditionValue1": 50
|
||||
},
|
||||
{
|
||||
"ItemGrade": 4,
|
||||
"ConditionalAbility": 26,
|
||||
"ConditionValue0": 1
|
||||
},
|
||||
{
|
||||
"ItemGrade": 5,
|
||||
"Ability": 6,
|
||||
"AbilityValue": 5
|
||||
},
|
||||
{
|
||||
"Index": 1379,
|
||||
"Desc": "\uB300\uBD80\uD638\uC758 \uC2E0\uBC1C",
|
||||
"ItemGrade": 2,
|
||||
"Ability": 2,
|
||||
"AbilityValue": 10
|
||||
},
|
||||
{
|
||||
"ItemGrade": 3,
|
||||
"ConditionalAbility": 27,
|
||||
"ConditionValue0": 1
|
||||
},
|
||||
{
|
||||
"ItemGrade": 4,
|
||||
"ConditionalAbility": 27,
|
||||
"ConditionValue0": 2
|
||||
},
|
||||
{
|
||||
"ItemGrade": 5,
|
||||
"Ability": 2,
|
||||
"AbilityValue": 5
|
||||
},
|
||||
{
|
||||
"Index": 1323,
|
||||
"Desc": "\uC9D1\uC911\uC758 \uBCA8\uD2B8",
|
||||
"ItemGrade": 2,
|
||||
"Ability": 2,
|
||||
"AbilityValue": 10
|
||||
},
|
||||
{
|
||||
"ItemGrade": 3,
|
||||
"ConditionalAbility": 29,
|
||||
"ConditionValue0": 15
|
||||
},
|
||||
{
|
||||
"ItemGrade": 4,
|
||||
"ConditionalAbility": 5,
|
||||
"ConditionValue0": 1,
|
||||
"ConditionValue1": 100
|
||||
},
|
||||
{
|
||||
"ItemGrade": 5,
|
||||
"Ability": 2,
|
||||
"AbilityValue": 5
|
||||
},
|
||||
{
|
||||
"Index": 1113,
|
||||
"Desc": "\uC548\uC2DD\uC758 \uBAA9\uAC78\uC774",
|
||||
"ItemGrade": 2,
|
||||
"Ability": 6,
|
||||
"AbilityValue": 10
|
||||
},
|
||||
{
|
||||
"ItemGrade": 3,
|
||||
"ConditionalAbility": 18,
|
||||
"ConditionValue0": 70,
|
||||
"ConditionValue1": 5
|
||||
},
|
||||
{
|
||||
"ItemGrade": 4,
|
||||
"ConditionalAbility": 19,
|
||||
"ConditionValue0": 70,
|
||||
"ConditionValue1": 20
|
||||
},
|
||||
{
|
||||
"ItemGrade": 5,
|
||||
"Ability": 6,
|
||||
"AbilityValue": 5
|
||||
},
|
||||
{
|
||||
"Index": 1253,
|
||||
"Desc": "\uCCA0\uBCBD\uC758 \uAC11\uC637",
|
||||
"ItemGrade": 2,
|
||||
"Ability": 2,
|
||||
"AbilityValue": 15
|
||||
},
|
||||
{
|
||||
"ItemGrade": 3,
|
||||
"Ability": 18,
|
||||
"AbilityValue": 20
|
||||
},
|
||||
{
|
||||
"ItemGrade": 4,
|
||||
"ConditionalAbility": 17,
|
||||
"ConditionValue0": 100,
|
||||
"ConditionValue1": 50
|
||||
},
|
||||
{
|
||||
"ItemGrade": 5,
|
||||
"Ability": 2,
|
||||
"AbilityValue": 5
|
||||
},
|
||||
{
|
||||
"Index": 1183,
|
||||
"Desc": "\uC9D1\uC911\uC758 \uC7A5\uAC11",
|
||||
"ItemGrade": 2,
|
||||
"Ability": 6,
|
||||
"AbilityValue": 10
|
||||
},
|
||||
{
|
||||
"ItemGrade": 3,
|
||||
"ConditionalAbility": 30,
|
||||
"ConditionValue0": 15
|
||||
},
|
||||
{
|
||||
"ItemGrade": 4,
|
||||
"ConditionalAbility": 31,
|
||||
"ConditionValue0": 5
|
||||
},
|
||||
{
|
||||
"ItemGrade": 5,
|
||||
"Ability": 6,
|
||||
"AbilityValue": 5
|
||||
},
|
||||
{
|
||||
"Index": 1393,
|
||||
"Desc": "\uD0C4\uB9C9 \uC2E0\uBC1C",
|
||||
"ItemGrade": 2,
|
||||
"Ability": 2,
|
||||
"AbilityValue": 10
|
||||
},
|
||||
{
|
||||
"ItemGrade": 3,
|
||||
"ConditionalAbility": 32,
|
||||
"ConditionValue0": 15
|
||||
},
|
||||
{
|
||||
"ItemGrade": 4,
|
||||
"ConditionalAbility": 33,
|
||||
"ConditionValue0": 10
|
||||
},
|
||||
{
|
||||
"ItemGrade": 5,
|
||||
"Ability": 2,
|
||||
"AbilityValue": 5
|
||||
},
|
||||
{
|
||||
"Index": 1337,
|
||||
"Desc": "\uC2E0\uB150\uC758 \uBCA8\uD2B8",
|
||||
"ItemGrade": 2,
|
||||
"Ability": 2,
|
||||
"AbilityValue": 10
|
||||
},
|
||||
{
|
||||
"ItemGrade": 3,
|
||||
"ConditionalAbility": 2,
|
||||
"ConditionValue0": 300,
|
||||
"ConditionValue1": 1
|
||||
},
|
||||
{
|
||||
"ItemGrade": 4,
|
||||
"ConditionalAbility": 9,
|
||||
"ConditionValue0": 1
|
||||
},
|
||||
{
|
||||
"ItemGrade": 5,
|
||||
"Ability": 2,
|
||||
"AbilityValue": 5
|
||||
},
|
||||
{
|
||||
"Index": 1127,
|
||||
"Desc": "\uC554\uC0B4\uC790 \uBAA9\uAC78\uC774",
|
||||
"ItemGrade": 2,
|
||||
"Ability": 6,
|
||||
"AbilityValue": 10
|
||||
},
|
||||
{
|
||||
"ItemGrade": 3,
|
||||
"ConditionalAbility": 6,
|
||||
"ConditionValue0": 1,
|
||||
"ConditionValue1": 3,
|
||||
"ConditionValue2": 15
|
||||
},
|
||||
{
|
||||
"ItemGrade": 4,
|
||||
"ConditionalAbility": 7,
|
||||
"ConditionValue0": 1,
|
||||
"ConditionValue1": 1,
|
||||
"ConditionValue2": 5
|
||||
},
|
||||
{
|
||||
"ItemGrade": 5,
|
||||
"Ability": 6,
|
||||
"AbilityValue": 5
|
||||
},
|
||||
{
|
||||
"Index": 1267,
|
||||
"Desc": "\uC218\uD638\uC790 \uAC11\uC637",
|
||||
"ItemGrade": 2,
|
||||
"Ability": 2,
|
||||
"AbilityValue": 15
|
||||
},
|
||||
{
|
||||
"ItemGrade": 3,
|
||||
"ConditionalAbility": 20,
|
||||
"ConditionValue0": 50,
|
||||
"ConditionValue1": 20
|
||||
},
|
||||
{
|
||||
"ItemGrade": 4,
|
||||
"ConditionalAbility": 17,
|
||||
"ConditionValue0": 50,
|
||||
"ConditionValue1": 20
|
||||
},
|
||||
{
|
||||
"ItemGrade": 5,
|
||||
"Ability": 2,
|
||||
"AbilityValue": 5
|
||||
},
|
||||
{
|
||||
"Index": 1197,
|
||||
"Desc": "\uD559\uC0B4\uC790 \uC7A5\uAC11",
|
||||
"ItemGrade": 2,
|
||||
"Ability": 6,
|
||||
"AbilityValue": 10
|
||||
},
|
||||
{
|
||||
"ItemGrade": 3,
|
||||
"Ability": 12,
|
||||
"AbilityValue": 20
|
||||
},
|
||||
{
|
||||
"ItemGrade": 4,
|
||||
"ConditionalAbility": 3,
|
||||
"ConditionValue0": 200,
|
||||
"ConditionValue1": 2,
|
||||
"ConditionValue2": 20
|
||||
},
|
||||
{
|
||||
"ItemGrade": 5,
|
||||
"Ability": 6,
|
||||
"AbilityValue": 5
|
||||
},
|
||||
{
|
||||
"Index": 1407,
|
||||
"Desc": "\uCD94\uC801\uC790 \uC2E0\uBC1C",
|
||||
"ItemGrade": 2,
|
||||
"Ability": 2,
|
||||
"AbilityValue": 10
|
||||
},
|
||||
{
|
||||
"ItemGrade": 3,
|
||||
"Ability": 4,
|
||||
"AbilityValue": 1
|
||||
},
|
||||
{
|
||||
"ItemGrade": 4,
|
||||
"ConditionalAbility": 4,
|
||||
"ConditionValue0": 200,
|
||||
"ConditionValue1": 2,
|
||||
"ConditionValue2": 30
|
||||
},
|
||||
{
|
||||
"ItemGrade": 5,
|
||||
"Ability": 2,
|
||||
"AbilityValue": 5
|
||||
},
|
||||
{
|
||||
"Index": 1351,
|
||||
"Desc": "\uC800\uC8FC\uBC1B\uC740 \uBCA8\uD2B8",
|
||||
"ItemGrade": 2,
|
||||
"Ability": 2,
|
||||
"AbilityValue": 10
|
||||
},
|
||||
{
|
||||
"ItemGrade": 3,
|
||||
"ConditionalAbility": 35,
|
||||
"ConditionValue0": 5,
|
||||
"ConditionValue1": 900013
|
||||
},
|
||||
{
|
||||
"ItemGrade": 4,
|
||||
"ConditionalAbility": 35,
|
||||
"ConditionValue0": 5,
|
||||
"ConditionValue1": 900014
|
||||
},
|
||||
{
|
||||
"ItemGrade": 5,
|
||||
"Ability": 2,
|
||||
"AbilityValue": 5
|
||||
},
|
||||
{
|
||||
"Index": 1141,
|
||||
"Desc": "\uC9D1\uD589\uAD00 \uBAA9\uAC78\uC774",
|
||||
"ItemGrade": 2,
|
||||
"Ability": 6,
|
||||
"AbilityValue": 10
|
||||
},
|
||||
{
|
||||
"ItemGrade": 3,
|
||||
"ConditionalAbility": 34,
|
||||
"ConditionValue0": 20
|
||||
},
|
||||
{
|
||||
"ItemGrade": 4,
|
||||
"ConditionalAbility": 34,
|
||||
"ConditionValue0": 50
|
||||
},
|
||||
{
|
||||
"ItemGrade": 5,
|
||||
"Ability": 6,
|
||||
"AbilityValue": 5
|
||||
},
|
||||
{
|
||||
"Index": 1281,
|
||||
"Desc": "\uC18C\uC0DD\uC758 \uAC11\uC637",
|
||||
"ItemGrade": 2,
|
||||
"Ability": 2,
|
||||
"AbilityValue": 15
|
||||
},
|
||||
{
|
||||
"ItemGrade": 3,
|
||||
"Ability": 17,
|
||||
"AbilityValue": 50
|
||||
},
|
||||
{
|
||||
"ItemGrade": 4,
|
||||
"ConditionalAbility": "10\n11",
|
||||
"ConditionValue0": "10\n10"
|
||||
},
|
||||
{
|
||||
"ItemGrade": 5,
|
||||
"Ability": 2,
|
||||
"AbilityValue": 5
|
||||
},
|
||||
{
|
||||
"Index": 1211,
|
||||
"Desc": "\uC6A9\uBCD1\uB2E8 \uC7A5\uAC11",
|
||||
"ItemGrade": 2,
|
||||
"Ability": 6,
|
||||
"AbilityValue": 10
|
||||
},
|
||||
{
|
||||
"ItemGrade": 3,
|
||||
"Ability": "6\n12",
|
||||
"AbilityValue": "-5\n10"
|
||||
},
|
||||
{
|
||||
"ItemGrade": 4,
|
||||
"Ability": "15\n16",
|
||||
"AbilityValue": "3\n10"
|
||||
},
|
||||
{
|
||||
"ItemGrade": 5,
|
||||
"Ability": 6,
|
||||
"AbilityValue": "10"
|
||||
},
|
||||
{
|
||||
"Index": 1421,
|
||||
"Desc": "\uC6A9\uBCD1\uB2E8 \uC2E0\uBC1C",
|
||||
"ItemGrade": 2,
|
||||
"Ability": 2,
|
||||
"AbilityValue": 10
|
||||
},
|
||||
{
|
||||
"ItemGrade": 3,
|
||||
"Ability": "4\n18",
|
||||
"AbilityValue": "10\n10"
|
||||
},
|
||||
{
|
||||
"ItemGrade": 4,
|
||||
"Ability": "5\n8",
|
||||
"AbilityValue": "10\n10"
|
||||
},
|
||||
{
|
||||
"ItemGrade": 5,
|
||||
"Ability": 2,
|
||||
"AbilityValue": 5
|
||||
},
|
||||
{
|
||||
"Index": 1001,
|
||||
"Desc": "\uC6D4\uAD11",
|
||||
"ItemGrade": 1,
|
||||
"Ability": 6,
|
||||
"AbilityValue": 15
|
||||
},
|
||||
{
|
||||
"ItemGrade": 2
|
||||
},
|
||||
{
|
||||
"ItemGrade": 3
|
||||
},
|
||||
{
|
||||
"ItemGrade": 4,
|
||||
"Ability": 6,
|
||||
"AbilityValue": 10
|
||||
},
|
||||
{
|
||||
"ItemGrade": 5
|
||||
},
|
||||
{
|
||||
"Index": 1015,
|
||||
"Desc": "\uD30C\uAD34\uC790",
|
||||
"ItemGrade": 1,
|
||||
"Ability": 6,
|
||||
"AbilityValue": 10
|
||||
},
|
||||
{
|
||||
"ItemGrade": 2
|
||||
},
|
||||
{
|
||||
"ItemGrade": 3
|
||||
},
|
||||
{
|
||||
"ItemGrade": 4,
|
||||
"Ability": 6,
|
||||
"AbilityValue": 5
|
||||
},
|
||||
{
|
||||
"ItemGrade": 5
|
||||
},
|
||||
{
|
||||
"Index": 1029,
|
||||
"Desc": "\uBC14\uB78C\uC410\uAE30",
|
||||
"ItemGrade": 1,
|
||||
"Ability": 6,
|
||||
"AbilityValue": 10
|
||||
},
|
||||
{
|
||||
"ItemGrade": 2
|
||||
},
|
||||
{
|
||||
"ItemGrade": 3
|
||||
},
|
||||
{
|
||||
"ItemGrade": 4,
|
||||
"Ability": 6,
|
||||
"AbilityValue": 5
|
||||
},
|
||||
{
|
||||
"ItemGrade": 5
|
||||
},
|
||||
{
|
||||
"Index": 1043,
|
||||
"Desc": "\uBE44\uC218",
|
||||
"ItemGrade": 1,
|
||||
"Ability": 6,
|
||||
"AbilityValue": 10
|
||||
},
|
||||
{
|
||||
"ItemGrade": 2
|
||||
},
|
||||
{
|
||||
"ItemGrade": 3
|
||||
},
|
||||
{
|
||||
"ItemGrade": 4,
|
||||
"Ability": 6,
|
||||
"AbilityValue": 5
|
||||
},
|
||||
{
|
||||
"ItemGrade": 5
|
||||
},
|
||||
{
|
||||
"Index": 1057,
|
||||
"Desc": "\uBA69\uC57D",
|
||||
"ItemGrade": 1,
|
||||
"Ability": 6,
|
||||
"AbilityValue": 10
|
||||
},
|
||||
{
|
||||
"ItemGrade": 2
|
||||
},
|
||||
{
|
||||
"ItemGrade": 3
|
||||
},
|
||||
{
|
||||
"ItemGrade": 4,
|
||||
"Ability": 6,
|
||||
"AbilityValue": 5
|
||||
},
|
||||
{
|
||||
"ItemGrade": 5
|
||||
},
|
||||
{
|
||||
"Index": 1071,
|
||||
"Desc": "\uB9C8\uBC95\uAC80",
|
||||
"ItemGrade": 1,
|
||||
"Ability": 6,
|
||||
"AbilityValue": 10
|
||||
},
|
||||
{
|
||||
"ItemGrade": 2
|
||||
},
|
||||
{
|
||||
"ItemGrade": 3
|
||||
},
|
||||
{
|
||||
"ItemGrade": 4,
|
||||
"Ability": 6,
|
||||
"AbilityValue": 5
|
||||
},
|
||||
{
|
||||
"ItemGrade": 5
|
||||
},
|
||||
{
|
||||
"Index": 1435,
|
||||
"Desc": "\uBB35\uAD11",
|
||||
"ItemGrade": 1,
|
||||
"Ability": 6,
|
||||
"AbilityValue": 10
|
||||
},
|
||||
{
|
||||
"ItemGrade": 2
|
||||
},
|
||||
{
|
||||
"ItemGrade": 3
|
||||
},
|
||||
{
|
||||
"ItemGrade": 4,
|
||||
"Ability": 6,
|
||||
"AbilityValue": 5
|
||||
},
|
||||
{
|
||||
"ItemGrade": 5
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: aa4e30bd270bb4c488494918e4b13587
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 68bf48d3810b37940b998391b07f1ecb
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 0f760bafe4ff5ab44be2f79c9b363a3b
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: c9d268ce35c9ec0459064070bc00e36b
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 1348a18e092e6c148be2d41c5248df63
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: dfe57775a8d2b1d4d964f4d72f93b2df
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 806cf6f49a2158c43928a1c0b02bae75
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 3a3e89c28e5310a459e09571fb036053
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: f80db86491929264a866457d2db3103e, type: 3}
|
||||
m_Name: WLEquipSettings
|
||||
m_EditorClassIdentifier: Assembly-CSharp::WL.Equip.WLEquipSettings
|
||||
enabled_: 1
|
||||
verboseLog: 1
|
||||
tableFolder: WL/FR
|
||||
saveName: WL_Equip
|
||||
resetSaveEveryStart: 1
|
||||
goldItemIndex: 1
|
||||
clearDropGroupId: 1001
|
||||
clearDropRolls: 1
|
||||
bossExtraRolls: 1
|
||||
clearStoneItemIds: 65000000660000006700000068000000690000006a000000
|
||||
clearStoneCountEach: 2
|
||||
clearGoldBonus: 0
|
||||
levelAttackStat: 8
|
||||
levelDefenceStat: 11
|
||||
levelHealthStat: 9
|
||||
levelStatScale: 1
|
||||
abilityMap:
|
||||
- ability: 2
|
||||
stat: 14
|
||||
scale: 10
|
||||
- ability: 6
|
||||
stat: 13
|
||||
scale: 10
|
||||
- ability: 5
|
||||
stat: 15
|
||||
scale: 10
|
||||
- ability: 13
|
||||
stat: 13
|
||||
scale: 10
|
||||
- ability: 13
|
||||
stat: 15
|
||||
scale: 10
|
||||
- ability: 4
|
||||
stat: 31
|
||||
scale: 10
|
||||
- ability: 15
|
||||
stat: 22
|
||||
scale: 10
|
||||
- ability: 16
|
||||
stat: 23
|
||||
scale: 10
|
||||
- ability: 12
|
||||
stat: 27
|
||||
scale: 10
|
||||
- ability: 3
|
||||
stat: 34
|
||||
scale: 10
|
||||
- ability: 14
|
||||
stat: 26
|
||||
scale: 10
|
||||
- ability: 7
|
||||
stat: 40
|
||||
scale: 10
|
||||
- ability: 7
|
||||
stat: 41
|
||||
scale: 10
|
||||
- ability: 7
|
||||
stat: 42
|
||||
scale: 10
|
||||
openMenuIds: weapon,upgrade
|
||||
cheatGrantEnabled: 1
|
||||
cheatGrantGroupId: 1001
|
||||
cheatGrantRolls: 5
|
||||
uiSortingOrder: 300
|
||||
dimColor: {r: 0, g: 0, b: 0, a: 0.75}
|
||||
panelColor: {r: 0.12, g: 0.12, b: 0.14, a: 0.98}
|
||||
boxColor: {r: 0.2, g: 0.2, b: 0.24, a: 1}
|
||||
buttonColor: {r: 0.32, g: 0.36, b: 0.5, a: 1}
|
||||
buttonDisabledColor: {r: 0.25, g: 0.25, b: 0.28, a: 1}
|
||||
equippedRowColor: {r: 0.2, g: 0.45, b: 0.3, a: 1}
|
||||
rowColor: {r: 0.22, g: 0.22, b: 0.26, a: 1}
|
||||
selectedRowColor: {r: 0.55, g: 0.45, b: 0.2, a: 1}
|
||||
panelMarginPx: 30
|
||||
titleFontPx: 44
|
||||
bodyFontPx: 30
|
||||
smallFontPx: 26
|
||||
rowHeightPx: 72
|
||||
slotHeightPx: 110
|
||||
buttonHeightPx: 84
|
||||
detailHeightPx: 420
|
||||
maxListRows: 300
|
||||
fontPath:
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5ee51d27b478a424dac5726aaf5a150b
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,263 @@
|
|||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// WLEquipData.cs — FR(FirstRising) 기존 장비 시스템의 데이터 모델 이식 (#821 · 2026-10-08)
|
||||
//
|
||||
// 원본 = FR `Assets/Scripts/GameHub/JsonCommon.cs` (ItemJson · ItemLevelJson · ItemComposeJson ·
|
||||
// ItemOptionJson · ItemDropGroup/Package · RawItem) 를 **필드 이름 그대로** 옮겼다 — FR 의 JSON
|
||||
// 테이블(`Resources/WL/FR/*.json`)을 수정 없이 Newtonsoft 로 읽기 위해서다.
|
||||
//
|
||||
// PD 지시: 탕탕특공대식 — 본 아이템 1 + 같은 등급 재료 2(표 RequiredItemCount) → 등급 업 ·
|
||||
// 등급마다 옵션 개방(ItemOptionTable) · 강화석+골드로 레벨업(Item_*.json) · 강등.
|
||||
// 능력치 본체는 힘민지(ActorStatInfo) 그대로 쓰고, FR eAbility 는 WLEquipSettings.abilityMap 으로 대응한다.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace WL.Equip
|
||||
{
|
||||
/// <summary>FR eAbility — 값(순서)이 JSON 의 정수와 일치해야 한다. BT47 이후 항목은 옮기지 않았다(PD 제외).</summary>
|
||||
public enum eFRAbility
|
||||
{
|
||||
None,
|
||||
ItemRange, // 1 아이템 획득 범위 %
|
||||
Health, // 2 체력 %
|
||||
RecoveryHealth, // 3 초당 체력 회복 %
|
||||
Speed, // 4 이동 속도 %
|
||||
Defence, // 5 모든 피해 감소 %
|
||||
Attack, // 6 공격력 %
|
||||
IncreaseDrop, // 7 경험치·골드·드랍율 %
|
||||
Rolling, // 8 구르기
|
||||
ProjectileSpeed, // 9 투사체 속도 %
|
||||
SkillRange, // 10 스킬 범위 %
|
||||
SkillDuration, // 11 스킬 지속 %
|
||||
Cooltime, // 12 쿨타임 감소 %
|
||||
AttackDefence, // 13 공격력+방어력 %
|
||||
Passive, // 14 패시브 효과 %
|
||||
Critical, // 15 치명타 확률 %
|
||||
CriticalDamage, // 16 치명타 피해 %
|
||||
Revive, // 17 부활 횟수
|
||||
HeartRecover, // 18 하트 회복 %
|
||||
SpeedValue, // 19 이동 속도 값
|
||||
Excellent, // 20 액설런트 확률
|
||||
ExcellentDamage, // 21 액설런트 피해
|
||||
Max,
|
||||
}
|
||||
|
||||
public enum eFRItemGrade { None = -1, Normal, Magic, Rare, Elite, Epic, Legend, Max }
|
||||
public enum eFREquipmentType { None = 0, Weapon, Necklace, Glove, Armor, Belt, Shose, Max }
|
||||
public enum eFRItemType { None = 0, Equipment, Buff, LevelUpScroll, EnchantScroll, Goods, Normal, RewardPoint, Max }
|
||||
public enum eFRDropType { All = 0, Random = 1, Max }
|
||||
|
||||
[Serializable]
|
||||
public class ItemJson
|
||||
{
|
||||
public int Index;
|
||||
public string Item_name;
|
||||
public string Desc;
|
||||
public byte IsEquip;
|
||||
public byte IsStack;
|
||||
public int ItemCategory;
|
||||
public eFRItemType ItemType;
|
||||
public int WeaponType;
|
||||
public eFREquipmentType EquipmentType;
|
||||
public eFRItemGrade ItemGrade;
|
||||
public int ItemClass;
|
||||
public int ItemDropGroup_ID;
|
||||
public int GradeLv;
|
||||
public int GroupNo;
|
||||
public int ItemMaxLv;
|
||||
public int ItemLvAbilityID;
|
||||
public int SkillIndex;
|
||||
public int CostumeIndex;
|
||||
// FR 의 `Equipments`(프리팹 이름)는 FR 전용이라 읽지 않는다.
|
||||
|
||||
public bool IsEquipment { get { return IsEquip == 1 && ItemType == eFRItemType.Equipment; } }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class ItemLevelInfo
|
||||
{
|
||||
public int Index;
|
||||
public eFRItemGrade Grade;
|
||||
public int GradeLv;
|
||||
public int Level;
|
||||
public int AttackAbility;
|
||||
public int DefenceAbility;
|
||||
public int HealthAbility;
|
||||
public int ItemKeyRequired;
|
||||
public int ItemCountRequired;
|
||||
public int MoneyKeyRequired;
|
||||
public int MoneyCountRequired;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class ItemLevelJson
|
||||
{
|
||||
public int Index;
|
||||
public List<ItemLevelInfo> LevelInfos = new List<ItemLevelInfo>();
|
||||
public ItemLevelJson(int index) { Index = index; }
|
||||
public ItemLevelInfo FindLevelInfo(int level) { return LevelInfos.Find(i => i.Level == level); }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class ItemComposeJson
|
||||
{
|
||||
public int Index;
|
||||
public int SameGroupNo;
|
||||
public eFRItemGrade ItemGrade;
|
||||
public eFRItemGrade ResultGrade;
|
||||
public int RequiredGradeLevel;
|
||||
public int ResultGradeLevel;
|
||||
public int RequiredItemCount;
|
||||
}
|
||||
|
||||
/// <summary>ItemOptionTable.json 의 한 행(엑셀 셀 병합 → 첫 행만 Index).</summary>
|
||||
[Serializable]
|
||||
public class ItemOptionReader
|
||||
{
|
||||
public int Index;
|
||||
public string Desc;
|
||||
public eFRItemGrade ItemGrade;
|
||||
public string Ability;
|
||||
public string AbilityValue;
|
||||
public string ConditionalAbility;
|
||||
public string ConditionValue0;
|
||||
public string ConditionValue1;
|
||||
public string ConditionValue2;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class ItemOptionAbility
|
||||
{
|
||||
public eFRAbility Ability;
|
||||
public int AbilityValue;
|
||||
public ItemOptionAbility(eFRAbility a, int v) { Ability = a; AbilityValue = v; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class ConditionalItemOptionAbility
|
||||
{
|
||||
public int Ability; // FR eConditionalItemOptionAbility — WL 에서는 「미적용」(표시만)
|
||||
public int ConditionValue0, ConditionValue1, ConditionValue2;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class ItemOptionElement
|
||||
{
|
||||
public eFRItemGrade ItemGrade;
|
||||
public List<ItemOptionAbility> Abilities = new List<ItemOptionAbility>();
|
||||
public List<ConditionalItemOptionAbility> ConditionalAbilities = new List<ConditionalItemOptionAbility>();
|
||||
|
||||
public ItemOptionElement(ItemOptionReader r)
|
||||
{
|
||||
ItemGrade = r.ItemGrade;
|
||||
if (!string.IsNullOrEmpty(r.Ability))
|
||||
{
|
||||
var a = r.Ability.Split('\n');
|
||||
var v = (r.AbilityValue ?? "").Split('\n');
|
||||
for (int i = 0; i < a.Length; i++)
|
||||
{
|
||||
int ai, vi;
|
||||
if (!int.TryParse(a[i].Trim(), out ai)) continue;
|
||||
vi = i < v.Length && int.TryParse(v[i].Trim(), out vi) ? vi : 0;
|
||||
Abilities.Add(new ItemOptionAbility((eFRAbility)ai, vi));
|
||||
}
|
||||
}
|
||||
if (!string.IsNullOrEmpty(r.ConditionalAbility))
|
||||
{
|
||||
var a = r.ConditionalAbility.Split('\n');
|
||||
var c0 = string.IsNullOrEmpty(r.ConditionValue0) ? null : r.ConditionValue0.Split('\n');
|
||||
var c1 = string.IsNullOrEmpty(r.ConditionValue1) ? null : r.ConditionValue1.Split('\n');
|
||||
var c2 = string.IsNullOrEmpty(r.ConditionValue2) ? null : r.ConditionValue2.Split('\n');
|
||||
for (int i = 0; i < a.Length; i++)
|
||||
{
|
||||
var ca = new ConditionalItemOptionAbility();
|
||||
int t;
|
||||
if (int.TryParse(a[i].Trim(), out t)) ca.Ability = t;
|
||||
if (c0 != null && i < c0.Length && int.TryParse(c0[i].Trim(), out t)) ca.ConditionValue0 = t;
|
||||
if (c1 != null && i < c1.Length && int.TryParse(c1[i].Trim(), out t)) ca.ConditionValue1 = t;
|
||||
if (c2 != null && i < c2.Length && int.TryParse(c2[i].Trim(), out t)) ca.ConditionValue2 = t;
|
||||
ConditionalAbilities.Add(ca);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class ItemOptionJson
|
||||
{
|
||||
public int Index; // = ItemJson.GroupNo
|
||||
public string Desc;
|
||||
public List<ItemOptionElement> ItemOptions = new List<ItemOptionElement>();
|
||||
public ItemOptionJson(int index, string desc) { Index = index; Desc = desc; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class ItemDropGroupInfo
|
||||
{
|
||||
public int Index;
|
||||
public string Name;
|
||||
public eFRDropType DropType;
|
||||
public int Rate; // 10000 분율
|
||||
public int Package_ID;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class ItemDropGroupJson
|
||||
{
|
||||
public int Index;
|
||||
public List<ItemDropGroupInfo> ItemDropGroupInfos = new List<ItemDropGroupInfo>();
|
||||
public ItemDropGroupJson(int index) { Index = index; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class ItemDropPackageInfo
|
||||
{
|
||||
public int Index;
|
||||
public string Name;
|
||||
public int Item_ID;
|
||||
public int MinCount;
|
||||
public int MaxCount;
|
||||
public int Rate;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class ItemDropPackageJson
|
||||
{
|
||||
public int Index;
|
||||
public int TotalRate;
|
||||
public List<ItemDropPackageInfo> ItemDropPackageInfos = new List<ItemDropPackageInfo>();
|
||||
public ItemDropPackageJson(int index) { Index = index; }
|
||||
}
|
||||
|
||||
/// <summary>보유 아이템 1건(FR RawItem). `_id` 는 세이브 안 고유 번호.</summary>
|
||||
[Serializable]
|
||||
public class RawItem
|
||||
{
|
||||
public int index;
|
||||
public int level;
|
||||
public int count;
|
||||
public int _id;
|
||||
public RawItem() { }
|
||||
public RawItem(int index, int level, int count, int id) { this.index = index; this.level = level; this.count = count; _id = id; }
|
||||
}
|
||||
|
||||
/// <summary>RawItem + 표 행.</summary>
|
||||
public class Item
|
||||
{
|
||||
public RawItem rawItem;
|
||||
public ItemJson itemJson;
|
||||
public Item(RawItem r, ItemJson j) { rawItem = r; itemJson = j; }
|
||||
}
|
||||
|
||||
/// <summary>세이브 파일 본문(JSON).</summary>
|
||||
[Serializable]
|
||||
public class WLEquipSaveData
|
||||
{
|
||||
public int version = 1;
|
||||
public int nextId = 1;
|
||||
public long gold; // 섬(IslandManager)이 없을 때만 쓰는 예비 골드
|
||||
public List<RawItem> items = new List<RawItem>();
|
||||
public List<int> equipItems = new List<int>(); // 장착 중인 _id
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 0368eeda61a55e74a96bd5c12b797cb0
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// WLEquipDrops.cs — 던전 클리어 때 FR 드랍표로 장비·강화석 지급 (#821)
|
||||
//
|
||||
// 훅 = `WLIslandBridge.OnStageCleared` 의 `WLIslandExpansionGate.Unlock(cfg);` 다음 1줄.
|
||||
// FR 의 드랍 규칙(ItemDropGroupTable 10000 분율 → ItemDropPackageTable 가중 추첨)을 그대로 돌린다.
|
||||
// 값(그룹 ID · 횟수 · 강화석)은 WLEquipSettings.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using WL.Combat.Stage;
|
||||
|
||||
namespace WL.Equip
|
||||
{
|
||||
public static class WLEquipDrops
|
||||
{
|
||||
public static string LastGrantLog { get; private set; } = "";
|
||||
public static int Grants { get; private set; }
|
||||
static readonly System.Random s_rng = new System.Random();
|
||||
static readonly List<WLEquipTables.Drop> s_drops = new List<WLEquipTables.Drop>(8);
|
||||
|
||||
public static void OnStageCleared(in StageClearResult r)
|
||||
{
|
||||
var cfg = WLEquipSettings.Instance;
|
||||
if (cfg == null || cfg.enabled_ == 0) return;
|
||||
if (!r.win) return;
|
||||
int rolls = cfg.clearDropRolls + (r.isBoss ? cfg.bossExtraRolls : 0);
|
||||
string log = Grant(cfg.clearDropGroupId, rolls, cfg.clearStoneItemIds, cfg.clearStoneCountEach, cfg.clearGoldBonus);
|
||||
Grants++;
|
||||
LastGrantLog = "클리어 드랍(" + r.index + ") — " + log;
|
||||
if (cfg.verboseLog != 0) Debug.Log("[WL-821] " + LastGrantLog);
|
||||
}
|
||||
|
||||
/// <summary>드랍 그룹을 rolls 번 굴려 지급 + 강화석 + 골드. 돌려주는 문자열 = 지급 내역.</summary>
|
||||
public static string Grant(int groupId, int rolls, int[] stoneIds, int stoneEach, int goldBonus)
|
||||
{
|
||||
if (!WLInventory.EnsureLoaded()) return "인벤 로드 실패";
|
||||
s_drops.Clear();
|
||||
for (int i = 0; i < rolls; i++) WLEquipTables.RollDropGroup(groupId, s_drops, s_rng);
|
||||
var gained = new List<Item>();
|
||||
var sb = new System.Text.StringBuilder();
|
||||
for (int i = 0; i < s_drops.Count; i++)
|
||||
{
|
||||
WLInventory.AddItem(s_drops[i].itemId, 1, s_drops[i].count, gained);
|
||||
var j = WLEquipTables.FindItemJson(s_drops[i].itemId);
|
||||
sb.Append(j != null ? j.Item_name : s_drops[i].itemId.ToString()).Append("×").Append(s_drops[i].count).Append(' ');
|
||||
}
|
||||
if (stoneIds != null && stoneEach > 0)
|
||||
{
|
||||
for (int i = 0; i < stoneIds.Length; i++) WLInventory.AddItem(stoneIds[i], 1, stoneEach);
|
||||
sb.Append("강화석 ").Append(stoneIds.Length).Append("종×").Append(stoneEach).Append(' ');
|
||||
}
|
||||
if (goldBonus > 0) { WLInventory.AddGold(goldBonus); sb.Append("골드+").Append(goldBonus); }
|
||||
WLInventory.Commit("획득 — " + sb);
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: ff59f286a796bb94897714b95909bb91
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// WLEquipSettings.cs — FR 장비 시스템 WL 이식 설정(SO · `Resources/WL/WLEquipSettings.asset`) (#821)
|
||||
//
|
||||
// C45: 세이브 이름 · 재화 ID · 드랍 규칙 · FR 능력치 → 힘민지 스탯 대응표 · UI 크기를 전부 여기 둔다.
|
||||
// 🔴 SO 는 클래스와 같은 이름의 .cs 파일에만 둔다(816g 교훈 — 아니면 .asset 의 m_Script 가 빈다).
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace WL.Equip
|
||||
{
|
||||
/// <summary>FR eAbility(%) 한 항목을 힘민지 eStat 하나에 더하는 규칙. 같은 ability 를 여러 행에 적으면 모두 더한다.</summary>
|
||||
[System.Serializable]
|
||||
public struct WLAbilityMap
|
||||
{
|
||||
public eFRAbility ability;
|
||||
public eStat stat;
|
||||
[Tooltip("FR 값(%)에 곱해 힘민지 값으로. 힘민지 Multiplier 계열은 1000 분율(100 = 1 %) 이라 FR 1 % → 10")]
|
||||
public float scale;
|
||||
}
|
||||
|
||||
public sealed class WLEquipSettings : ScriptableObject
|
||||
{
|
||||
[Header("스위치")]
|
||||
[Tooltip("0 이면 장비 시스템 전체 무동작(스탯 훅·드랍·메뉴 전부)")]
|
||||
public int enabled_ = 1;
|
||||
public int verboseLog = 1;
|
||||
|
||||
[Header("데이터")]
|
||||
[Tooltip("FR 표 json 이 든 Resources 폴더")]
|
||||
public string tableFolder = "WL/FR";
|
||||
|
||||
[Header("세이브")]
|
||||
[Tooltip("persistentDataPath 아래 파일 이름(확장자 제외)")]
|
||||
public string saveName = "WL_Equip";
|
||||
[Tooltip("1 이면 Play(실행) 1회당 첫 로드 때 세이브를 지운다 — 섬 세이브(resetSaveEveryStart)와 같은 정책")]
|
||||
public int resetSaveEveryStart = 1;
|
||||
|
||||
[Header("재화 — FR ItemTable 의 특수 인덱스")]
|
||||
[Tooltip("FR 골드 아이템 Index(강화 비용 MoneyKeyRequired). 섬이 떠 있으면 섬 코인(IslandManager.Coin)을 쓴다")]
|
||||
public int goldItemIndex = 1;
|
||||
|
||||
[Header("던전 클리어 드랍 (FR ItemDropGroupTable)")]
|
||||
[Tooltip("클리어 때 굴릴 드랍 그룹 ID(1001 = 일반 장비 상자 · 패키지 10001 에서 1개)")]
|
||||
public int clearDropGroupId = 1001;
|
||||
public int clearDropRolls = 1;
|
||||
public int bossExtraRolls = 1;
|
||||
[Tooltip("클리어마다 주는 강화석 Item Index 목록(101~106 = 부위별 강화석)")]
|
||||
public int[] clearStoneItemIds = { 101, 102, 103, 104, 105, 106 };
|
||||
public int clearStoneCountEach = 2;
|
||||
[Tooltip("클리어 골드 보상에 더해 FR 골드도 지급할지(섬 코인으로 들어간다) · 0 = 안 줌")]
|
||||
public int clearGoldBonus = 0;
|
||||
|
||||
[Header("플레이어 기본 공격력 (PD 2026-10-09 「기본 공격력은 5」)")]
|
||||
[Tooltip("0 보다 크면 스탯 재계산 뒤 PC 의 ATK 를 「이 값 + 장착 장비의 레벨표 공격력」 으로 덮고 BaseDmg 도 같은 비율로 맞춘다. 0 = 힘민지 원본 공식(주스탯 × 배율) 그대로")]
|
||||
public double pcBaseAttackOverride = 5;
|
||||
|
||||
[Header("레벨표 능력치 → 힘민지 스탯")]
|
||||
public eStat levelAttackStat = eStat.ATK;
|
||||
public eStat levelDefenceStat = eStat.DEF;
|
||||
public eStat levelHealthStat = eStat.HP;
|
||||
public float levelStatScale = 1f;
|
||||
|
||||
[Header("등급 옵션(FR eAbility %) → 힘민지 스탯")]
|
||||
public List<WLAbilityMap> abilityMap = new List<WLAbilityMap>
|
||||
{
|
||||
new WLAbilityMap { ability = eFRAbility.Health, stat = eStat.HP_Multiplier, scale = 10f },
|
||||
new WLAbilityMap { ability = eFRAbility.Attack, stat = eStat.ATK_Multiplier, scale = 10f },
|
||||
new WLAbilityMap { ability = eFRAbility.Defence, stat = eStat.DEF_Multiplier, scale = 10f },
|
||||
new WLAbilityMap { ability = eFRAbility.AttackDefence, stat = eStat.ATK_Multiplier, scale = 10f },
|
||||
new WLAbilityMap { ability = eFRAbility.AttackDefence, stat = eStat.DEF_Multiplier, scale = 10f },
|
||||
new WLAbilityMap { ability = eFRAbility.Speed, stat = eStat.MOVE_SPD_Multiplier, scale = 10f },
|
||||
new WLAbilityMap { ability = eFRAbility.Critical, stat = eStat.CRI, scale = 10f },
|
||||
new WLAbilityMap { ability = eFRAbility.CriticalDamage, stat = eStat.CRI_DMG_Multiplier, scale = 10f },
|
||||
new WLAbilityMap { ability = eFRAbility.Cooltime, stat = eStat.COOLTIME_Multiplier, scale = 10f },
|
||||
new WLAbilityMap { ability = eFRAbility.RecoveryHealth, stat = eStat.HP_REGEN_Multiplier, scale = 10f },
|
||||
new WLAbilityMap { ability = eFRAbility.Passive, stat = eStat.SKILL_Multiplier, scale = 10f },
|
||||
new WLAbilityMap { ability = eFRAbility.IncreaseDrop, stat = eStat.GOLD_Multiplier, scale = 10f },
|
||||
new WLAbilityMap { ability = eFRAbility.IncreaseDrop, stat = eStat.EXP_Multiplier, scale = 10f },
|
||||
new WLAbilityMap { ability = eFRAbility.IncreaseDrop, stat = eStat.ITEM_Multiplier, scale = 10f },
|
||||
};
|
||||
|
||||
[Header("메뉴 연결")]
|
||||
[Tooltip("하단 메뉴 DummyMenuButton 의 menuId — 이 값이면 장비 화면을 연다(쉼표로 여러 개)")]
|
||||
public string openMenuIds = "weapon,upgrade";
|
||||
[Tooltip("1 이면 화면에 「테스트 획득」 버튼(드랍 그룹을 굴려 바로 지급)을 둔다")]
|
||||
public int cheatGrantEnabled = 1;
|
||||
public int cheatGrantGroupId = 1001;
|
||||
public int cheatGrantRolls = 5;
|
||||
|
||||
[Header("런처 버튼 — 인게임(섬·던전)에서 항상 보이는 「장비」 버튼 (하단 메뉴는 섬에서 숨겨져 있어 별도 진입점)")]
|
||||
public int launcherEnabled = 1;
|
||||
[Tooltip("이 씬이 활성 씬이고 서버 데이터가 있을 때만 보인다")]
|
||||
public string launcherSceneName = "InGame";
|
||||
public string launcherLabel = "장비";
|
||||
public Vector2 launcherSizePx = new Vector2(200f, 84f);
|
||||
[Tooltip("화면 오른쪽·아래 모서리에서의 거리(px · 1080×1920 기준)")]
|
||||
public Vector2 launcherOffsetPx = new Vector2(24f, 300f);
|
||||
[Tooltip("1 이면 장비 화면을 열 때 힘민지 원본 EquipmentUI 가 켜져 있으면 끈다(하단 「무기」 버튼의 원본 onClick 이 초기화 없이 켜는 사고 방지)")]
|
||||
public int closeLegacyEquipmentUi = 1;
|
||||
|
||||
[Header("더미 UI (기준 1080×1920 px)")]
|
||||
public int uiSortingOrder = 300;
|
||||
public Color dimColor = new Color(0f, 0f, 0f, 0.75f);
|
||||
public Color panelColor = new Color(0.12f, 0.12f, 0.14f, 0.98f);
|
||||
public Color boxColor = new Color(0.2f, 0.2f, 0.24f, 1f);
|
||||
public Color buttonColor = new Color(0.32f, 0.36f, 0.5f, 1f);
|
||||
public Color buttonDisabledColor = new Color(0.25f, 0.25f, 0.28f, 1f);
|
||||
public Color equippedRowColor = new Color(0.2f, 0.45f, 0.3f, 1f);
|
||||
public Color rowColor = new Color(0.22f, 0.22f, 0.26f, 1f);
|
||||
public Color selectedRowColor = new Color(0.55f, 0.45f, 0.2f, 1f);
|
||||
public float panelMarginPx = 30f;
|
||||
public float titleFontPx = 44f;
|
||||
public float bodyFontPx = 30f;
|
||||
public float smallFontPx = 26f;
|
||||
public float rowHeightPx = 72f;
|
||||
public float slotHeightPx = 110f;
|
||||
public float buttonHeightPx = 84f;
|
||||
public float detailHeightPx = 420f;
|
||||
public int maxListRows = 300;
|
||||
[Tooltip("한글 TMP 폰트 에셋 경로(비우면 화면에 있는 TMP 라벨의 폰트를 빌린다)")]
|
||||
public string fontPath = "";
|
||||
|
||||
static WLEquipSettings s_cache;
|
||||
public static WLEquipSettings Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (s_cache == null) s_cache = Resources.Load<WLEquipSettings>("WL/WLEquipSettings");
|
||||
return s_cache;
|
||||
}
|
||||
}
|
||||
public static bool Enabled { get { var c = Instance; return c != null && c.enabled_ != 0; } }
|
||||
|
||||
public bool IsOpenMenuId(string id)
|
||||
{
|
||||
if (string.IsNullOrEmpty(openMenuIds) || string.IsNullOrEmpty(id)) return false;
|
||||
var parts = openMenuIds.Split(',');
|
||||
for (int i = 0; i < parts.Length; i++) if (parts[i].Trim() == id) return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: f80db86491929264a866457d2db3103e
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// WLEquipStats.cs — FR 장비 능력치를 힘민지 ActorStatInfo 에 더한다 (#821)
|
||||
//
|
||||
// 훅 = `MyValue.cs` ActorStatInfo.Reset() 의 `Set_EquipItem();` 다음 1줄(`WL.Equip.WLEquipStats.Apply(this, m_Role);`).
|
||||
// 능력치 본체(힘·민첩·지능·운·ATK·HP…)는 힘민지 그대로이고, 여기서는 **장착 중인 FR 장비**의
|
||||
// ① 레벨표 수치(AttackAbility/DefenceAbility/HealthAbility → ATK/DEF/HP)
|
||||
// ② 등급별 개방 옵션(FR eAbility % → WLEquipSettings.abilityMap 의 eStat)
|
||||
// 만 Set_Stat(true, …) 로 더한다. 설정이 없거나 꺼져 있으면 아무것도 하지 않는다.
|
||||
// 🔴 장비 능력치 체계(어떤 스탯을 줄지)는 PD 와 후속 논의 예정 — 대응표는 전부 SO 라 코드 수정 없이 바꾼다.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace WL.Equip
|
||||
{
|
||||
public static class WLEquipStats
|
||||
{
|
||||
public static string LastReport { get; private set; } = "";
|
||||
public static int LastAppliedItems { get; private set; }
|
||||
/// <summary>마지막 Apply 에서 장착 장비 레벨표로 더한 공격력 합(기본 공격력 덮어쓰기의 가산분).</summary>
|
||||
public static double LastEquipAttack { get; private set; }
|
||||
public static int BaseAttackEnforced;
|
||||
public static string LastEnforceLog { get; private set; } = "";
|
||||
static readonly List<ItemOptionElement> s_tmp = new List<ItemOptionElement>(8);
|
||||
static readonly HashSet<eFRAbility> s_mapped = new HashSet<eFRAbility>();
|
||||
static bool s_mappedBuilt;
|
||||
|
||||
public static bool IsMapped(eFRAbility a)
|
||||
{
|
||||
if (!s_mappedBuilt)
|
||||
{
|
||||
s_mappedBuilt = true;
|
||||
var cfg = WLEquipSettings.Instance;
|
||||
if (cfg != null && cfg.abilityMap != null)
|
||||
for (int i = 0; i < cfg.abilityMap.Count; i++) s_mapped.Add(cfg.abilityMap[i].ability);
|
||||
}
|
||||
return s_mapped.Contains(a);
|
||||
}
|
||||
|
||||
/// <summary>ActorStatInfo.Reset() 안에서 부른다(PC 만).</summary>
|
||||
public static void Apply(ActorStatInfo info, eRole role)
|
||||
{
|
||||
if (info == null || role != eRole.PC) return;
|
||||
var cfg = WLEquipSettings.Instance;
|
||||
if (cfg == null || cfg.enabled_ == 0) return;
|
||||
if (!WLInventory.EnsureLoaded()) return;
|
||||
var data = WLInventory.Data;
|
||||
LastEquipAttack = 0;
|
||||
if (data == null || data.equipItems.Count == 0) { LastReport = "장착 0"; LastAppliedItems = 0; return; }
|
||||
|
||||
var sb = new System.Text.StringBuilder();
|
||||
int applied = 0;
|
||||
double equipAtk = 0;
|
||||
for (int i = 0; i < data.equipItems.Count; i++)
|
||||
{
|
||||
var item = WLInventory.FindItem(data.equipItems[i]);
|
||||
if (item == null || !item.itemJson.IsEquipment) continue;
|
||||
applied++;
|
||||
sb.Append(item.itemJson.Item_name).Append('(').Append(WLEquipTables.GradeName(item.itemJson.ItemGrade)).Append(" Lv").Append(item.rawItem.level).Append("): ");
|
||||
|
||||
var lt = WLEquipTables.FindItemLevelJson(item.itemJson.ItemLvAbilityID);
|
||||
var lv = lt != null ? lt.FindLevelInfo(item.rawItem.level) : null;
|
||||
if (lv != null)
|
||||
{
|
||||
if (lv.AttackAbility != 0) { info.Set_Stat(true, cfg.levelAttackStat, lv.AttackAbility * cfg.levelStatScale); sb.Append(cfg.levelAttackStat).Append('+').Append(lv.AttackAbility).Append(' '); if (cfg.levelAttackStat == eStat.ATK) equipAtk += lv.AttackAbility * cfg.levelStatScale; }
|
||||
if (lv.DefenceAbility != 0) { info.Set_Stat(true, cfg.levelDefenceStat, lv.DefenceAbility * cfg.levelStatScale); sb.Append(cfg.levelDefenceStat).Append('+').Append(lv.DefenceAbility).Append(' '); }
|
||||
if (lv.HealthAbility != 0) { info.Set_Stat(true, cfg.levelHealthStat, lv.HealthAbility * cfg.levelStatScale); sb.Append(cfg.levelHealthStat).Append('+').Append(lv.HealthAbility).Append(' '); }
|
||||
}
|
||||
|
||||
s_tmp.Clear();
|
||||
WLEquipTables.CollectOpenedOptions(item.itemJson, s_tmp);
|
||||
for (int o = 0; o < s_tmp.Count; o++)
|
||||
{
|
||||
var e = s_tmp[o];
|
||||
for (int a = 0; a < e.Abilities.Count; a++)
|
||||
{
|
||||
var ab = e.Abilities[a];
|
||||
bool any = false;
|
||||
for (int m = 0; m < cfg.abilityMap.Count; m++)
|
||||
{
|
||||
var map = cfg.abilityMap[m];
|
||||
if (map.ability != ab.Ability) continue;
|
||||
info.Set_Stat(true, map.stat, ab.AbilityValue * map.scale);
|
||||
sb.Append(map.stat).Append('+').Append(ab.AbilityValue * map.scale).Append(' ');
|
||||
any = true;
|
||||
}
|
||||
if (!any) sb.Append(ab.Ability).Append("(미매핑) ");
|
||||
}
|
||||
}
|
||||
sb.Append("| ");
|
||||
}
|
||||
LastAppliedItems = applied;
|
||||
LastEquipAttack = equipAtk;
|
||||
LastReport = sb.ToString();
|
||||
if (cfg.verboseLog != 0) Debug.Log("[WL-821] 장비 스탯 적용 " + applied + "개 — " + LastReport);
|
||||
}
|
||||
|
||||
/// <summary>PC 스탯 재계산(힘민지 EquipmentUI 와 같은 호출). 게임 밖(타이틀)에서는 아무 일도 없다.</summary>
|
||||
public static void RefreshPc()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (ServerInfo.Ins == null || ServerInfo.Ins.m_ServerData == null) return;
|
||||
var info = MyValue.Get_ActorStatInfoorNull(eRole.PC);
|
||||
EnforceBaseAttack(info);
|
||||
}
|
||||
catch (System.Exception ex) { Debug.LogWarning("[WL-821] PC 스탯 재계산 실패 — " + ex.Message); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PD 2026-10-09 「플레이어 기본 공격력은 5」 — 힘민지 Reset 은 ATK 를 주스탯(STR/DEX/INT)×배율로 만든다(MyValue.cs:336).
|
||||
/// Reset 끝에 훅이 없으므로 **재계산 뒤 사후 보정**: ATK = 기본값 + 장착 장비 공격력, BaseDmg(최종 공격력)는 같은 비율(BaseDmg/ATK)로.
|
||||
/// Reset 이 다시 돌면 원본 값으로 돌아오므로 주기 폴링(WLEquipLauncher 0.5 s)에서 다시 맞춘다. 0 이면 무동작.
|
||||
/// </summary>
|
||||
public static bool EnforceBaseAttack(ActorStatInfo info)
|
||||
{
|
||||
var cfg = WLEquipSettings.Instance;
|
||||
if (cfg == null || cfg.enabled_ == 0 || cfg.pcBaseAttackOverride <= 0) return false;
|
||||
if (info == null) return false;
|
||||
double atk = info.Get_Stat(eStat.ATK);
|
||||
double target = cfg.pcBaseAttackOverride + LastEquipAttack;
|
||||
if (System.Math.Abs(atk - target) < 0.01) return false;
|
||||
double bd = info.Get_Stat(eStat.BaseDmg);
|
||||
double k = atk > 0.0001 ? bd / atk : 1.0;
|
||||
info.Set_Stat(eStat.ATK, target);
|
||||
info.Set_Stat(eStat.BaseDmg, target * k);
|
||||
BaseAttackEnforced++;
|
||||
LastEnforceLog = "ATK " + atk.ToString("F1") + " → " + target.ToString("F1") + " · BaseDmg " + bd.ToString("F1") + " → " + (target * k).ToString("F1") + " (장비 +" + LastEquipAttack + ")";
|
||||
if (cfg.verboseLog != 0 && BaseAttackEnforced <= 3) Debug.Log("[WL-821] 기본 공격력 보정 — " + LastEnforceLog);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>주기 폴링용 — PC 가 있으면 현재 스탯 객체를 가져와(재계산 없이) 보정한다.</summary>
|
||||
public static void EnforceBaseAttackTick()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!MyValue.bMyPC || MyValue.MyPC == null) return;
|
||||
EnforceBaseAttack(MyValue.MyPC.Get_StatInfo());
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 0e4ded02e5ff17845900c10da1d5f0a6
|
||||
|
|
@ -0,0 +1,316 @@
|
|||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// WLEquipTables.cs — FR 장비 테이블 로더 (#821)
|
||||
//
|
||||
// FR 은 xlsm → json → Cook(prefab) 3단이지만 WL 에서는 **json 을 Resources 에서 바로** 읽는다.
|
||||
// FR 표는 엑셀 셀 병합 때문에 묶음의 첫 행에만 Index 가 있다 → FR `ItemLevelTable.LoadFromText` ·
|
||||
// `JsonUtil.ReadItemDropPackageJson` · `ItemOptionTable.LoadFromText` 의 묶기 규칙을 그대로 재현한다.
|
||||
// 파일 이름은 FR 원본 그대로(`WLEquipSettings.tableFolder` 아래).
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using UnityEngine;
|
||||
|
||||
namespace WL.Equip
|
||||
{
|
||||
public static class WLEquipTables
|
||||
{
|
||||
public static bool Loaded { get; private set; }
|
||||
public static string LastLog { get; private set; } = "";
|
||||
|
||||
static readonly Dictionary<int, ItemJson> s_items = new Dictionary<int, ItemJson>();
|
||||
static readonly List<ItemJson> s_itemList = new List<ItemJson>();
|
||||
static readonly Dictionary<int, ItemLevelJson> s_levels = new Dictionary<int, ItemLevelJson>();
|
||||
static readonly Dictionary<int, ItemComposeJson> s_compose = new Dictionary<int, ItemComposeJson>();
|
||||
static readonly Dictionary<int, ItemOptionJson> s_options = new Dictionary<int, ItemOptionJson>();
|
||||
static readonly Dictionary<int, ItemDropGroupJson> s_dropGroups = new Dictionary<int, ItemDropGroupJson>();
|
||||
static readonly Dictionary<int, ItemDropPackageJson> s_dropPackages = new Dictionary<int, ItemDropPackageJson>();
|
||||
|
||||
public static IReadOnlyList<ItemJson> Items { get { EnsureLoaded(); return s_itemList; } }
|
||||
public static int ItemCount { get { return s_items.Count; } }
|
||||
public static int LevelTableCount { get { return s_levels.Count; } }
|
||||
public static int OptionCount { get { return s_options.Count; } }
|
||||
|
||||
public static bool EnsureLoaded()
|
||||
{
|
||||
if (Loaded) return true;
|
||||
var cfg = WLEquipSettings.Instance;
|
||||
string folder = cfg != null ? cfg.tableFolder : "WL/FR";
|
||||
try
|
||||
{
|
||||
LoadItems(folder + "/ItemTable");
|
||||
LoadCompose(folder + "/ItemComposeTable");
|
||||
LoadOptions(folder + "/ItemOptionTable");
|
||||
string[] levelFiles = { "Item_Weapon", "Item_Necklace", "Item_Gloves", "Item_Amor", "Item_Belt", "Item_Shoes" };
|
||||
for (int i = 0; i < levelFiles.Length; i++) LoadLevels(folder + "/" + levelFiles[i]);
|
||||
LoadDropGroups(folder + "/ItemDropGroupTable");
|
||||
LoadDropPackages(folder + "/ItemDropPackageTable");
|
||||
Loaded = true;
|
||||
LastLog = "FR 표 로드 — 아이템 " + s_items.Count + " · 레벨표 " + s_levels.Count + " · 합성 " + s_compose.Count +
|
||||
" · 옵션 " + s_options.Count + " · 드랍그룹 " + s_dropGroups.Count + " · 드랍패키지 " + s_dropPackages.Count;
|
||||
if (cfg != null && cfg.verboseLog != 0) Debug.Log("[WL-821] " + LastLog);
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
LastLog = "FR 표 로드 실패 — " + ex.Message;
|
||||
Debug.LogError("[WL-821] " + LastLog);
|
||||
}
|
||||
return Loaded;
|
||||
}
|
||||
|
||||
static string ReadText(string path)
|
||||
{
|
||||
var ta = Resources.Load<TextAsset>(path);
|
||||
if (ta == null) throw new System.IO.FileNotFoundException("Resources/" + path + ".json 없음");
|
||||
return ta.text;
|
||||
}
|
||||
|
||||
static void LoadItems(string path)
|
||||
{
|
||||
s_items.Clear(); s_itemList.Clear();
|
||||
var arr = JsonConvert.DeserializeObject<ItemJson[]>(ReadText(path));
|
||||
for (int i = 0; i < arr.Length; i++)
|
||||
{
|
||||
if (arr[i].Index <= 0) continue;
|
||||
if (s_items.ContainsKey(arr[i].Index)) continue;
|
||||
s_items.Add(arr[i].Index, arr[i]);
|
||||
s_itemList.Add(arr[i]);
|
||||
}
|
||||
}
|
||||
|
||||
static void LoadCompose(string path)
|
||||
{
|
||||
s_compose.Clear();
|
||||
var arr = JsonConvert.DeserializeObject<ItemComposeJson[]>(ReadText(path));
|
||||
for (int i = 0; i < arr.Length; i++) if (!s_compose.ContainsKey(arr[i].Index)) s_compose.Add(arr[i].Index, arr[i]);
|
||||
}
|
||||
|
||||
static void LoadOptions(string path)
|
||||
{
|
||||
s_options.Clear();
|
||||
var readers = JsonConvert.DeserializeObject<ItemOptionReader[]>(ReadText(path));
|
||||
ItemOptionJson cur = null;
|
||||
for (int i = 0; i < readers.Length; i++)
|
||||
{
|
||||
var r = readers[i];
|
||||
if (cur == null || (r.Index != 0 && cur.Index != r.Index))
|
||||
{
|
||||
cur = new ItemOptionJson(r.Index, r.Desc);
|
||||
if (!s_options.ContainsKey(cur.Index)) s_options.Add(cur.Index, cur);
|
||||
}
|
||||
cur.ItemOptions.Add(new ItemOptionElement(r));
|
||||
}
|
||||
}
|
||||
|
||||
static void LoadLevels(string path)
|
||||
{
|
||||
var infos = JsonConvert.DeserializeObject<ItemLevelInfo[]>(ReadText(path));
|
||||
ItemLevelJson cur = null;
|
||||
for (int i = 0; i < infos.Length; i++)
|
||||
{
|
||||
int idx = infos[i].Index;
|
||||
if (cur == null || (idx != 0 && cur.Index != idx))
|
||||
{
|
||||
cur = new ItemLevelJson(idx);
|
||||
if (!s_levels.ContainsKey(idx)) s_levels.Add(idx, cur); else cur = s_levels[idx];
|
||||
}
|
||||
else if (idx == 0) infos[i].Index = cur.Index;
|
||||
cur.LevelInfos.Add(infos[i]);
|
||||
}
|
||||
}
|
||||
|
||||
static void LoadDropGroups(string path)
|
||||
{
|
||||
s_dropGroups.Clear();
|
||||
var infos = JsonConvert.DeserializeObject<ItemDropGroupInfo[]>(ReadText(path));
|
||||
ItemDropGroupJson cur = null;
|
||||
for (int i = 0; i < infos.Length; i++)
|
||||
{
|
||||
int idx = infos[i].Index;
|
||||
if (cur == null || (idx != 0 && cur.Index != idx))
|
||||
{
|
||||
cur = new ItemDropGroupJson(idx);
|
||||
if (!s_dropGroups.ContainsKey(idx)) s_dropGroups.Add(idx, cur); else cur = s_dropGroups[idx];
|
||||
}
|
||||
else if (idx == 0) infos[i].Index = cur.Index;
|
||||
cur.ItemDropGroupInfos.Add(infos[i]);
|
||||
}
|
||||
}
|
||||
|
||||
static void LoadDropPackages(string path)
|
||||
{
|
||||
s_dropPackages.Clear();
|
||||
var infos = JsonConvert.DeserializeObject<ItemDropPackageInfo[]>(ReadText(path));
|
||||
ItemDropPackageJson cur = null;
|
||||
for (int i = 0; i < infos.Length; i++)
|
||||
{
|
||||
int idx = infos[i].Index;
|
||||
if (cur == null || (idx != 0 && cur.Index != idx))
|
||||
{
|
||||
cur = new ItemDropPackageJson(idx);
|
||||
if (!s_dropPackages.ContainsKey(idx)) s_dropPackages.Add(idx, cur); else cur = s_dropPackages[idx];
|
||||
}
|
||||
else if (idx == 0) infos[i].Index = cur.Index;
|
||||
cur.ItemDropPackageInfos.Add(infos[i]);
|
||||
cur.TotalRate += infos[i].Rate;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 조회 ───────────────────────────────────────────────────────────
|
||||
public static ItemJson FindItemJson(int index) { EnsureLoaded(); ItemJson j; return s_items.TryGetValue(index, out j) ? j : null; }
|
||||
public static ItemLevelJson FindItemLevelJson(int index) { EnsureLoaded(); ItemLevelJson j; return s_levels.TryGetValue(index, out j) ? j : null; }
|
||||
public static ItemOptionJson FindItemOptionJson(int groupNo) { EnsureLoaded(); ItemOptionJson j; return s_options.TryGetValue(groupNo, out j) ? j : null; }
|
||||
public static ItemDropGroupJson FindDropGroup(int index) { EnsureLoaded(); ItemDropGroupJson j; return s_dropGroups.TryGetValue(index, out j) ? j : null; }
|
||||
public static ItemDropPackageJson FindDropPackage(int index) { EnsureLoaded(); ItemDropPackageJson j; return s_dropPackages.TryGetValue(index, out j) ? j : null; }
|
||||
|
||||
/// <summary>FR `ItemComposeTable.FindItemComposeJson` — 키 = 등급×1000 + 등급 단계.</summary>
|
||||
public static ItemComposeJson FindItemComposeJson(eFRItemGrade grade, int gradeLv)
|
||||
{
|
||||
EnsureLoaded();
|
||||
ItemComposeJson j;
|
||||
return s_compose.TryGetValue((int)grade * 1000 + gradeLv, out j) ? j : null;
|
||||
}
|
||||
|
||||
/// <summary>강등용 역조회 — 결과(등급·단계)가 주어진 값인 합성 행.</summary>
|
||||
public static ItemComposeJson FindComposeByResult(eFRItemGrade resultGrade, int resultGradeLv)
|
||||
{
|
||||
EnsureLoaded();
|
||||
foreach (var kv in s_compose)
|
||||
if (kv.Value.ResultGrade == resultGrade && kv.Value.ResultGradeLevel == resultGradeLv) return kv.Value;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>FR `ComposePopup.FindResultItemJson` — 같은 그룹에서 결과 등급·단계의 장비.</summary>
|
||||
public static ItemJson FindResultItemJson(ItemJson source, ItemComposeJson compose)
|
||||
{
|
||||
return FindGroupItem(source.GroupNo, compose.ResultGrade, compose.ResultGradeLevel);
|
||||
}
|
||||
|
||||
public static ItemJson FindGroupItem(int groupNo, eFRItemGrade grade, int gradeLv)
|
||||
{
|
||||
EnsureLoaded();
|
||||
for (int i = 0; i < s_itemList.Count; i++)
|
||||
{
|
||||
var j = s_itemList[i];
|
||||
if (j.ItemType != eFRItemType.Equipment) continue;
|
||||
if (j.GroupNo == groupNo && j.ItemGrade == grade && j.GradeLv == gradeLv) return j;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>표의 등급 옵션 중 지금 등급까지 열린 것(FR `PlayerManager.GetTotalAbilities` 규칙 — 등급 오름차순, 초과 시 중단).</summary>
|
||||
public static void CollectOpenedOptions(ItemJson item, List<ItemOptionElement> into)
|
||||
{
|
||||
var oj = FindItemOptionJson(item.GroupNo);
|
||||
if (oj == null) return;
|
||||
for (int i = 0; i < oj.ItemOptions.Count; i++)
|
||||
{
|
||||
var e = oj.ItemOptions[i];
|
||||
if (e == null) continue;
|
||||
if (e.ItemGrade > item.ItemGrade) break;
|
||||
into.Add(e);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 드랍 (FR `Data.GetRandomDropPackageInfoInGroup` + `ItemDropPackageTable.GetRandomDropPackageInfo`) ──
|
||||
public struct Drop { public int itemId; public int count; }
|
||||
|
||||
public static void RollDropGroup(int groupId, List<Drop> into, System.Random rng)
|
||||
{
|
||||
var g = FindDropGroup(groupId);
|
||||
if (g == null) return;
|
||||
for (int i = 0; i < g.ItemDropGroupInfos.Count; i++)
|
||||
{
|
||||
var gi = g.ItemDropGroupInfos[i];
|
||||
if (rng.Next(1, 10001) > gi.Rate) continue;
|
||||
var pkg = FindDropPackage(gi.Package_ID);
|
||||
if (pkg == null) continue;
|
||||
if (gi.DropType == eFRDropType.All)
|
||||
{
|
||||
for (int d = 0; d < pkg.ItemDropPackageInfos.Count; d++) into.Add(ToDrop(pkg.ItemDropPackageInfos[d], rng));
|
||||
}
|
||||
else
|
||||
{
|
||||
var p = RollPackage(pkg, rng);
|
||||
if (p != null) into.Add(ToDrop(p, rng));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static ItemDropPackageInfo RollPackage(ItemDropPackageJson pkg, System.Random rng)
|
||||
{
|
||||
if (pkg == null || pkg.TotalRate <= 0) return null;
|
||||
int r = rng.Next(1, pkg.TotalRate + 1);
|
||||
for (int i = 0; i < pkg.ItemDropPackageInfos.Count; i++)
|
||||
{
|
||||
r -= pkg.ItemDropPackageInfos[i].Rate;
|
||||
if (r <= 0) return pkg.ItemDropPackageInfos[i];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Drop ToDrop(ItemDropPackageInfo p, System.Random rng)
|
||||
{
|
||||
int min = p.MinCount <= 0 ? 1 : p.MinCount;
|
||||
int max = p.MaxCount < min ? min : p.MaxCount;
|
||||
return new Drop { itemId = p.Item_ID, count = rng.Next(min, max + 1) };
|
||||
}
|
||||
|
||||
public static string GradeName(eFRItemGrade g)
|
||||
{
|
||||
switch (g)
|
||||
{
|
||||
case eFRItemGrade.Normal: return "일반";
|
||||
case eFRItemGrade.Magic: return "매직";
|
||||
case eFRItemGrade.Rare: return "레어";
|
||||
case eFRItemGrade.Elite: return "엘리트";
|
||||
case eFRItemGrade.Epic: return "에픽";
|
||||
case eFRItemGrade.Legend: return "전설";
|
||||
}
|
||||
return g.ToString();
|
||||
}
|
||||
|
||||
public static string SlotName(eFREquipmentType t)
|
||||
{
|
||||
switch (t)
|
||||
{
|
||||
case eFREquipmentType.Weapon: return "무기";
|
||||
case eFREquipmentType.Necklace: return "목걸이";
|
||||
case eFREquipmentType.Glove: return "장갑";
|
||||
case eFREquipmentType.Armor: return "갑옷";
|
||||
case eFREquipmentType.Belt: return "벨트";
|
||||
case eFREquipmentType.Shose: return "신발";
|
||||
}
|
||||
return t.ToString();
|
||||
}
|
||||
|
||||
public static string AbilityName(eFRAbility a)
|
||||
{
|
||||
switch (a)
|
||||
{
|
||||
case eFRAbility.ItemRange: return "획득 범위 %";
|
||||
case eFRAbility.Health: return "체력 %";
|
||||
case eFRAbility.RecoveryHealth: return "체력 회복 %";
|
||||
case eFRAbility.Speed: return "이동 속도 %";
|
||||
case eFRAbility.Defence: return "피해 감소 %";
|
||||
case eFRAbility.Attack: return "공격력 %";
|
||||
case eFRAbility.IncreaseDrop: return "획득량 %";
|
||||
case eFRAbility.Rolling: return "구르기 %";
|
||||
case eFRAbility.ProjectileSpeed: return "투사체 속도 %";
|
||||
case eFRAbility.SkillRange: return "스킬 범위 %";
|
||||
case eFRAbility.SkillDuration: return "스킬 지속 %";
|
||||
case eFRAbility.Cooltime: return "쿨타임 감소 %";
|
||||
case eFRAbility.AttackDefence: return "공격·방어 %";
|
||||
case eFRAbility.Passive: return "패시브 효과 %";
|
||||
case eFRAbility.Critical: return "치명타 확률 %";
|
||||
case eFRAbility.CriticalDamage: return "치명타 피해 %";
|
||||
case eFRAbility.Revive: return "부활 횟수";
|
||||
case eFRAbility.HeartRecover: return "하트 회복 %";
|
||||
case eFRAbility.SpeedValue: return "이동 속도 +";
|
||||
case eFRAbility.Excellent: return "액설런트 확률 %";
|
||||
case eFRAbility.ExcellentDamage: return "액설런트 피해 %";
|
||||
}
|
||||
return a.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: d747852d081d02a4b99e9b06e2f2c379
|
||||
|
|
@ -0,0 +1,472 @@
|
|||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// WLEquipUi.cs — FR 장비 시스템 더미 UI (#821 · PD 「더미 UI 라도 좋으니 실제 기능 동작」)
|
||||
//
|
||||
// 코드로 만드는 uGUI(프리팹 0 · 스프라이트 0 · 글자만). 자기 Canvas(Overlay · 1080×1920 기준)를 들고
|
||||
// 하단 메뉴 DummyMenuButton(menuId = weapon/upgrade · WLEquipSettings.openMenuIds)에서 연다.
|
||||
// 화면: 장착 슬롯 6 → 보유 목록(스크롤) → 선택 아이템 상세 + 버튼(장착/해제 · 강화 · 전부 강화 · 합성 · 전부 합성 · 강등 · 테스트 획득 · 초기화).
|
||||
// 정식 UI 는 PD 기획 후 교체 — 로직(WLInventory)은 그대로 쓴다.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace WL.Equip
|
||||
{
|
||||
public class WLEquipUi : MonoBehaviour
|
||||
{
|
||||
public static WLEquipUi Instance { get; private set; }
|
||||
public static bool IsOpen { get { return Instance != null && Instance.gameObject.activeSelf; } }
|
||||
public static int Opens { get; private set; }
|
||||
|
||||
const int UILayer = 5;
|
||||
|
||||
WLEquipSettings _cfg;
|
||||
TMP_FontAsset _font;
|
||||
Canvas _canvas;
|
||||
TextMeshProUGUI _title, _info, _detail, _message, _gains;
|
||||
readonly Button[] _slotBtn = new Button[6];
|
||||
readonly TextMeshProUGUI[] _slotLabel = new TextMeshProUGUI[6];
|
||||
RectTransform _listContent;
|
||||
ScrollRect _scroll;
|
||||
readonly List<Button> _rows = new List<Button>();
|
||||
readonly List<TextMeshProUGUI> _rowLabels = new List<TextMeshProUGUI>();
|
||||
readonly List<Image> _rowBg = new List<Image>();
|
||||
readonly List<int> _rowIds = new List<int>();
|
||||
Button _equipBtn, _lvBtn, _lvAllBtn, _composeBtn, _composeAllBtn, _downBtn, _cheatBtn, _resetBtn;
|
||||
TextMeshProUGUI _equipLabel;
|
||||
int _selectedId = -1;
|
||||
|
||||
// ── 진입점 ───────────────────────────────────────────────────────
|
||||
/// <summary>DummyMenuButton 1줄 훅 — 이 menuId 가 장비 메뉴면 열고 true.</summary>
|
||||
public static bool TryOpen(string menuId)
|
||||
{
|
||||
var cfg = WLEquipSettings.Instance;
|
||||
if (cfg == null || cfg.enabled_ == 0 || !cfg.IsOpenMenuId(menuId)) return false;
|
||||
Open();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 🔴 하단 「무기」 버튼(HUD_BottomMenu.prefab)에는 힘민지 원본 `NewGameUI.OnClick_Button`(원본 EquipmentUI 열기)이
|
||||
/// 프리팹에 연결돼 있고 persistent 호출이 런타임 리스너보다 먼저 돈다 → 원본 메뉴가 초기화 없이 켜지면
|
||||
/// `FollowTextEnd` 가 매 프레임 NRE 를 낸다(2026-10-08 실측 · 500건). 장비 화면을 열 때 그 메뉴가 켜져 있으면 끈다.
|
||||
/// </summary>
|
||||
public static bool CloseLegacyEquipmentUi()
|
||||
{
|
||||
var cfg = WLEquipSettings.Instance;
|
||||
if (cfg == null || cfg.closeLegacyEquipmentUi == 0) return false;
|
||||
try
|
||||
{
|
||||
var ng = NewGameUI.Ins;
|
||||
if (ng != null && ng.m_EquipmentUI != null && ng.m_EquipmentUI.gameObject.activeSelf)
|
||||
{
|
||||
ng.m_EquipmentUI.gameObject.SetActive(false);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex) { Debug.LogWarning("[WL-821] 원본 EquipmentUI 닫기 실패 — " + ex.Message); }
|
||||
return false;
|
||||
}
|
||||
|
||||
public static WLEquipUi Open()
|
||||
{
|
||||
var cfg = WLEquipSettings.Instance;
|
||||
if (cfg == null || cfg.enabled_ == 0) return null;
|
||||
CloseLegacyEquipmentUi();
|
||||
if (Instance == null)
|
||||
{
|
||||
var go = new GameObject("WL_EquipUi", typeof(RectTransform));
|
||||
go.layer = UILayer;
|
||||
DontDestroyOnLoad(go);
|
||||
Instance = go.AddComponent<WLEquipUi>();
|
||||
Instance._cfg = cfg;
|
||||
Instance.Build();
|
||||
}
|
||||
Instance.gameObject.SetActive(true);
|
||||
Opens++;
|
||||
WLInventory.EnsureLoaded();
|
||||
Instance.Refresh();
|
||||
return Instance;
|
||||
}
|
||||
|
||||
public static void Close() { if (Instance != null) Instance.gameObject.SetActive(false); }
|
||||
|
||||
void OnEnable() { WLInventory.Changed += Refresh; }
|
||||
void OnDisable() { WLInventory.Changed -= Refresh; }
|
||||
void OnDestroy() { if (Instance == this) Instance = null; }
|
||||
|
||||
// ── 조립 ─────────────────────────────────────────────────────────
|
||||
void Build()
|
||||
{
|
||||
_font = ResolveFont();
|
||||
if (EventSystem.current == null)
|
||||
{
|
||||
var es = new GameObject("WL_EventSystem(Equip)", typeof(EventSystem), typeof(StandaloneInputModule));
|
||||
DontDestroyOnLoad(es);
|
||||
}
|
||||
|
||||
_canvas = gameObject.AddComponent<Canvas>();
|
||||
_canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
_canvas.sortingOrder = _cfg.uiSortingOrder;
|
||||
var scaler = gameObject.AddComponent<CanvasScaler>();
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = new Vector2(1080f, 1920f);
|
||||
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
|
||||
scaler.matchWidthOrHeight = 0.5f;
|
||||
gameObject.AddComponent<GraphicRaycaster>();
|
||||
|
||||
var root = (RectTransform)transform;
|
||||
var dim = NewImage(root, "Dim", _cfg.dimColor); Stretch(dim.rectTransform); dim.raycastTarget = true;
|
||||
|
||||
var panel = NewImage(root, "Panel", _cfg.panelColor);
|
||||
Stretch(panel.rectTransform);
|
||||
float m = _cfg.panelMarginPx;
|
||||
panel.rectTransform.offsetMin = new Vector2(m, m); panel.rectTransform.offsetMax = new Vector2(-m, -m);
|
||||
var vl = panel.gameObject.AddComponent<VerticalLayoutGroup>();
|
||||
vl.padding = new RectOffset(16, 16, 16, 16); vl.spacing = 10f;
|
||||
vl.childControlWidth = true; vl.childControlHeight = true; vl.childForceExpandWidth = true; vl.childForceExpandHeight = false;
|
||||
var prt = panel.rectTransform;
|
||||
|
||||
// 제목 + 닫기
|
||||
var titleRow = NewRow(prt, "TitleRow", 70f);
|
||||
_title = NewText(titleRow, "Title", _cfg.titleFontPx, TextAlignmentOptions.MidlineLeft); Flex(_title.rectTransform, 1f);
|
||||
NewButton(titleRow, "닫기", 160f, Close);
|
||||
|
||||
_info = NewText(prt, "Info", _cfg.smallFontPx, TextAlignmentOptions.MidlineLeft); Pref(_info.rectTransform, 44f);
|
||||
|
||||
// 장착 슬롯 2×3
|
||||
for (int r = 0; r < 2; r++)
|
||||
{
|
||||
var row = NewRow(prt, "SlotRow" + r, _cfg.slotHeightPx);
|
||||
for (int c = 0; c < 3; c++)
|
||||
{
|
||||
int slotIndex = r * 3 + c;
|
||||
_slotBtn[slotIndex] = NewButton(row, "", 0f, () => OnSlotClick(slotIndex));
|
||||
Flex((RectTransform)_slotBtn[slotIndex].transform, 1f);
|
||||
_slotLabel[slotIndex] = _slotBtn[slotIndex].GetComponentInChildren<TextMeshProUGUI>();
|
||||
_slotLabel[slotIndex].fontSize = _cfg.smallFontPx;
|
||||
}
|
||||
}
|
||||
|
||||
// 목록(스크롤)
|
||||
var listGo = new GameObject("List", typeof(RectTransform), typeof(Image), typeof(ScrollRect));
|
||||
listGo.layer = UILayer; listGo.transform.SetParent(prt, false);
|
||||
var listImg = listGo.GetComponent<Image>(); listImg.color = _cfg.boxColor;
|
||||
var le = listGo.AddComponent<LayoutElement>(); le.flexibleHeight = 1f; le.minHeight = 200f;
|
||||
_scroll = listGo.GetComponent<ScrollRect>();
|
||||
var viewport = new GameObject("Viewport", typeof(RectTransform), typeof(Image), typeof(RectMask2D));
|
||||
viewport.layer = UILayer; viewport.transform.SetParent(listGo.transform, false);
|
||||
var vrt = (RectTransform)viewport.transform; Stretch(vrt);
|
||||
viewport.GetComponent<Image>().color = new Color(0f, 0f, 0f, 0.01f);
|
||||
var content = new GameObject("Content", typeof(RectTransform), typeof(VerticalLayoutGroup), typeof(ContentSizeFitter));
|
||||
content.layer = UILayer; content.transform.SetParent(viewport.transform, false);
|
||||
_listContent = (RectTransform)content.transform;
|
||||
_listContent.anchorMin = new Vector2(0f, 1f); _listContent.anchorMax = new Vector2(1f, 1f); _listContent.pivot = new Vector2(0.5f, 1f);
|
||||
_listContent.offsetMin = Vector2.zero; _listContent.offsetMax = Vector2.zero;
|
||||
var cvl = content.GetComponent<VerticalLayoutGroup>();
|
||||
cvl.padding = new RectOffset(8, 8, 8, 8); cvl.spacing = 6f;
|
||||
cvl.childControlWidth = true; cvl.childControlHeight = true; cvl.childForceExpandWidth = true; cvl.childForceExpandHeight = false;
|
||||
content.GetComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||||
_scroll.content = _listContent; _scroll.viewport = vrt; _scroll.horizontal = false; _scroll.vertical = true;
|
||||
_scroll.movementType = ScrollRect.MovementType.Clamped; _scroll.scrollSensitivity = 40f;
|
||||
|
||||
// 상세 + 버튼
|
||||
var detailBox = NewImage(prt, "Detail", _cfg.boxColor); Pref(detailBox.rectTransform, _cfg.detailHeightPx);
|
||||
var dvl = detailBox.gameObject.AddComponent<VerticalLayoutGroup>();
|
||||
dvl.padding = new RectOffset(12, 12, 10, 10); dvl.spacing = 8f;
|
||||
dvl.childControlWidth = true; dvl.childControlHeight = true; dvl.childForceExpandWidth = true; dvl.childForceExpandHeight = false;
|
||||
_detail = NewText(detailBox.rectTransform, "Text", _cfg.smallFontPx, TextAlignmentOptions.TopLeft); Flex(_detail.rectTransform, 1f);
|
||||
var b1 = NewRow(detailBox.rectTransform, "Buttons1", _cfg.buttonHeightPx);
|
||||
_equipBtn = NewButton(b1, "장착", 0f, OnEquipToggle); Flex((RectTransform)_equipBtn.transform, 1f);
|
||||
_equipLabel = _equipBtn.GetComponentInChildren<TextMeshProUGUI>();
|
||||
_lvBtn = NewButton(b1, "강화 +1", 0f, () => Act(() => WLInventory.ItemLevelUp(_selectedId, false))); Flex((RectTransform)_lvBtn.transform, 1f);
|
||||
_lvAllBtn = NewButton(b1, "전부 강화", 0f, () => Act(() => WLInventory.ItemLevelUp(_selectedId, true))); Flex((RectTransform)_lvAllBtn.transform, 1f);
|
||||
_composeBtn = NewButton(b1, "합성", 0f, () => Act(() => WLInventory.ComposeOne(_selectedId))); Flex((RectTransform)_composeBtn.transform, 1f);
|
||||
var b2 = NewRow(detailBox.rectTransform, "Buttons2", _cfg.buttonHeightPx);
|
||||
_composeAllBtn = NewButton(b2, "전부 합성", 0f, () => Act(() => WLInventory.ComposeAll() > 0)); Flex((RectTransform)_composeAllBtn.transform, 1f);
|
||||
_downBtn = NewButton(b2, "강등", 0f, () => Act(() => WLInventory.Downgrade(_selectedId))); Flex((RectTransform)_downBtn.transform, 1f);
|
||||
_cheatBtn = NewButton(b2, "테스트 획득", 0f, OnCheat); Flex((RectTransform)_cheatBtn.transform, 1f);
|
||||
_cheatBtn.gameObject.SetActive(_cfg.cheatGrantEnabled != 0);
|
||||
_resetBtn = NewButton(b2, "초기화", 0f, () => { WLInventory.ResetAll(); _selectedId = -1; Refresh(); }); Flex((RectTransform)_resetBtn.transform, 1f);
|
||||
|
||||
_message = NewText(prt, "Message", _cfg.smallFontPx, TextAlignmentOptions.MidlineLeft); Pref(_message.rectTransform, 40f);
|
||||
_gains = NewText(prt, "Gains", _cfg.smallFontPx, TextAlignmentOptions.MidlineLeft); Pref(_gains.rectTransform, 40f);
|
||||
_gains.color = new Color(0.8f, 0.9f, 0.6f, 1f);
|
||||
}
|
||||
|
||||
TMP_FontAsset ResolveFont() { return ResolveKoreanFont(_cfg); }
|
||||
|
||||
/// <summary>한글 글리프('장'·'비')를 가진 TMP 폰트를 고른다 — 아무 라벨의 폰트를 빌리면 FI 데모 폰트(한글 없음)가 걸려 □ 로 나온다(2026-10-08 캡처 실측).</summary>
|
||||
public static TMP_FontAsset ResolveKoreanFont(WLEquipSettings cfg)
|
||||
{
|
||||
TMP_FontAsset f = null;
|
||||
if (cfg != null && !string.IsNullOrEmpty(cfg.fontPath)) f = WL.Island.WLIslandAssets.Font(cfg.fontPath);
|
||||
if (f != null && HasKorean(f)) return f;
|
||||
var labels = FindObjectsByType<TextMeshProUGUI>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||||
TMP_FontAsset fallback = null;
|
||||
for (int i = 0; i < labels.Length; i++)
|
||||
{
|
||||
var lf = labels[i] != null ? labels[i].font : null;
|
||||
if (lf == null) continue;
|
||||
if (fallback == null) fallback = lf;
|
||||
if (HasKorean(lf)) return lf;
|
||||
}
|
||||
return f != null ? f : fallback;
|
||||
}
|
||||
|
||||
static bool HasKorean(TMP_FontAsset f)
|
||||
{
|
||||
try { return f.HasCharacter('장', true, true) && f.HasCharacter('비', true, true); }
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
// ── 갱신 ─────────────────────────────────────────────────────────
|
||||
public void Refresh()
|
||||
{
|
||||
if (!gameObject.activeSelf) return;
|
||||
if (!WLInventory.EnsureLoaded()) { _title.text = "장비 — 표 로드 실패"; _detail.text = WLEquipTables.LastLog; return; }
|
||||
_title.text = "장비 (FR 방식 · 더미 UI) 보유 " + WLInventory.ItemCount;
|
||||
|
||||
var sb = new System.Text.StringBuilder();
|
||||
sb.Append("코인 ").Append(WLInventory.GetGold()).Append(" 강화석:");
|
||||
if (_cfg.clearStoneItemIds != null)
|
||||
for (int i = 0; i < _cfg.clearStoneItemIds.Length; i++)
|
||||
{
|
||||
var sj = WLEquipTables.FindItemJson(_cfg.clearStoneItemIds[i]);
|
||||
sb.Append(' ').Append(sj != null ? sj.Item_name.Replace(" 강화석", "") : _cfg.clearStoneItemIds[i].ToString()).Append(' ').Append(WLInventory.GetItemCountByIndex(_cfg.clearStoneItemIds[i]));
|
||||
}
|
||||
_info.text = sb.ToString();
|
||||
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
var slot = (eFREquipmentType)(i + 1);
|
||||
var it = WLInventory.EquippedInSlot(slot);
|
||||
_slotLabel[i].text = WLEquipTables.SlotName(slot) + "\n" + (it != null
|
||||
? it.itemJson.Item_name + " [" + WLEquipTables.GradeName(it.itemJson.ItemGrade) + (it.itemJson.GradeLv > 0 ? "+" + it.itemJson.GradeLv : "") + "] Lv" + it.rawItem.level
|
||||
: "(비어 있음)");
|
||||
SetButtonColor(_slotBtn[i], it != null && it.rawItem._id == _selectedId ? _cfg.selectedRowColor : (it != null ? _cfg.equippedRowColor : _cfg.buttonDisabledColor));
|
||||
}
|
||||
|
||||
var items = WLInventory.SortedItems();
|
||||
int n = Mathf.Min(items.Count, _cfg.maxListRows);
|
||||
while (_rows.Count < n) AddRow();
|
||||
for (int i = 0; i < _rows.Count; i++)
|
||||
{
|
||||
bool on = i < n;
|
||||
_rows[i].gameObject.SetActive(on);
|
||||
if (!on) continue;
|
||||
var it = items[i];
|
||||
_rowIds[i] = it.rawItem._id;
|
||||
_rowLabels[i].text = WLInventory.RowText(it);
|
||||
_rowBg[i].color = it.rawItem._id == _selectedId ? _cfg.selectedRowColor : (WLInventory.IsEquipped(it.rawItem._id) ? _cfg.equippedRowColor : _cfg.rowColor);
|
||||
}
|
||||
if (_selectedId >= 0 && WLInventory.FindItem(_selectedId) == null) _selectedId = -1;
|
||||
|
||||
var sel = WLInventory.FindItem(_selectedId);
|
||||
_detail.text = sel != null ? WLInventory.Describe(sel) : "목록에서 아이템을 고르세요.\n던전 클리어 때 FR 드랍표(그룹 " + _cfg.clearDropGroupId + ")로 장비 1개 + 강화석이 들어옵니다.";
|
||||
bool isEq = sel != null && sel.itemJson.IsEquipment;
|
||||
bool equipped = sel != null && WLInventory.IsEquipped(sel.rawItem._id);
|
||||
_equipLabel.text = equipped ? "해제" : "장착";
|
||||
_equipBtn.interactable = isEq;
|
||||
_lvBtn.interactable = isEq && WLInventory.NextLevelCost(sel) != null;
|
||||
_lvAllBtn.interactable = _lvBtn.interactable;
|
||||
_composeBtn.interactable = isEq && WLInventory.PlanCompose(sel) != null;
|
||||
_downBtn.interactable = isEq && WLEquipTables.FindComposeByResult(sel.itemJson.ItemGrade, sel.itemJson.GradeLv) != null;
|
||||
SetButtonColor(_equipBtn, _equipBtn.interactable ? _cfg.buttonColor : _cfg.buttonDisabledColor);
|
||||
SetButtonColor(_lvBtn, _lvBtn.interactable ? _cfg.buttonColor : _cfg.buttonDisabledColor);
|
||||
SetButtonColor(_lvAllBtn, _lvAllBtn.interactable ? _cfg.buttonColor : _cfg.buttonDisabledColor);
|
||||
SetButtonColor(_composeBtn, _composeBtn.interactable ? _cfg.buttonColor : _cfg.buttonDisabledColor);
|
||||
SetButtonColor(_downBtn, _downBtn.interactable ? _cfg.buttonColor : _cfg.buttonDisabledColor);
|
||||
|
||||
_message.text = WLInventory.LastMessage;
|
||||
_gains.text = WLInventory.RecentGains.Count > 0 ? "최근 획득: " + string.Join(" · ", WLInventory.RecentGains.GetRange(0, Mathf.Min(4, WLInventory.RecentGains.Count))) : "";
|
||||
}
|
||||
|
||||
void AddRow()
|
||||
{
|
||||
int i = _rows.Count;
|
||||
var b = NewButton(_listContent, "", _cfg.rowHeightPx, () => OnRowClick(i));
|
||||
var lbl = b.GetComponentInChildren<TextMeshProUGUI>();
|
||||
lbl.alignment = TextAlignmentOptions.MidlineLeft; lbl.fontSize = _cfg.bodyFontPx;
|
||||
lbl.rectTransform.offsetMin = new Vector2(14f, 0f);
|
||||
_rows.Add(b); _rowLabels.Add(lbl); _rowBg.Add(b.GetComponent<Image>()); _rowIds.Add(-1);
|
||||
_rowBg[i].color = _cfg.rowColor;
|
||||
}
|
||||
|
||||
// ── 조작 ─────────────────────────────────────────────────────────
|
||||
void OnRowClick(int i) { if (i < _rowIds.Count) { _selectedId = _rowIds[i]; Refresh(); } }
|
||||
void OnSlotClick(int slotIndex)
|
||||
{
|
||||
var it = WLInventory.EquippedInSlot((eFREquipmentType)(slotIndex + 1));
|
||||
if (it != null) { _selectedId = it.rawItem._id; Refresh(); }
|
||||
}
|
||||
void OnEquipToggle()
|
||||
{
|
||||
Act(() => WLInventory.IsEquipped(_selectedId) ? WLInventory.Unequip(_selectedId) : WLInventory.Equip(_selectedId));
|
||||
}
|
||||
void OnCheat()
|
||||
{
|
||||
WLEquipDrops.Grant(_cfg.cheatGrantGroupId, _cfg.cheatGrantRolls, _cfg.clearStoneItemIds, _cfg.clearStoneCountEach, 0);
|
||||
Refresh();
|
||||
}
|
||||
void Act(System.Func<bool> f)
|
||||
{
|
||||
try { f(); } catch (System.Exception ex) { Debug.LogError("[WL-821] UI 동작 예외 — " + ex); }
|
||||
Refresh();
|
||||
}
|
||||
|
||||
// ── uGUI 헬퍼 ───────────────────────────────────────────────────
|
||||
static void Stretch(RectTransform rt)
|
||||
{
|
||||
rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one; rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero; rt.pivot = new Vector2(0.5f, 0.5f);
|
||||
}
|
||||
static void Pref(RectTransform rt, float h) { var le = rt.gameObject.GetComponent<LayoutElement>() ?? rt.gameObject.AddComponent<LayoutElement>(); le.preferredHeight = h; le.minHeight = h; }
|
||||
static void Flex(RectTransform rt, float w) { var le = rt.gameObject.GetComponent<LayoutElement>() ?? rt.gameObject.AddComponent<LayoutElement>(); le.flexibleWidth = w; le.flexibleHeight = w; }
|
||||
|
||||
Image NewImage(RectTransform parent, string name, Color c)
|
||||
{
|
||||
var go = new GameObject(name, typeof(RectTransform), typeof(Image));
|
||||
go.layer = UILayer; go.transform.SetParent(parent, false);
|
||||
var img = go.GetComponent<Image>(); img.color = c; img.raycastTarget = false;
|
||||
return img;
|
||||
}
|
||||
|
||||
RectTransform NewRow(RectTransform parent, string name, float h)
|
||||
{
|
||||
var go = new GameObject(name, typeof(RectTransform), typeof(HorizontalLayoutGroup));
|
||||
go.layer = UILayer; go.transform.SetParent(parent, false);
|
||||
var hl = go.GetComponent<HorizontalLayoutGroup>();
|
||||
hl.spacing = 8f; hl.childControlWidth = true; hl.childControlHeight = true; hl.childForceExpandWidth = false; hl.childForceExpandHeight = true;
|
||||
var rt = (RectTransform)go.transform; Pref(rt, h);
|
||||
return rt;
|
||||
}
|
||||
|
||||
TextMeshProUGUI NewText(RectTransform parent, string name, float size, TextAlignmentOptions align)
|
||||
{
|
||||
var go = new GameObject(name, typeof(RectTransform), typeof(TextMeshProUGUI));
|
||||
go.layer = UILayer; go.transform.SetParent(parent, false);
|
||||
var t = go.GetComponent<TextMeshProUGUI>();
|
||||
if (_font != null) t.font = _font;
|
||||
t.fontSize = size; t.alignment = align; t.raycastTarget = false; t.color = Color.white;
|
||||
t.overflowMode = TextOverflowModes.Truncate;
|
||||
return t;
|
||||
}
|
||||
|
||||
Button NewButton(RectTransform parent, string label, float width, UnityEngine.Events.UnityAction onClick)
|
||||
{
|
||||
var go = new GameObject("btn_" + label, typeof(RectTransform), typeof(Image), typeof(Button));
|
||||
go.layer = UILayer; go.transform.SetParent(parent, false);
|
||||
var img = go.GetComponent<Image>(); img.color = _cfg.buttonColor; img.raycastTarget = true;
|
||||
var b = go.GetComponent<Button>(); b.targetGraphic = img; b.onClick.AddListener(onClick);
|
||||
var le = go.AddComponent<LayoutElement>();
|
||||
if (width > 0f) { le.preferredWidth = width; le.minWidth = width; }
|
||||
le.preferredHeight = _cfg.buttonHeightPx;
|
||||
var t = NewText((RectTransform)go.transform, "Label", _cfg.bodyFontPx, TextAlignmentOptions.Center);
|
||||
t.text = label; Stretch(t.rectTransform);
|
||||
return b;
|
||||
}
|
||||
|
||||
static void SetButtonColor(Button b, Color c) { var img = b.GetComponent<Image>(); if (img != null) img.color = c; }
|
||||
|
||||
// ── 런처 ─────────────────────────────────────────────────────────
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
||||
static void Boot()
|
||||
{
|
||||
var cfg = WLEquipSettings.Instance;
|
||||
if (cfg == null || cfg.enabled_ == 0 || cfg.launcherEnabled == 0) return;
|
||||
WLEquipLauncher.Ensure(cfg);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 인게임(섬·던전)에서 항상 보이는 「장비」 버튼 — 하단 5메뉴는 섬에서 비활성이라(실측 2026-10-08) 별도 진입점이 필요하다.
|
||||
/// 자기 Canvas(Overlay) 1장 · 버튼 1개 · 0.5 s 마다 보일 조건(활성 씬 = launcherSceneName · 서버 데이터 있음 · 장비 화면 닫힘)을 다시 본다.
|
||||
/// </summary>
|
||||
public class WLEquipLauncher : MonoBehaviour
|
||||
{
|
||||
public static WLEquipLauncher Instance { get; private set; }
|
||||
public static int Clicks { get; private set; }
|
||||
public bool Visible { get { return _btn != null && _btn.gameObject.activeSelf; } }
|
||||
|
||||
WLEquipSettings _cfg;
|
||||
Button _btn;
|
||||
float _timer;
|
||||
|
||||
public static WLEquipLauncher Ensure(WLEquipSettings cfg)
|
||||
{
|
||||
if (Instance != null) return Instance;
|
||||
var go = new GameObject("WL_EquipLauncher", typeof(RectTransform));
|
||||
go.layer = 5;
|
||||
DontDestroyOnLoad(go);
|
||||
Instance = go.AddComponent<WLEquipLauncher>();
|
||||
Instance._cfg = cfg;
|
||||
Instance.Build();
|
||||
return Instance;
|
||||
}
|
||||
|
||||
void Build()
|
||||
{
|
||||
var canvas = gameObject.AddComponent<Canvas>();
|
||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
canvas.sortingOrder = _cfg.uiSortingOrder - 1;
|
||||
var scaler = gameObject.AddComponent<CanvasScaler>();
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = new Vector2(1080f, 1920f);
|
||||
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
|
||||
scaler.matchWidthOrHeight = 0.5f;
|
||||
gameObject.AddComponent<GraphicRaycaster>();
|
||||
|
||||
var bgo = new GameObject("btn_equip", typeof(RectTransform), typeof(Image), typeof(Button));
|
||||
bgo.layer = 5; bgo.transform.SetParent(transform, false);
|
||||
var rt = (RectTransform)bgo.transform;
|
||||
rt.anchorMin = rt.anchorMax = new Vector2(1f, 0f); rt.pivot = new Vector2(1f, 0f);
|
||||
rt.sizeDelta = _cfg.launcherSizePx;
|
||||
rt.anchoredPosition = new Vector2(-_cfg.launcherOffsetPx.x, _cfg.launcherOffsetPx.y);
|
||||
var img = bgo.GetComponent<Image>(); img.color = _cfg.buttonColor;
|
||||
_btn = bgo.GetComponent<Button>(); _btn.targetGraphic = img;
|
||||
_btn.onClick.AddListener(() => { Clicks++; WLEquipUi.Open(); });
|
||||
|
||||
var tgo = new GameObject("Label", typeof(RectTransform), typeof(TextMeshProUGUI));
|
||||
tgo.layer = 5; tgo.transform.SetParent(bgo.transform, false);
|
||||
var t = tgo.GetComponent<TextMeshProUGUI>();
|
||||
var trt = t.rectTransform; trt.anchorMin = Vector2.zero; trt.anchorMax = Vector2.one; trt.offsetMin = trt.offsetMax = Vector2.zero;
|
||||
t.text = _cfg.launcherLabel; t.fontSize = _cfg.bodyFontPx; t.alignment = TextAlignmentOptions.Center; t.raycastTarget = false;
|
||||
_label = t;
|
||||
_btn.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
_timer += Time.unscaledDeltaTime;
|
||||
if (_timer < 0.5f) return;
|
||||
_timer = 0f;
|
||||
WLEquipStats.EnforceBaseAttackTick(); // PD 「기본 공격력 5」 — Reset 이 돌 때마다 원본 공식으로 돌아오므로 0.5 s 마다 다시 맞춘다
|
||||
bool show = false;
|
||||
try
|
||||
{
|
||||
show = _cfg != null && _cfg.enabled_ != 0 && _cfg.launcherEnabled != 0 && !WLEquipUi.IsOpen &&
|
||||
UnityEngine.SceneManagement.SceneManager.GetActiveScene().name == _cfg.launcherSceneName &&
|
||||
ServerInfo.Ins != null && ServerInfo.Ins.m_ServerData != null;
|
||||
}
|
||||
catch { show = false; }
|
||||
if (_btn != null && _btn.gameObject.activeSelf != show)
|
||||
{
|
||||
// 폰트는 처음 보일 때(인게임) 고른다 — 타이틀 시점에 고르면 인게임 한글 폰트(Font SDF)가 아직 없다(2026-10-08 실측 「장□」).
|
||||
if (show && !_fontResolved)
|
||||
{
|
||||
_fontResolved = true;
|
||||
var kf = WLEquipUi.ResolveKoreanFont(_cfg);
|
||||
if (kf != null && _label != null) _label.font = kf;
|
||||
}
|
||||
_btn.gameObject.SetActive(show);
|
||||
}
|
||||
}
|
||||
|
||||
TextMeshProUGUI _label;
|
||||
bool _fontResolved;
|
||||
public string FontName { get { return _label != null && _label.font != null ? _label.font.name : "null"; } }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 636dacd2657838b4198e66cf0d03382d
|
||||
|
|
@ -0,0 +1,593 @@
|
|||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// WLInventory.cs — FR 기존 장비 시스템 로직 이식 (#821)
|
||||
//
|
||||
// 원본 = FR `NetPlayer.cs`(AddItem · EquipItem · ItemLevelUp · ComposeItem · DowngradeItem) +
|
||||
// `ComposePopup.cs`(IsMaterialItem · FindComposeItemInfo · FindResultItemJson) + `UIUtil`(강등 반환).
|
||||
// FR 은 서버/오프라인 4단 패턴이지만 WL 은 서버 없이 **로컬 세이브**(PD 결정 보류 → 가정 · persistentDataPath/WL_Equip.dat)로 돈다.
|
||||
// 골드 = 섬 코인(IslandManager.Coin · 던전 보상이 들어오는 재화) · 섬이 없으면 세이브 안 예비 골드.
|
||||
//
|
||||
// 규칙(PD 확인 2026-10-08): 합성 = 본 아이템 1 + 재료 RequiredItemCount(2) = 3개 소모 · 본 아이템이 상위 등급으로 바뀐다.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json;
|
||||
using UnityEngine;
|
||||
|
||||
namespace WL.Equip
|
||||
{
|
||||
public static class WLInventory
|
||||
{
|
||||
public static WLEquipSaveData Data { get; private set; }
|
||||
public static bool Loaded { get; private set; }
|
||||
public static string LastMessage { get; private set; } = "";
|
||||
/// <summary>최근 획득 로그(UI 표시용 · 최신이 앞).</summary>
|
||||
public static readonly List<string> RecentGains = new List<string>();
|
||||
public static event System.Action Changed;
|
||||
|
||||
static readonly Dictionary<int, Item> s_items = new Dictionary<int, Item>();
|
||||
static bool s_resetDoneThisRun;
|
||||
|
||||
// ── 로드 · 세이브 ──────────────────────────────────────────────────
|
||||
static string SavePath()
|
||||
{
|
||||
var cfg = WLEquipSettings.Instance;
|
||||
string name = cfg != null && !string.IsNullOrEmpty(cfg.saveName) ? cfg.saveName : "WL_Equip";
|
||||
return Application.persistentDataPath + "/" + name + ".dat";
|
||||
}
|
||||
|
||||
public static bool EnsureLoaded()
|
||||
{
|
||||
if (Loaded) return true;
|
||||
if (!WLEquipTables.EnsureLoaded()) return false;
|
||||
var cfg = WLEquipSettings.Instance;
|
||||
string path = SavePath();
|
||||
try
|
||||
{
|
||||
if (cfg != null && cfg.resetSaveEveryStart != 0 && !s_resetDoneThisRun)
|
||||
{
|
||||
s_resetDoneThisRun = true;
|
||||
if (File.Exists(path)) { File.Delete(path); Log("세이브 초기화(실행 1회당 1번) — " + path); }
|
||||
}
|
||||
Data = File.Exists(path) ? JsonConvert.DeserializeObject<WLEquipSaveData>(File.ReadAllText(path)) : null;
|
||||
}
|
||||
catch (System.Exception ex) { Debug.LogWarning("[WL-821] 세이브 읽기 실패 — " + ex.Message); Data = null; }
|
||||
if (Data == null) Data = new WLEquipSaveData();
|
||||
if (Data.items == null) Data.items = new List<RawItem>();
|
||||
if (Data.equipItems == null) Data.equipItems = new List<int>();
|
||||
if (Data.nextId <= 0) Data.nextId = 1;
|
||||
|
||||
s_items.Clear();
|
||||
for (int i = Data.items.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var raw = Data.items[i];
|
||||
var json = WLEquipTables.FindItemJson(raw.index);
|
||||
if (json == null || raw.count <= 0 || s_items.ContainsKey(raw._id)) { Data.items.RemoveAt(i); continue; }
|
||||
s_items[raw._id] = new Item(raw, json);
|
||||
}
|
||||
for (int i = Data.equipItems.Count - 1; i >= 0; i--)
|
||||
if (!s_items.ContainsKey(Data.equipItems[i])) Data.equipItems.RemoveAt(i);
|
||||
Loaded = true;
|
||||
Log("로드 — 아이템 " + s_items.Count + " · 장착 " + Data.equipItems.Count + " · " + path);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void Save()
|
||||
{
|
||||
if (!Loaded) return;
|
||||
try { File.WriteAllText(SavePath(), JsonConvert.SerializeObject(Data)); }
|
||||
catch (System.Exception ex) { Debug.LogWarning("[WL-821] 세이브 쓰기 실패 — " + ex.Message); }
|
||||
}
|
||||
|
||||
/// <summary>세이브를 지우고 빈 상태로(UI 「세이브 초기화」).</summary>
|
||||
public static void ResetAll()
|
||||
{
|
||||
try { var p = SavePath(); if (File.Exists(p)) File.Delete(p); } catch { }
|
||||
Data = new WLEquipSaveData();
|
||||
s_items.Clear();
|
||||
RecentGains.Clear();
|
||||
Loaded = true;
|
||||
Save();
|
||||
LastMessage = "세이브 초기화";
|
||||
Notify();
|
||||
}
|
||||
|
||||
static void Notify()
|
||||
{
|
||||
Save();
|
||||
try { if (Changed != null) Changed(); } catch (System.Exception ex) { Debug.LogWarning("[WL-821] Changed 콜백 예외 — " + ex.Message); }
|
||||
WLEquipStats.RefreshPc();
|
||||
}
|
||||
|
||||
static void Log(string s)
|
||||
{
|
||||
var cfg = WLEquipSettings.Instance;
|
||||
if (cfg != null && cfg.verboseLog != 0) Debug.Log("[WL-821] " + s);
|
||||
}
|
||||
|
||||
// ── 재화 ──────────────────────────────────────────────────────────
|
||||
public static long GetGold()
|
||||
{
|
||||
int c;
|
||||
if (WL.Island.WLIslandCoin.TryGet(out c)) return c;
|
||||
return Data != null ? Data.gold : 0;
|
||||
}
|
||||
|
||||
static bool SpendGold(long amount)
|
||||
{
|
||||
if (amount <= 0) return true;
|
||||
int c;
|
||||
if (WL.Island.WLIslandCoin.TryGet(out c))
|
||||
{
|
||||
if (c < amount) return false;
|
||||
return WL.Island.WLIslandCoin.Add(-(int)amount);
|
||||
}
|
||||
if (Data.gold < amount) return false;
|
||||
Data.gold -= amount;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void AddGold(long amount)
|
||||
{
|
||||
if (amount <= 0) return;
|
||||
if (!WL.Island.WLIslandCoin.Add((int)amount)) Data.gold += amount;
|
||||
}
|
||||
|
||||
// ── 조회 ──────────────────────────────────────────────────────────
|
||||
public static Item FindItem(int id) { Item it; return s_items.TryGetValue(id, out it) ? it : null; }
|
||||
|
||||
public static Item FindItemAnyByIndex(int index)
|
||||
{
|
||||
foreach (var it in s_items.Values) if (it.itemJson.Index == index) return it;
|
||||
return null;
|
||||
}
|
||||
|
||||
public static long GetItemCountByIndex(int index)
|
||||
{
|
||||
var cfg = WLEquipSettings.Instance;
|
||||
if (cfg != null && index == cfg.goldItemIndex) return GetGold();
|
||||
long n = 0;
|
||||
foreach (var it in s_items.Values) if (it.itemJson.Index == index) n += it.rawItem.count;
|
||||
return n;
|
||||
}
|
||||
|
||||
public static bool IsEquipped(int id) { return Data != null && Data.equipItems.Contains(id); }
|
||||
|
||||
public static Item EquippedInSlot(eFREquipmentType slot)
|
||||
{
|
||||
if (Data == null) return null;
|
||||
for (int i = 0; i < Data.equipItems.Count; i++)
|
||||
{
|
||||
var it = FindItem(Data.equipItems[i]);
|
||||
if (it != null && it.itemJson.EquipmentType == slot) return it;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>목록(장착 → 장비(등급 높은 순) → 소모품).</summary>
|
||||
public static List<Item> SortedItems()
|
||||
{
|
||||
var list = new List<Item>(s_items.Values);
|
||||
list.Sort((a, b) =>
|
||||
{
|
||||
bool ea = IsEquipped(a.rawItem._id), eb = IsEquipped(b.rawItem._id);
|
||||
if (ea != eb) return ea ? -1 : 1;
|
||||
bool qa = a.itemJson.IsEquipment, qb = b.itemJson.IsEquipment;
|
||||
if (qa != qb) return qa ? -1 : 1;
|
||||
if (qa)
|
||||
{
|
||||
int t = a.itemJson.EquipmentType.CompareTo(b.itemJson.EquipmentType); if (t != 0) return t;
|
||||
int g = b.itemJson.ItemGrade.CompareTo(a.itemJson.ItemGrade); if (g != 0) return g;
|
||||
int gl = b.itemJson.GradeLv.CompareTo(a.itemJson.GradeLv); if (gl != 0) return gl;
|
||||
int lv = b.rawItem.level.CompareTo(a.rawItem.level); if (lv != 0) return lv;
|
||||
}
|
||||
int ix = a.itemJson.Index.CompareTo(b.itemJson.Index); if (ix != 0) return ix;
|
||||
return a.rawItem._id.CompareTo(b.rawItem._id);
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
public static int ItemCount { get { return s_items.Count; } }
|
||||
|
||||
// ── 획득 (FR AddItem / AddItemInternal) ──────────────────────────
|
||||
/// <summary>아이템 지급. 골드 인덱스는 코인으로. 비스택 장비는 count 개를 각각 1개짜리로 만든다.</summary>
|
||||
public static void AddItem(int itemIndex, int level, int count, List<Item> gained = null)
|
||||
{
|
||||
if (count <= 0) return;
|
||||
if (!EnsureLoaded()) return;
|
||||
var cfg = WLEquipSettings.Instance;
|
||||
if (cfg != null && itemIndex == cfg.goldItemIndex) { AddGold(count); PushGain("골드 +" + count); return; }
|
||||
var json = WLEquipTables.FindItemJson(itemIndex);
|
||||
if (json == null) { Log("AddItem — 표에 없는 Index " + itemIndex); return; }
|
||||
if (level <= 0) level = 1;
|
||||
|
||||
if (json.IsStack == 1)
|
||||
{
|
||||
var exist = FindItemAnyByIndex(itemIndex);
|
||||
if (exist != null) { exist.rawItem.count += count; if (gained != null) gained.Add(exist); }
|
||||
else { var it = NewItem(json, level, count); if (gained != null) gained.Add(it); }
|
||||
PushGain(json.Item_name + " ×" + count);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var it = NewItem(json, level, 1);
|
||||
if (gained != null) gained.Add(it);
|
||||
PushGain(json.Item_name + " [" + WLEquipTables.GradeName(json.ItemGrade) + (json.GradeLv > 0 ? "+" + json.GradeLv : "") + "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Item NewItem(ItemJson json, int level, int count)
|
||||
{
|
||||
var raw = new RawItem(json.Index, level, count, Data.nextId++);
|
||||
Data.items.Add(raw);
|
||||
var it = new Item(raw, json);
|
||||
s_items[raw._id] = it;
|
||||
return it;
|
||||
}
|
||||
|
||||
static void PushGain(string s)
|
||||
{
|
||||
RecentGains.Insert(0, s);
|
||||
if (RecentGains.Count > 30) RecentGains.RemoveAt(RecentGains.Count - 1);
|
||||
}
|
||||
|
||||
/// <summary>여러 번 AddItem 한 뒤 한 번만 저장·통지.</summary>
|
||||
public static void Commit(string message)
|
||||
{
|
||||
LastMessage = message;
|
||||
Notify();
|
||||
}
|
||||
|
||||
static void UpdateItemCount(RawItem raw)
|
||||
{
|
||||
if (raw.count <= 0) RemoveItem(raw._id);
|
||||
}
|
||||
|
||||
public static bool RemoveItem(int id)
|
||||
{
|
||||
Item it;
|
||||
if (!s_items.TryGetValue(id, out it)) return false;
|
||||
Data.equipItems.Remove(id);
|
||||
Data.items.Remove(it.rawItem);
|
||||
s_items.Remove(id);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── 장착 (FR EquipItem / UnequipItem) ────────────────────────────
|
||||
public static bool Equip(int id)
|
||||
{
|
||||
var item = FindItem(id);
|
||||
if (item == null) { LastMessage = "아이템 없음"; return false; }
|
||||
if (!item.itemJson.IsEquipment) { LastMessage = "장비가 아닙니다"; return false; }
|
||||
for (int i = 0; i < Data.equipItems.Count; i++)
|
||||
{
|
||||
var e = FindItem(Data.equipItems[i]);
|
||||
if (e != null && e.itemJson.EquipmentType == item.itemJson.EquipmentType)
|
||||
{
|
||||
if (e == item) { LastMessage = "이미 장착 중"; return false; }
|
||||
Data.equipItems.RemoveAt(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Data.equipItems.Add(id);
|
||||
LastMessage = item.itemJson.Item_name + " 장착";
|
||||
Notify();
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool Unequip(int id)
|
||||
{
|
||||
if (!Data.equipItems.Remove(id)) { LastMessage = "장착 중이 아닙니다"; return false; }
|
||||
var item = FindItem(id);
|
||||
LastMessage = (item != null ? item.itemJson.Item_name : id.ToString()) + " 해제";
|
||||
Notify();
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── 강화(레벨업 · FR ItemLevelUp) ─────────────────────────────────
|
||||
/// <summary>다음 레벨 비용(골드 · 강화석) — 없으면 null.</summary>
|
||||
public static ItemLevelInfo NextLevelCost(Item item)
|
||||
{
|
||||
if (item == null || !item.itemJson.IsEquipment) return null;
|
||||
if (item.rawItem.level >= item.itemJson.ItemMaxLv) return null;
|
||||
var lt = WLEquipTables.FindItemLevelJson(item.itemJson.ItemLvAbilityID);
|
||||
if (lt == null) return null;
|
||||
return lt.FindLevelInfo(item.rawItem.level);
|
||||
}
|
||||
|
||||
public static bool ItemLevelUp(int id, bool all)
|
||||
{
|
||||
var item = FindItem(id);
|
||||
if (item == null) { LastMessage = "아이템 없음"; return false; }
|
||||
if (!item.itemJson.IsEquipment) { LastMessage = "장비가 아닙니다"; return false; }
|
||||
if (item.rawItem.level >= item.itemJson.ItemMaxLv) { LastMessage = "최대 레벨"; return false; }
|
||||
var lt = WLEquipTables.FindItemLevelJson(item.itemJson.ItemLvAbilityID);
|
||||
if (lt == null) { LastMessage = "레벨표 없음(" + item.itemJson.ItemLvAbilityID + ")"; return false; }
|
||||
|
||||
int start = item.rawItem.level;
|
||||
string stop = "";
|
||||
for (int lv = start; lv < item.itemJson.ItemMaxLv; lv++)
|
||||
{
|
||||
var info = lt.FindLevelInfo(lv);
|
||||
var next = lt.FindLevelInfo(lv + 1);
|
||||
if (info == null || next == null) { stop = "표 끝"; break; }
|
||||
if (GetGold() < info.MoneyCountRequired) { stop = "골드 부족(" + info.MoneyCountRequired + ")"; break; }
|
||||
Item stone = null;
|
||||
if (info.ItemKeyRequired > 0 && info.ItemCountRequired > 0)
|
||||
{
|
||||
stone = FindItemAnyByIndex(info.ItemKeyRequired);
|
||||
if (stone == null || stone.rawItem.count < info.ItemCountRequired)
|
||||
{
|
||||
var sj = WLEquipTables.FindItemJson(info.ItemKeyRequired);
|
||||
stop = (sj != null ? sj.Item_name : "강화석") + " 부족(" + info.ItemCountRequired + ")";
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!SpendGold(info.MoneyCountRequired)) { stop = "골드 차감 실패"; break; }
|
||||
if (stone != null) { stone.rawItem.count -= info.ItemCountRequired; UpdateItemCount(stone.rawItem); }
|
||||
item.rawItem.level = next.Level;
|
||||
if (!all) break;
|
||||
}
|
||||
|
||||
if (item.rawItem.level == start) { LastMessage = "강화 불가 — " + stop; return false; }
|
||||
LastMessage = item.itemJson.Item_name + " Lv " + start + " → " + item.rawItem.level + (stop != "" ? " (" + stop + ")" : "");
|
||||
Notify();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>FR ReturnItemLevel — 레벨 1 초과분에 쓴 강화석을 돌려준다(골드는 돌려주지 않는다 · FR 동일 「추정」).</summary>
|
||||
static void RefundLevelMaterials(Item item)
|
||||
{
|
||||
if (item.rawItem.level <= 1) return;
|
||||
var lt = WLEquipTables.FindItemLevelJson(item.itemJson.ItemLvAbilityID);
|
||||
if (lt == null) return;
|
||||
for (int lv = 1; lv < item.rawItem.level; lv++)
|
||||
{
|
||||
var info = lt.FindLevelInfo(lv);
|
||||
if (info == null) continue;
|
||||
if (info.ItemKeyRequired > 0 && info.ItemCountRequired > 0) AddItem(info.ItemKeyRequired, 1, info.ItemCountRequired);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 합성(등급 업 · FR ComposePopup + NetPlayer.ComposeItem) ─────────
|
||||
public class ComposePlan
|
||||
{
|
||||
public Item source;
|
||||
public ItemComposeJson compose;
|
||||
public ItemJson result;
|
||||
public readonly List<Item> materials = new List<Item>();
|
||||
}
|
||||
|
||||
/// <summary>FR `IsMaterialItem`(장비 분기 그대로).</summary>
|
||||
public static bool IsMaterialItem(Item check, Item source, ItemComposeJson c)
|
||||
{
|
||||
if (check.itemJson.EquipmentType != source.itemJson.EquipmentType) return false;
|
||||
if (c.RequiredGradeLevel != check.itemJson.GradeLv) return false;
|
||||
if (c.SameGroupNo == 0)
|
||||
{
|
||||
return check.itemJson.ItemGrade == c.ItemGrade;
|
||||
}
|
||||
if (check.itemJson.ItemGrade != c.ItemGrade) return false;
|
||||
if (check.itemJson.ItemType == eFRItemType.EnchantScroll) return false;
|
||||
return check.itemJson.GroupNo == source.itemJson.GroupNo;
|
||||
}
|
||||
|
||||
/// <summary>본 아이템 하나에 대한 합성 계획(재료는 장착 중이 아닌 것만 · FR 과 같이 재료끼리는 같은 Index).</summary>
|
||||
public static ComposePlan PlanCompose(Item source, HashSet<int> excludeIds = null)
|
||||
{
|
||||
if (source == null || !source.itemJson.IsEquipment) return null;
|
||||
var c = WLEquipTables.FindItemComposeJson(source.itemJson.ItemGrade, source.itemJson.GradeLv);
|
||||
if (c == null) return null;
|
||||
var result = WLEquipTables.FindResultItemJson(source.itemJson, c);
|
||||
if (result == null) return null;
|
||||
|
||||
// 재료 후보를 Index 별로 묶는다(FR: 비스택 재료는 첫 재료와 같은 Index 로 RequiredItemCount 개)
|
||||
var byIndex = new Dictionary<int, List<Item>>();
|
||||
var order = new List<int>();
|
||||
foreach (var it in s_items.Values)
|
||||
{
|
||||
if (it == source) continue;
|
||||
if (IsEquipped(it.rawItem._id)) continue;
|
||||
if (excludeIds != null && excludeIds.Contains(it.rawItem._id)) continue;
|
||||
if (!IsMaterialItem(it, source, c)) continue;
|
||||
List<Item> l;
|
||||
if (!byIndex.TryGetValue(it.itemJson.Index, out l)) { l = new List<Item>(); byIndex[it.itemJson.Index] = l; order.Add(it.itemJson.Index); }
|
||||
l.Add(it);
|
||||
}
|
||||
// 같은 그룹(같은 이름) 재료를 우선, 그다음 레벨 낮은 것부터
|
||||
order.Sort((a, b) =>
|
||||
{
|
||||
bool ga = a == source.itemJson.Index, gb = b == source.itemJson.Index;
|
||||
if (ga != gb) return ga ? -1 : 1;
|
||||
return a.CompareTo(b);
|
||||
});
|
||||
for (int o = 0; o < order.Count; o++)
|
||||
{
|
||||
var l = byIndex[order[o]];
|
||||
l.Sort((x, y) => x.rawItem.level.CompareTo(y.rawItem.level));
|
||||
int have = 0;
|
||||
var plan = new ComposePlan { source = source, compose = c, result = result };
|
||||
for (int i = 0; i < l.Count && have < c.RequiredItemCount; i++)
|
||||
{
|
||||
int n = l[i].itemJson.IsStack == 1 ? l[i].rawItem.count : 1;
|
||||
plan.materials.Add(l[i]);
|
||||
have += n;
|
||||
}
|
||||
if (have >= c.RequiredItemCount) return plan;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static bool Compose(ComposePlan p)
|
||||
{
|
||||
if (p == null || p.source == null) { LastMessage = "합성 재료 부족"; return false; }
|
||||
int need = p.compose.RequiredItemCount;
|
||||
string names = "";
|
||||
for (int i = 0; i < p.materials.Count && need > 0; i++)
|
||||
{
|
||||
var m = p.materials[i];
|
||||
if (m.itemJson.IsStack == 1)
|
||||
{
|
||||
int take = Mathf.Min(need, m.rawItem.count);
|
||||
m.rawItem.count -= take; need -= take;
|
||||
UpdateItemCount(m.rawItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
RefundLevelMaterials(m);
|
||||
m.rawItem.count = 0; need -= 1;
|
||||
UpdateItemCount(m.rawItem);
|
||||
}
|
||||
names += (names == "" ? "" : ", ") + m.itemJson.Item_name;
|
||||
}
|
||||
string before = WLEquipTables.GradeName(p.source.itemJson.ItemGrade) + (p.source.itemJson.GradeLv > 0 ? "+" + p.source.itemJson.GradeLv : "");
|
||||
p.source.rawItem.index = p.result.Index;
|
||||
p.source.itemJson = p.result;
|
||||
if (p.source.rawItem.level > p.result.ItemMaxLv) p.source.rawItem.level = p.result.ItemMaxLv;
|
||||
string after = WLEquipTables.GradeName(p.result.ItemGrade) + (p.result.GradeLv > 0 ? "+" + p.result.GradeLv : "");
|
||||
LastMessage = p.result.Item_name + " " + before + " → " + after + " (재료 " + names + ")";
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool ComposeOne(int sourceId)
|
||||
{
|
||||
var src = FindItem(sourceId);
|
||||
var plan = PlanCompose(src);
|
||||
if (plan == null) { LastMessage = "합성 불가 — 같은 등급·부위 재료 " + (src != null ? ReqCountText(src) : "?") + "개 필요"; return false; }
|
||||
bool ok = Compose(plan);
|
||||
if (ok) Notify();
|
||||
return ok;
|
||||
}
|
||||
|
||||
static string ReqCountText(Item src)
|
||||
{
|
||||
var c = WLEquipTables.FindItemComposeJson(src.itemJson.ItemGrade, src.itemJson.GradeLv);
|
||||
return c != null ? c.RequiredItemCount.ToString() : "-";
|
||||
}
|
||||
|
||||
/// <summary>전부 합성(FR 「모두 합성」) — 장착 중인 것부터 본 아이템으로, 더 안 될 때까지 반복.</summary>
|
||||
public static int ComposeAll()
|
||||
{
|
||||
int done = 0;
|
||||
for (int guard = 0; guard < 500; guard++)
|
||||
{
|
||||
ComposePlan plan = null;
|
||||
foreach (var it in SortedItems())
|
||||
{
|
||||
if (!it.itemJson.IsEquipment) continue;
|
||||
plan = PlanCompose(it);
|
||||
if (plan != null) break;
|
||||
}
|
||||
if (plan == null) break;
|
||||
if (!Compose(plan)) break;
|
||||
done++;
|
||||
}
|
||||
LastMessage = done > 0 ? "전부 합성 — " + done + "회" : "합성할 조합 없음";
|
||||
if (done > 0) Notify();
|
||||
return done;
|
||||
}
|
||||
|
||||
// ── 강등(FR DowngradeItem · 한 단계) ───────────────────────────────
|
||||
public static bool Downgrade(int id)
|
||||
{
|
||||
var item = FindItem(id);
|
||||
if (item == null || !item.itemJson.IsEquipment) { LastMessage = "장비가 아닙니다"; return false; }
|
||||
var c = WLEquipTables.FindComposeByResult(item.itemJson.ItemGrade, item.itemJson.GradeLv);
|
||||
if (c == null) { LastMessage = "최하 등급 — 강등 불가"; return false; }
|
||||
var result = WLEquipTables.FindGroupItem(item.itemJson.GroupNo, c.ItemGrade, c.RequiredGradeLevel);
|
||||
if (result == null) { LastMessage = "강등 결과 아이템 없음"; return false; }
|
||||
|
||||
string before = WLEquipTables.GradeName(item.itemJson.ItemGrade) + (item.itemJson.GradeLv > 0 ? "+" + item.itemJson.GradeLv : "");
|
||||
item.rawItem.index = result.Index;
|
||||
item.itemJson = result;
|
||||
if (item.rawItem.level > result.ItemMaxLv) item.rawItem.level = result.ItemMaxLv;
|
||||
// 재료 반환 — 같은 그룹의 재료 등급 아이템 RequiredItemCount 개(FR UIUtil 「현재 등급까지 지급」의 한 단계 판)
|
||||
AddItem(result.Index, 1, c.RequiredItemCount);
|
||||
LastMessage = result.Item_name + " " + before + " → " + WLEquipTables.GradeName(result.ItemGrade) + (result.GradeLv > 0 ? "+" + result.GradeLv : "") +
|
||||
" (재료 " + c.RequiredItemCount + "개 반환)";
|
||||
Notify();
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── 설명 ───────────────────────────────────────────────────────────
|
||||
public static string Describe(Item it)
|
||||
{
|
||||
if (it == null) return "";
|
||||
var j = it.itemJson;
|
||||
var sb = new System.Text.StringBuilder();
|
||||
sb.Append(j.Item_name).Append(" [").Append(WLEquipTables.GradeName(j.ItemGrade));
|
||||
if (j.GradeLv > 0) sb.Append('+').Append(j.GradeLv);
|
||||
sb.Append("] ");
|
||||
if (j.IsEquipment)
|
||||
{
|
||||
sb.Append(WLEquipTables.SlotName(j.EquipmentType)).Append(" Lv ").Append(it.rawItem.level).Append('/').Append(j.ItemMaxLv);
|
||||
if (IsEquipped(it.rawItem._id)) sb.Append(" (장착 중)");
|
||||
sb.Append('\n');
|
||||
var lt = WLEquipTables.FindItemLevelJson(j.ItemLvAbilityID);
|
||||
var info = lt != null ? lt.FindLevelInfo(it.rawItem.level) : null;
|
||||
if (info != null)
|
||||
{
|
||||
if (info.AttackAbility != 0) sb.Append("공격 +").Append(info.AttackAbility).Append(" ");
|
||||
if (info.DefenceAbility != 0) sb.Append("방어 +").Append(info.DefenceAbility).Append(" ");
|
||||
if (info.HealthAbility != 0) sb.Append("생명 +").Append(info.HealthAbility).Append(" ");
|
||||
}
|
||||
var cost = NextLevelCost(it);
|
||||
if (cost != null)
|
||||
{
|
||||
var sj = WLEquipTables.FindItemJson(cost.ItemKeyRequired);
|
||||
sb.Append("\n다음 강화: 골드 ").Append(cost.MoneyCountRequired);
|
||||
if (cost.ItemKeyRequired > 0) sb.Append(" + ").Append(sj != null ? sj.Item_name : "강화석").Append(' ').Append(cost.ItemCountRequired)
|
||||
.Append(" (보유 ").Append(GetItemCountByIndex(cost.ItemKeyRequired)).Append(')');
|
||||
}
|
||||
else sb.Append("\n최대 레벨");
|
||||
var comp = WLEquipTables.FindItemComposeJson(j.ItemGrade, j.GradeLv);
|
||||
if (comp != null)
|
||||
{
|
||||
var res = WLEquipTables.FindResultItemJson(j, comp);
|
||||
sb.Append("\n합성: 같은 ").Append(comp.SameGroupNo == 1 ? "아이템" : "등급·부위").Append(" ").Append(WLEquipTables.GradeName(comp.ItemGrade))
|
||||
.Append(comp.RequiredGradeLevel > 0 ? "+" + comp.RequiredGradeLevel : "").Append(" 재료 ").Append(comp.RequiredItemCount).Append("개 → ")
|
||||
.Append(res != null ? WLEquipTables.GradeName(res.ItemGrade) + (res.GradeLv > 0 ? "+" + res.GradeLv : "") : "?");
|
||||
}
|
||||
else sb.Append("\n합성: 최고 등급");
|
||||
sb.Append("\n옵션(등급별 개방):");
|
||||
var oj = WLEquipTables.FindItemOptionJson(j.GroupNo);
|
||||
if (oj == null) sb.Append(" 없음");
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < oj.ItemOptions.Count; i++)
|
||||
{
|
||||
var e = oj.ItemOptions[i];
|
||||
bool open = e.ItemGrade <= j.ItemGrade;
|
||||
sb.Append("\n ").Append(open ? "● " : "○ ").Append(WLEquipTables.GradeName(e.ItemGrade)).Append(": ");
|
||||
for (int a = 0; a < e.Abilities.Count; a++)
|
||||
{
|
||||
var ab = e.Abilities[a];
|
||||
sb.Append(WLEquipTables.AbilityName(ab.Ability)).Append(' ').Append(ab.AbilityValue);
|
||||
sb.Append(WLEquipStats.IsMapped(ab.Ability) ? "" : "(미적용)");
|
||||
if (a < e.Abilities.Count - 1) sb.Append(", ");
|
||||
}
|
||||
if (e.ConditionalAbilities.Count > 0) sb.Append(e.Abilities.Count > 0 ? ", " : "").Append("조건부 ").Append(e.ConditionalAbilities.Count).Append("(미적용)");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append("×").Append(it.rawItem.count);
|
||||
if (!string.IsNullOrEmpty(j.Desc)) sb.Append('\n').Append(j.Desc);
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public static string RowText(Item it)
|
||||
{
|
||||
var j = it.itemJson;
|
||||
string g = "[" + WLEquipTables.GradeName(j.ItemGrade) + (j.GradeLv > 0 ? "+" + j.GradeLv : "") + "]";
|
||||
if (j.IsEquipment)
|
||||
return (IsEquipped(it.rawItem._id) ? "E " : " ") + WLEquipTables.SlotName(j.EquipmentType) + " " + j.Item_name + " " + g + " Lv" + it.rawItem.level;
|
||||
return " " + j.Item_name + " ×" + it.rawItem.count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8e7b1ca4ed7e1044989e1d4932a9b625
|
||||
|
|
@ -142,6 +142,12 @@ namespace WL.Island
|
|||
while (t < 5f && !WLIslandCoin.Ready) { t += Time.unscaledDeltaTime; yield return null; }
|
||||
GrantPending(cfg);
|
||||
|
||||
// WL-822 — 섬 몬스터 존(땅 1칸 1곳 · 한도·쿨타임·남은 시간 글자) · 설정 off 면 무동작
|
||||
WL.IslandMob.WLIslandMobZones.Begin(cfg, scene);
|
||||
|
||||
// WL-822 — 섬 기본 아바타 = 전투 PC(농부는 투명 그림자) · 켜져 있으면 아래 「PC 숨김」 대신 이쪽이 PC 를 다룬다
|
||||
if (WL.IslandMob.WLIslandAvatar.Enabled) { WL.IslandMob.WLIslandAvatar.Begin(cfg, scene); yield break; }
|
||||
|
||||
// 🔴 WL 전투 PC 는 Addressables 콜백으로 **나중에** 생긴다(PCInfo.cs:16) — 나타나면 그때 숨긴다.
|
||||
t = 0f;
|
||||
while (t < cfg.hidePcWatchSeconds)
|
||||
|
|
@ -265,6 +271,7 @@ namespace WL.Island
|
|||
|
||||
// 🔴 WL-816zc ② — 「전투 승리 후 섬 확장 노드 추가」. 보상과 무관하게 승리 그 자체로 연다.
|
||||
WLIslandExpansionGate.Unlock(cfg);
|
||||
WL.Equip.WLEquipDrops.OnStageCleared(in r); // WL-821 FR 드랍표로 장비·강화석 지급(설정 off 면 무동작)
|
||||
|
||||
if (cfg.rewardEnabled == 0) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ namespace WL.Island
|
|||
WLIslandExpansionGate.ClearSpotMemory(); // 2026-09-17 — 확장 가격판 자리 기억도 세이브와 함께 초기화
|
||||
PlayerPrefs.Save();
|
||||
WLIslandBridge.ClearPending(); // 보류 보상도 비운다(정적 값까지)
|
||||
WL.IslandMob.WLIslandMobZones.ResetSave(); // WL-822 — 몬스터 존 누적·재등장 시각도 같이 초기화
|
||||
}
|
||||
|
||||
// ② 시작 골드 — `startingMoney` 는 `data == null`(세이브 없음)일 때만 쓰인다(IslandManager.cs:397)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 845545a9c25297d4e88757e11972b809
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 30f58a0bcbd4706489847634477e8900
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e95412f77ae60814797a2bef5894d1d3
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: da6d8eae99c32414ca494c2c81db0d34, type: 3}
|
||||
m_Name: WLIslandMobSettings
|
||||
m_EditorClassIdentifier: Assembly-CSharp::WL.IslandMob.WLIslandMobSettings
|
||||
enabled_: 1
|
||||
verboseLog: 1
|
||||
combatAvatarEnabled: 1
|
||||
farmerModeRadius: 2
|
||||
farmerExitDelay: 0.4
|
||||
moveDeadzone: 0.15
|
||||
pcNavSnapRadius: 3
|
||||
pcNavCheckSeconds: 1
|
||||
pcWaitSeconds: 10
|
||||
spawnerId: 1
|
||||
aliveMax: 3
|
||||
zoneOffset: {x: 2.5, y: 0, z: 2.5}
|
||||
zoneRadius: 3
|
||||
zoneNavSnap: 4
|
||||
skipBridgeTiles: 1
|
||||
avoidFarmRadius: 3
|
||||
rescanSeconds: 3
|
||||
spawnLimit: 10
|
||||
cooldownSeconds: 10800
|
||||
recallOverLimit: 1
|
||||
prefsPrefix: WL_MobZone_
|
||||
resetWithIslandSave: 1
|
||||
labelHeight: 1.8
|
||||
labelActiveFormat: "\uBAAC\uC2A4\uD130 {0}/{1}"
|
||||
labelCooldownFormat: "\uC7AC\uB4F1\uC7A5 {0:hh\\:mm\\:ss}"
|
||||
labelRemainSuffix: " \xB7 \uB0A8\uC740 \uBAAC\uC2A4\uD130 {0}"
|
||||
labelFontSize: 3.2
|
||||
labelActiveColor: {r: 1, g: 0.85, b: 0.4, a: 1}
|
||||
labelCooldownColor: {r: 0.8, g: 0.9, b: 1, a: 1}
|
||||
labelRefreshSeconds: 1
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: b5e0af224fdb3304581b94ecccb2fb08
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,343 @@
|
|||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 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();
|
||||
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") : "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 36a6a22b08ab57240863b607f7a6b23c
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// WLIslandMobSettings.cs — 섬 몬스터 존 + 섬 전투 아바타 설정(SO · Resources/WL/WLIslandMobSettings.asset) (#822)
|
||||
//
|
||||
// PD 지시(2026-10-09): 섬에서도 몬스터 등장 · 확장 땅 1칸 = 섬 1개 · 섬마다 1곳에서만 · 한도(10) 뒤 긴 쿨타임(3시간) ·
|
||||
// 남은 시간 텍스트. PD 역제안: 「기본 = 전투 캐릭터 · 농작물 있을 때만 농부」.
|
||||
// 🔴 SO 는 클래스와 같은 이름의 .cs 에만(816g 교훈).
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
namespace WL.IslandMob
|
||||
{
|
||||
public sealed class WLIslandMobSettings : ScriptableObject
|
||||
{
|
||||
[Header("스위치")]
|
||||
[Tooltip("0 이면 전부 무동작(섬은 지금처럼 농부만 · 몬스터 없음)")]
|
||||
public int enabled_ = 1;
|
||||
public int verboseLog = 1;
|
||||
|
||||
[Header("섬 아바타 — 기본 전투 PC · 농부는 보이지 않는 그림자")]
|
||||
[Tooltip("1 이면 섬에서 힘민지 전투 PC 를 켜고 조이스틱으로 움직인다(FI 농부는 PC 를 따라다니는 투명 그림자 = 발판·상인·밭 트리거 유지)")]
|
||||
public int combatAvatarEnabled = 1;
|
||||
[Tooltip("농부 모드 조건 ① FI 가 농사 중(IsWorking) · 조작 잠김(낚시·나무 흔들기) ② 밭(Farm) 콜라이더까지 이 거리(m) 안 — 0 이면 ②는 안 씀")]
|
||||
public float farmerModeRadius = 2f;
|
||||
[Tooltip("농부 모드 조건이 사라진 뒤 전투로 돌아가기까지 대기(초) — 밭 가장자리에서 깜빡임 방지")]
|
||||
public float farmerExitDelay = 0.4f;
|
||||
[Tooltip("조이스틱 입력 데드존(0~1)")]
|
||||
public float moveDeadzone = 0.15f;
|
||||
[Tooltip("PC 를 FI 자리로 되돌릴 때 NavMesh 탐색 반경(m) — 섬 확장 재베이크 뒤 이탈 복구에도 쓴다")]
|
||||
public float pcNavSnapRadius = 3f;
|
||||
[Tooltip("PC 가 NavMesh 밖인지 확인하는 주기(초)")]
|
||||
public float pcNavCheckSeconds = 1f;
|
||||
[Tooltip("전투 PC 가 생길 때까지 기다리는 상한(초 · Addressables 비동기)")]
|
||||
public float pcWaitSeconds = 10f;
|
||||
[Tooltip("1 이면 섬에서 PC 가 죽으면 부활 팝업 없이 제자리에서 자동 부활(던전의 실패·복귀 흐름이 섬을 재로드하는 사고 방지)")]
|
||||
public int autoReviveOnIsland = 1;
|
||||
|
||||
[Header("몬스터 존 — 땅 1칸(FI Island) 당 1곳")]
|
||||
[Tooltip("MonsterAppear 표의 SpawnerID(몹 종류·리젠 간격 f_SpawnerDelay 는 표가 정한다). 🔴 e_SpawnerType = Random 이어야 한 마리 죽을 때마다 리젠된다 · 1 = 힘민지 Lv1 잡몹(101~104 · 딜레이 4 s). 813001 은 PhaseAllKill 이라 부적합")]
|
||||
public int spawnerId = 1;
|
||||
[Tooltip("존에 동시에 살아 있는 몬스터 상한 — 표의 n_MaxMonsterCount 가 더 커도 초과분은 즉시 회수(Off)하고 누적에 세지 않는다")]
|
||||
public int aliveMax = 3;
|
||||
[Tooltip("등장하는 몬스터의 최대 HP 를 이 값으로 덮는다(PD 2026-10-09 「플레이어 공격 3~4방」 · 기본 공격력 5 기준 18). 0 = MonsterAppear 표 값(스포너 1 은 300)")]
|
||||
public double mobMaxHp = 18;
|
||||
[Tooltip("타일 중심에서 존 자리까지의 오프셋(m · 타일 한 변 8 m)")]
|
||||
public Vector3 zoneOffset = new Vector3(2.5f, 0f, 2.5f);
|
||||
[Tooltip("존 반경(m) = 몹 생성 범위(MobGenRange)")]
|
||||
public float zoneRadius = 3f;
|
||||
[Tooltip("존 자리를 NavMesh 위로 맞출 탐색 반경(m)")]
|
||||
public float zoneNavSnap = 4f;
|
||||
[Tooltip("1 이면 다리 타일에는 존을 두지 않는다")]
|
||||
public int skipBridgeTiles = 1;
|
||||
[Tooltip("밭·플로우 설치물과 이 거리(m) 안이면 다른 모서리 오프셋을 시도한다 · 0 = 안 피함")]
|
||||
public float avoidFarmRadius = 3f;
|
||||
[Tooltip("새 타일 검색 주기(초)")]
|
||||
public float rescanSeconds = 3f;
|
||||
|
||||
[Header("한도 · 쿨타임")]
|
||||
[Tooltip("존당 누적 등장 한도(마리) — 이 수가 생성되면 더 안 나오고 쿨타임 시작")]
|
||||
public int spawnLimit = 10;
|
||||
[Tooltip("쿨타임(초) · 10800 = 3시간 · 실시간(앱을 꺼도 흐른다)")]
|
||||
public double cooldownSeconds = 10800;
|
||||
[Tooltip("한도를 넘긴 뒤 리젠으로 새로 나온 몹은 즉시 회수한다(원본 리젠 예약은 끊을 수 없어 사후 회수)")]
|
||||
public int recallOverLimit = 1;
|
||||
[Tooltip("PlayerPrefs 키 접두 — 존 누적 수·재등장 시각 저장")]
|
||||
public string prefsPrefix = "WL_MobZone_";
|
||||
[Tooltip("1 이면 섬 세이브 초기화(실행마다) 때 존 기록도 지운다")]
|
||||
public int resetWithIslandSave = 1;
|
||||
|
||||
[Header("존 라벨(3D 글자)")]
|
||||
public float labelHeight = 1.8f;
|
||||
[Tooltip("{0} = 누적 등장 · {1} = 한도")]
|
||||
public string labelActiveFormat = "몬스터 {0}/{1}";
|
||||
[Tooltip("{0} = 남은 시간 TimeSpan · {1} = 남아 있는 몬스터")]
|
||||
public string labelCooldownFormat = "재등장 {0:hh\\:mm\\:ss}";
|
||||
public string labelRemainSuffix = " · 남은 몬스터 {0}";
|
||||
public float labelFontSize = 3.2f;
|
||||
public Color labelActiveColor = new Color(1f, 0.85f, 0.4f, 1f);
|
||||
public Color labelCooldownColor = new Color(0.8f, 0.9f, 1f, 1f);
|
||||
[Tooltip("라벨 갱신 주기(초)")]
|
||||
public float labelRefreshSeconds = 1f;
|
||||
|
||||
static WLIslandMobSettings s_cache;
|
||||
public static WLIslandMobSettings Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (s_cache == null) s_cache = Resources.Load<WLIslandMobSettings>("WL/WLIslandMobSettings");
|
||||
return s_cache;
|
||||
}
|
||||
}
|
||||
public static bool Enabled { get { var c = Instance; return c != null && c.enabled_ != 0; } }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: da6d8eae99c32414ca494c2c81db0d34
|
||||
|
|
@ -0,0 +1,393 @@
|
|||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// WLIslandMobZones.cs — 섬 몬스터 존: 열린 땅 1칸마다 1곳 · 한도 뒤 긴 쿨타임 · 남은 시간 글자 (#822)
|
||||
//
|
||||
// ■ 존 1개 = 힘민지 `MobControlMgr` 1개(SpawnerID = 설정 · MonsterAppear 표가 몹 종류·동시 수·리젠 간격을 정한다 · 던전 스포너와 같은 조립)
|
||||
// + 3D 라벨(WLIslandLabel) + PlayerPrefs 기록(누적 등장 수 · 재등장 시각).
|
||||
// ■ 누적 등장 = `CombatEvents.Spawned`(MobActor.On_Regen 원본 훅)를 존 반경으로 가려내 센다.
|
||||
// 한도 도달 → 리젠 예약 차단(`StopAllCoroutines` · StageSpawner 와 같은 경로) + 이후 리젠으로 또 나오면 즉시 회수(`Off`).
|
||||
// 남아 있는 몹은 잡을 때까지 유지. 쿨타임(실시간)이 끝나면 기록을 0 으로 하고 비활성 몹을 깨운다(`On_Regen` · StageSpawner.DeployWave 와 같은 경로).
|
||||
// 🔴 원본 수정 0줄 · Debug.LogError 금지(원본 ErrorLogHook 이 팝업을 띄운다).
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using CryingSnow.FarmingIsland;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AI;
|
||||
using UnityEngine.SceneManagement;
|
||||
using WL.Combat.Core;
|
||||
using WL.Island;
|
||||
using FIIsland = CryingSnow.FarmingIsland.Island; // 🔴 `WL.Island` 네임스페이스와 이름이 겹친다
|
||||
using FIIslandManager = CryingSnow.FarmingIsland.IslandManager;
|
||||
using UObject = UnityEngine.Object;
|
||||
|
||||
namespace WL.IslandMob
|
||||
{
|
||||
public sealed class WLIslandMobZones : MonoBehaviour
|
||||
{
|
||||
public sealed class Zone
|
||||
{
|
||||
public Vector3Int tile;
|
||||
public string key;
|
||||
public GameObject root;
|
||||
public MobControlMgr mgr;
|
||||
public TextMeshPro label;
|
||||
public Vector3 pos;
|
||||
public int spawned;
|
||||
public long cooldownUntilTicks; // 0 = 쿨타임 아님
|
||||
public bool capped;
|
||||
public int recalled;
|
||||
/// <summary>이 존이 「살려 둔」 몹(누적에 센 것) — 죽거나 꺼지면 빠진다.</summary>
|
||||
public readonly HashSet<Actor> live = new HashSet<Actor>();
|
||||
public int Alive
|
||||
{
|
||||
get
|
||||
{
|
||||
live.RemoveWhere(a => a == null || !a.isActiveAndEnabled || a.IsDead());
|
||||
return live.Count;
|
||||
}
|
||||
}
|
||||
public bool Cooling { get { return cooldownUntilTicks > 0 && DateTime.UtcNow.Ticks < cooldownUntilTicks; } }
|
||||
public TimeSpan Remaining { get { return cooldownUntilTicks > 0 ? new TimeSpan(Math.Max(0, cooldownUntilTicks - DateTime.UtcNow.Ticks)) : TimeSpan.Zero; } }
|
||||
}
|
||||
|
||||
public static WLIslandMobZones Instance { get; private set; }
|
||||
public static string LastLog = "";
|
||||
public static int SpawnEvents, CapHits, Rearms;
|
||||
public readonly List<Zone> Zones = new List<Zone>(16);
|
||||
|
||||
WLIslandMobSettings _cfg;
|
||||
Scene _scene;
|
||||
float _scanTimer, _labelTimer;
|
||||
readonly Dictionary<Vector3Int, Zone> _byTile = new Dictionary<Vector3Int, Zone>(16);
|
||||
|
||||
public static WLIslandMobZones Begin(WLIslandSettings islandCfg, Scene scene)
|
||||
{
|
||||
if (Instance != null) return Instance;
|
||||
var cfg = WLIslandMobSettings.Instance;
|
||||
if (cfg == null || cfg.enabled_ == 0) return null;
|
||||
if (InGameInfo.Ins == null) return null; // 섬 단독 Play(룩 확인)에는 테이블·ActorInfo 가 없다
|
||||
var go = new GameObject("~WL_IslandMobZones");
|
||||
if (scene.IsValid()) SceneManager.MoveGameObjectToScene(go, scene);
|
||||
Instance = go.AddComponent<WLIslandMobZones>();
|
||||
Instance._cfg = cfg;
|
||||
Instance._scene = scene;
|
||||
CombatEvents.Spawned.Add(Instance.OnSpawned);
|
||||
CombatEvents.Killed.Add(Instance.OnKilled);
|
||||
Instance.Rescan();
|
||||
return Instance;
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
CombatEvents.Spawned.Remove(OnSpawned);
|
||||
CombatEvents.Killed.Remove(OnKilled);
|
||||
if (Instance == this) Instance = null;
|
||||
}
|
||||
|
||||
void Log(string s) { LastLog = s; if (_cfg != null && _cfg.verboseLog != 0) Debug.Log("[WL-822] " + s); }
|
||||
|
||||
void Update()
|
||||
{
|
||||
_scanTimer += Time.unscaledDeltaTime;
|
||||
if (_scanTimer >= _cfg.rescanSeconds) { _scanTimer = 0f; Rescan(); }
|
||||
_labelTimer += Time.unscaledDeltaTime;
|
||||
if (_labelTimer >= _cfg.labelRefreshSeconds) { _labelTimer = 0f; Tick(); }
|
||||
}
|
||||
|
||||
// ── 타일 → 존 ───────────────────────────────────────────────────
|
||||
public void Rescan()
|
||||
{
|
||||
var mgr = FIIslandManager.Instance;
|
||||
if (mgr == null) return;
|
||||
var tiles = UObject.FindObjectsByType<FIIsland>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
|
||||
for (int i = 0; i < tiles.Length; i++)
|
||||
{
|
||||
var isl = tiles[i];
|
||||
if (isl == null || !isl.IsUnlocked || !isl.gameObject.activeInHierarchy) continue;
|
||||
if (_cfg.skipBridgeTiles != 0 && isl.IsBridge) continue;
|
||||
if (isl.transform.localScale.x < 0.999f) continue; // 활성화 애니(점프) 중
|
||||
var key = isl.GetPosition();
|
||||
if (_byTile.ContainsKey(key)) continue;
|
||||
var z = MakeZone(isl, key);
|
||||
if (z != null) { _byTile[key] = z; Zones.Add(z); }
|
||||
}
|
||||
}
|
||||
|
||||
Zone MakeZone(FIIsland isl, Vector3Int key)
|
||||
{
|
||||
Vector3 center = isl.transform.position;
|
||||
Vector3 pos;
|
||||
if (!PickZonePos(center, out pos)) { Log("존 자리 없음(NavMesh) — 타일 " + key); return null; }
|
||||
|
||||
var z = new Zone { tile = key, key = _cfg.prefsPrefix + key.x + "_" + key.z, pos = pos };
|
||||
z.spawned = PlayerPrefs.GetInt(z.key + "_n", 0);
|
||||
long ticks;
|
||||
z.cooldownUntilTicks = long.TryParse(PlayerPrefs.GetString(z.key + "_t", "0"), out ticks) ? ticks : 0;
|
||||
if (z.cooldownUntilTicks > 0 && DateTime.UtcNow.Ticks >= z.cooldownUntilTicks) { z.cooldownUntilTicks = 0; z.spawned = 0; SaveZone(z); }
|
||||
z.capped = z.cooldownUntilTicks > 0;
|
||||
|
||||
z.root = new GameObject("~WL_MobZone_" + key.x + "_" + key.z);
|
||||
if (_scene.IsValid()) SceneManager.MoveGameObjectToScene(z.root, _scene);
|
||||
z.root.transform.position = pos;
|
||||
WLIslandLabel.Attach(z.root.transform, new Vector3(0f, _cfg.labelHeight, 0f), "");
|
||||
var lt = z.root.transform.Find("label");
|
||||
z.label = lt != null ? lt.GetComponent<TextMeshPro>() : null;
|
||||
if (z.label != null) z.label.fontSize = _cfg.labelFontSize;
|
||||
|
||||
if (!z.capped) ArmSpawner(z);
|
||||
RegisterKey(z.key);
|
||||
UpdateLabel(z);
|
||||
Log("존 생성 — 타일 " + key + " @" + pos.ToString("F1") + " 누적 " + z.spawned + "/" + _cfg.spawnLimit + (z.capped ? " 쿨타임 " + z.Remaining : ""));
|
||||
return z;
|
||||
}
|
||||
|
||||
bool PickZonePos(Vector3 center, out Vector3 pos)
|
||||
{
|
||||
// 설정 오프셋 → 4 모서리 순으로 NavMesh 위이고 밭에서 떨어진 자리를 고른다
|
||||
Vector3 o = _cfg.zoneOffset;
|
||||
Vector3[] cands = { center + o, center + new Vector3(-o.x, o.y, o.z), center + new Vector3(o.x, o.y, -o.z), center + new Vector3(-o.x, o.y, -o.z), center };
|
||||
for (int i = 0; i < cands.Length; i++)
|
||||
{
|
||||
NavMeshHit hit;
|
||||
if (!NavMesh.SamplePosition(cands[i], out hit, _cfg.zoneNavSnap, NavMesh.AllAreas)) continue;
|
||||
if (_cfg.avoidFarmRadius > 0f && NearFarm(hit.position, _cfg.avoidFarmRadius)) continue;
|
||||
pos = hit.position;
|
||||
return true;
|
||||
}
|
||||
pos = center;
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool NearFarm(Vector3 p, float r)
|
||||
{
|
||||
var farms = UObject.FindObjectsByType<Farm>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
|
||||
float r2 = r * r;
|
||||
for (int i = 0; i < farms.Length; i++)
|
||||
{
|
||||
if (farms[i] == null) continue;
|
||||
var c = farms[i].transform.position; c.y = p.y;
|
||||
if ((c - p).sqrMagnitude <= r2) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── 스포너 ─────────────────────────────────────────────────────
|
||||
void ArmSpawner(Zone z)
|
||||
{
|
||||
if (z.mgr != null) return;
|
||||
var go = new GameObject("~WL_Spawner_" + _cfg.spawnerId);
|
||||
go.SetActive(false); // 🔴 필드부터 채우고 깨운다(던전과 동일)
|
||||
go.transform.SetParent(z.root.transform, false);
|
||||
go.transform.localPosition = Vector3.zero;
|
||||
var labelGo = new GameObject("lv");
|
||||
labelGo.transform.SetParent(go.transform, false);
|
||||
var tmp = labelGo.AddComponent<TextMeshPro>();
|
||||
tmp.text = "Lv.???"; tmp.fontSize = 2.4f;
|
||||
labelGo.SetActive(false);
|
||||
var mgr = go.AddComponent<MobControlMgr>();
|
||||
mgr.SpawnerID = _cfg.spawnerId;
|
||||
mgr.MobGenRange = _cfg.zoneRadius;
|
||||
mgr.text_lv = tmp;
|
||||
go.SetActive(true);
|
||||
z.mgr = mgr;
|
||||
}
|
||||
|
||||
/// <summary>쿨타임이 끝났을 때 — 기록 0 · 비활성 몹을 표의 동시 수만큼 깨운다(StageSpawner.DeployWave 와 같은 경로).</summary>
|
||||
void Rearm(Zone z)
|
||||
{
|
||||
z.capped = false; z.spawned = 0; z.cooldownUntilTicks = 0; z.recalled = 0;
|
||||
SaveZone(z);
|
||||
Rearms++;
|
||||
if (z.mgr == null) { ArmSpawner(z); Log("존 재가동(스포너 신설) — 타일 " + z.tile); return; }
|
||||
int want = Mathf.Min(TableMaxCount(), _cfg.aliveMax);
|
||||
int done = 0;
|
||||
var info = ActorInfo.Ins;
|
||||
if (DSUtil.CheckNull(info) || want <= 0) { Log("존 재가동 — 깨울 대상 없음(ActorInfo/표)"); return; }
|
||||
List<Actor> actors = null;
|
||||
try
|
||||
{
|
||||
actors = info.Get_Actors(eRole.Mob, _cfg.spawnerId, true);
|
||||
if (actors != null)
|
||||
{
|
||||
int alive = z.Alive;
|
||||
for (int i = 0; i < actors.Count && alive + done < want; i++)
|
||||
{
|
||||
var mob = actors[i] as MobActor;
|
||||
if (DSUtil.CheckNull(mob) || mob.isActiveAndEnabled) continue;
|
||||
_pendingZone = z; // 깨우는 순간 Spawned 이벤트가 이 존에 귀속되게
|
||||
mob.On_Regen();
|
||||
_pendingZone = null;
|
||||
NavMeshHit hit;
|
||||
var want2 = z.pos + new Vector3(UnityEngine.Random.Range(-_cfg.zoneRadius, _cfg.zoneRadius), 0f, UnityEngine.Random.Range(-_cfg.zoneRadius, _cfg.zoneRadius));
|
||||
if (NavMesh.SamplePosition(want2, out hit, _cfg.zoneNavSnap, NavMesh.AllAreas)) mob.Set_Warp(hit.position); else mob.Set_Warp(z.pos);
|
||||
done++;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex) { Log("존 재가동 실패 — " + ex.Message); }
|
||||
finally { if (actors != null) info.Release_Actors(actors); }
|
||||
Log("존 재가동 — 타일 " + z.tile + " 깨움 " + done);
|
||||
}
|
||||
|
||||
int TableMaxCount()
|
||||
{
|
||||
try { var d = table_monsterappear.Ins.Get_Data_orNull(_cfg.spawnerId); return d != null ? d.n_MaxMonsterCount : 0; }
|
||||
catch { return 0; }
|
||||
}
|
||||
|
||||
bool InZone(Zone z, Vector3 p)
|
||||
{
|
||||
float r = _cfg.zoneRadius + 2.5f;
|
||||
var d = p - z.pos; d.y = 0f;
|
||||
return d.sqrMagnitude <= r * r;
|
||||
}
|
||||
|
||||
Zone ZoneOf(Vector3 p)
|
||||
{
|
||||
Zone best = null; float bestD = float.MaxValue;
|
||||
for (int i = 0; i < Zones.Count; i++)
|
||||
{
|
||||
var z = Zones[i];
|
||||
var d = p - z.pos; d.y = 0f;
|
||||
float sq = d.sqrMagnitude;
|
||||
if (sq < bestD) { bestD = sq; best = z; }
|
||||
}
|
||||
float r = _cfg.zoneRadius + 2.5f;
|
||||
return best != null && bestD <= r * r ? best : null;
|
||||
}
|
||||
|
||||
// ── 등장 집계(원본 훅 이벤트) ──────────────────────────────────
|
||||
Zone _pendingZone;
|
||||
|
||||
void OnSpawned(in SpawnedEvent e)
|
||||
{
|
||||
if (e.actor == null) return;
|
||||
if (e.actor.Get_Group() != _cfg.spawnerId) return;
|
||||
var z = _pendingZone ?? ZoneOf(e.position);
|
||||
if (z == null) return;
|
||||
SpawnEvents++;
|
||||
|
||||
// 한도 뒤(쿨타임) · 동시 상한 초과 → 즉시 회수(누적에 안 센다)
|
||||
if (z.capped || z.Cooling || z.Alive >= _cfg.aliveMax)
|
||||
{
|
||||
if (_cfg.recallOverLimit != 0) { try { e.actor.Off(); z.recalled++; } catch { } }
|
||||
return;
|
||||
}
|
||||
z.live.Add(e.actor);
|
||||
z.spawned++;
|
||||
ApplyMobHp(e.actor);
|
||||
if (z.spawned >= _cfg.spawnLimit)
|
||||
{
|
||||
z.capped = true;
|
||||
z.cooldownUntilTicks = DateTime.UtcNow.Ticks + (long)(_cfg.cooldownSeconds * TimeSpan.TicksPerSecond);
|
||||
CapHits++;
|
||||
try { if (z.mgr != null && z.mgr.Get_LoadComplete()) z.mgr.StopAllCoroutines(); } catch { }
|
||||
Log("존 한도 도달 — 타일 " + z.tile + " " + z.spawned + "/" + _cfg.spawnLimit + " → 쿨타임 " + TimeSpan.FromSeconds(_cfg.cooldownSeconds));
|
||||
}
|
||||
SaveZone(z);
|
||||
UpdateLabel(z);
|
||||
}
|
||||
|
||||
public static int HpOverrides;
|
||||
public static string LastHpLog = "";
|
||||
/// <summary>PD 「플레이어 공격 3~4방에 죽게」 — 등장 직후 MaxHP·HP 를 설정값으로(표 수정 0 · 던전 몹 무변경).</summary>
|
||||
void ApplyMobHp(Actor a)
|
||||
{
|
||||
if (_cfg.mobMaxHp <= 0) return;
|
||||
try
|
||||
{
|
||||
var st = a.Get_StatInfo();
|
||||
if (st == null) return;
|
||||
double before = st.Get_Stat(eStat.MaxHP);
|
||||
st.Set_Stat(eStat.MaxHP, _cfg.mobMaxHp);
|
||||
st.Set_Stat(eStat.HP, _cfg.mobMaxHp);
|
||||
HpOverrides++;
|
||||
LastHpLog = a.name + " MaxHP " + before.ToString("F0") + " → " + _cfg.mobMaxHp.ToString("F0") + " (HP " + a.Get_HP().ToString("F0") + "/" + a.Get_MaxHP().ToString("F0") + ")";
|
||||
}
|
||||
catch (Exception ex) { LastHpLog = "HP 덮기 실패 — " + ex.Message; }
|
||||
}
|
||||
|
||||
void OnKilled(in KilledEvent e)
|
||||
{
|
||||
if (e.victim == null) return;
|
||||
for (int i = 0; i < Zones.Count; i++) if (Zones[i].live.Remove(e.victim)) { UpdateLabel(Zones[i]); break; }
|
||||
}
|
||||
|
||||
// ── 주기 처리 · 라벨 ───────────────────────────────────────────
|
||||
void Tick()
|
||||
{
|
||||
for (int i = 0; i < Zones.Count; i++)
|
||||
{
|
||||
var z = Zones[i];
|
||||
if (z.capped && !z.Cooling) Rearm(z);
|
||||
UpdateLabel(z);
|
||||
}
|
||||
}
|
||||
|
||||
int AliveIn(Zone z) { return z.Alive; }
|
||||
|
||||
void UpdateLabel(Zone z)
|
||||
{
|
||||
if (z.label == null) return;
|
||||
if (z.capped || z.Cooling)
|
||||
{
|
||||
string s = string.Format(_cfg.labelCooldownFormat, z.Remaining);
|
||||
int alive = AliveIn(z);
|
||||
if (alive > 0) s += string.Format(_cfg.labelRemainSuffix, alive);
|
||||
z.label.text = s;
|
||||
z.label.color = _cfg.labelCooldownColor;
|
||||
}
|
||||
else
|
||||
{
|
||||
z.label.text = string.Format(_cfg.labelActiveFormat, z.spawned, _cfg.spawnLimit);
|
||||
z.label.color = _cfg.labelActiveColor;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 저장 ───────────────────────────────────────────────────────
|
||||
void SaveZone(Zone z)
|
||||
{
|
||||
PlayerPrefs.SetInt(z.key + "_n", z.spawned);
|
||||
PlayerPrefs.SetString(z.key + "_t", z.cooldownUntilTicks.ToString());
|
||||
}
|
||||
|
||||
void RegisterKey(string key)
|
||||
{
|
||||
string list = PlayerPrefs.GetString(_cfg.prefsPrefix + "keys", "");
|
||||
if (list.Contains(key + ";")) return;
|
||||
PlayerPrefs.SetString(_cfg.prefsPrefix + "keys", list + key + ";");
|
||||
}
|
||||
|
||||
/// <summary>섬 세이브 초기화 때(WLIslandStart.Prepare) 존 기록도 지운다.</summary>
|
||||
public static void ResetSave()
|
||||
{
|
||||
var cfg = WLIslandMobSettings.Instance;
|
||||
if (cfg == null || cfg.resetWithIslandSave == 0) return;
|
||||
string list = PlayerPrefs.GetString(cfg.prefsPrefix + "keys", "");
|
||||
var keys = list.Split(';');
|
||||
for (int i = 0; i < keys.Length; i++)
|
||||
{
|
||||
if (string.IsNullOrEmpty(keys[i])) continue;
|
||||
PlayerPrefs.DeleteKey(keys[i] + "_n");
|
||||
PlayerPrefs.DeleteKey(keys[i] + "_t");
|
||||
}
|
||||
PlayerPrefs.DeleteKey(cfg.prefsPrefix + "keys");
|
||||
}
|
||||
|
||||
public string Describe()
|
||||
{
|
||||
var sb = new System.Text.StringBuilder();
|
||||
sb.Append("zones=").Append(Zones.Count).Append(" spawnEvents=").Append(SpawnEvents).Append(" caps=").Append(CapHits).Append(" rearms=").Append(Rearms).Append(" hpOverrides=").Append(HpOverrides).Append(" lastHp=[").Append(LastHpLog).Append("]\n");
|
||||
for (int i = 0; i < Zones.Count; i++)
|
||||
{
|
||||
var z = Zones[i];
|
||||
sb.Append(" ").Append(z.tile).Append(" @").Append(z.pos.ToString("F1")).Append(" spawned=").Append(z.spawned).Append(" capped=").Append(z.capped)
|
||||
.Append(" cooling=").Append(z.Cooling).Append(" alive=").Append(AliveIn(z)).Append(" mgr=").Append(z.mgr != null ? (z.mgr.Get_LoadComplete() ? "loaded" : "loading") : "none")
|
||||
.Append(" label='").Append(z.label != null ? z.label.text : "-").Append("'\n");
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 84088cb294be0884a972637324d665fb
|
||||
|
|
@ -30,6 +30,7 @@ namespace WL.UI
|
|||
|
||||
private void OnClick()
|
||||
{
|
||||
if (WL.Equip.WLEquipUi.TryOpen(menuId)) return; // WL-821 장비 메뉴(weapon/upgrade · WLEquipSettings.openMenuIds)
|
||||
Debug.Log("[DummyMenuButton] " + menuId + " (더미 · 기능 미구현)");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue