diff --git a/Fantasy-Grower/.claude/commands/commit.md b/Fantasy-Grower/.claude/commands/commit.md new file mode 100644 index 0000000..93ce5ee --- /dev/null +++ b/Fantasy-Grower/.claude/commands/commit.md @@ -0,0 +1,20 @@ +Perform the following steps in order to git commit the current project changes. + +## Steps + +1. Run `git status --porcelain` to check the list of changed files. + - If there are no changes, notify "No changes to commit." and exit. + +2. Run `git diff HEAD` to analyze the changes. + - If there are no staged changes, also check unstaged changes with `git diff`. + +3. Based on the changed files and content, write a commit message in Conventional Commits format in English. + - Format: `type: subject` (choose from feat / fix / refactor / docs / chore) + - Summarize the changes concisely + - Example: `feat: add /report slash command` + +4. Show me the generated commit message and proceed with the commit. + +5. Stage all changes with `git add -A`, then commit with `git commit -m "..."`. + +6. After the commit, run `git log --oneline -1` to verify and display the result. diff --git a/Fantasy-Grower/.claude/commands/push.md b/Fantasy-Grower/.claude/commands/push.md new file mode 100644 index 0000000..1c72ee7 --- /dev/null +++ b/Fantasy-Grower/.claude/commands/push.md @@ -0,0 +1,22 @@ +Perform the following steps in order to commit and push the current project changes to the remote repository. + +## Steps + +1. Run `git status --porcelain` to check the list of changed files. + - If there are no changes, skip to step 5 to push any unpushed commits. + +2. Run `git diff HEAD` to analyze the changes. + - If there are no staged changes, also check unstaged changes with `git diff`. + +3. Based on the changed files and content, write a commit message in Conventional Commits format. + - Format: `type: subject` (choose from feat / fix / refactor / docs / chore) + - **Write the subject in Korean** + - Summarize the changes concisely + - Example: `feat: /report 슬래시 커맨드 추가` + +4. Stage all changes with `git add -A`, then commit with `git commit -m "..."`. + +5. Run `git push` to push to the remote repository. + - If the upstream is not set, run `git push -u origin HEAD` instead. + +6. After the push, run `git log --oneline -1` to verify and display the result. \ No newline at end of file diff --git a/Fantasy-Grower/.claude/settings.local.json b/Fantasy-Grower/.claude/settings.local.json new file mode 100644 index 0000000..25df048 --- /dev/null +++ b/Fantasy-Grower/.claude/settings.local.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Bash(npm install:*)" + ] + } +} diff --git a/Fantasy-Grower/.gemini/commands/commit.toml b/Fantasy-Grower/.gemini/commands/commit.toml new file mode 100644 index 0000000..ae5d907 --- /dev/null +++ b/Fantasy-Grower/.gemini/commands/commit.toml @@ -0,0 +1,64 @@ +description = "Git 변경사항을 논리적 단위로 분리하여 커밋합니다." +prompt = """ +Create Git commits following the project's commit conventions. +## Current Changes +### git status +!{git status} +### git diff (unstaged) +!{git diff} +### git diff (staged) +!{git diff --cached} +--- +## Commit Message Format +{type}: {Korean description} +**Types:** +| Type | When to use | +|--------|-------------| +| feat | New file(s) added (new system / component / UI / shader / animation / scene) | +| fix | Broken behavior fixed, or missing reference / config corrected | +| modify | Existing file(s) modified — rename, restructure, method added to existing class | +| docs | Documentation changes only | +| chore | Tooling, CI/CD, dependency updates, config changes with no behavior change | +| asset | Art / audio / VFX / animation asset added or updated (no code change) | +**Boundary rules:** +- New script file added (System / Manager / Controller / Handler) → `feat` +- New method added to an existing script → `modify` +- Missing component reference or Inspector binding corrected → `fix` +- New system file + its initialization/registration together → `feat` (one logical unit) +- New scene or prefab added → `feat` +- Existing scene or prefab modified → `modify` +- New UI screen or panel added → `feat` +- Existing UI layout or logic modified → `modify` +- New shader or VFX graph added → `feat` +- Shader / VFX parameter tuning only → `modify` +- Art / audio / animation asset added or swapped → `asset` +- Refactoring without behavior change → `modify` +- Input binding / build setting / project setting changed → `chore` +**Description rules:** +- Written in **Korean** +- Short and imperative (단문) +- No trailing punctuation (`.`, `!`, etc.) +- Prefer verb style over noun style +**Examples:** +- feat: 전투 콤보 시스템 추가 +- feat: 인벤토리 UI 화면 추가 +- fix: 점프 중 이동 입력 씹히는 문제 수정 +- fix: PlayerController Inspector 참조 누락 수정 +- modify: 캐릭터 이동 속도 보간 방식 변경 +- modify: 보스 패턴 스크립트 리팩토링 +- asset: 플레이어 공격 이펙트 VFX 추가 +- chore: Input System 패키지 버전 업데이트 +**Do NOT:** +- Add Claude/Gemini as co-author +- Write descriptions in English +- Add a commit body — subject line only +--- +## Steps +1. Analyze all changes from the git status and diff above. +2. Categorize changes into **logical units** — group files that belong to the same feature or fix. +3. For each logical unit: + a. Stage only the relevant files with `git add ` + b. Write a concise commit message following the format above + c. Execute `git commit -m "message"` +4. After all commits, verify with `git log --oneline -n {number of commits made}`. +""" \ No newline at end of file diff --git a/Fantasy-Grower/.gemini/commands/pr.toml b/Fantasy-Grower/.gemini/commands/pr.toml new file mode 100644 index 0000000..5b2b6f6 --- /dev/null +++ b/Fantasy-Grower/.gemini/commands/pr.toml @@ -0,0 +1,160 @@ +description = "현재 브랜치 기반으로 GitHub PR을 생성합니다. 사용법: /pr 또는 /pr {base-branch}" + +prompt = """ +Generate and create a GitHub Pull Request based on the current branch. + +## Runtime Context + +### Current branch +!{git branch --show-current} + +### Recent tags +!{git tag --sort=-v:refname | head -10} + +### Existing release branches +!{git branch -a | grep release} + +### User-provided argument (base branch override) +{{args}} + +--- + +## Step 0. Determine behavior + +- If `{{args}}` is **not empty**: set Base Branch = `{{args}}`, skip to **Case 3** immediately. +- If `{{args}}` is **empty**: check the current branch name and follow the rules below. + +--- + +## Case 1: Current branch is `develop` + +**Step 1.** Determine the latest version from tags and release branches. + +**Step 2.** Analyze changes from `main`: +- `git log main..HEAD --oneline` +- `git diff main...HEAD --stat` + +**Step 3.** Recommend a version bump (Major / Minor / Patch) and explain why briefly. + +**Step 4.** Ask the user: "현재 버전: {current_version} / 추천: {recommended_version} ({reason}) — 사용할 버전 번호를 입력해주세요. (예: 1.0.1)" + +**Step 5.** After the user replies with a version number: +- Run: `git checkout -b release/{version}` +- Analyze changes from `main` for the PR body + +**Step 6.** Write the PR body following the **PR Body Template** below. Save to `PR_BODY.md`. + +**Step 7.** Run: +``` +gh pr create --title "release/{version}" --body-file PR_BODY.md --base main +``` + +**Step 8.** Run: `rm PR_BODY.md` + +--- + +## Case 2: Current branch matches `release/x.x.x` + +**Step 1.** Extract version from branch name (e.g., `release/1.2.0` → `1.2.0`). + +**Step 2.** Analyze changes from `main`: +- `git log main..HEAD --oneline` +- `git diff main...HEAD --stat` + +**Step 3.** Write PR body following the **PR Body Template** below. Save to `PR_BODY.md`. + +**Step 4.** Run: +``` +gh pr create --title "release/{version}" --body-file PR_BODY.md --base main +``` + +**Step 5.** Run: `rm PR_BODY.md` + +--- + +## Case 3: Any other branch (or base branch was specified via argument) + +**Step 1.** Set Base Branch: +- If `{{args}}` is not empty → Base Branch = `{{args}}` +- Otherwise → Base Branch = `develop` + +**Step 2.** Analyze changes from Base Branch: +- `git log {Base Branch}..HEAD --oneline` +- `git diff {Base Branch}...HEAD --stat` +- `git diff {Base Branch}...HEAD` + +**Step 3.** Suggest **three PR title options** following the **PR Title Convention** below. + +**Step 4.** Write the PR body following the **PR Body Template** below. Save to `PR_BODY.md`. + +**Step 5.** Show the PR body preview and the three title options to the user. Ask: +"PR 제목을 선택해주세요. (1 / 2 / 3 / 직접 입력)" + +**Step 6.** After the user selects or types a title: +``` +gh pr create --title "{chosen title}" --body-file PR_BODY.md --base {Base Branch} +``` + +**Step 7.** Run: `rm PR_BODY.md` + +--- + +## PR Title Convention + +Format: `{type}: {Korean description}` + +**Types:** +- `feature` — new system / UI / scene / shader / mechanic added +- `fix` — bug fix or missing component reference / binding corrected +- `update` — modification to existing script, prefab, or UI +- `asset` — art / audio / VFX / animation asset added or updated +- `refactor` — refactoring without behavior change +- `docs` — documentation changes +- `chore` — tooling, CI/CD, build setting, dependency updates + +**Rules:** +- Description in Korean +- Short and imperative (단문) +- No trailing punctuation + +**Examples:** +- `feature: 콤보 전투 시스템 추가` +- `fix: 점프 중 이동 입력 씹히는 문제 수정` +- `update: 보스 패턴 밸런스 조정` +- `asset: 플레이어 공격 VFX 추가` +- `refactor: PlayerController 구조 정리` + +--- + +## PR Body Template + +Use this exact structure (keep the emoji headers): + +``` +## 📚작업 내용 + +- {change item 1} +- {change item 2} + +## ◀️참고 사항 + +{additional notes, context, before/after comparisons if relevant. Write "." if nothing to add.} + +## ✅체크리스트 + +> `[ ]`안에 x를 작성하면 체크박스를 체크할 수 있습니다. + +- [x] 현재 의도하고자 하는 기능이 정상적으로 작동하나요? +- [x] 변경한 기능이 다른 기능을 깨뜨리지 않나요? + + +> *추후 필요한 체크리스트는 업데이트 될 예정입니다.* +``` + +**Writing rules:** +- Fill `작업 내용` bullets by grouping commits meaningfully — not one bullet per commit +- `참고 사항`: Inspector 설정, 씬 구성, 에셋 경로, before/after 비교 등. Write `"."` if nothing to add +- Keep total body under 2500 characters +- All text content in Korean (keep section header emojis as-is) +- No emojis in body text — section headers only +""" \ No newline at end of file diff --git a/Fantasy-Grower/Assets/Prefabs.meta b/Fantasy-Grower/Assets/Prefabs.meta new file mode 100644 index 0000000..49b9e67 --- /dev/null +++ b/Fantasy-Grower/Assets/Prefabs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 71292b37a0e8bed4abca4ad42fc54a6f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Prefabs/Enemy.meta b/Fantasy-Grower/Assets/Prefabs/Enemy.meta new file mode 100644 index 0000000..b1c68d2 --- /dev/null +++ b/Fantasy-Grower/Assets/Prefabs/Enemy.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: beff064fe6e40fd4c88854db06cd2e05 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Prefabs/Enemy/Enemy.prefab b/Fantasy-Grower/Assets/Prefabs/Enemy/Enemy.prefab new file mode 100644 index 0000000..48b1adb --- /dev/null +++ b/Fantasy-Grower/Assets/Prefabs/Enemy/Enemy.prefab @@ -0,0 +1,343 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &4193620460523036667 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5678651543255339951} + - component: {fileID: 5311048965299034863} + - component: {fileID: 3158600927115594800} + - component: {fileID: 846388253315880859} + - component: {fileID: 5471916572661113379} + - component: {fileID: 6811259245424818294} + m_Layer: 0 + m_Name: Enemy + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &5678651543255339951 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4193620460523036667} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 1, y: 0.5, z: 0} + m_LocalScale: {x: 0.5, y: 0.5, z: 0.5} + m_ConstrainProportionsScale: 1 + m_Children: + - {fileID: 9122292137280017520} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!212 &5311048965299034863 +SpriteRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4193620460523036667} + m_Enabled: 1 + m_CastShadows: 0 + m_ReceiveShadows: 0 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 0 + m_RayTraceProcedural: 0 + m_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: a97c105638bdf8b4a8650670310a4cd3, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 0 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 + m_Sprite: {fileID: 7482667652216324306, guid: 48e93eef0688c4a259cb0eddcd8661f7, type: 3} + m_Color: {r: 1, g: 0, b: 0.02439022, a: 1} + m_FlipX: 0 + m_FlipY: 0 + m_DrawMode: 0 + m_Size: {x: 1, y: 1} + m_AdaptiveModeThreshold: 0.5 + m_SpriteTileMode: 0 + m_WasSpriteAssigned: 1 + m_MaskInteraction: 0 + m_SpriteSortPoint: 0 +--- !u!61 &3158600927115594800 +BoxCollider2D: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4193620460523036667} + m_Enabled: 1 + serializedVersion: 3 + m_Density: 1 + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_ForceSendLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_ForceReceiveLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_ContactCaptureLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_CallbackLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_IsTrigger: 0 + m_UsedByEffector: 0 + m_CompositeOperation: 0 + m_CompositeOrder: 0 + m_Offset: {x: 0, y: 0} + m_SpriteTilingProperty: + border: {x: 0, y: 0, z: 0, w: 0} + pivot: {x: 0.5, y: 0.5} + oldSize: {x: 1, y: 1} + newSize: {x: 1, y: 1} + adaptiveTilingThreshold: 0.5 + drawMode: 0 + adaptiveTiling: 0 + m_AutoTiling: 0 + m_Size: {x: 1, y: 1} + m_EdgeRadius: 0 +--- !u!50 &846388253315880859 +Rigidbody2D: + serializedVersion: 5 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4193620460523036667} + m_BodyType: 1 + m_Simulated: 1 + m_UseFullKinematicContacts: 0 + m_UseAutoMass: 0 + m_Mass: 1 + m_LinearDamping: 0 + m_AngularDamping: 0.05 + m_GravityScale: 1 + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_Interpolate: 0 + m_SleepingMode: 1 + m_CollisionDetection: 0 + m_Constraints: 0 +--- !u!114 &5471916572661113379 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4193620460523036667} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d42734d476595bc40835c071beb08767, type: 3} + m_Name: + m_EditorClassIdentifier: +--- !u!114 &6811259245424818294 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4193620460523036667} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5180275fdefeff94fbdcd78cf158830b, type: 3} + m_Name: + m_EditorClassIdentifier: + statData: {fileID: 11400000, guid: 947637eac36383b49827043094c83699, type: 2} + _rewardData: {fileID: 11400000, guid: c7bbabc7ae6924a4ca2dd98f7f57abf7, type: 2} + _gold: {fileID: 0} + _xp: {fileID: 0} +--- !u!1 &9090472883474544113 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 9122292137280017520} + - component: {fileID: 3119395137440285820} + - component: {fileID: 1204192879292246265} + - component: {fileID: 5944105346798640134} + m_Layer: 0 + m_Name: AttackHitbox + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &9122292137280017520 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 9090472883474544113} + serializedVersion: 2 + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: -2.06, y: 0.18000007, z: 0} + m_LocalScale: {x: 3.15, y: 1.32278, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 5678651543255339951} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!212 &3119395137440285820 +SpriteRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 9090472883474544113} + m_Enabled: 1 + m_CastShadows: 0 + m_ReceiveShadows: 0 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 0 + m_RayTraceProcedural: 0 + m_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: a97c105638bdf8b4a8650670310a4cd3, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 0 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 3 + m_Sprite: {fileID: 7482667652216324306, guid: 311925a002f4447b3a28927169b83ea6, type: 3} + m_Color: {r: 0, g: 0.4271555, b: 1, a: 1} + m_FlipX: 0 + m_FlipY: 0 + m_DrawMode: 0 + m_Size: {x: 1, y: 1} + m_AdaptiveModeThreshold: 0.5 + m_SpriteTileMode: 0 + m_WasSpriteAssigned: 1 + m_MaskInteraction: 0 + m_SpriteSortPoint: 0 +--- !u!61 &1204192879292246265 +BoxCollider2D: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 9090472883474544113} + m_Enabled: 1 + serializedVersion: 3 + m_Density: 1 + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_ForceSendLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_ForceReceiveLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_ContactCaptureLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_CallbackLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_IsTrigger: 1 + m_UsedByEffector: 0 + m_CompositeOperation: 0 + m_CompositeOrder: 0 + m_Offset: {x: 0, y: 0} + m_SpriteTilingProperty: + border: {x: 0, y: 0, z: 0, w: 0} + pivot: {x: 0.5, y: 0.5} + oldSize: {x: 1, y: 1} + newSize: {x: 1, y: 1} + adaptiveTilingThreshold: 0.5 + drawMode: 0 + adaptiveTiling: 0 + m_AutoTiling: 0 + m_Size: {x: 1, y: 1} + m_EdgeRadius: 0 +--- !u!114 &5944105346798640134 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 9090472883474544113} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 0ef49aa0271c9724c9e7489b33226faf, type: 3} + m_Name: + m_EditorClassIdentifier: + type: 1 diff --git a/Fantasy-Grower/Assets/Prefabs/Enemy/Enemy.prefab.meta b/Fantasy-Grower/Assets/Prefabs/Enemy/Enemy.prefab.meta new file mode 100644 index 0000000..796201b --- /dev/null +++ b/Fantasy-Grower/Assets/Prefabs/Enemy/Enemy.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 1e9e570aba71bd04990b068952d11e1a +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scenes/BattleTestScene.unity b/Fantasy-Grower/Assets/Scenes/BattleTestScene.unity new file mode 100644 index 0000000..1dd53ff --- /dev/null +++ b/Fantasy-Grower/Assets/Scenes/BattleTestScene.unity @@ -0,0 +1,1154 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 10 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 3 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 13 + m_BakeOnSceneLoad: 0 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 0 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 1 + m_PVRFilteringGaussRadiusAO: 1 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 3 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + buildHeightMesh: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1 &212572579 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 212572580} + - component: {fileID: 212572581} + m_Layer: 0 + m_Name: WaveController + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &212572580 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 212572579} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &212572581 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 212572579} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 0e69eb428298bef47b787286759308c2, type: 3} + m_Name: + m_EditorClassIdentifier: +--- !u!1 &258795091 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 258795092} + m_Layer: 0 + m_Name: Spawn1 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &258795092 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 258795091} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0.76, y: 0.5, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1308738509} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &301892951 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 301892952} + - component: {fileID: 301892953} + - component: {fileID: 301892954} + - component: {fileID: 301892955} + m_Layer: 0 + m_Name: AttackCollider + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &301892952 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 301892951} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 2.76, y: 0.18, z: 0} + m_LocalScale: {x: 5.036067, y: 1.32278, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1107896317} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!212 &301892953 +SpriteRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 301892951} + m_Enabled: 1 + m_CastShadows: 0 + m_ReceiveShadows: 0 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 0 + m_RayTraceProcedural: 0 + m_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: a97c105638bdf8b4a8650670310a4cd3, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 0 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 2 + m_Sprite: {fileID: 7482667652216324306, guid: 311925a002f4447b3a28927169b83ea6, type: 3} + m_Color: {r: 0.43710685, g: 0.8935439, b: 1, a: 1} + m_FlipX: 0 + m_FlipY: 0 + m_DrawMode: 0 + m_Size: {x: 1, y: 1} + m_AdaptiveModeThreshold: 0.5 + m_SpriteTileMode: 0 + m_WasSpriteAssigned: 1 + m_MaskInteraction: 0 + m_SpriteSortPoint: 0 +--- !u!61 &301892954 +BoxCollider2D: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 301892951} + m_Enabled: 1 + serializedVersion: 3 + m_Density: 1 + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_ForceSendLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_ForceReceiveLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_ContactCaptureLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_CallbackLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_IsTrigger: 1 + m_UsedByEffector: 0 + m_CompositeOperation: 0 + m_CompositeOrder: 0 + m_Offset: {x: 0, y: 0} + m_SpriteTilingProperty: + border: {x: 0, y: 0, z: 0, w: 0} + pivot: {x: 0.5, y: 0.5} + oldSize: {x: 1, y: 1} + newSize: {x: 1, y: 1} + adaptiveTilingThreshold: 0.5 + drawMode: 0 + adaptiveTiling: 0 + m_AutoTiling: 0 + m_Size: {x: 1, y: 1} + m_EdgeRadius: 0 +--- !u!114 &301892955 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 301892951} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 0ef49aa0271c9724c9e7489b33226faf, type: 3} + m_Name: + m_EditorClassIdentifier: + type: 0 +--- !u!1 &568675756 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 568675757} + m_Layer: 0 + m_Name: Spawn2 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &568675757 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 568675756} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 1, y: 0.5, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1308738509} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &572941894 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 572941895} + - component: {fileID: 572941897} + - component: {fileID: 572941896} + m_Layer: 5 + m_Name: Text (Legacy) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &572941895 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 572941894} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1589174768} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &572941896 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 572941894} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 144 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 0 + m_MaxSize: 151 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 'Start + +' +--- !u!222 &572941897 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 572941894} + m_CullTransparentMesh: 1 +--- !u!1 &597768151 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 597768154} + - component: {fileID: 597768153} + - component: {fileID: 597768152} + - component: {fileID: 597768155} + m_Layer: 0 + m_Name: Main Camera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!81 &597768152 +AudioListener: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 597768151} + m_Enabled: 1 +--- !u!20 &597768153 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 597768151} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 1 + m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0} + m_projectionMatrixMode: 1 + m_GateFitMode: 2 + m_FOVAxisMode: 0 + m_Iso: 200 + m_ShutterSpeed: 0.005 + m_Aperture: 16 + m_FocusDistance: 10 + m_FocalLength: 50 + m_BladeCount: 5 + m_Curvature: {x: 2, y: 11} + m_BarrelClipping: 0.25 + m_Anamorphism: 0 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + near clip plane: 0.3 + far clip plane: 1000 + field of view: 60 + orthographic: 1 + orthographic size: 5 + m_Depth: -1 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 3 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!4 &597768154 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 597768151} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: -10} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &597768155 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 597768151} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: a79441f348de89743a2939f4d699eac1, type: 3} + m_Name: + m_EditorClassIdentifier: + m_RenderShadows: 1 + m_RequiresDepthTextureOption: 2 + m_RequiresOpaqueTextureOption: 2 + m_CameraType: 0 + m_Cameras: [] + m_RendererIndex: -1 + m_VolumeLayerMask: + serializedVersion: 2 + m_Bits: 1 + m_VolumeTrigger: {fileID: 0} + m_VolumeFrameworkUpdateModeOption: 2 + m_RenderPostProcessing: 0 + m_Antialiasing: 0 + m_AntialiasingQuality: 2 + m_StopNaN: 0 + m_Dithering: 0 + m_ClearDepth: 1 + m_AllowXRRendering: 1 + m_AllowHDROutput: 1 + m_UseScreenCoordOverride: 0 + m_ScreenSizeOverride: {x: 0, y: 0, z: 0, w: 0} + m_ScreenCoordScaleBias: {x: 0, y: 0, z: 0, w: 0} + m_RequiresDepthTexture: 0 + m_RequiresColorTexture: 0 + m_Version: 2 + m_TaaSettings: + m_Quality: 3 + m_FrameInfluence: 0.1 + m_JitterScale: 1 + m_MipBias: 0 + m_VarianceClampScale: 0.9 + m_ContrastAdaptiveSharpening: 0 +--- !u!1 &863810191 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 863810195} + - component: {fileID: 863810194} + - component: {fileID: 863810193} + - component: {fileID: 863810192} + m_Layer: 5 + m_Name: Canvas + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &863810192 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 863810191} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: dc42784cf147c0c48a680349fa168899, type: 3} + m_Name: + m_EditorClassIdentifier: + m_IgnoreReversedGraphics: 1 + m_BlockingObjects: 0 + m_BlockingMask: + serializedVersion: 2 + m_Bits: 4294967295 +--- !u!114 &863810193 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 863810191} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 0cd44c1031e13a943bb63640046fad76, type: 3} + m_Name: + m_EditorClassIdentifier: + m_UiScaleMode: 1 + m_ReferencePixelsPerUnit: 100 + m_ScaleFactor: 1 + m_ReferenceResolution: {x: 1920, y: 1080} + m_ScreenMatchMode: 0 + m_MatchWidthOrHeight: 0.3 + m_PhysicalUnit: 3 + m_FallbackScreenDPI: 96 + m_DefaultSpriteDPI: 96 + m_DynamicPixelsPerUnit: 1 + m_PresetInfoIsWorld: 0 +--- !u!223 &863810194 +Canvas: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 863810191} + m_Enabled: 1 + serializedVersion: 3 + m_RenderMode: 1 + m_Camera: {fileID: 597768153} + m_PlaneDistance: 100 + m_PixelPerfect: 0 + m_ReceivesEvents: 1 + m_OverrideSorting: 0 + m_OverridePixelPerfect: 0 + m_SortingBucketNormalizedSize: 0 + m_VertexColorAlwaysGammaSpace: 0 + m_AdditionalShaderChannelsFlag: 0 + m_UpdateRectTransformForStandalone: 0 + m_SortingLayerID: 0 + m_SortingOrder: 0 + m_TargetDisplay: 0 +--- !u!224 &863810195 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 863810191} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 0, y: 0, z: 0} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 1589174768} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0, y: 0} +--- !u!1 &984133403 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 984133406} + - component: {fileID: 984133405} + - component: {fileID: 984133404} + m_Layer: 0 + m_Name: EventSystem + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &984133404 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 984133403} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 01614664b831546d2ae94a42149d80ac, type: 3} + m_Name: + m_EditorClassIdentifier: + m_SendPointerHoverToParent: 1 + m_MoveRepeatDelay: 0.5 + m_MoveRepeatRate: 0.1 + m_XRTrackingOrigin: {fileID: 0} + m_ActionsAsset: {fileID: -944628639613478452, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_PointAction: {fileID: -1654692200621890270, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_MoveAction: {fileID: -8784545083839296357, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_SubmitAction: {fileID: 392368643174621059, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_CancelAction: {fileID: 7727032971491509709, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_LeftClickAction: {fileID: 3001919216989983466, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_MiddleClickAction: {fileID: -2185481485913320682, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_RightClickAction: {fileID: -4090225696740746782, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_ScrollWheelAction: {fileID: 6240969308177333660, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_TrackedDevicePositionAction: {fileID: 6564999863303420839, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_TrackedDeviceOrientationAction: {fileID: 7970375526676320489, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_DeselectOnBackgroundClick: 1 + m_PointerBehavior: 0 + m_CursorLockBehavior: 0 + m_ScrollDeltaPerTick: 6 +--- !u!114 &984133405 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 984133403} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 76c392e42b5098c458856cdf6ecaaaa1, type: 3} + m_Name: + m_EditorClassIdentifier: + m_FirstSelected: {fileID: 0} + m_sendNavigationEvents: 1 + m_DragThreshold: 10 +--- !u!4 &984133406 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 984133403} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1047311747 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1047311748} + m_Layer: 0 + m_Name: Spawn3 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1047311748 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1047311747} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 1.43, y: 0.5, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1308738509} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1084119749 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1084119750} + - component: {fileID: 1084119751} + m_Layer: 0 + m_Name: BattleManager + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1084119750 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1084119749} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &1084119751 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1084119749} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: c944bded2de69ce47982eaff981c54e7, type: 3} + m_Name: + m_EditorClassIdentifier: + _player: {fileID: 1107896319} + _autoAttack: {fileID: 1107896320} + _waveController: {fileID: 212572581} + _spawnPoints: + - {fileID: 258795092} + - {fileID: 568675757} + - {fileID: 1047311748} + _dungeonData: {fileID: 11400000, guid: 0a0276ae91b1a0c40a8184ce9a87f3be, type: 2} + _gold: {fileID: 0} + _xp: {fileID: 0} + _mithril: {fileID: 0} +--- !u!1 &1107896316 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1107896317} + - component: {fileID: 1107896318} + - component: {fileID: 1107896319} + - component: {fileID: 1107896320} + m_Layer: 0 + m_Name: Player + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1107896317 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1107896316} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: -1, y: 0.5, z: 0} + m_LocalScale: {x: 0.5, y: 0.5, z: 0.5} + m_ConstrainProportionsScale: 1 + m_Children: + - {fileID: 301892952} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!212 &1107896318 +SpriteRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1107896316} + m_Enabled: 1 + m_CastShadows: 0 + m_ReceiveShadows: 0 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 0 + m_RayTraceProcedural: 0 + m_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: a97c105638bdf8b4a8650670310a4cd3, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 0 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 + m_Sprite: {fileID: 7482667652216324306, guid: 48e93eef0688c4a259cb0eddcd8661f7, type: 3} + m_Color: {r: 0.119439125, g: 1, b: 0, a: 1} + m_FlipX: 0 + m_FlipY: 0 + m_DrawMode: 0 + m_Size: {x: 1, y: 1} + m_AdaptiveModeThreshold: 0.5 + m_SpriteTileMode: 0 + m_WasSpriteAssigned: 1 + m_MaskInteraction: 0 + m_SpriteSortPoint: 0 +--- !u!114 &1107896319 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1107896316} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 388f00d7b35e3f04b88f0364310333ef, type: 3} + m_Name: + m_EditorClassIdentifier: + statData: {fileID: 11400000, guid: fe352f708efa65540819d5d6bf040e6b, type: 2} +--- !u!114 &1107896320 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1107896316} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 41833eeeae4a47b4e956be891274a363, type: 3} + m_Name: + m_EditorClassIdentifier: + _waveController: {fileID: 212572581} +--- !u!1 &1308738508 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1308738509} + m_Layer: 0 + m_Name: SpawnPoint + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1308738509 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1308738508} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 258795092} + - {fileID: 568675757} + - {fileID: 1047311748} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1589174767 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1589174768} + - component: {fileID: 1589174771} + - component: {fileID: 1589174770} + - component: {fileID: 1589174769} + m_Layer: 5 + m_Name: Start + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1589174768 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1589174767} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 572941895} + m_Father: {fileID: 863810195} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: -0.0000019073, y: -734} + m_SizeDelta: {x: 1029.7, y: 250.7} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1589174769 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1589174767} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 1589174770} + m_OnClick: + m_PersistentCalls: + m_Calls: + - m_Target: {fileID: 1084119751} + m_TargetAssemblyTypeName: BattleManager, Assembly-CSharp + m_MethodName: StartDungeon + m_Mode: 1 + m_Arguments: + m_ObjectArgument: {fileID: 0} + m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine + m_IntArgument: 0 + m_FloatArgument: 0 + m_StringArgument: + m_BoolArgument: 0 + m_CallState: 2 +--- !u!114 &1589174770 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1589174767} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &1589174771 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1589174767} + m_CullTransparentMesh: 1 +--- !u!1660057539 &9223372036854775807 +SceneRoots: + m_ObjectHideFlags: 0 + m_Roots: + - {fileID: 597768154} + - {fileID: 1084119750} + - {fileID: 212572580} + - {fileID: 1308738509} + - {fileID: 1107896317} + - {fileID: 863810195} + - {fileID: 984133406} diff --git a/Fantasy-Grower/Assets/Scenes/BattleTestScene.unity.meta b/Fantasy-Grower/Assets/Scenes/BattleTestScene.unity.meta new file mode 100644 index 0000000..a5831f9 --- /dev/null +++ b/Fantasy-Grower/Assets/Scenes/BattleTestScene.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: b73d6b2ded5ddba478ef65d56a8c7fd9 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts.meta b/Fantasy-Grower/Assets/Scripts.meta new file mode 100644 index 0000000..b4d485d --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 493d2c619d43c944d8c01f605764fe6b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Battle.meta b/Fantasy-Grower/Assets/Scripts/Battle.meta new file mode 100644 index 0000000..9f4cf34 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: fbd7b20813b6cfb418bffbe318b666aa +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Battle/AttackCollider.cs b/Fantasy-Grower/Assets/Scripts/Battle/AttackCollider.cs new file mode 100644 index 0000000..8f748fe --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/AttackCollider.cs @@ -0,0 +1,41 @@ +using UnityEngine; + +[RequireComponent(typeof(Collider2D))] +public class AttackCollider : MonoBehaviour +{ + [SerializeField] + private EntityType type; + + private Entity entity; + + private void Awake() + { + entity = GetComponentInParent(); + } + + private void OnTriggerEnter2D(Collider2D collision) + { + if (!collision.gameObject.TryGetComponent(out Entity target)) + return; + + bool shouldHit = + (type == EntityType.Player && target is Enemy) + || (type == EntityType.Enemy && target is Player); + + if (!shouldHit) + return; + + var (damage, _) = DamageCalculator.Calculate( + entity.AttackPower, + target.DamageReduction, + entity.CriticalPercentage + ); + target.TakeDamage(damage); + } +} + +public enum EntityType +{ + Player, + Enemy, +} diff --git a/Fantasy-Grower/Assets/Scripts/Battle/AttackCollider.cs.meta b/Fantasy-Grower/Assets/Scripts/Battle/AttackCollider.cs.meta new file mode 100644 index 0000000..7391df6 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/AttackCollider.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 0ef49aa0271c9724c9e7489b33226faf \ No newline at end of file diff --git a/Fantasy-Grower/Assets/Scripts/Battle/AutoAttackController.cs b/Fantasy-Grower/Assets/Scripts/Battle/AutoAttackController.cs new file mode 100644 index 0000000..d175599 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/AutoAttackController.cs @@ -0,0 +1,50 @@ +using System.Collections; +using UnityEngine; + +/// +/// 플레이어의 자동 공격 타이밍을 관리하는 컴포넌트. +/// AttackSpeed 스탯을 기반으로 초당 공격 횟수를 결정한다. +/// BattleManager가 StartAutoAttack() / StopAutoAttack()으로 제어한다. +/// +[RequireComponent(typeof(Player))] +public class AutoAttackController : MonoBehaviour +{ + [SerializeField] + private WaveController waveController; + + private Player player; + private Coroutine attackCoroutine; + + private void Awake() + { + player = GetComponent(); + } + + public void StartAutoAttack() + { + StopAutoAttack(); + attackCoroutine = StartCoroutine(AutoAttackLoop()); + } + + public void StopAutoAttack() + { + if (attackCoroutine != null) + { + StopCoroutine(attackCoroutine); + attackCoroutine = null; + } + } + + private IEnumerator AutoAttackLoop() + { + while (true) + { + if (waveController.GetFirstAliveEnemy() != null) + player.Attack(); // AttackCollider가 실제 피해를 처리 + + // AttackSpeed = 초당 공격 횟수 (예: 1.0 → 1초마다 공격) + float interval = player.AttackSpeed > 0f ? 1f / player.AttackSpeed : 1f; + yield return new WaitForSeconds(interval); + } + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Battle/AutoAttackController.cs.meta b/Fantasy-Grower/Assets/Scripts/Battle/AutoAttackController.cs.meta new file mode 100644 index 0000000..cc57eef --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/AutoAttackController.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 41833eeeae4a47b4e956be891274a363 \ No newline at end of file diff --git a/Fantasy-Grower/Assets/Scripts/Battle/BATTLELOOP_GUIDE.md b/Fantasy-Grower/Assets/Scripts/Battle/BATTLELOOP_GUIDE.md new file mode 100644 index 0000000..171b4ac --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/BATTLELOOP_GUIDE.md @@ -0,0 +1,212 @@ +# 전투 루프 시스템 사용 가이드 + +## 개요 + +방치형 자동 전투 루프 시스템. +플레이어는 AttackSpeed 기반으로 적을 자동 공격하고, 적은 EnemyAI로 플레이어를 공격한다. +던전은 웨이브 단위로 구성되며, 모든 웨이브 클리어 시 보상을 지급한다. + +``` +BattleManager (상태 머신) +├── WaveController — 적 스폰/생존 추적 +├── AutoAttackController — 플레이어 자동 공격 코루틴 +├── DamageCalculator — 데미지 계산 (DamageReduction + CriticalPercentage) +└── 데이터 SO + ├── DungeonData — 던전 전체 (웨이브 목록 + 클리어 보상) + ├── WaveData — 단일 웨이브 (적 프리팹 × 수량) + └── EnemyRewardData — 적 사망 보상 (Gold, XP) +``` + +--- + +## 1. 데이터 에셋 만들기 + +### 1-1. EnemyRewardData SO 생성 + +`Assets > Create > Battle/EnemyRewardData` + +| 필드 | 설명 | +|------|------| +| `goldAmount` | 적 사망 시 지급되는 Gold 양 | +| `xpAmount` | 적 사망 시 지급되는 XP 양 | + +### 1-2. WaveData SO 생성 + +`Assets > Create > Battle/WaveData` + +| 필드 | 설명 | +|------|------| +| `entries[]` | 스폰할 적 프리팹과 수량 목록 | + +**예시 (웨이브 1):** +- entries[0]: TestEnemyPrefab × 3 +- entries[1]: EliteEnemyPrefab × 1 + +### 1-3. DungeonData SO 생성 + +`Assets > Create > Battle/DungeonData` + +| 필드 | 설명 | +|------|------| +| `dungeonType` | `Basic` / `Gold` / `Weapon` / `Boss` | +| `waves[]` | 웨이브 SO 목록 (순서대로 진행) | +| `bonusGoldReward` | 던전 클리어 보너스 Gold | +| `bonusXpReward` | 던전 클리어 보너스 XP | +| `mithrilAsset` | Boss 던전 전용 — Mithril SO 연결 | +| `mithrilRewardAmount` | Boss 클리어 시 Mithril 지급량 | + +--- + +## 2. 적 프리팹 구성하기 + +Enemy 프리팹에는 다음 컴포넌트가 있어야 한다: + +| 컴포넌트 | 용도 | +|----------|------| +| `Enemy` (또는 서브클래스) | 전투 엔티티, 사망 보상 지급 | +| `EnemyAI` | 자동 공격 코루틴 | +| `EntityStatData` SO 연결 | HP/공격력/공격속도 등 스탯 | +| `EnemyRewardData` SO 연결 | 사망 보상 | +| `SO_Gold` / `SO_XP` SO 연결 | 보상 지급 대상 | + +**AttackCollider가 있는 경우 (물리 판정):** +- 프리팹 하위에 `AttackHitbox` 오브젝트 추가 +- `AttackCollider.cs` 부착, `type = Enemy`, Collider2D(trigger) 부착 +- EnemyAI는 `Attack()` → 충돌로 피해 처리 + +**AttackCollider가 없는 경우 (직접 호출):** +- EnemyAI가 `DamageCalculator`를 사용하여 `_player.TakeDamage()` 직접 호출 + +--- + +## 3. 씬 구성하기 + +``` +BattleScene +├── BattleManager [BattleManager.cs] +├── WaveController [WaveController.cs] +├── SpawnPoints/ +│ ├── SpawnPoint_1 (빈 GameObject) +│ └── SpawnPoint_2 +└── Warrior [Warrior.cs] [AutoAttackController.cs] + └── AttackHitbox [AttackCollider.cs] type=Player +``` + +**BattleManager Inspector 연결:** + +| 필드 | 연결 대상 | +|------|-----------| +| `_player` | Warrior GameObject | +| `_autoAttack` | Warrior의 AutoAttackController 컴포넌트 | +| `_waveController` | WaveController GameObject | +| `_spawnPoints` | SpawnPoint_1, SpawnPoint_2 ... | +| `_dungeonData` | 위에서 만든 DungeonData SO | +| `_gold / _xp / _mithril` | 프로젝트의 재화 SO 에셋 | + +**AutoAttackController Inspector 연결:** + +| 필드 | 연결 대상 | +|------|-----------| +| `_waveController` | WaveController GameObject | + +--- + +## 4. 전투 루프 상태 흐름 + +``` +Idle + ↓ StartDungeon() 호출 +WaveStart ← (1.5초 딜레이 후 다음 웨이브) + ↓ 적 스폰 + EnemyAI 초기화 +Fighting + ↓ OnAllEnemiesDead (WaveController → BattleManager) +WaveCleared + ↓ 마지막 웨이브였다면 +DungeonCleared → 보너스 보상 지급 + ↓ 중간 웨이브였다면 1.5초 후 WaveStart 반복 + +(언제든) 플레이어 HP = 0 +PlayerDead → RetryDungeon() 또는 Exit 처리 +``` + +--- + +## 5. 데미지 계산 공식 + +`DamageCalculator.Calculate(rawAttackPower, targetDamageReduction, attackerCriticalPercentage)` + +``` +감소 데미지 = Max(1, RoundToInt(공격력 × (1 - DamageReduction))) +크리티컬 여부 = Random(0~100) < CriticalPercentage +최종 데미지 = 크리티컬 ? 감소 데미지 × 2 : 감소 데미지 +``` + +| 스탯 | 범위 | 예시 | +|------|------|------| +| `DamageReduction` | 0.0 ~ 1.0 | `0.2` = 20% 피해 감소 | +| `CriticalPercentage` | 0 ~ 100 | `25` = 25% 크리티컬 확률 | + +--- + +## 6. 런타임 사용법 + +### 던전 시작 (UI 버튼) +```csharp +BattleManager battleManager = FindObjectOfType(); +battleManager.StartDungeon(); +``` + +### 재시도 (사망 화면 버튼) +```csharp +battleManager.RetryDungeon(); +``` + +### 상태 변화 구독 (UI 패널 전환) +```csharp +battleManager.OnStateChanged += state => +{ + switch (state) + { + case BattleState.Fighting: ShowBattleUI(); break; + case BattleState.DungeonCleared: ShowClearUI(); break; + case BattleState.PlayerDead: ShowDeadUI(); break; + } +}; + +battleManager.OnWaveChanged += waveIndex => +{ + waveText.text = $"Wave {waveIndex + 1}"; +}; +``` + +--- + +## 7. 새 던전 타입 추가 방법 + +1. `DungeonType` 열거형에 새 타입 추가 +2. `DungeonData` SO 생성 후 `dungeonType` 선택 +3. `BattleManager.StartDungeon()`에 분기 추가 (Gold 던전처럼 별도 씬이 필요한 경우) +4. `EnterDungeonCleared()`에 타입별 특수 보상 로직 추가 + +--- + +## 8. 클래스 의존 관계 + +``` +[DungeonData] ──► [WaveData] ──► EnemyPrefab + │ + ▼ +[BattleManager] + ├── [AutoAttackController] ──► [WaveController] + │ │ │ + │ Player.Attack() SpawnWave() + │ │ OnAllEnemiesDead + │ ▼ │ + │ [AttackCollider] ──► [DamageCalculator] ◄── [EnemyAI] + │ ▲ + │ Entity.TakeDamage() + └── Entity.OnDied ──► HandlePlayerDied / OnEnemyDied + +[Enemy] ──► [EnemyRewardData] + ──► SO_Gold / SO_XP (사망 시 Increase) +``` diff --git a/Fantasy-Grower/Assets/Scripts/Battle/BATTLELOOP_GUIDE.md.meta b/Fantasy-Grower/Assets/Scripts/Battle/BATTLELOOP_GUIDE.md.meta new file mode 100644 index 0000000..a35a7b4 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/BATTLELOOP_GUIDE.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 1c40028fec0a29247811b5b384c743a8 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Battle/BattleManager.cs b/Fantasy-Grower/Assets/Scripts/Battle/BattleManager.cs new file mode 100644 index 0000000..2c5c881 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/BattleManager.cs @@ -0,0 +1,210 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +public enum BattleState +{ + Idle, + WaveStart, + Fighting, + WaveCleared, + DungeonCleared, + PlayerDead, +} + +/// +/// 전투 루프의 중심 오케스트레이터. +/// 던전 시작 → 웨이브 스폰 → 전투 → 클리어/사망 상태 전환을 관리한다. +/// UI 레이어는 OnStateChanged / OnWaveChanged 이벤트를 구독하여 화면을 갱신한다. +/// +public class BattleManager : MonoBehaviour +{ + // ─── Inspector 연결 ────────────────────────────────────────── + [SerializeField] + private Player player; + + [SerializeField] + private AutoAttackController autoAttack; + + [SerializeField] + private WaveController waveController; + + [SerializeField] + private Transform[] spawnPoints; + + [Header("던전 데이터")] + [SerializeField] + private DungeonData dungeonData; + + [Header("재화 SO")] + [SerializeField] + private SO_Gold gold; + + [SerializeField] + private SO_XP xp; + + [SerializeField] + private SO_Mithril mithril; + + // ─── 런타임 상태 ───────────────────────────────────────────── + private BattleState state = BattleState.Idle; + private int currentWaveIndex; + private List currentWaveEnemies; + + // ─── UI 알림 이벤트 ─────────────────────────────────────────── + /// 상태가 변경될 때마다 발화된다. UI 패널 전환에 사용한다. + public event Action OnStateChanged; + + /// 새 웨이브가 시작될 때 웨이브 번호(0-based)를 전달한다. + public event Action OnWaveChanged; + + // ─── 유니티 라이프사이클 ────────────────────────────────────── + private void Awake() + { + waveController.OnAllEnemiesDead += HandleAllEnemiesDead; + player.OnDied += HandlePlayerDied; + } + + private void OnDestroy() + { + waveController.OnAllEnemiesDead -= HandleAllEnemiesDead; + player.OnDied -= HandlePlayerDied; + } + + // ─── 공개 API (UI 버튼에서 호출) ───────────────────────────── + /// 던전을 시작한다. + public void StartDungeon() + { + if (dungeonData == null) + { + Debug.LogError("[BattleManager] DungeonData가 연결되지 않았습니다."); + return; + } + + if (dungeonData.DungeonType == DungeonType.Gold) + { + Debug.Log("[BattleManager] 골드 던전은 미니게임 씬으로 전환해야 합니다."); + // TODO: SceneManager.LoadScene("GoldDungeonScene"); + return; + } + + currentWaveIndex = 0; + TransitionTo(BattleState.WaveStart); + } + + /// 플레이어 사망 후 던전을 처음부터 재시도한다. + public void RetryDungeon() + { + waveController.Clear(); + player.ResetHp(); + currentWaveIndex = 0; + TransitionTo(BattleState.WaveStart); + } + + // ─── 상태 머신 ──────────────────────────────────────────────── + private void TransitionTo(BattleState newState) + { + state = newState; + OnStateChanged?.Invoke(state); + Debug.Log($"[BattleManager] 상태 전환: {newState}"); + + switch (state) + { + case BattleState.WaveStart: + EnterWaveStart(); + break; + case BattleState.Fighting: + EnterFighting(); + break; + case BattleState.WaveCleared: + EnterWaveCleared(); + break; + case BattleState.DungeonCleared: + EnterDungeonCleared(); + break; + case BattleState.PlayerDead: + EnterPlayerDead(); + break; + } + } + + private void EnterWaveStart() + { + Debug.Log( + $"[BattleManager] 웨이브 {currentWaveIndex + 1} / {dungeonData.Waves.Length} 시작" + ); + OnWaveChanged?.Invoke(currentWaveIndex); + + WaveData wave = dungeonData.Waves[currentWaveIndex]; + currentWaveEnemies = waveController.SpawnWave(wave, spawnPoints); + + foreach (Enemy e in currentWaveEnemies) + e.GetComponent()?.Initialize(player); + + TransitionTo(BattleState.Fighting); + } + + private void EnterFighting() + { + autoAttack.StartAutoAttack(); + + foreach (Enemy e in currentWaveEnemies) + e.GetComponent()?.StartAttacking(); + } + + private void HandleAllEnemiesDead() + { + autoAttack.StopAutoAttack(); + + foreach (Enemy e in currentWaveEnemies) + e.GetComponent()?.StopAttacking(); + + TransitionTo(BattleState.WaveCleared); + } + + private void EnterWaveCleared() + { + currentWaveIndex++; + + if (currentWaveIndex >= dungeonData.Waves.Length) + { + TransitionTo(BattleState.DungeonCleared); + } + else + { + StartCoroutine(DelayedTransition(BattleState.WaveStart, 1.5f)); + } + } + + private void EnterDungeonCleared() + { + Debug.Log("[BattleManager] 던전 클리어!"); + + gold?.Increase(dungeonData.BonusGoldReward); + xp?.Increase(dungeonData.BonusXpReward); + + if (dungeonData.DungeonType == DungeonType.Boss && dungeonData.MithrilAsset != null) + dungeonData.MithrilAsset.Increase(dungeonData.MithrilRewardAmount); + } + + private void HandlePlayerDied(Entity _) + { + autoAttack.StopAutoAttack(); + waveController.Clear(); + TransitionTo(BattleState.PlayerDead); + } + + private void EnterPlayerDead() + { + Debug.Log("[BattleManager] 플레이어 사망."); + // UI에서 OnStateChanged(PlayerDead)를 받아 재시도/종료 화면을 표시한다. + } + + // ─── 내부 유틸 ──────────────────────────────────────────────── + private IEnumerator DelayedTransition(BattleState next, float delay) + { + yield return new WaitForSeconds(delay); + TransitionTo(next); + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Battle/BattleManager.cs.meta b/Fantasy-Grower/Assets/Scripts/Battle/BattleManager.cs.meta new file mode 100644 index 0000000..f9d43b8 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/BattleManager.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c944bded2de69ce47982eaff981c54e7 \ No newline at end of file diff --git a/Fantasy-Grower/Assets/Scripts/Battle/Data.meta b/Fantasy-Grower/Assets/Scripts/Battle/Data.meta new file mode 100644 index 0000000..3ce9908 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/Data.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 248f25b9d8d2ef64d9a0fa68984db2a6 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Battle/Data/DungeonData.cs b/Fantasy-Grower/Assets/Scripts/Battle/Data/DungeonData.cs new file mode 100644 index 0000000..8fec5a5 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/Data/DungeonData.cs @@ -0,0 +1,31 @@ +using UnityEngine; + +public enum DungeonType +{ + Basic, + Gold, + Weapon, + Boss, +} + +/// +/// 던전 전체 구성 데이터 (웨이브 목록 + 던전 유형 + 클리어 보상). +/// BattleManager에 연결하여 던전을 정의한다. +/// +[CreateAssetMenu(fileName = "DungeonData", menuName = "Battle/DungeonData")] +public class DungeonData : ScriptableObject +{ + [Header("던전 유형")] + public DungeonType DungeonType; + + [Header("웨이브 목록 (순서대로 진행)")] + public WaveData[] Waves; + + [Header("던전 클리어 보너스 보상")] + public uint BonusGoldReward; + public uint BonusXpReward; + + [Header("보스 던전 전용 보상")] + public SO_Mithril MithrilAsset; + public uint MithrilRewardAmount; +} diff --git a/Fantasy-Grower/Assets/Scripts/Battle/Data/DungeonData.cs.meta b/Fantasy-Grower/Assets/Scripts/Battle/Data/DungeonData.cs.meta new file mode 100644 index 0000000..cd367e6 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/Data/DungeonData.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 12ba244a718458e4097ed6e5be48a7f4 \ No newline at end of file diff --git a/Fantasy-Grower/Assets/Scripts/Battle/Data/EnemyRewardData.cs b/Fantasy-Grower/Assets/Scripts/Battle/Data/EnemyRewardData.cs new file mode 100644 index 0000000..975a480 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/Data/EnemyRewardData.cs @@ -0,0 +1,13 @@ +using UnityEngine; + +/// +/// 적 사망 시 지급되는 보상 데이터. +/// Enemy 프리팹에 할당되며, 같은 적 종류는 하나의 SO 에셋을 공유한다. +/// +[CreateAssetMenu(fileName = "EnemyRewardData", menuName = "Battle/EnemyRewardData")] +public class EnemyRewardData : ScriptableObject +{ + [Header("사망 보상")] + public uint GoldAmount; + public uint XpAmount; +} diff --git a/Fantasy-Grower/Assets/Scripts/Battle/Data/EnemyRewardData.cs.meta b/Fantasy-Grower/Assets/Scripts/Battle/Data/EnemyRewardData.cs.meta new file mode 100644 index 0000000..521b838 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/Data/EnemyRewardData.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9e3fab7113fbdbd4e86847cd9d15e830 \ No newline at end of file diff --git a/Fantasy-Grower/Assets/Scripts/Battle/Data/WaveData.cs b/Fantasy-Grower/Assets/Scripts/Battle/Data/WaveData.cs new file mode 100644 index 0000000..ab8b155 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/Data/WaveData.cs @@ -0,0 +1,23 @@ +using System.Linq; +using UnityEngine; + +/// +/// 던전의 단일 웨이브 구성 데이터. +/// 적 프리팹과 수량을 지정하여 에디터에서 웨이브를 설계한다. +/// +[CreateAssetMenu(fileName = "WaveData", menuName = "Battle/WaveData")] +public class WaveData : ScriptableObject +{ + [System.Serializable] + public struct EnemySpawnEntry + { + public GameObject EnemyPrefab; + public int Count; + } + + [Header("스폰 목록 (적 종류 × 수량)")] + public EnemySpawnEntry[] Entries; + + /// 이 웨이브의 총 적 수 + public int TotalEnemyCount => Entries?.Sum(e => e.Count) ?? 0; +} diff --git a/Fantasy-Grower/Assets/Scripts/Battle/Data/WaveData.cs.meta b/Fantasy-Grower/Assets/Scripts/Battle/Data/WaveData.cs.meta new file mode 100644 index 0000000..c3b274a --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/Data/WaveData.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 94f9a6b0432990949b668a67629525a6 \ No newline at end of file diff --git a/Fantasy-Grower/Assets/Scripts/Battle/DungeonDataSO.meta b/Fantasy-Grower/Assets/Scripts/Battle/DungeonDataSO.meta new file mode 100644 index 0000000..1767d78 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/DungeonDataSO.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0ddf79eadc388df46a1e0706cdcdf31b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Battle/DungeonDataSO/TestDungeonData.asset b/Fantasy-Grower/Assets/Scripts/Battle/DungeonDataSO/TestDungeonData.asset new file mode 100644 index 0000000..0540bad --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/DungeonDataSO/TestDungeonData.asset @@ -0,0 +1,23 @@ +%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: 12ba244a718458e4097ed6e5be48a7f4, type: 3} + m_Name: TestDungeonData + m_EditorClassIdentifier: + dungeonType: 0 + waves: + - {fileID: 11400000, guid: fed3edd92df95224e95ac553fd8093dc, type: 2} + - {fileID: 11400000, guid: fed3edd92df95224e95ac553fd8093dc, type: 2} + - {fileID: 11400000, guid: fed3edd92df95224e95ac553fd8093dc, type: 2} + bonusGoldReward: 1000 + bonusXpReward: 100 + mithrilAsset: {fileID: 0} + mithrilRewardAmount: 0 diff --git a/Fantasy-Grower/Assets/Scripts/Battle/DungeonDataSO/TestDungeonData.asset.meta b/Fantasy-Grower/Assets/Scripts/Battle/DungeonDataSO/TestDungeonData.asset.meta new file mode 100644 index 0000000..44cee4e --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/DungeonDataSO/TestDungeonData.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0a0276ae91b1a0c40a8184ce9a87f3be +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Battle/Enemy.meta b/Fantasy-Grower/Assets/Scripts/Battle/Enemy.meta new file mode 100644 index 0000000..f4d740b --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/Enemy.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6c9cc4f3fcf3949418b95e3ddd80dbe7 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Battle/Enemy/Enemy.cs b/Fantasy-Grower/Assets/Scripts/Battle/Enemy/Enemy.cs new file mode 100644 index 0000000..a3b07ae --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/Enemy/Enemy.cs @@ -0,0 +1,30 @@ +using UnityEngine; + +/// +/// 적 엔티티 기반 클래스. +/// 사망 시 EnemyRewardData에 정의된 Gold/XP를 지급한다. +/// +public class Enemy : Entity +{ + [SerializeField] + private EnemyRewardData rewardData; + + [SerializeField] + private SO_Gold gold; + + [SerializeField] + private SO_XP xp; + + public override void Death() + { + base.Death(); // OnDied 이벤트 발화 (WaveController가 구독 중) + + if (rewardData != null) + { + gold?.Increase(rewardData.GoldAmount); + xp?.Increase(rewardData.XpAmount); + } + + Destroy(gameObject, 0.5f); // 사망 연출 시간 확보 후 제거 + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Battle/Enemy/Enemy.cs.meta b/Fantasy-Grower/Assets/Scripts/Battle/Enemy/Enemy.cs.meta new file mode 100644 index 0000000..bed3a1b --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/Enemy/Enemy.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 6d282a7221d9d894fafad9c8a8e3f9d7 \ No newline at end of file diff --git a/Fantasy-Grower/Assets/Scripts/Battle/Enemy/EnemyAI.cs b/Fantasy-Grower/Assets/Scripts/Battle/Enemy/EnemyAI.cs new file mode 100644 index 0000000..4ce106f --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/Enemy/EnemyAI.cs @@ -0,0 +1,73 @@ +using System.Collections; +using UnityEngine; + +/// +/// 적의 자동 공격 AI. +/// BattleManager가 스폰 후 Initialize()와 StartAttacking()을 호출하여 동작을 시작한다. +/// +[RequireComponent(typeof(Enemy))] +public class EnemyAI : MonoBehaviour +{ + private Enemy enemy; + private Player player; + private Coroutine attackCoroutine; + + private bool hasAttackCollider => childCollider != null; + + private AttackCollider childCollider; + + private void Awake() + { + enemy = GetComponent(); + childCollider = GetComponentInChildren(includeInactive: true); + } + + /// 타겟 플레이어를 설정한다. BattleManager가 스폰 직후 호출한다. + public void Initialize(Player player) + { + this.player = player; + } + + public void StartAttacking() + { + StopAttacking(); + attackCoroutine = StartCoroutine(AttackLoop()); + } + + public void StopAttacking() + { + if (attackCoroutine != null) + { + StopCoroutine(attackCoroutine); + attackCoroutine = null; + } + } + + private IEnumerator AttackLoop() + { + while (true) + { + if (player != null && player.Hp > 0) + { + // Enemy 프리팹에 AttackCollider가 있으면 Attack()으로 애니메이션+충돌 처리. + // AttackCollider가 없는 경우 DamageCalculator로 직접 피해 적용. + if (hasAttackCollider) + { + enemy.Attack(); + } + else + { + var (damage, _) = DamageCalculator.Calculate( + enemy.AttackPower, + player.DamageReduction, + enemy.CriticalPercentage + ); + player.TakeDamage(damage); + } + } + + float interval = enemy.AttackSpeed > 0f ? 1f / enemy.AttackSpeed : 2f; + yield return new WaitForSeconds(interval); + } + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Battle/Enemy/EnemyAI.cs.meta b/Fantasy-Grower/Assets/Scripts/Battle/Enemy/EnemyAI.cs.meta new file mode 100644 index 0000000..7bc74a6 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/Enemy/EnemyAI.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d42734d476595bc40835c071beb08767 \ No newline at end of file diff --git a/Fantasy-Grower/Assets/Scripts/Battle/Enemy/TestEnemy.cs b/Fantasy-Grower/Assets/Scripts/Battle/Enemy/TestEnemy.cs new file mode 100644 index 0000000..36d7129 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/Enemy/TestEnemy.cs @@ -0,0 +1,38 @@ +using System.Collections; +using UnityEngine; + +public class TestEnemy : Enemy +{ + private AttackCollider col; + + protected override void Awake() + { + base.Awake(); + col = GetComponentInChildren(); + col.gameObject.SetActive(false); + } + + public override void Death() + { + Debug.Log("TestEnemy 죽음"); + base.Death(); // Enemy.Death() → 보상 지급 + OnDied 이벤트 + } + + private bool isAttacking; + + public override void Attack() + { + if (isAttacking) + return; + StartCoroutine(AttackCoroutine()); + } + + private IEnumerator AttackCoroutine() + { + isAttacking = true; + col.gameObject.SetActive(true); + yield return new WaitForSeconds(0.2f); // 히트 판정 윈도우 (고정) + col.gameObject.SetActive(false); + isAttacking = false; + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Battle/Enemy/TestEnemy.cs.meta b/Fantasy-Grower/Assets/Scripts/Battle/Enemy/TestEnemy.cs.meta new file mode 100644 index 0000000..9289d4b --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/Enemy/TestEnemy.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5180275fdefeff94fbdcd78cf158830b \ No newline at end of file diff --git a/Fantasy-Grower/Assets/Scripts/Battle/Entity.cs b/Fantasy-Grower/Assets/Scripts/Battle/Entity.cs new file mode 100644 index 0000000..5b66848 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/Entity.cs @@ -0,0 +1,77 @@ +using System; +using UnityEngine; + +public abstract class Entity : MonoBehaviour +{ + public int Hp { get; private set; } + public int MaxHp { get; private set; } + public float DamageReduction { get; private set; } + public int AttackPower { get; private set; } + public float AttackSpeed { get; private set; } + public float CriticalPercentage { get; private set; } + + [SerializeField] + private EntityStatData statData; + + /// HP가 0이 되어 Death()가 호출될 때 발화된다. + public event Action OnDied; + + protected virtual void Awake() + { + if (statData == null) + { + Debug.LogWarning("[경고] StatData가 비어 있습니다!"); + return; + } + + Hp = statData.Hp; + MaxHp = statData.Hp; + DamageReduction = statData.DamageReduction; + AttackPower = statData.AttackPower; + AttackSpeed = statData.AttackSpeed; + CriticalPercentage = statData.CriticalPercentage; + } + + public virtual void Attack() { } + + /// HP를 MaxHp로 복구한다. 던전 재시도 시 사용. + public void ResetHp() + { + Hp = MaxHp; + } + + public virtual void Death() + { + OnDied?.Invoke(this); + } + + public virtual void TakeDamage(int damage) + { + if (Hp <= 0) + return; // 이미 사망 — 중복 호출 무시 + + Debug.Log($"데미지 받음: {damage}"); + Hp = Mathf.Max(0, Hp - damage); + + if (Hp <= 0) + Death(); + } + + /// + /// 해금된 패시브 스킬 효과를 스탯에 반영한다. + /// SkillTreeComponent.RecalculatePassives()에서 패시브 집계 후 호출된다. + /// statData 기반값에 modifier를 합산하여 런타임 스탯을 갱신한다. + /// + public void ApplyStatModifier(EntityStatModifier modifier) + { + if (statData == null) + return; + + MaxHp = statData.Hp + modifier.BonusHp; + Hp = MaxHp; + DamageReduction = statData.DamageReduction + modifier.BonusDamageReduction; + AttackPower = statData.AttackPower + modifier.BonusAttackPower; + AttackSpeed = statData.AttackSpeed + modifier.BonusAttackSpeed; + CriticalPercentage = statData.CriticalPercentage + modifier.BonusCriticalPercentage; + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Battle/Entity.cs.meta b/Fantasy-Grower/Assets/Scripts/Battle/Entity.cs.meta new file mode 100644 index 0000000..20b5308 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/Entity.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: dc2992df8ca20684984a514a7cb32031 \ No newline at end of file diff --git a/Fantasy-Grower/Assets/Scripts/Battle/Player.meta b/Fantasy-Grower/Assets/Scripts/Battle/Player.meta new file mode 100644 index 0000000..a1022b6 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/Player.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f7761a8f7b495b84994ff95723efb1a3 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Battle/Player/Player.cs b/Fantasy-Grower/Assets/Scripts/Battle/Player/Player.cs new file mode 100644 index 0000000..d8afb34 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/Player/Player.cs @@ -0,0 +1,32 @@ +using System.Collections; +using UnityEngine; + +public class Player : Entity +{ + private AttackCollider col; + + protected override void Awake() + { + base.Awake(); + col = GetComponentInChildren(); + col.gameObject.SetActive(false); + } + + private bool isAttacking; + + public override void Attack() + { + if (isAttacking) + return; + StartCoroutine(AttackCoroutine()); + } + + private IEnumerator AttackCoroutine() + { + isAttacking = true; + col.gameObject.SetActive(true); + yield return new WaitForSeconds(0.2f); // 히트 판정 윈도우 (고정) + col.gameObject.SetActive(false); + isAttacking = false; + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Battle/Player/Player.cs.meta b/Fantasy-Grower/Assets/Scripts/Battle/Player/Player.cs.meta new file mode 100644 index 0000000..a3918e3 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/Player/Player.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e179c8fd34877d8478a588e4fb7330f5 \ No newline at end of file diff --git a/Fantasy-Grower/Assets/Scripts/Battle/Player/Warrior.cs b/Fantasy-Grower/Assets/Scripts/Battle/Player/Warrior.cs new file mode 100644 index 0000000..e058176 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/Player/Warrior.cs @@ -0,0 +1,3 @@ +using UnityEngine; + +public class Warrior : Player { } diff --git a/Fantasy-Grower/Assets/Scripts/Battle/Player/Warrior.cs.meta b/Fantasy-Grower/Assets/Scripts/Battle/Player/Warrior.cs.meta new file mode 100644 index 0000000..0429a59 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/Player/Warrior.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 388f00d7b35e3f04b88f0364310333ef \ No newline at end of file diff --git a/Fantasy-Grower/Assets/Scripts/Battle/RewardSO.meta b/Fantasy-Grower/Assets/Scripts/Battle/RewardSO.meta new file mode 100644 index 0000000..33b5732 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/RewardSO.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: fc27e9c9c9df7fa49b689c716ae8f9dc +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Battle/RewardSO/TestRewardData.asset b/Fantasy-Grower/Assets/Scripts/Battle/RewardSO/TestRewardData.asset new file mode 100644 index 0000000..1a8a974 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/RewardSO/TestRewardData.asset @@ -0,0 +1,16 @@ +%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: 9e3fab7113fbdbd4e86847cd9d15e830, type: 3} + m_Name: TestRewardData + m_EditorClassIdentifier: + goldAmount: 100 + xpAmount: 100 diff --git a/Fantasy-Grower/Assets/Scripts/Battle/RewardSO/TestRewardData.asset.meta b/Fantasy-Grower/Assets/Scripts/Battle/RewardSO/TestRewardData.asset.meta new file mode 100644 index 0000000..077f55e --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/RewardSO/TestRewardData.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c7bbabc7ae6924a4ca2dd98f7f57abf7 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Battle/WaveController.cs b/Fantasy-Grower/Assets/Scripts/Battle/WaveController.cs new file mode 100644 index 0000000..82738eb --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/WaveController.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +/// +/// 현재 웨이브의 적 스폰과 생존 수를 추적하는 컴포넌트. +/// 모든 적이 사망하면 OnAllEnemiesDead 이벤트를 발화한다. +/// +public class WaveController : MonoBehaviour +{ + /// 현재 웨이브의 모든 적이 사망했을 때 발화된다. + public event Action OnAllEnemiesDead; + + private int _aliveCount; + private readonly List _activeEnemies = new(); + + /// + /// 웨이브 데이터에 따라 적을 스폰한다. + /// 스폰된 Enemy 인스턴스 목록을 반환한다. + /// + public List SpawnWave(WaveData waveData, Transform[] spawnPoints) + { + if (spawnPoints == null || spawnPoints.Length == 0) + { + Debug.LogError("[WaveController] 스폰 포인트가 설정되지 않았습니다."); + return null; + } + + _activeEnemies.Clear(); + int spawnIndex = 0; + + foreach (var entry in waveData.Entries) + { + for (int i = 0; i < entry.Count; i++) + { + Vector3 pos = spawnPoints[spawnIndex % spawnPoints.Length].position; + GameObject go = Instantiate(entry.EnemyPrefab, pos, Quaternion.identity); + + Enemy enemy = go.GetComponent(); + if (enemy == null) + { + Debug.LogWarning( + $"[WaveController] 프리팹 {entry.EnemyPrefab.name}에 Enemy 컴포넌트가 없습니다." + ); + Destroy(go); + continue; + } + + _activeEnemies.Add(enemy); + enemy.OnDied += OnEnemyDied; + spawnIndex++; + } + } + + _aliveCount = _activeEnemies.Count; + + if (_aliveCount == 0) + { + Debug.LogWarning("[WaveController] 웨이브에 적이 없습니다. 즉시 완료 처리됩니다."); + OnAllEnemiesDead?.Invoke(); + } + + return _activeEnemies; + } + + /// 현재 살아있는 첫 번째 적을 반환한다. 없으면 null. + public Enemy GetFirstAliveEnemy() => _activeEnemies.FirstOrDefault(e => e != null && e.Hp > 0); + + public IReadOnlyList ActiveEnemies => _activeEnemies.AsReadOnly(); + + /// 살아있는 적을 모두 파괴하고 상태를 초기화한다. 재시도 또는 씬 정리 시 사용. + public void Clear() + { + foreach (Enemy e in _activeEnemies) + { + if (e != null) + Destroy(e.gameObject); + } + _activeEnemies.Clear(); + _aliveCount = 0; + } + + private void OnEnemyDied(Entity entity) + { + _aliveCount = Mathf.Max(0, _aliveCount - 1); + + if (_aliveCount == 0) + OnAllEnemiesDead?.Invoke(); + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Battle/WaveController.cs.meta b/Fantasy-Grower/Assets/Scripts/Battle/WaveController.cs.meta new file mode 100644 index 0000000..58bf494 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/WaveController.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 0e69eb428298bef47b787286759308c2 \ No newline at end of file diff --git a/Fantasy-Grower/Assets/Scripts/Battle/WaveDataSO.meta b/Fantasy-Grower/Assets/Scripts/Battle/WaveDataSO.meta new file mode 100644 index 0000000..b8fd227 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/WaveDataSO.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: feb40389af2e72c428f55053aed87492 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Battle/WaveDataSO/TestWaveData.asset b/Fantasy-Grower/Assets/Scripts/Battle/WaveDataSO/TestWaveData.asset new file mode 100644 index 0000000..93b19c5 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/WaveDataSO/TestWaveData.asset @@ -0,0 +1,17 @@ +%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: 94f9a6b0432990949b668a67629525a6, type: 3} + m_Name: TestWaveData + m_EditorClassIdentifier: + entries: + - enemyPrefab: {fileID: 4193620460523036667, guid: 1e9e570aba71bd04990b068952d11e1a, type: 3} + count: 3 diff --git a/Fantasy-Grower/Assets/Scripts/Battle/WaveDataSO/TestWaveData.asset.meta b/Fantasy-Grower/Assets/Scripts/Battle/WaveDataSO/TestWaveData.asset.meta new file mode 100644 index 0000000..099ba04 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/WaveDataSO/TestWaveData.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: fed3edd92df95224e95ac553fd8093dc +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core.meta b/Fantasy-Grower/Assets/Scripts/Core.meta new file mode 100644 index 0000000..4a8d2de --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 367738f021b059b4ebfd70b77bca4b40 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/DamageCalculator.cs b/Fantasy-Grower/Assets/Scripts/Core/DamageCalculator.cs new file mode 100644 index 0000000..2580717 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/DamageCalculator.cs @@ -0,0 +1,27 @@ +using UnityEngine; + +/// +/// 데미지 계산 공식을 담당하는 정적 유틸리티. +/// AttackCollider와 EnemyAI에서 공통으로 사용한다. +/// +public static class DamageCalculator +{ + /// + /// 최종 데미지와 크리티컬 여부를 계산한다. + /// + /// 공격자의 공격력 + /// 피격자의 피해 감소율 (0.0 ~ 1.0, 예: 0.2 = 20% 감소) + /// 공격자의 크리티컬 확률 (0 ~ 100, 예: 25f = 25%) + /// (최종 데미지, 크리티컬 여부) + public static (int damage, bool isCritical) Calculate( + int rawAttackPower, + float targetDamageReduction, + float attackerCriticalPercentage + ) + { + int reduced = Mathf.Max(1, Mathf.RoundToInt(rawAttackPower * (1f - targetDamageReduction))); + bool isCritical = Random.value * 100f < attackerCriticalPercentage; + int finalDamage = isCritical ? reduced * 2 : reduced; + return (finalDamage, isCritical); + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/DamageCalculator.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/DamageCalculator.cs.meta new file mode 100644 index 0000000..d1eb4ef --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/DamageCalculator.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4ba6586415045e8438d89eae6a3efede \ No newline at end of file diff --git a/Fantasy-Grower/Assets/Scripts/Core/EntityStatData.cs b/Fantasy-Grower/Assets/Scripts/Core/EntityStatData.cs new file mode 100644 index 0000000..c47604a --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/EntityStatData.cs @@ -0,0 +1,11 @@ +using UnityEngine; + +[CreateAssetMenu(fileName = "EntityStat", menuName = "Stat/Entity")] +public class EntityStatData : ScriptableObject +{ + public int Hp; + public float DamageReduction; + public int AttackPower; + public float AttackSpeed; + public float CriticalPercentage; +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/EntityStatData.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/EntityStatData.cs.meta new file mode 100644 index 0000000..732a2d1 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/EntityStatData.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5df7739d4b9038a4d9cb6566170314b6 \ No newline at end of file diff --git a/Fantasy-Grower/Assets/Scripts/Core/EntityStatModifier.cs b/Fantasy-Grower/Assets/Scripts/Core/EntityStatModifier.cs new file mode 100644 index 0000000..cca521c --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/EntityStatModifier.cs @@ -0,0 +1,25 @@ +using UnityEngine; + +[System.Serializable] +public struct EntityStatModifier +{ + public int BonusHp; + public float BonusDamageReduction; + public int BonusAttackPower; + public float BonusAttackSpeed; + public float BonusCriticalPercentage; + + public static EntityStatModifier operator +(EntityStatModifier a, EntityStatModifier b) + { + return new EntityStatModifier + { + BonusHp = a.BonusHp + b.BonusHp, + BonusDamageReduction = a.BonusDamageReduction + b.BonusDamageReduction, + BonusAttackPower = a.BonusAttackPower + b.BonusAttackPower, + BonusAttackSpeed = a.BonusAttackSpeed + b.BonusAttackSpeed, + BonusCriticalPercentage = a.BonusCriticalPercentage + b.BonusCriticalPercentage, + }; + } + + public static EntityStatModifier Zero => default; +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/EntityStatModifier.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/EntityStatModifier.cs.meta new file mode 100644 index 0000000..3e0f328 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/EntityStatModifier.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: aee70212a874f0a4c9732365e2cd86e3 \ No newline at end of file diff --git a/Fantasy-Grower/Assets/Scripts/Core/Goods.meta b/Fantasy-Grower/Assets/Scripts/Core/Goods.meta new file mode 100644 index 0000000..e1313dd --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/Goods.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f5992035d8fdfc642bfdaa7c550f0c79 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/Goods/SO_Gold.cs b/Fantasy-Grower/Assets/Scripts/Core/Goods/SO_Gold.cs new file mode 100644 index 0000000..d182e7e --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/Goods/SO_Gold.cs @@ -0,0 +1,7 @@ +using UnityEngine; + +[CreateAssetMenu(fileName = "Gold", menuName = "ScriptableObjects/Goods/Gold", order = 1)] +public class SO_Gold : SO_Goods +{ + protected override string GoodsName { get; } = "골드"; +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/Goods/SO_Gold.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/Goods/SO_Gold.cs.meta new file mode 100644 index 0000000..beeed9b --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/Goods/SO_Gold.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e7ca0e0a681014c4cb3416296c3fa11f \ No newline at end of file diff --git a/Fantasy-Grower/Assets/Scripts/Core/Goods/SO_Goods.cs b/Fantasy-Grower/Assets/Scripts/Core/Goods/SO_Goods.cs new file mode 100644 index 0000000..20e03c0 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/Goods/SO_Goods.cs @@ -0,0 +1,40 @@ +using UnityEngine; + +/// +/// Parent class for goods +/// +public abstract class SO_Goods : ScriptableObject +{ + /// + /// The number of goods + /// + [SerializeField] + protected uint value; + + protected abstract string GoodsName { get; } + + /// + /// Get the number of goods + /// + /// The number of goods + public virtual uint Get() => value; + + /// + /// Increase goods by the value of 'amount' + /// + public virtual void Increase(uint amount) => value += amount; + + /// + /// Decrease goods by the value of 'amount' + /// + public virtual void Decrease(uint amount) + { + if (amount > value) + { + Debug.LogError($"{GoodsName}은(는) {amount - value}만큼 부족합니다!!!"); + return; + } + + value -= amount; + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/Goods/SO_Goods.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/Goods/SO_Goods.cs.meta new file mode 100644 index 0000000..6723e80 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/Goods/SO_Goods.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 791b805accf761340bc40f3be5ee7a6a \ No newline at end of file diff --git a/Fantasy-Grower/Assets/Scripts/Core/Goods/SO_Mithril.cs b/Fantasy-Grower/Assets/Scripts/Core/Goods/SO_Mithril.cs new file mode 100644 index 0000000..d49795e --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/Goods/SO_Mithril.cs @@ -0,0 +1,7 @@ +using UnityEngine; + +[CreateAssetMenu(fileName = "Mithril", menuName = "ScriptableObjects/Goods/Mithril", order = 5)] +public class SO_Mithril : SO_Goods +{ + protected override string GoodsName { get; } = "미스릴"; +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/Goods/SO_Mithril.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/Goods/SO_Mithril.cs.meta new file mode 100644 index 0000000..8ae4121 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/Goods/SO_Mithril.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 47f69483fa07a7649852392a6f8de886 \ No newline at end of file diff --git a/Fantasy-Grower/Assets/Scripts/Core/Goods/SO_SP.cs b/Fantasy-Grower/Assets/Scripts/Core/Goods/SO_SP.cs new file mode 100644 index 0000000..b4b4157 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/Goods/SO_SP.cs @@ -0,0 +1,7 @@ +using UnityEngine; + +[CreateAssetMenu(fileName = "SP", menuName = "ScriptableObjects/Goods/SP", order = 3)] +public class SO_SP : SO_Goods +{ + protected override string GoodsName { get; } = "스킬 포인트"; +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/Goods/SO_SP.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/Goods/SO_SP.cs.meta new file mode 100644 index 0000000..816f7d4 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/Goods/SO_SP.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: dbe95d35f6d0b844cb4805abb1436779 \ No newline at end of file diff --git a/Fantasy-Grower/Assets/Scripts/Core/Goods/SO_UpgradeScroll.cs b/Fantasy-Grower/Assets/Scripts/Core/Goods/SO_UpgradeScroll.cs new file mode 100644 index 0000000..c16b5f1 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/Goods/SO_UpgradeScroll.cs @@ -0,0 +1,11 @@ +using UnityEngine; + +[CreateAssetMenu( + fileName = "UpgradeScroll", + menuName = "ScriptableObjects/Goods/UpgradeScroll", + order = 4 +)] +public class SO_UpgradeScroll : SO_Goods +{ + protected override string GoodsName { get; } = "강화 스크롤"; +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/Goods/SO_UpgradeScroll.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/Goods/SO_UpgradeScroll.cs.meta new file mode 100644 index 0000000..3a8c6d9 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/Goods/SO_UpgradeScroll.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: dfa2553b272b09f4d9b24f9778e9fc62 \ No newline at end of file diff --git a/Fantasy-Grower/Assets/Scripts/Core/Goods/SO_XP.cs b/Fantasy-Grower/Assets/Scripts/Core/Goods/SO_XP.cs new file mode 100644 index 0000000..28803ed --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/Goods/SO_XP.cs @@ -0,0 +1,14 @@ +using System; +using UnityEngine; + +[CreateAssetMenu(fileName = "XP", menuName = "ScriptableObjects/Goods/XP", order = 2)] +public class SO_XP : SO_Goods +{ + protected override string GoodsName { get; } = "경험치"; + + [Obsolete("XP는 감소시킬 수 없습니다!!!")] + public override void Decrease(uint amount) + { + // 감소 기능 삭제 (경험치는 감소하지 않음) + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/Goods/SO_XP.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/Goods/SO_XP.cs.meta new file mode 100644 index 0000000..c292bac --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/Goods/SO_XP.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: bc2f6b88ab706b942b8a72969030edac \ No newline at end of file diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillData.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillData.cs new file mode 100644 index 0000000..a808978 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillData.cs @@ -0,0 +1,27 @@ +using UnityEngine; + +public enum SkillCategory +{ + Active, + Passive, +} + +[CreateAssetMenu(fileName = "SkillStat", menuName = "Stat/Skill")] +public abstract class SkillData : ScriptableObject +{ + public string SkillName; + public Sprite SkillIcon; + + [TextArea] + public string SkillDescription; + + [Space(40)] + public float Cooldown; + public int Damage; + + [Header("스킬 트리")] + public SkillCategory Category; + public int SPCost; + + public abstract void UseSkill(); +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillData.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillData.cs.meta new file mode 100644 index 0000000..6beff3a --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillData.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 0c074805eb6fdc84eb032f40ad30f1e5 \ No newline at end of file diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree.meta new file mode 100644 index 0000000..5bcfdc0 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3ddd8d8697a19d54dac553ecfd98c572 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ActiveSkillData.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ActiveSkillData.cs new file mode 100644 index 0000000..dc8827f --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ActiveSkillData.cs @@ -0,0 +1,7 @@ +using UnityEngine; + +/// +/// 액티브 스킬 데이터의 추상 기반 클래스. +/// UseSkill()을 구현하여 스킬별 발동 로직을 정의한다. +/// +public abstract class ActiveSkillData : SkillData { } diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ActiveSkillData.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ActiveSkillData.cs.meta new file mode 100644 index 0000000..9b98f1b --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ActiveSkillData.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 99ca49da1f93f30469491baaf8907351 \ No newline at end of file diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/PassiveSkillData.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/PassiveSkillData.cs new file mode 100644 index 0000000..caddb82 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/PassiveSkillData.cs @@ -0,0 +1,17 @@ +using UnityEngine; + +/// +/// 패시브 스킬 데이터의 추상 기반 클래스. +/// ApplyPassive()에서 EntityStatModifier를 수정하여 스탯 보정값을 정의한다. +/// UseSkill()은 패시브 스킬에서 호출되지 않으므로 빈 구현으로 봉인된다. +/// +public abstract class PassiveSkillData : SkillData +{ + public override void UseSkill() { } + + /// + /// 패시브 효과를 스탯 수정자에 누산한다. + /// SkillTreeComponent.RecalculatePassives()에서 일괄 호출된다. + /// + public abstract void ApplyPassive(ref EntityStatModifier modifier); +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/PassiveSkillData.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/PassiveSkillData.cs.meta new file mode 100644 index 0000000..3330ff5 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/PassiveSkillData.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: eda03d011e0724e48b96373340d00955 \ No newline at end of file diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/SKILLTREE_GUIDE.md b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/SKILLTREE_GUIDE.md new file mode 100644 index 0000000..8d2b8cb --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/SKILLTREE_GUIDE.md @@ -0,0 +1,215 @@ +# 스킬 트리 시스템 사용 가이드 + +## 개요 + +스킬 트리 시스템은 ScriptableObject 기반 데이터 설정 + 전략 패턴 기반 해금 규칙으로 구성된다. +직업별 트리 방식(검사 분기형 / 궁수 행-택1 / 법사 선형)을 코드 수정 없이 에디터에서 선택할 수 있다. + +``` +SkillData (추상) +├── ActiveSkillData (추상) → 구체 스킬 클래스 상속 +└── PassiveSkillData (추상) → 구체 스킬 클래스 상속 + +SkillNodeData (SO) → 트리의 단일 노드, 에디터에서 연결 +SkillTreeData (SO) → 직업별 트리 전체, StrategyType 선택 +SkillTreeComponent → 플레이어에 붙는 런타임 컴포넌트 +``` + +--- + +## 1. 스킬 클래스 만들기 + +### 1-1. 액티브 스킬 + +`ActiveSkillData`를 상속하고 `UseSkill()`을 구현한다. +`[CreateAssetMenu]`를 붙여 에디터에서 에셋 생성이 가능하게 한다. + +```csharp +[CreateAssetMenu(menuName = "ScriptableObjects/SkillData/Warrior/WhirlSlash")] +public class WhirlSlashData : ActiveSkillData +{ + public int HitCount = 3; + + public override void UseSkill() + { + // 회선 베기: 전방 3마리 공격 + Debug.Log($"회선 베기 발동! {HitCount}마리 공격"); + } +} +``` + +### 1-2. 패시브 스킬 + +`PassiveSkillData`를 상속하고 `ApplyPassive()`에서 `EntityStatModifier`를 수정한다. +`UseSkill()`은 구현하지 않아도 된다 (부모에서 빈 구현으로 봉인). + +```csharp +[CreateAssetMenu(menuName = "ScriptableObjects/SkillData/Warrior/SpeedUp")] +public class SpeedUpData : PassiveSkillData +{ + public float BonusAttackSpeed = 0.3f; + + public override void ApplyPassive(ref EntityStatModifier modifier) + { + modifier.BonusAttackSpeed += BonusAttackSpeed; + } +} +``` + +--- + +## 2. 에디터에서 노드 구성하기 + +### 2-1. SkillNodeData 에셋 생성 + +`Assets > Create > ScriptableObjects/SkillTree/Node` + +| 필드 | 설명 | 예시 | +|---|---|---| +| `Skill` | 이 노드가 담는 스킬 에셋 참조 | WhirlSlash 에셋 | +| `Prerequisites` | 이 노드 해금에 필요한 선행 노드 목록 | 기본 베기 노드 | +| `TierIndex` | 티어 번호 (0 = 기본 평타, 1 = 1티어 ...) | `1` | +| `SlotIndex` | 같은 티어 내 분기 그룹 구분 | `0` | +| `AttributeTag` | 법사 속성 구분 (법사 전용) | `"Fire"` | + +**검사 예시 구성:** + +``` +[TierIndex=0, SlotIndex=0] 기본 베기 (Prerequisites: 없음) +[TierIndex=0, SlotIndex=0] 기본 찌르기 (Prerequisites: 없음) + ↓ (둘 중 하나만 선택 가능) +[TierIndex=1, SlotIndex=1] 회선 베기 (Prerequisites: [기본 베기]) +[TierIndex=1, SlotIndex=2] 머리치기 (Prerequisites: [기본 베기]) +[TierIndex=1, SlotIndex=3] 관통 (Prerequisites: [기본 찌르기]) +[TierIndex=1, SlotIndex=4] 연속 찌르기 (Prerequisites: [기본 찌르기]) +``` + +> **검사 분기 규칙**: 같은 `TierIndex + SlotIndex` 조합을 가진 노드끼리는 하나만 선택 가능. +> 평타 두 개(`SlotIndex=0`)는 서로 배타적이다. + +### 2-2. SkillTreeData 에셋 생성 + +`Assets > Create > ScriptableObjects/SkillTree/Tree` + +| 필드 | 설명 | +|---|---| +| `JobClassName` | `"Warrior"` / `"Archer"` / `"Mage"` | +| `StrategyType` | `Branching` / `RowSelect` / `Linear` | +| `AllNodes` | 위에서 만든 모든 SkillNodeData 에셋 등록 | +| `MaxActiveSkillSlots` | 장착 가능한 액티브 스킬 수 (기본 3) | + +--- + +## 3. 플레이어에 컴포넌트 연결하기 + +1. 씬의 Warrior 오브젝트 선택 +2. `Add Component > SkillTreeComponent` 추가 +3. 인스펙터에서: + - `Tree Data` → 위에서 만든 `SkillTreeData` 에셋 연결 + - `Sp Resource` → 프로젝트의 `SO_SP` 에셋 연결 + +--- + +## 4. 런타임 사용법 + +### 4-1. 노드 해금 + +```csharp +SkillTreeComponent skillTree = player.GetComponent(); + +// 해금 가능 여부 확인 (SP 조건 + 전략 조건 통합) +if (skillTree.CanUnlock(whirlSlashNode)) +{ + skillTree.TryUnlockNode(whirlSlashNode); + // → SP 자동 차감, 패시브라면 Entity 스탯 자동 재계산 +} +``` + +### 4-2. 액티브 스킬 장착 + +```csharp +// 슬롯 0에 장착 (해금된 스킬만 장착 가능) +skillTree.TryEquipActiveSkill(whirlSlashData, slotIndex: 0); + +// 슬롯 해제 +skillTree.UnequipActiveSkill(slotIndex: 0); + +// 현재 장착된 스킬 조회 +ActiveSkillData equipped = skillTree.GetEquippedSkill(0); +equipped?.UseSkill(); +``` + +### 4-3. 전투 루프에서 스킬 발동 + +```csharp +// 장착된 모든 액티브 스킬 순회 (쿨타임 관리는 별도 구현 필요) +foreach (var skill in skillTree.GetEquippedActives()) +{ + if (skill != null) + skill.UseSkill(); +} +``` + +--- + +## 5. 직업별 트리 설정 요약 + +### 검사 — `StrategyType: Branching` + +- 같은 `TierIndex + SlotIndex` 그룹에서 하나만 선택 +- 선행 노드 중 **하나라도** 해금되면 다음 노드 진행 가능 +- 패시브 트리도 동일한 Branching 전략 사용 (방어 라인 / 공격 라인 분기) + +### 궁수 — `StrategyType: RowSelect` + +- 각 `TierIndex`(행)에서 단 하나만 선택 +- 이전 행(`TierIndex - 1`)에서 선택이 완료되어야 다음 행 선택 가능 +- `TierIndex: 0` → 1행, `TierIndex: 1` → 2행, `TierIndex: 2` → 3행 + +### 법사 — `StrategyType: Linear` + +- 첫 번째 노드(`TierIndex == 0`) 해금 시 `AttributeTag`(속성)가 자동 확정 +- 이후 확정된 속성의 `AttributeTag`를 가진 노드만 해금 가능 +- 모든 선행 노드가 해금되어야 다음 노드 진행 가능 (선형) + +--- + +## 6. 신규 직업 추가 방법 + +1. `Player`를 상속하는 클래스 생성 (예: `Archer`) +2. 해당 직업의 스킬 클래스들 작성 (`ActiveSkillData` / `PassiveSkillData` 상속) +3. 에디터에서 `SkillNodeData` 에셋 구성 +4. `SkillTreeData` 에셋 생성 후 `StrategyType` 선택 +5. 플레이어 오브젝트에 `SkillTreeComponent` 추가 후 에셋 연결 + +--- + +## 7. 클래스 의존 관계 + +``` +[SO_SP]──────────────────────────────────┐ + │ +[EntityStatData]──►[Entity]◄─────────────┤ + ▲ │ + RequireComponent │ +[SkillTreeData]──►[SkillTreeComponent]───┘ + │ │ + │ CreateStrategy() │ TryUnlockNode() + ▼ ▼ +[ISkillTreeStrategy] [SkillTreeValidator] + ▲ ▲ ▲ + │ │ │ +Branch Row Linear +Strategy Strategy Strategy + +[SkillNodeData]──►[SkillData] + ▲ ▲ + [Active] [Passive] + │ ApplyPassive() + ▼ + [EntityStatModifier] + │ + ▼ + [Entity] + ApplyStatModifier() +``` diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/SKILLTREE_GUIDE.md.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/SKILLTREE_GUIDE.md.meta new file mode 100644 index 0000000..e1b7b90 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/SKILLTREE_GUIDE.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: a1a6995e0b6702047a66259fc13303d4 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/SkillNodeData.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/SkillNodeData.cs new file mode 100644 index 0000000..fb8a05c --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/SkillNodeData.cs @@ -0,0 +1,19 @@ +using UnityEngine; + +/// +/// 스킬 트리의 단일 노드. 에디터에서 드래그&드롭으로 선행 노드를 연결한다. +/// +[CreateAssetMenu(fileName = "SkillNode", menuName = "ScriptableObjects/SkillTree/Node")] +public class SkillNodeData : ScriptableObject +{ + [Header("스킬 참조")] + public SkillData Skill; + + [Header("트리 구조")] + public SkillNodeData[] Prerequisites; + public int TierIndex; // 0 = 기본 평타, 1 = 1티어, 2 = 2티어, ... + public int SlotIndex; // 같은 티어 내 위치 + + [Header("속성 태그 (법사 전용)")] + public string AttributeTag; // "Fire" / "Ice" / "Wind" +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/SkillNodeData.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/SkillNodeData.cs.meta new file mode 100644 index 0000000..37a3a9b --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/SkillNodeData.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: dfcb3861838a4ec46924a354ad034191 \ No newline at end of file diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/SkillTreeComponent.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/SkillTreeComponent.cs new file mode 100644 index 0000000..6e8f47c --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/SkillTreeComponent.cs @@ -0,0 +1,162 @@ +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +/// +/// 플레이어의 스킬 트리 런타임 상태를 관리하는 컴포넌트. +/// 해금 상태를 Dictionary로 보관하여 ScriptableObject 에셋 오염을 방지한다. +/// +[RequireComponent(typeof(Player))] +public class SkillTreeComponent : MonoBehaviour +{ + [SerializeField] + private SkillTreeData treeData; + + [SerializeField] + private SO_SP spResource; + + // ScriptableObject를 직접 수정하지 않고 런타임 상태를 Dictionary로 격리 + private Dictionary unlockedState; + + // 장착된 액티브 스킬 슬롯 (null = 비어있음) + private List equippedActives; + + private ISkillTreeStrategy strategy; + private Entity entity; + + private void Awake() + { + entity = GetComponent(); + unlockedState = new Dictionary(); + strategy = treeData != null ? treeData.CreateStrategy() : null; + + int slotCount = treeData != null ? treeData.MaxActiveSkillSlots : 3; + equippedActives = new List(new ActiveSkillData[slotCount]); + + if (treeData == null) + Debug.LogWarning("[SkillTreeComponent] SkillTreeData가 비어 있습니다!"); + + if (spResource == null) + Debug.LogWarning("[SkillTreeComponent] SO_SP가 비어 있습니다!"); + + // 모든 노드를 초기 잠금 상태로 등록 + if (treeData != null && treeData.AllNodes != null) + { + foreach (var node in treeData.AllNodes) + unlockedState[node] = false; + } + } + + // ─── 해금 흐름 ─────────────────────────────────────────────── + + /// + /// UI에서 노드 선택 시 진입점. SP 소비 + 전략 조건 모두 통과해야 해금된다. + /// + public bool TryUnlockNode(SkillNodeData node) + { + if (node == null || strategy == null) + return false; + + if (IsUnlocked(node)) + { + Debug.Log($"[SkillTree] {node.Skill?.SkillName}은 이미 해금되어 있습니다."); + return false; + } + + if (!CanUnlock(node)) + { + Debug.Log($"[SkillTree] {node.Skill?.SkillName} 해금 조건 미충족."); + return false; + } + + // SP 소비 + spResource.Decrease((uint)node.Skill.SPCost); + + unlockedState[node] = true; + strategy.OnNodeUnlocked(node, unlockedState); + + Debug.Log($"[SkillTree] {node.Skill?.SkillName} 해금 완료."); + + RecalculatePassives(); + return true; + } + + /// + /// 해금된 모든 패시브 스킬 효과를 재계산하여 Entity 스탯에 반영한다. + /// + private void RecalculatePassives() + { + var modifier = EntityStatModifier.Zero; + + foreach (var kv in unlockedState) + { + if (!kv.Value) + continue; + if (kv.Key.Skill is PassiveSkillData passive) + passive.ApplyPassive(ref modifier); + } + + entity.ApplyStatModifier(modifier); + } + + // ─── 액티브 스킬 장착 ───────────────────────────────────────── + + /// + /// 해금된 액티브 스킬을 지정 슬롯에 장착한다. + /// + public bool TryEquipActiveSkill(ActiveSkillData skill, int slotIndex) + { + if (skill == null || slotIndex < 0 || slotIndex >= equippedActives.Count) + return false; + + if (!IsUnlocked(FindNodeBySkill(skill))) + { + Debug.Log($"[SkillTree] {skill.SkillName}이 해금되지 않았습니다."); + return false; + } + + equippedActives[slotIndex] = skill; + return true; + } + + public void UnequipActiveSkill(int slotIndex) + { + if (slotIndex >= 0 && slotIndex < equippedActives.Count) + equippedActives[slotIndex] = null; + } + + public ActiveSkillData GetEquippedSkill(int slotIndex) + { + if (slotIndex < 0 || slotIndex >= equippedActives.Count) + return null; + return equippedActives[slotIndex]; + } + + // ─── 조회 ──────────────────────────────────────────────────── + + public bool IsUnlocked(SkillNodeData node) + { + return node != null && unlockedState.TryGetValue(node, out bool v) && v; + } + + /// + /// SP 조건과 전략 조건을 통합하여 해금 가능 여부를 반환한다. + /// + public bool CanUnlock(SkillNodeData node) + { + if (node == null || node.Skill == null) + return false; + if (!SkillTreeValidator.HasEnoughSP(node, spResource)) + return false; + return strategy.CanUnlock(node, unlockedState); + } + + public IReadOnlyList GetEquippedActives() => equippedActives.AsReadOnly(); + + // ─── 내부 유틸 ──────────────────────────────────────────────── + + private SkillNodeData FindNodeBySkill(SkillData skill) + { + return treeData?.AllNodes?.FirstOrDefault(n => n.Skill == skill); + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/SkillTreeComponent.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/SkillTreeComponent.cs.meta new file mode 100644 index 0000000..422b4f8 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/SkillTreeComponent.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 276db779e5ff7bc41a98d39b1eff25d2 \ No newline at end of file diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/SkillTreeData.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/SkillTreeData.cs new file mode 100644 index 0000000..2282e80 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/SkillTreeData.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using UnityEngine; + +public enum SkillTreeStrategyType +{ + Branching, + RowSelect, + Linear, +} + +/// +/// 직업별 스킬 트리 전체 정의. 에디터에서 AllNodes에 노드를 등록하고 +/// StrategyType으로 해금 규칙 방식을 선택한다. +/// +[CreateAssetMenu(fileName = "SkillTreeData", menuName = "ScriptableObjects/SkillTree/Tree")] +public class SkillTreeData : ScriptableObject +{ + [Header("직업 정보")] + public string JobClassName; + + [Header("트리 전략")] + public SkillTreeStrategyType StrategyType; + + [Header("노드 목록")] + public List AllNodes; + + [Header("액티브 슬롯 제한")] + public int MaxActiveSkillSlots = 3; + + /// + /// StrategyType에 맞는 전략 인스턴스를 생성한다. + /// + public ISkillTreeStrategy CreateStrategy() + { + return StrategyType switch + { + SkillTreeStrategyType.Branching => new BranchingTreeStrategy(), + SkillTreeStrategyType.RowSelect => new RowSelectTreeStrategy(), + SkillTreeStrategyType.Linear => new LinearTreeStrategy(), + _ => new BranchingTreeStrategy(), + }; + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/SkillTreeData.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/SkillTreeData.cs.meta new file mode 100644 index 0000000..16d8ca3 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/SkillTreeData.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 714ac6181a5a647479fea7f51342d8b1 \ No newline at end of file diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/SkillTreeValidator.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/SkillTreeValidator.cs new file mode 100644 index 0000000..0df9451 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/SkillTreeValidator.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Linq; + +/// +/// 스킬 트리 노드 해금의 공통 조건을 검증하는 정적 유틸리티. +/// 전략별 규칙(ISkillTreeStrategy)과 독립적으로 동작한다. +/// +public static class SkillTreeValidator +{ + /// + /// 선행 노드가 모두 해금되어 있는지 확인한다. + /// + public static bool ArePrerequisitesMet( + SkillNodeData node, + IReadOnlyDictionary state + ) + { + if (node.Prerequisites == null || node.Prerequisites.Length == 0) + return true; + + return node.Prerequisites.All(pre => + pre != null && state.TryGetValue(pre, out bool unlocked) && unlocked + ); + } + + /// + /// SP가 해금 비용을 충당할 수 있는지 확인한다. + /// + public static bool HasEnoughSP(SkillNodeData node, SO_SP sp) + { + if (node.Skill == null) + return false; + return sp.Get() >= (uint)node.Skill.SPCost; + } + + /// + /// 액티브 스킬 슬롯에 여유가 있는지 확인한다. + /// + public static bool HasActiveSlotAvailable(IReadOnlyList equipped, int maxSlots) + { + int occupiedSlots = equipped.Count(skill => skill != null); + return occupiedSlots < maxSlots; + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/SkillTreeValidator.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/SkillTreeValidator.cs.meta new file mode 100644 index 0000000..6d29bdd --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/SkillTreeValidator.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 58849c074fe740b44b9e720530b1d432 \ No newline at end of file diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Strategies.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Strategies.meta new file mode 100644 index 0000000..b8b0846 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Strategies.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 605c3580e70fa6c4fac66d892266c59c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Strategies/BranchingTreeStrategy.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Strategies/BranchingTreeStrategy.cs new file mode 100644 index 0000000..b462446 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Strategies/BranchingTreeStrategy.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.Linq; + +/// +/// 검사 분기형 트리 전략. +/// 같은 TierIndex 내에서 SlotIndex가 동일한 그룹(분기 지점)에는 하나만 선택 가능하다. +/// 선행 노드 중 하나라도 해금되어 있으면 진행 가능하다. +/// +public class BranchingTreeStrategy : ISkillTreeStrategy +{ + public bool CanUnlock(SkillNodeData node, IReadOnlyDictionary state) + { + // 선행 노드가 없으면 (기본 노드) 즉시 해금 가능 + if (node.Prerequisites == null || node.Prerequisites.Length == 0) + return true; + + // 선행 노드 중 하나라도 해금되어 있으면 진행 가능 + bool anyPrerequisiteMet = node.Prerequisites.Any(pre => + pre != null && state.TryGetValue(pre, out bool unlocked) && unlocked + ); + + if (!anyPrerequisiteMet) + return false; + + // 같은 TierIndex + SlotIndex를 가진 노드(분기 그룹)에서 이미 다른 노드가 해금되었다면 불가 + bool conflictExists = state + .Where(kv => kv.Value) + .Select(kv => kv.Key) + .Any(unlocked => + unlocked != node + && unlocked.TierIndex == node.TierIndex + && unlocked.SlotIndex == node.SlotIndex + ); + + return !conflictExists; + } + + public void OnNodeUnlocked( + SkillNodeData node, + IReadOnlyDictionary state + ) { } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Strategies/BranchingTreeStrategy.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Strategies/BranchingTreeStrategy.cs.meta new file mode 100644 index 0000000..94ac0f5 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Strategies/BranchingTreeStrategy.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 384b1f6796b7a3746a3dfd7e33eb91a3 \ No newline at end of file diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Strategies/ISkillTreeStrategy.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Strategies/ISkillTreeStrategy.cs new file mode 100644 index 0000000..2fa785b --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Strategies/ISkillTreeStrategy.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +/// +/// 직업별 스킬 트리 해금 규칙을 추상화하는 전략 인터페이스. +/// 검사(분기형), 궁수(행-택1), 법사(선형) 각각 구현한다. +/// +public interface ISkillTreeStrategy +{ + /// + /// 현재 해금 상태와 트리 규칙을 기반으로 노드 해금 가능 여부를 판별한다. + /// SP 조건 및 선행 노드 조건은 SkillTreeValidator에서 별도 검증한다. + /// + bool CanUnlock(SkillNodeData node, IReadOnlyDictionary state); + + /// + /// 노드 해금 직후 호출된다. 전략별 추가 상태 처리에 사용한다. + /// + void OnNodeUnlocked(SkillNodeData node, IReadOnlyDictionary state); +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Strategies/ISkillTreeStrategy.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Strategies/ISkillTreeStrategy.cs.meta new file mode 100644 index 0000000..ed21dd3 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Strategies/ISkillTreeStrategy.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2e528846e5f2cbf46878fea6ac1d9b67 \ No newline at end of file diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Strategies/LinearTreeStrategy.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Strategies/LinearTreeStrategy.cs new file mode 100644 index 0000000..11bd526 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Strategies/LinearTreeStrategy.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +/// +/// 법사 선형 속성 트리 전략. +/// 속성(Fire/Ice/Wind) 선택 후 해당 AttributeTag 라인만 진행 가능하다. +/// 선행 노드가 모두 해금되어야 다음 노드 해금 가능하다. +/// +public class LinearTreeStrategy : ISkillTreeStrategy +{ + private string selectedAttribute; + + /// + /// 속성을 선택한다. 첫 번째 노드를 해금할 때 자동 결정되며, + /// 이후 다른 속성 라인은 해금 불가 상태가 된다. + /// + public void SelectAttribute(string attribute) + { + selectedAttribute = attribute; + } + + public bool CanUnlock(SkillNodeData node, IReadOnlyDictionary state) + { + // 속성이 아직 선택되지 않은 경우: 어떤 속성이든 첫 번째 노드(TierIndex == 0) 해금 가능 + if (string.IsNullOrEmpty(selectedAttribute)) + return node.TierIndex == 0; + + // 선택된 속성 라인만 허용 + if (node.AttributeTag != selectedAttribute) + return false; + + // 선행 노드가 없으면 해금 가능 + if (node.Prerequisites == null || node.Prerequisites.Length == 0) + return true; + + // 선형 트리: 모든 선행 노드가 해금되어 있어야 한다 + return node.Prerequisites.All(pre => + pre != null && state.TryGetValue(pre, out bool unlocked) && unlocked + ); + } + + public void OnNodeUnlocked(SkillNodeData node, IReadOnlyDictionary state) + { + // 첫 번째 노드 해금 시 속성을 확정한다 + if (string.IsNullOrEmpty(selectedAttribute) && !string.IsNullOrEmpty(node.AttributeTag)) + SelectAttribute(node.AttributeTag); + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Strategies/LinearTreeStrategy.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Strategies/LinearTreeStrategy.cs.meta new file mode 100644 index 0000000..8f41489 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Strategies/LinearTreeStrategy.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b5fa10981812ba04da5883734c774600 \ No newline at end of file diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Strategies/RowSelectTreeStrategy.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Strategies/RowSelectTreeStrategy.cs new file mode 100644 index 0000000..8749bbc --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Strategies/RowSelectTreeStrategy.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Linq; + +/// +/// 궁수 행-택1 트리 전략 (롤토체스식). +/// 각 TierIndex(행)에서 단 하나의 노드만 선택 가능하다. +/// 이전 행이 선택되어야 다음 행 선택 가능하다. +/// +public class RowSelectTreeStrategy : ISkillTreeStrategy +{ + public bool CanUnlock(SkillNodeData node, IReadOnlyDictionary state) + { + // 이미 이 행에서 선택된 노드가 있으면 불가 + bool rowAlreadySelected = state.Any(kv => + kv.Value && kv.Key != node && kv.Key.TierIndex == node.TierIndex + ); + + if (rowAlreadySelected) + return false; + + // 첫 번째 행(TierIndex == 0)은 선행 조건 없이 해금 가능 + if (node.TierIndex == 0) + return true; + + // 이전 행(TierIndex - 1)에서 선택된 노드가 있어야 다음 행 선택 가능 + bool previousRowSelected = state.Any(kv => + kv.Value && kv.Key.TierIndex == node.TierIndex - 1 + ); + + return previousRowSelected; + } + + public void OnNodeUnlocked( + SkillNodeData node, + IReadOnlyDictionary state + ) { } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Strategies/RowSelectTreeStrategy.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Strategies/RowSelectTreeStrategy.cs.meta new file mode 100644 index 0000000..8ef0994 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Strategies/RowSelectTreeStrategy.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: be906072356950a44b2f302420dbf745 \ No newline at end of file diff --git a/Fantasy-Grower/Assets/Scripts/SO.meta b/Fantasy-Grower/Assets/Scripts/SO.meta new file mode 100644 index 0000000..768b8e5 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/SO.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ebf9135db08abe44fa9cd411fc12b818 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/SO/SkillData.meta b/Fantasy-Grower/Assets/Scripts/SO/SkillData.meta new file mode 100644 index 0000000..8ba5971 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/SO/SkillData.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 18ac5e714cc0639468bf457b5c43a336 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior.meta b/Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior.meta new file mode 100644 index 0000000..f6bea78 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ff9d46ab3f948624cb4e5d932dc372ad +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/BasicSkill.asset b/Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/BasicSkill.asset new file mode 100644 index 0000000..a9adaba --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/BasicSkill.asset @@ -0,0 +1,19 @@ +%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: 0c074805eb6fdc84eb032f40ad30f1e5, type: 3} + m_Name: BasicSkill + m_EditorClassIdentifier: + SkillName: + SkillIcon: {fileID: 0} + SkillDescription: + Cooldown: 0 + Damage: 0 diff --git a/Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/BasicSkill.asset.meta b/Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/BasicSkill.asset.meta new file mode 100644 index 0000000..6ab5c5f --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/BasicSkill.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: da12e4dcc00447545828f224e06be591 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/SO/StatData.meta b/Fantasy-Grower/Assets/Scripts/SO/StatData.meta new file mode 100644 index 0000000..8c6933c --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/SO/StatData.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a808d23574297d7489802be26a33176d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy.meta b/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy.meta new file mode 100644 index 0000000..7e513e8 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a03e8e8a3fadc1347bd03fa932568758 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/TestEnemyStat.asset b/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/TestEnemyStat.asset new file mode 100644 index 0000000..39f2c02 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/TestEnemyStat.asset @@ -0,0 +1,19 @@ +%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: 5df7739d4b9038a4d9cb6566170314b6, type: 3} + m_Name: TestEnemyStat + m_EditorClassIdentifier: + Hp: 150 + DamageReduction: 0 + AttackPower: 10 + AttackSpeed: 1 + CriticalPercentage: 0 diff --git a/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/TestEnemyStat.asset.meta b/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/TestEnemyStat.asset.meta new file mode 100644 index 0000000..3a86e88 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/TestEnemyStat.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 947637eac36383b49827043094c83699 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/SO/StatData/Player.meta b/Fantasy-Grower/Assets/Scripts/SO/StatData/Player.meta new file mode 100644 index 0000000..636e09e --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/SO/StatData/Player.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 27c989ae2ea147c4ea465b4b1414b4c9 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/SO/StatData/Player/WarriorStat.asset b/Fantasy-Grower/Assets/Scripts/SO/StatData/Player/WarriorStat.asset new file mode 100644 index 0000000..259557f --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/SO/StatData/Player/WarriorStat.asset @@ -0,0 +1,19 @@ +%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: 5df7739d4b9038a4d9cb6566170314b6, type: 3} + m_Name: WarriorStat + m_EditorClassIdentifier: + Hp: 200 + DamageReduction: 0 + AttackPower: 20 + AttackSpeed: 1 + CriticalPercentage: 0 diff --git a/Fantasy-Grower/Assets/Scripts/SO/StatData/Player/WarriorStat.asset.meta b/Fantasy-Grower/Assets/Scripts/SO/StatData/Player/WarriorStat.asset.meta new file mode 100644 index 0000000..74febaa --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/SO/StatData/Player/WarriorStat.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: fe352f708efa65540819d5d6bf040e6b +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/CLAUDE.md b/Fantasy-Grower/CLAUDE.md new file mode 100644 index 0000000..f679941 --- /dev/null +++ b/Fantasy-Grower/CLAUDE.md @@ -0,0 +1,197 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +**Fantasy Grower** — 2D Idle RPG Mobile Game (Unity 6.0.0, Android) + +Players build characters by combining jobs, weapons, and skill trees, then progress through dungeons via auto-combat. + +## Development Environment + +- **Engine**: Unity 6.0.0+ +- **IDE**: Visual Studio 2022 or JetBrains Rider (open `Fantasy-Grower.sln`) +- **Rendering**: Universal Render Pipeline (URP) 2D +- **Input**: Unity New Input System +- **Platform**: Android (target), Desktop (test) + +No CLI build commands. All build/run operations are performed inside the Unity Editor: +- **Play**: Unity Editor > Play button +- **Build**: File > Build and Run +- **Tests**: Window > General > Test Runner (Unity Test Framework) + +## C# Naming Conventions + +Follows Microsoft C# naming conventions. + +| Target | Rule | Example | +|--------|------|---------| +| Class / Struct | PascalCase | `EntityStatData`, `AttackCollider` | +| Interface | `I` + PascalCase | `ISkillData`, `IAttackable` | +| Method | PascalCase | `TakeDamage()`, `GetGoods()` | +| Property | PascalCase | `AttackPower`, `MaxHealth` | +| public field | PascalCase | `public int AttackPower;` | +| private field | camelCase | `private int attackPower;` | +| Local variable | camelCase | `int currentHp = 0;` | +| Parameter | camelCase | `void TakeDamage(int damageAmount)` | +| Constant (`const`) | PascalCase | `const int MaxLevel = 100;` | +| enum type | PascalCase | `EntityType` | +| enum value | PascalCase | `EntityType.Player`, `EntityType.Enemy` | +| ScriptableObject class | PascalCase (no prefix) | `GoodsBase`, `SkillData` (~~`SO_Goods`~~ forbidden) | + +> **Note**: Hungarian prefixes such as `SO_`, `m_`, `_` are not allowed. + +## Code Architecture + +### Class Hierarchy + +``` +MonoBehaviour +└── Entity (abstract) — HP, AttackPower, TakeDamage(), Death() + ├── Player — Implements Attack() + │ └── Warrior — Swordsman subclass + └── Enemy — Enemy base + └── TestEnemy — Overrides Death() +``` + +### Key Design Patterns + +**ScriptableObject-based data separation** +- `EntityStatData` — Stores stats: HP, AttackPower, AttackSpeed, CriticalChance, etc. +- `SkillData` (abstract) — Skill data base class +- `GoodsBase` family — Currency system (Gold, XP, SP, Mithril, UpgradeScroll) + +**Combat hit detection** +- `AttackCollider` component handles damage via 2D Trigger collision +- `EntityType` enum prevents friendly fire (`EntityType.Player` hits `EntityType.Enemy` only, and vice versa) +- Damage = attacker's `AttackPower` applied directly + +**Goods (Currency) system** +- `GoodsBase` abstract class: `Get()`, `Increase()`, `Decrease()` +- XP cannot call `Decrease()` (marked `[Obsolete]`) +- Range checks prevent overspending + +### Directory Structure + +``` +Assets/Scripts/ +├── Battle/ +│ ├── Entity.cs — Combat entity base class +│ ├── AttackCollider.cs — Attack hitbox component +│ ├── Player/ — Player classes +│ └── Enemy/ — Enemy classes +└── Core/ + ├── EntityStatData.cs — Stat ScriptableObject + ├── SkillData.cs — Skill data base + └── Goods/ — Currency ScriptableObject family +``` + +--- + +## Game Design Document Summary + +> Use this as the reference for all implementation decisions. + +### Growth Resources + +| Resource | Usage | +|----------|-------| +| Gold | Buy/upgrade weapons, refresh shop | +| XP | Character level up | +| SP (Skill Point) | Upgrade skill tree; gained on level up | +| Upgrade Scroll | Weapon upgrade (separate scroll per weapon type) | +| Mithril | Weapon synthesis | + +### Job System (3 types) + +| Job | Traits | +|-----|--------| +| Warrior | High HP, stable combat, AoE/single-target builds | +| Archer | Low HP, crit-focused, weapon passives are key | +| Mage | Medium HP, specializes in one element: Fire / Ice / Wind | + +**Mage element traits**: +- Fire: DoT damage, AoE +- Ice: Reduces enemy move/attack speed (utility) +- Wind: Increases attack speed, low cooldowns + +### Weapon System + +**Grades**: S > A > B > C +- A and above: provide special passives +- S grade: craftable only via synthesis (smelting) + +**Job weapons**: +- Warrior: Rapier (crit), Longsword (balanced), Greatsword (attack power) +- Archer: Longbow (attack+crit), Crossbow (attack speed+utility) +- Mage: Staff (cooldown reduction), Spellbook (element boost) + +**Equipment management**: +- Upgrade: Uses Upgrade Scrolls; higher grade requires more scrolls +- Synthesis: Mithril + 2 weapons → new weapon (average grade) +- Sell: Gain Gold + +### Dungeon System (4 types) + +| Dungeon | Description | +|---------|-------------| +| Basic Dungeon | Auto-combat; rewards Gold+XP; wave progression; elite monsters appear | +| Gold Dungeon | Mining minigame; tap screen to mine; 30–60s time limit; rare Mithril drop | +| Weapon Dungeon | Auto-combat; drops C-grade weapons; low chance for B-grade/scrolls | +| Boss Dungeon | Many powerful elites; rewards Mithril+A-grade weapon+XP on clear | + +### Shop System + +- **Blacksmith**: Random weapon stock; refresh with Gold; customer rank system (rank up by buying/upgrading → better weapon grades available) +- **Magic Shop**: Sells Upgrade Scrolls; refresh with Gold + +### Skill Tree + +Common rules: Gain SP on level up; equip up to 3 active skills; passives and actives are separate trees. + +#### Warrior — Active Tree (branching) + +| Tier | Skill | Type | Description | +|------|-------|------|-------------| +| Basic | Slash | Basic Attack | Hits 2 enemies, low damage, fast attack speed | +| Basic | Thrust | Basic Attack | Hits 1 enemy, high damage, slow attack speed | +| Tier 1 | Spin Slash | Active | Hits 3 enemies, low damage, normal cooldown | +| Tier 1 | Headbutt | Active | Hits 2 enemies, medium damage, long cooldown, 2s stun | +| Tier 1 | Pierce | Active | Hits 2 enemies, high damage, fairly long cooldown | +| Tier 1 | Rapid Thrust | Active | Hits 1 enemy, high damage, normal cooldown | +| Tier 2 | Sword Dance | Active | Hits 4 enemies, low damage, long cooldown | +| Tier 2 | Endure | Active | Long cooldown; reduces HP by half per enemy present | +| Tier 2 | Breath | Active | Very long cooldown; resets all skill cooldowns except itself | +| Tier 2 | One-Sword | Active | Hits 1 enemy, high damage, long cooldown; insta-kill on crit | +| Final | Final Skill | Active | (TBD) | + +#### Warrior — Passive Tree (2 branches) + +**Defense line**: Cloth Armor (damage reduction) → Preparation (reduce all skill CDs) → Broken Blade (recover n HP/s below 50% HP) → Tempering (max HP + attack power n%) + +**Offense line**: Quick Stone (attack speed up) → Slow Starter (attack power increases per kill, resets on entering dungeon) → Broken Armor (recover n% damage dealt below 50% HP) → Tempering (shared final node) + +#### Archer — TFT-style Pick 1 of 3 (3 rows) + +Choose 1 option per row. + +| Row | Skills | +|-----|--------| +| Row 1 | Focus (next attack deals n× damage), Arc Shot (AoE, low damage), Multi-Shot (2 shots at 75% damage), Heavy Arrow (pierce + slow debuff), Sixth Sense (bonus damage to elite monsters) | +| Row 2 | Fire (basic: single, +20% crit), Rapid Fire (5s crit window), Silver Arrow (100% on attack), Falling Shot (+n% attack speed), Headshot (n% of max HP damage), Expertise (range increase) | +| Row 3 | Spread Shot (hits 3 enemies), Snipe (single, very high damage), Adversity (+n% crit rate), Poison Arrow (3 shots, very high damage), Glory (crit damage increase) | + +#### Mage — Linear Tree per Element + +**Fire**: Fire Element (basic, AoE) → Burn (passive: n damage/s status) → Fireball (single) → Fire Pillar (ground AoE) → [Ash (bonus burn damage) / Explosion (AoE size up)] → Meteor (AoE, very long CD) → [Magic Mastery (active damage+) / 4th Degree Burn (burn stackable)] + +**Ice**: Ice Element (basic, single, slow) → Frostbite (passive: reduces move/attack speed) → Ice Shatter (AoE) → Ice Shard (single, high damage) → [Chill Boost (stronger frostbite) / Ice Armor (damage reduction for 10s on hit)] → Blizzard (AoE, very long CD) → [Magic Mastery / Sub-Zero (frostbite stacks → immobilize)] + +**Wind**: Wind Element (basic, hits 2, fast) → Gale (passive: stack 1 wound per attack, deal damage at 5 stacks) → Wind Blade (15s attack speed buff, hits 3) → Tornado (AoE, knockback effect) → [Accelerate (chance to ignore enemy attack) / Tailwind (chance for extra basic attack)] → Gust (AoE, very long CD) → [Magic Mastery / Abyss (Gale proc resets all skill CDs)] + +### Game Loop + +``` +Clear dungeon → Earn Gold/XP → Level up + gain SP → Upgrade skill tree → Acquire/upgrade weapons → Challenge higher dungeons +``` \ No newline at end of file diff --git a/Fantasy-Grower/Packages/manifest.json b/Fantasy-Grower/Packages/manifest.json index c320cc0..4a8ae41 100644 --- a/Fantasy-Grower/Packages/manifest.json +++ b/Fantasy-Grower/Packages/manifest.json @@ -1,6 +1,7 @@ { "dependencies": { "com.unity.collab-proxy": "2.11.3", + "com.unity.device-simulator.devices": "1.0.1", "com.unity.feature.2d": "2.0.1", "com.unity.ide.rider": "3.0.39", "com.unity.ide.visualstudio": "2.0.27", diff --git a/Fantasy-Grower/Packages/packages-lock.json b/Fantasy-Grower/Packages/packages-lock.json index f005646..2d05d0e 100644 --- a/Fantasy-Grower/Packages/packages-lock.json +++ b/Fantasy-Grower/Packages/packages-lock.json @@ -122,6 +122,13 @@ }, "url": "https://packages.unity.com" }, + "com.unity.device-simulator.devices": { + "version": "1.0.1", + "depth": 0, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, "com.unity.ext.nunit": { "version": "2.0.5", "depth": 1, diff --git a/Fantasy-Grower/ProjectSettings/SceneTemplateSettings.json b/Fantasy-Grower/ProjectSettings/SceneTemplateSettings.json new file mode 100644 index 0000000..ede5887 --- /dev/null +++ b/Fantasy-Grower/ProjectSettings/SceneTemplateSettings.json @@ -0,0 +1,121 @@ +{ + "templatePinStates": [], + "dependencyTypeInfos": [ + { + "userAdded": false, + "type": "UnityEngine.AnimationClip", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEditor.Animations.AnimatorController", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.AnimatorOverrideController", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEditor.Audio.AudioMixerController", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.ComputeShader", + "defaultInstantiationMode": 1 + }, + { + "userAdded": false, + "type": "UnityEngine.Cubemap", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.GameObject", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEditor.LightingDataAsset", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.LightingSettings", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.Material", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEditor.MonoScript", + "defaultInstantiationMode": 1 + }, + { + "userAdded": false, + "type": "UnityEngine.PhysicsMaterial", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.PhysicsMaterial2D", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.Rendering.PostProcessing.PostProcessProfile", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.Rendering.PostProcessing.PostProcessResources", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.Rendering.VolumeProfile", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEditor.SceneAsset", + "defaultInstantiationMode": 1 + }, + { + "userAdded": false, + "type": "UnityEngine.Shader", + "defaultInstantiationMode": 1 + }, + { + "userAdded": false, + "type": "UnityEngine.ShaderVariantCollection", + "defaultInstantiationMode": 1 + }, + { + "userAdded": false, + "type": "UnityEngine.Texture", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.Texture2D", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.Timeline.TimelineAsset", + "defaultInstantiationMode": 0 + } + ], + "defaultDependencyTypeInfo": { + "userAdded": false, + "type": "", + "defaultInstantiationMode": 1 + }, + "newSceneOverride": 0 +} \ No newline at end of file