extends Node ## GachaManager — Autoload singleton ## Handles pull logic, pity counter, and fragment reward dispatch. const DATA_PATH = "res://assets/data/gacha_data.json" var data: Dictionary = {} # pity_counters[banner_id] = int var pity_counters: Dictionary = {"star": 0, "gold": 0} signal pull_completed(results: Array) # Array of {id, rarity, name} func _ready() -> void: _load_data() func _load_data() -> void: var f := FileAccess.open(DATA_PATH, FileAccess.READ) if f: var parsed = JSON.parse_string(f.get_as_text()) if parsed is Dictionary: data = parsed else: push_error("[GachaManager] Could not open gacha_data.json") # ─── Public API ────────────────────────────────────────────────────────────── func pull(banner_id: String, count: int) -> Array: if data.is_empty(): _load_data() var banner: Dictionary = data.get("banners", {}).get(banner_id, {}) if banner.is_empty(): push_error("[GachaManager] Unknown banner: " + banner_id) return [] var currency: String = banner.get("currency", "star") var cost_key: String = "pull_%d_cost" % count var cost: int = banner.get(cost_key, banner.get("pull_1_cost", 999) * count) var pity_at: int = banner.get("pity_at", 90) # Deduct currency var bal: int = UserProfileManager.wallet.get(currency, 0) if bal < cost: push_warning("[GachaManager] Not enough %s (have %d need %d)" % [currency, bal, cost]) return [] UserProfileManager.wallet[currency] = bal - cost UserProfileManager.save_wallet() var results: Array = [] for _i in range(count): var pity: int = pity_counters.get(banner_id, 0) var rarity: String if pity + 1 >= pity_at: rarity = "real_prize" pity_counters[banner_id] = 0 else: rarity = _roll_rarity(banner.get("rates", {})) pity_counters[banner_id] = (pity + 1) if rarity != "real_prize" else 0 var item_id: String = _pick_from_pool(rarity, banner_id) var item_data: Dictionary = _resolve_item(item_id, rarity) results.append({ "id": item_id, "rarity": rarity, "name": item_data.get("name", item_id) }) _grant_item(item_id, rarity) pull_completed.emit(results) return results func get_pity(banner_id: String) -> int: return pity_counters.get(banner_id, 0) func get_balance(banner_id: String) -> int: var banner: Dictionary = data.get("banners", {}).get(banner_id, {}) var currency: String = banner.get("currency", "star") return UserProfileManager.wallet.get(currency, 0) # ─── Internal ───────────────────────────────────────────────────────────────── func _roll_rarity(rates: Dictionary) -> String: var roll := randf() var cumulative := 0.0 for rarity in ["real_prize", "rare", "uncommon", "common"]: cumulative += rates.get(rarity, 0.0) if roll <= cumulative: return rarity return "common" func _pick_from_pool(rarity: String, _banner_id: String) -> String: var pool: Array = data.get("pools", {}).get(rarity, []) if pool.is_empty(): return "" return pool[randi() % pool.size()] func _resolve_item(item_id: String, rarity: String) -> Dictionary: if rarity == "real_prize": return data.get("real_prize_catalog", {}).get(item_id, {}) return data.get("fragments", {}).get(item_id, {}) func _grant_item(item_id: String, rarity: String) -> void: if rarity == "real_prize": # Add skin directly to inventory if not UserProfileManager.inventory.has(item_id): UserProfileManager.inventory.append(item_id) else: # Add fragment count var frags: Dictionary = UserProfileManager.fragments frags[item_id] = frags.get(item_id, 0) + 1 UserProfileManager.fragments = frags # ─── Crafting ───────────────────────────────────────────────────────────────── func get_all_recipes() -> Dictionary: return data.get("craft_recipes", {}) func can_craft(recipe_id: String) -> bool: var recipe: Dictionary = data.get("craft_recipes", {}).get(recipe_id, {}) if recipe.is_empty(): return false var cost: Dictionary = recipe.get("cost", {}) for frag_id in cost: var needed: int = cost[frag_id] var have: int = UserProfileManager.fragments.get(frag_id, 0) if have < needed: return false return true func craft(recipe_id: String) -> bool: if not can_craft(recipe_id): return false var recipe: Dictionary = data.get("craft_recipes", {}).get(recipe_id, {}) var cost: Dictionary = recipe.get("cost", {}) for frag_id in cost: UserProfileManager.fragments[frag_id] -= cost[frag_id] var result_id: String = recipe.get("result_id", "") if not result_id.is_empty(): if not UserProfileManager.inventory.has(result_id): UserProfileManager.inventory.append(result_id) UserProfileManager.save_wallet() return true