Project_WL/Assets/FarmingIsland/Scripts/Island/IslandManager.cs

515 lines
19 KiB
C#

using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.SceneManagement;
using Unity.AI.Navigation;
using DG.Tweening;
namespace CryingSnow.FarmingIsland
{
public class IslandManager : MonoBehaviour
{
public static IslandManager Instance { get; private set; }
public event System.Action<int> OnCoinChanged;
public event System.Action<GameData> OnSaveGame;
#region Serialized Fields
[Header("Settings")]
[SerializeField, Tooltip("The size of each island, used to determine spacing and placement.")]
private int islandSize = 8;
[SerializeField, Tooltip("The base price for unlocking an island, which increases with each subsequent purchase.")]
private int baseUnlockPrice = 100;
[SerializeField, Tooltip("The factor by which the unlock price increases for each island.")]
private float unlockGrowthFactor = 1.1f;
[SerializeField, Tooltip("The starting amount of money the player begins with.")]
private int startingMoney = 500;
[Header("References")]
[SerializeField, Tooltip("List of meshes used to represent different island configurations.")]
private List<Mesh> islandMeshes;
[SerializeField, Tooltip("List of meshes used to represent different road configurations connecting islands.")]
private List<Mesh> roadMeshes;
[SerializeField, Tooltip("Prefab used to display purchasing option when an island is available for unlocking.")]
private Purchaser purchaserPrefab;
[SerializeField, Tooltip("Theme song that will be played for current level.")]
private AudioClip themeSong;
#if UNITY_EDITOR
[Header("Editor Tool")]
[SerializeField, Tooltip("Whether to draw road connections between islands in the editor for debugging.")]
private bool drawRoads = true;
#endif
#endregion
#region Private Fields
private string saveFileName;
private GameData data;
private NavMeshSurface navMeshSurface;
private List<Island> islands = new List<Island>();
private Dictionary<Vector3Int, Island> islandGrid = new Dictionary<Vector3Int, Island>();
private Dictionary<Vector3Int, Purchaser> activePurchasers = new Dictionary<Vector3Int, Purchaser>();
private Coroutine navMeshUpdateCoroutine;
private readonly Dictionary<int, int> tileIndexMap = new Dictionary<int, int>
{
{ 2, 1 }, { 8, 2 }, { 10, 3 }, { 11, 4 }, { 16, 5 },
{ 18, 6 }, { 22, 7 }, { 24, 8 }, { 26, 9 }, { 27, 10 },
{ 30, 11 }, { 31, 12 }, { 64, 13 }, { 66, 14 }, { 72, 15 },
{ 74, 16 }, { 75, 17 }, { 80, 18 }, { 82, 19 }, { 86, 20 },
{ 88, 21 }, { 90, 22 }, { 91, 23 }, { 94, 24 }, { 95, 25 },
{ 104, 26 }, { 106, 27 }, { 107, 28 }, { 120, 29 }, { 122, 30 },
{ 123, 31 }, { 126, 32 }, { 127, 33 }, { 208, 34 }, { 210, 35 },
{ 214, 36 }, { 216, 37 }, { 218, 38 }, { 219, 39 }, { 222, 40 },
{ 223, 41 }, { 248, 42 }, { 250, 43 }, { 251, 44 }, { 254, 45 },
{ 255, 46 }, { 0, 47 }
};
#endregion
#region Public Properties
public int IslandSize => islandSize;
public List<Island> UnlockedIslands { get; private set; } = new List<Island>();
public List<PurchaserData> Purchasers { get; private set; } = new List<PurchaserData>();
public List<(Vector3Int, Location)> PropLocations { get; private set; } = new List<(Vector3Int, Location)>();
public bool HasNavMesh { get; private set; } = false;
public int[] AnimalLevels => data.AnimalLevels;
public int Coin
{
get => data.Coin;
set
{
data.Coin = value;
OnCoinChanged?.Invoke(value);
}
}
#endregion
#region Unity Lifecycle
private void Awake()
{
Instance = this;
navMeshSurface = GetComponent<NavMeshSurface>();
islands = GetComponentsInChildren<Island>().ToList();
foreach (var island in islands)
{
Vector3Int pos = island.GetPosition();
if (!islandGrid.ContainsKey(pos))
{
islandGrid.Add(pos, island);
}
}
ItemDatabase.Initialize();
saveFileName = WL.Island.WLIslandSave.ResolveSaveName(SceneManager.GetActiveScene().name); // WL-816: 경로마다 세이브가 갈리지 않게 이름 고정(끄면 원래대로)
LoadGameData();
}
private void Start()
{
// Events Subscription
islands.ForEach(island =>
{
island.OnActivated += () => OnIslandUnlocked(island);
island.OnPropsInitialized += RequestNavMeshUpdate;
});
// Islands Initialization
islands.ForEach(island => RefreshIsland(island));
RequestNavMeshUpdate();
AudioManager.Instance.PlayBGM(themeSong);
}
private void OnDisable()
{
SaveGameData();
DOTween.KillAll();
}
private void OnApplicationPause(bool pauseStatus)
{
if (pauseStatus) SaveGameData();
}
private void OnApplicationQuit()
{
SaveGameData();
}
#endregion
#region Core Logic
private void OnIslandUnlocked(Island newlyUnlockedIsland)
{
// 1. Refresh the island we just bought (to update its visual mesh)
RefreshIsland(newlyUnlockedIsland);
// 2. Refresh neighbors to update the Coastline/Road connections of adjacent UNLOCKED islands.
Vector3Int center = newlyUnlockedIsland.GetPosition();
for (int x = -1; x <= 1; x++)
{
for (int z = -1; z <= 1; z++)
{
if (x == 0 && z == 0) continue;
Vector3Int neighborPos = center + new Vector3Int(x, 0, z) * islandSize;
if (islandGrid.TryGetValue(neighborPos, out Island neighbor))
{
RefreshIsland(neighbor);
}
}
}
// 3. Check ALL Locked Islands.
// Why? An island far away might have a "Constraint" that requires the island
// we just bought. Since it's not a neighbor, the loop above missed it.
foreach (var island in islands)
{
// We only need to check Locked islands here.
// Unlocked islands were handled by the neighbor loop if they needed mesh updates.
if (!island.IsUnlocked)
{
RefreshIsland(island);
}
}
RequestNavMeshUpdate();
}
private void RefreshIsland(Island island)
{
Vector3Int islandPos = island.GetPosition();
// 1. Get all 8 neighbors for Tiling (Corners require diagonal data)
var allNeighbors = CheckIslandsFast(islandPos);
// 2. Extract ONLY Cardinal Neighbors for Colliders [North, West, East, South]
// Indices from CheckIslandsFast: 0=NW, 1=N, 2=NE, 3=W, 4=E, 5=SW, 6=S, 7=SE
bool[] cardinalNeighbors = new bool[]
{
allNeighbors[1].exists, // North
allNeighbors[3].exists, // West
allNeighbors[4].exists, // East
allNeighbors[6].exists // South
};
if (island.IsUnlocked)
{
if (!UnlockedIslands.Contains(island)) UnlockedIslands.Add(island);
RemovePurchaserAt(islandPos);
// Update Meshes (Uses all 8 neighbors for correct corner tiling)
int islandMeshIndex = GetIslandTileIndex(islandPos, allNeighbors);
if (islandMeshIndex >= islandMeshes.Count) islandMeshIndex = 0;
Mesh islandMesh = island.IsBridge ? islandMeshes[0] : islandMeshes[islandMeshIndex];
int roadTileIndex = GetRoadTileIndex(islandPos);
Mesh roadMesh = roadMeshes[roadTileIndex];
// Pass the "cardinalNeighbors" array (4 items) instead of "allNeighbors" (8 items)
island.ConfigureIsland(islandMesh, roadMesh, cardinalNeighbors);
}
else
{
island.gameObject.SetActive(false);
// Use the cardinal check for purchasing logic (we can only buy from cardinal adjacency)
if (cardinalNeighbors.Any(x => x))
{
if (island.Constraint != null && !island.Constraint.IsUnlocked) return;
int price = CalculateIslandPrice(island.transform.GetSiblingIndex());
SpawnPurchaserFor(island, price);
}
else
{
RemovePurchaserAt(islandPos);
}
}
}
private void SpawnPurchaserFor(Island island, int price)
{
Vector3Int pos = island.GetPosition();
if (activePurchasers.ContainsKey(pos)) return;
var purchaser = Instantiate(purchaserPrefab, pos, Quaternion.identity);
var newPurchaserData = new PurchaserData(pos, price);
purchaser.Init(island, price, newPurchaserData);
activePurchasers.Add(pos, purchaser);
Purchasers.Add(newPurchaserData);
}
private void RemovePurchaserAt(Vector3Int position)
{
if (activePurchasers.TryGetValue(position, out Purchaser purchaser))
{
if (purchaser != null) Destroy(purchaser.gameObject);
activePurchasers.Remove(position);
// Also sync the data list
Purchasers.RemoveAll(p => p.Position == position);
}
}
private int GetIslandTileIndex(Vector3Int position, (bool exists, bool isBridge)[] adj = null)
{
int index = 0;
// If we didn't pass cached neighbors, calculate them now
if (adj == null) adj = CheckIslandsFast(position);
// Mapping results to readable booleans
// adj index: 0=NW, 1=N, 2=NE, 3=W, 4=E, 5=SW, 6=S, 7=SE
bool nw = adj[0].exists && !adj[0].isBridge;
bool n = adj[1].exists && !adj[1].isBridge;
bool ne = adj[2].exists && !adj[2].isBridge;
bool w = adj[3].exists && !adj[3].isBridge;
bool e = adj[4].exists && !adj[4].isBridge;
bool sw = adj[5].exists && !adj[5].isBridge;
bool s = adj[6].exists && !adj[6].isBridge;
bool se = adj[7].exists && !adj[7].isBridge;
if (nw && n && w) index += 1;
if (n) index += 2;
if (ne && n && e) index += 4;
if (w) index += 8;
if (e) index += 16;
if (sw && s && w) index += 32;
if (s) index += 64;
if (se && s && e) index += 128;
return tileIndexMap.TryGetValue(index, out int mappedIndex) ? mappedIndex : -1;
}
private (bool exists, bool isBridge)[] CheckIslandsFast(Vector3Int position)
{
// Standardizes the order for the whole system:
// 0:NW, 1:N, 2:NE, 3:W, 4:E, 5:SW, 6:S, 7:SE
Vector3Int[] offsets = {
new Vector3Int(-1, 0, 1), new Vector3Int(0, 0, 1), new Vector3Int(1, 0, 1),
new Vector3Int(-1, 0, 0), new Vector3Int(1, 0, 0),
new Vector3Int(-1, 0, -1), new Vector3Int(0, 0, -1), new Vector3Int(1, 0, -1)
};
var results = new (bool, bool)[8];
for (int i = 0; i < 8; i++)
{
Vector3Int checkPos = position + (offsets[i] * islandSize);
// Important: We only consider the neighbor "Existing" if it is UNLOCKED
if (islandGrid.TryGetValue(checkPos, out Island island) && island.IsUnlocked)
{
results[i] = (true, island.IsBridge);
}
else
{
results[i] = (false, false);
}
}
return results;
}
private int GetRoadTileIndex(Vector3Int position)
{
int index = 0;
// Check N, W, E, S
if (HasRoadAt(position + new Vector3Int(0, 0, 1) * islandSize)) index += 1;
if (HasRoadAt(position + new Vector3Int(-1, 0, 0) * islandSize)) index += 2;
if (HasRoadAt(position + new Vector3Int(1, 0, 0) * islandSize)) index += 4;
if (HasRoadAt(position + new Vector3Int(0, 0, -1) * islandSize)) index += 8;
return index;
}
private bool HasRoadAt(Vector3Int pos)
{
if (islandGrid.TryGetValue(pos, out Island island))
{
// A road connects if the island is there, has a road, AND is unlocked
return island.IsUnlocked && island.HasRoad;
}
return false;
}
#endregion
#region NavMesh Management
private void RequestNavMeshUpdate()
{
if (!HasNavMesh)
{
// Immediate build for first run
navMeshSurface.BuildNavMesh();
HasNavMesh = true;
return;
}
// If an update is already pending, don't start another one
if (navMeshUpdateCoroutine != null) return;
navMeshUpdateCoroutine = StartCoroutine(UpdateNavMeshDelayed());
}
private IEnumerator UpdateNavMeshDelayed()
{
// Wait for end of frame to allow all batch updates (like loading 20 props) to finish
yield return new WaitForEndOfFrame();
var op = navMeshSurface.UpdateNavMesh(navMeshSurface.navMeshData);
yield return op;
navMeshUpdateCoroutine = null;
}
#endregion
#region Data Handling
public InventoryData LoadInventoryData(string inventoryId) => data.InventoryDatabase.FirstOrDefault(data => data.InventoryId == inventoryId);
public SiloData LoadSiloData(string siloId) => data.SiloDatabase.FirstOrDefault(data => data.SiloId == siloId);
private void LoadGameData()
{
data = SaveSystem.LoadData<GameData>(saveFileName);
if (data == null)
{
var unlockedBools = islands.Select(island => island.IsUnlocked).ToList();
data = new GameData(startingMoney, unlockedBools);
}
else
{
// Apply unlocked states
if (data.UnlockedIslands.Count == islands.Count)
{
for (int i = 0; i < islands.Count; i++)
{
islands[i].IsUnlocked = data.UnlockedIslands[i];
}
}
// Spawn Saved Purchasers
foreach (var pData in data.Purchasers)
{
if (islandGrid.TryGetValue(pData.Position, out Island island))
{
// Ensure we don't spawn purchasers on already unlocked islands (save file mismatch protection)
if (!island.IsUnlocked)
{
SpawnPurchaserFor(island, pData.Price);
}
}
}
}
}
private void SaveGameData()
{
data.UnlockedIslands = islands.Select(island => island.IsUnlocked).ToList();
data.Purchasers = Purchasers; // This list is kept in sync in Spawn/Remove methods
OnSaveGame?.Invoke(data);
SaveSystem.SaveData<GameData>(data, saveFileName);
}
#endregion
#region Helpers & Debug
public Island GetCurrentIsland(Vector3 currentPosition)
{
// Simple spatial check - Grid check would be harder here due to float-to-int conversion
// sticking to bounds check is fine for player position
foreach (var island in islands)
{
Bounds islandBounds = new Bounds((Vector3)island.GetPosition(), Vector3.one * islandSize);
if (islandBounds.Contains(currentPosition)) return island;
}
return null;
}
public Island GetRandomIsland()
{
return UnlockedIslands.Where(i => !i.IsBridge).ToList().SelectRandom();
}
public void RegisterLocation(Vector3Int position, Location location)
{
PropLocations.Add((position, location));
}
private int CalculateIslandPrice(int islandIndex)
{
return Mathf.RoundToInt(Mathf.Round(baseUnlockPrice * Mathf.Pow(unlockGrowthFactor, islandIndex)) / 5f) * 5;
}
private void Update()
{
if (SimpleInput.GetButtonDown("One Million Coins"))
{
Coin += 1000000;
AudioManager.Instance.PlaySFX(AudioID.Cash);
Debug.Log("TESTING: Added 1M Coins. REMOVE ON BUILD!");
}
}
#if UNITY_EDITOR
private void OnDrawGizmos()
{
if (!drawRoads) return;
// In Editor, islandGrid might not be populated if game isn't running
var debugIslands = GetComponentsInChildren<Island>().ToList();
foreach (var island in debugIslands)
{
if (!island.HasRoad) continue;
Vector3Int islandPos = island.GetPosition();
float radius = 0.3f;
Vector3 offset = Vector3.up * radius;
// Simple Check for Gizmos
var dirs = new[] { Vector3.forward, Vector3.left, Vector3.right, Vector3.back };
var neighbors = new[] {
islandPos + new Vector3Int(0,0,1)*islandSize,
islandPos + new Vector3Int(-1,0,0)*islandSize,
islandPos + new Vector3Int(1,0,0)*islandSize,
islandPos + new Vector3Int(0,0,-1)*islandSize
};
for (int i = 0; i < 4; i++)
{
var neighbor = debugIslands.FirstOrDefault(x => x.GetPosition() == neighbors[i]);
if (neighbor != null && neighbor.HasRoad)
{
Debug.DrawLine(islandPos + offset, islandPos + offset + dirs[i] * 4, Color.blue);
}
}
Gizmos.color = island.IsBridge ? Color.red : Color.yellow;
Gizmos.DrawWireSphere(islandPos + (island.IsBridge ? offset * 2 : offset), radius);
}
}
#endif
#endregion
}
}