class_name UnlockDataResource extends Resource @export var unlock_id: int = 0 @export var unlock_name: String = "" @export var unlock_description: String = "" @export var base_cost: int = 0 @export var is_unlocked: bool = false @export_group("Scaling Settings") @export var is_scaling: bool = false @export var current_rank: int = 0 @export var max_rank: int = -1 # -1 means infinite scaling @export_subgroup("Cost Scaling") @export_enum("Linear", "Exponential") var cost_scaling_type: int = 1 # Default to Exponential @export var cost_scaling_multiplier: float = 1.5 # Exponential: multiplier per rank, Linear: flat increase @export var cost_linear_increase: int = 100 # Only used if cost_scaling_type is Linear @export var cost_ladder: Array[int] = [] # Fixed costs per rank (overrides scaling if defined) @export_subgroup("Effect Scaling") @export_enum("Linear", "Exponential") var effect_scaling_type: int = 1 # Default to Exponential @export var effect_scaling_multiplier: float = 1.2 # Exponential: multiplier per rank @export var effect_linear_increase: float = 0.1 # Linear: flat increase per rank (e.g., 0.1 = +10% per rank) @export_group("Base Modifiers") @export var base_modifiers: Dictionary = {} ## Returns the cost for the next rank/unlock func get_next_cost() -> int: if not is_scaling: return base_cost # Use fixed cost ladder if defined (overrides scaling) if cost_ladder.size() > 0 and current_rank < cost_ladder.size(): return cost_ladder[current_rank] # Fallback to scaling formulas for backwards compatibility # Cost scaling should start from rank 0 (first purchase at base_cost) if cost_scaling_type == 0: # Linear return base_cost + (cost_linear_increase * current_rank) else: # Exponential return int(base_cost * pow(cost_scaling_multiplier, current_rank)) ## Returns the current modifiers based on rank func get_current_modifiers() -> Dictionary: # If not unlocked yet, return empty modifiers if not is_unlocked or current_rank == 0: return {} # Rank 1 should give base modifiers without scaling if current_rank == 1: return base_modifiers.duplicate() # Rank 2+ applies scaling return get_modifiers_at_rank(current_rank) ## Returns modifiers for a specific rank (useful for preview) func get_modifiers_at_rank(rank: int) -> Dictionary: if not is_scaling or rank == 0: return base_modifiers.duplicate() # Rank 1 returns base modifiers without scaling if rank == 1: return base_modifiers.duplicate() var scaled_modifiers = {} for key in base_modifiers.keys(): var base_value = base_modifiers[key] if effect_scaling_type == 0: # Linear scaling # Linear: add flat increase per rank above 1 # e.g., base 1.5 (+50%), linear 0.1 (+10%), rank 2 = 1.6 (+60%), rank 3 = 1.7 (+70%) var additional_ranks = rank - 1 scaled_modifiers[key] = base_value + (effect_linear_increase * additional_ranks) else: # Exponential scaling # Exponential: scale the bonus part from rank 1 # The bonus at rank 1 is (base_value - 1.0) # At higher ranks, multiply this bonus by multiplier^(rank-1) var base_bonus = base_value - 1.0 var scaled_bonus = base_bonus * pow(effect_scaling_multiplier, rank - 1) scaled_modifiers[key] = 1.0 + scaled_bonus return scaled_modifiers ## Convert a modifier value to a percentage string func _modifier_to_percentage(value: float) -> String: var percentage = (value - 1.0) * 100.0 if percentage >= 0: return "+%.1f%%" % percentage else: return "%.1f%%" % percentage ## Get a formatted string of current modifiers with percentages func get_current_modifiers_string() -> String: var modifiers = get_current_modifiers() if modifiers.is_empty(): return "No modifiers" var lines = [] for key in modifiers.keys(): var display_name = key.replace("_", " ").capitalize() var percentage = _modifier_to_percentage(modifiers[key]) lines.append("%s: %s" % [display_name, percentage]) return "\n".join(lines) ## Get a formatted string of next rank modifiers with percentages func get_next_modifiers_string() -> String: if not can_rank_up(): return "Max rank reached" var next_rank = get_next_rank() var modifiers = get_modifiers_at_rank(next_rank) if modifiers.is_empty(): return "No modifiers" var lines = [] for key in modifiers.keys(): var display_name = key.replace("_", " ").capitalize() var percentage = _modifier_to_percentage(modifiers[key]) lines.append("%s: %s" % [display_name, percentage]) return "\n".join(lines) ## Get comparison string showing current -> next modifiers func get_modifiers_comparison_string() -> String: if not can_rank_up(): return get_current_modifiers_string() var current = get_current_modifiers() var next_rank = get_next_rank() var next = get_modifiers_at_rank(next_rank) # If no current modifiers (not unlocked yet), show next only if current.is_empty(): var lines = [] for key in next.keys(): var display_name = key.replace("_", " ").capitalize() var next_pct = _modifier_to_percentage(next[key]) lines.append("%s: %s" % [display_name, next_pct]) return "\n".join(lines) var lines = [] for key in current.keys(): var display_name = key.replace("_", " ").capitalize() var current_pct = _modifier_to_percentage(current[key]) var next_pct = _modifier_to_percentage(next[key]) lines.append("%s: %s → %s" % [display_name, current_pct, next_pct]) return "\n".join(lines) ## Get a single-line summary of modifiers func get_modifiers_summary() -> String: var modifiers = get_current_modifiers() if modifiers.is_empty(): return "No effects" var parts = [] for key in modifiers.keys(): var display_name = key.replace("_modifier", "").replace("_", " ").capitalize() var percentage = _modifier_to_percentage(modifiers[key]) parts.append("%s %s" % [display_name, percentage]) return ", ".join(parts) ## Check if the unlock can be purchased/ranked up func can_rank_up() -> bool: # Non-scaling: can only unlock once if not is_scaling: return not is_unlocked # Scaling: check if we haven't hit max rank if max_rank > 0 and current_rank >= max_rank: return false return true ## Check if this is a one-off unlock (non-scaling) func is_one_off() -> bool: return not is_scaling ## Check if max rank has been reached (for scaling unlocks) func is_max_rank() -> bool: if not is_scaling: return is_unlocked if max_rank <= 0: # Infinite scaling return false return current_rank >= max_rank ## Unlock or rank up the unlock func unlock() -> bool: if not can_rank_up(): return false if not is_scaling: # Simple one-off unlock is_unlocked = true current_rank = 1 else: # Scaling unlock - increase rank current_rank += 1 is_unlocked = true return true ## Get the next rank number (for display purposes) func get_next_rank() -> int: if not is_scaling: return 1 if not is_unlocked else 1 return current_rank + 1 ## Get display text for current status func get_rank_display() -> String: if not is_scaling: return "Unlocked" if is_unlocked else "Locked" if max_rank > 0: return "Rank %d/%d" % [current_rank, max_rank] else: return "Rank %d" % current_rank