class_name XPSystem extends RefCounted ## Experience and level system. Pure math, no Node dependencies. ## Level IS power. Each tier spans 5 levels. XP is capped per tier. var current_level: int = 1 var current_xp: int = 0 var _xp_to_next: int = 100 # XP required for next level (scales) # Tier configuration: each tier has a min_level, max_level (cap), and xp_per_encounter [enc1, enc2, enc3, enc4] var _tiers: Array[Dictionary] = [] ## Loads tier configuration. Each tier dict: ## { "id": int, "min_level": int, "max_level": int, "xp_per_encounter": [int, int, int, int] } func load_tiers(tier_data: Array[Dictionary]) -> void: _tiers = tier_data ## Returns the XP required to go from current_level to current_level + 1. ## Curve: early levels are cheap, later levels cost more. static func xp_for_level(level: int) -> int: # Quadratic curve: level 1->2 costs 100, scales up. # Tunable. At launch (max level 15), total XP to max ≈ 15,000-20,000. return int(80.0 + (level - 1) * 40.0 + pow(level - 1, 2) * 5.0) ## Returns the tier that a given level belongs to. func get_tier_for_level(level: int) -> Dictionary: for tier in _tiers: if level >= tier["min_level"] and level <= tier["max_level"]: return tier return {} ## Returns the tier cap for the current tier. func get_current_tier_cap() -> int: var tier := get_tier_for_level(current_level) if tier.is_empty(): return current_level return tier["max_level"] ## Awards XP from a raid clear. ## tier_id: The tier the encounter belongs to. ## encounter_index: 0-3 (which of the 4 encounters). ## Returns a result dictionary: ## { "xp_gained": int, "leveled_up": bool, "new_level": int, "tier_capped": bool } func award_xp(tier_id: int, encounter_index: int) -> Dictionary: # Check if we're at the tier cap — no XP from this tier var tier_cap := get_current_tier_cap() if current_level >= tier_cap: return {"xp_gained": 0, "leveled_up": false, "new_level": current_level, "tier_capped": true} # Find the tier and get XP for this encounter var xp_gained := 0 for tier in _tiers: if tier["id"] == tier_id: var xp_table: Array = tier["xp_per_encounter"] if encounter_index >= 0 and encounter_index < xp_table.size(): xp_gained = xp_table[encounter_index] break if xp_gained <= 0: return {"xp_gained": 0, "leveled_up": false, "new_level": current_level, "tier_capped": false} # Apply XP and check for level ups current_xp += xp_gained var leveled_up := false while current_xp >= _xp_to_next and current_level < tier_cap: current_xp -= _xp_to_next current_level += 1 leveled_up = true _xp_to_next = xp_for_level(current_level) # If we hit the cap, clear remaining XP if current_level >= tier_cap: current_xp = 0 return { "xp_gained": xp_gained, "leveled_up": leveled_up, "new_level": current_level, "tier_capped": current_level >= tier_cap } ## Returns the effective level when visiting a specific tier. ## Capped at that tier's max_level. func get_effective_level_for_tier(tier_id: int) -> int: for tier in _tiers: if tier["id"] == tier_id: return min(current_level, tier["max_level"]) return current_level ## Returns progress toward next level (0.0 to 1.0). For UI. func get_level_progress() -> float: if _xp_to_next <= 0: return 1.0 return float(current_xp) / float(_xp_to_next) ## Resets the system (new character). func reset() -> void: current_level = 1 current_xp = 0 _xp_to_next = xp_for_level(1)