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() if not NakamaManager.session: push_error("[GachaManager] Not authenticated") return [] var payload = JSON.stringify({ "banner_id": banner_id, "count": count }) var result = await BackendService.perform_gacha_pull(banner_id, count) if result.get("success", false) == false: var msg = str(result.get("error", "Unknown error")) push_error("[GachaManager] Gacha pull failed: " + msg) return [] var parsed = result.get("data", {}) if typeof(parsed) != TYPE_DICTIONARY or not parsed.has("results"): return [] var results: Array = parsed.get("results", []) if parsed.has("new_pity"): pity_counters[banner_id] = int(parsed.new_pity) if UserProfileManager.has_method("_reload_wallet"): await UserProfileManager._reload_wallet() for res in results: var item_id = res.get("id", "") var rarity = res.get("rarity", "") _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