
Godot Rpg Stats
- 239 installs
- 454 repo stars
- Updated July 28, 2026
- thedivergentai/gd-agentic-skills
Use godot-rpg-stats for development tasks
About
godot-rpg-stats: A skill for development. This provides functionality for development workflows.
- godot-rpg-stats
Godot Rpg Stats by the numbers
- 239 all-time installs (skills.sh)
- +15 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #1,579 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-rpg-statsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 239 |
|---|---|
| repo stars | ★ 454 |
| Last updated | July 28, 2026 |
| Repository | thedivergentai/gd-agentic-skills ↗ |
What it does
Use godot-rpg-stats for development tasks
Files
RPG Stats
Resource-based stats, modifier stacks, and derived calculations define flexible character progression.
Available Scripts
base_stats_resource.gd
Core data container for base attributes (Str, Dex, Int) and derived scaling rules.
status_effect_data.gd
Serialized data definition for buffs/debuffs (Additive, Multiplicative, Override).
stats_component_reactive.gd
Orchestrator for JIT (Just-In-Time) stat calculation with active modifier stacking.
exp_progression_resource.gd
Data-driven level-up curve definition using growth factors and base XP.
dynamic_stat_label_sync.gd
Reactive UI hook for syncing Labels to stat changes without polling.
damage_formula_handler.gd
Centralized RefCounted utility for complex combat math and damage calculations.
stat_modifier_stacking.gd
Logic for handling unique vs. stackable buffs and refreshing durations.
resource_stat_inheritance.gd
Pattern for extending base stats with specialized attributes (Elemental Resists).
persistent_character_stats.gd
Managing the serialization of character progression to .tres files.
level_up_system.gd
Logic for awarding experience and triggering level-up benefits.
NEVER Do in RPG Stats
- NEVER use integers for percentages —
critical_chance = 50? Integer division (e.g., in formulas) causes truncation. Always usefloat(0.0 to 1.0 or 0.0 to 100.0) [20]. - NEVER modify current_health without emitting signals — UI elements like health bars will desync if you don't broadcast changes to the system [21].
- NEVER rely solely on additive modifiers — +10 strength is huge at level 1 but negligible at level 50. Use multiplicative or hybrid scaling for balance [22].
- NEVER add modifiers without a unique ID or Key — Without a reference (e.g., "potion_buff"), you cannot remove specific effects without clearing the entire stack [23].
- NEVER use exponential XP formulas without a growth cap — Uncapped
pow()scaling quickly leads to unreachable levels or integer overflows [24]. - NEVER forget to clamp derived values — Negative vitality from a debuff could result in negative max HP, crashing your health logic. Use
maxi(val, 1)[25]. - NEVER perform heavy stat recalculations in `_process()` — Only recalculate when a modifier is added/removed or base stats change. Use the "Reactive" pattern.
- NEVER hardcode stat names in logic — Use StringNames or an Enum for attributes to prevent typos and facilitate refactoring (e.g.,
get_attribute("strength")). - NEVER store temporary "Runtime Only" buffs in a permanent Save Resource — Clear short-duration modifiers before serializing player progress to disk.
- NEVER calculate damage directly in the Character script — Centralize combat math in a
DamageFormulaclass to ensure consistency across Players and NPCs.
---
# stats.gd
class_name Stats
extends Resource
signal stat_changed(stat_name: String, old_value: float, new_value: float)
signal level_up(new_level: int)
@export var level: int = 1
@export var experience: int = 0
@export var experience_to_next_level: int = 100
# Base stats
@export var strength: int = 10
@export var dexterity: int = 10
@export var intelligence: int = 10
@export var vitality: int = 10
# Derived stats (calculated from base)
var max_health: int:
get: return vitality * 10
var attack_power: int:
get: return strength * 2
var defense: int:
get: return strength + (vitality / 2)
var magic_power: int:
get: return intelligence * 3
var critical_chance: float:
get: return dexterity * 0.01
# Modifiers
var modifiers: Dictionary = {}
func add_experience(amount: int) -> void:
experience += amount
while experience >= experience_to_next_level:
level_up_character()
func level_up_character() -> void:
level += 1
experience -= experience_to_next_level
experience_to_next_level = int(experience_to_next_level * 1.5)
# Increase base stats
strength += 2
dexterity += 2
intelligence += 2
vitality += 2
level_up.emit(level)
func get_stat(stat_name: String) -> float:
var base_value: float = get(stat_name)
var modifier_bonus := get_modifier_total(stat_name)
return base_value + modifier_bonus
func add_modifier(stat_name: String, modifier_id: String, value: float) -> void:
if not modifiers.has(stat_name):
modifiers[stat_name] = {}
modifiers[stat_name][modifier_id] = value
func remove_modifier(stat_name: String, modifier_id: String) -> void:
if modifiers.has(stat_name):
modifiers[stat_name].erase(modifier_id)
func get_modifier_total(stat_name: String) -> float:
if not modifiers.has(stat_name):
return 0.0
var total := 0.0
for value in modifiers[stat_name].values():
total += value
return totalEquipment Stats
# equipment_item.gd
extends Item
class_name EquipmentItem
@export var stat_bonuses: Dictionary = {
"strength": 5,
"dexterity": 3
}
func on_equip(stats: Stats) -> void:
for stat_name in stat_bonuses:
stats.add_modifier(stat_name, "equipment_" + id, stat_bonuses[stat_name])
func on_unequip(stats: Stats) -> void:
for stat_name in stat_bonuses:
stats.remove_modifier(stat_name, "equipment_" + id)Status Effects
# status_effect.gd
class_name StatusEffect
extends Resource
@export var effect_id: String
@export var duration: float
@export var stat_modifiers: Dictionary = {}
func apply(stats: Stats) -> void:
for stat_name in stat_modifiers:
stats.add_modifier(stat_name, "status_" + effect_id, stat_modifiers[stat_name])
func remove(stats: Stats) -> void:
for stat_name in stat_modifiers:
stats.remove_modifier(stat_name, "status_" + effect_id)Damage Calculation
func calculate_damage(attacker_stats: Stats, defender_stats: Stats) -> float:
var base_damage := float(attacker_stats.attack_power)
var defense := float(defender_stats.defense)
# Damage reduction formula
var damage := base_damage * (100.0 / (100.0 + defense))
# Critical hit
if randf() < attacker_stats.critical_chance:
damage *= 2.0
return maxf(damage, 1.0) # Minimum 1 damageSkill Requirements
# skill.gd
class_name Skill
extends Resource
@export var required_level: int = 1
@export var required_stats: Dictionary = {
"strength": 15,
"intelligence": 10
}
func can_use(stats: Stats) -> bool:
if stats.level < required_level:
return false
for stat_name in required_stats:
if stats.get_stat(stat_name) < required_stats[stat_name]:
return false
return trueBest Practices
1. Derived Stats - Calculate from base stats 2. Modifiers - Temporary/permanent bonuses 3. Formula Balance - Avoid exponential power creep
---
Elite Godot 4.x Patterns
1. Robust Stat Cap System
Encapsulate stats in a Resource and use property setters to enforce caps and prevent technical overflows.
# rpg_stat.gd
class_name RPGStat extends Resource
signal stat_changed(old_value: int, new_value: int)
@export var stat_name: String = "Strength"
@export var max_cap: int = 999
@export var current_value: int = 10:
set(value):
var old := current_value
current_value = clampi(value, 0, max_cap)
if old != current_value:
stat_changed.emit(old, current_value)2. Reactive Stat Dependency Graphs
Use the Observer pattern to handle derived stats. Instead of polling every frame, connect signals so derived stats (like Speed) only recalculate when their dependencies (like Agility) change.
# derived_stat.gd
class_name DerivedStat extends RPGStat
@export var multiplier: float = 1.0
@export var base_stat: RPGStat:
set(new_base):
if base_stat: base_stat.stat_changed.disconnect(_on_dependency_changed)
base_stat = new_base
if base_stat:
base_stat.stat_changed.connect(_on_dependency_changed)
_recalculate()
func _recalculate() -> void:
current_value = int(base_stat.current_value * multiplier)
func _on_dependency_changed(_old, _new) -> void:
_recalculate()3. Equipment Comparison UI Helper
Override _make_custom_tooltip on UI controls to generate dynamic, BBCode-formatted stat differentials when hovering over equipment.
# equipment_slot_ui.gd
func _make_custom_tooltip(_text: String) -> Object:
var container := VBoxContainer.new()
var rtf := RichTextLabel.new()
rtf.bbcode_enabled = true
var diff := hovered_stat.current_value - equipped_stat.current_value
var color := "green" if diff > 0 else "red"
var sign := "+" if diff > 0 else ""
rtf.text = "[color=%s]%s: %d (%s%d)[/color]" % [
color, hovered_stat.stat_name, hovered_stat.current_value, sign, diff
]
container.add_child(rtf)
return containerReference
- Master Skill: godot-master
# base_stats_resource.gd
# Core data container for RPG attributes
class_name BaseStats extends Resource
# EXPERT NOTE: Using a Resource for base stats allows for
# "Template" creation in the Inspector (e.g., GoblinStats, BossStats).
@export_group("Primary Attributes")
@export var strength: int = 10
@export var dexterity: int = 10
@export var intelligence: int = 10
@export_group("Derived Scaling")
@export var hp_per_strength: float = 5.0
@export var mp_per_intelligence: float = 3.0
func get_max_hp() -> int:
return int(strength * hp_per_strength)
func get_max_mp() -> int:
return int(intelligence * mp_per_intelligence)
# damage_formula_handler.gd
# Centralized logic for combat math
class_name DamageFormula extends RefCounted
# EXPERT NOTE: Move complex math out of Node scripts and into
# RefCounted classes to keep your core scripts clean.
static func calculate_damage(attacker: StatsComponent, defender: StatsComponent) -> int:
var atk = attacker.get_attribute("strength")
var def = defender.get_attribute("dexterity") # Dodge chance
# Simple formula: Atk - Def (Clamped)
var raw = atk - (def * 0.5)
return int(max(raw, 1.0))
# dynamic_stat_label_sync.gd
# UI hook for reactive stat displays
extends Label
# EXPERT NOTE: UI should listen for stats_recalculated
# rather than polling in _process.
@export var stats: StatsComponent
@export var target_attribute: String = "strength"
func _ready():
if stats:
stats.stats_recalculated.connect(_update_display)
_update_display()
func _update_display():
text = "%s: %d" % [target_attribute.capitalize(), stats.get_attribute(target_attribute)]
# exp_progression_resource.gd
# Data-driven level up curve
class_name ExpProgression extends Resource
@export var base_exp: int = 100
@export var growth_factor: float = 1.2
func get_required_exp(level: int) -> int:
return int(base_exp * pow(growth_factor, level - 1))
# level_up_system.gd
# Orchestrating level up benefits
class_name LevelUpSystem extends Node
@export var stats: StatsComponent
@export var curve: ExpProgression
var current_exp: int = 0
var level: int = 1
func add_exp(amount: int):
current_exp += amount
var req = curve.get_required_exp(level)
if current_exp >= req:
_level_up()
func _level_up():
level += 1
# Apply permanent "Level Up" modifier
var mod = StatusEffectData.new()
mod.attribute = "strength"
mod.value = 2.0
mod.duration = 0 # Permanent
stats.apply_modifier(mod)
print("LEVEL UP! Now Level ", level)
# skills/rpg-stats/code/modifier_stack_stats.gd
extends Node
## RPG Stats Expert Pattern
## Implements Reactive Updates and Additive/Multiplicative Modifiers.
signal stat_changed(stat_name: String, new_value: float)
# 1. Base Stat vs Modifier Pattern
# Expert logic: Treat Stats as Resources to allow easy sharing.
class Stat:
var base_value: float = 0.0
var modifiers_add: float = 0.0
var modifiers_mult: float = 1.0 # 1.0 = 100% (No change)
func get_total() -> float:
# Standard RPG Formula: (Base + Additions) * Multipliers
return (base_value + modifiers_add) * modifiers_mult
var stats: Dictionary = {
"strength": Stat.new(),
"agility": Stat.new(),
"max_health": Stat.new()
}
func add_modifier(stat_name: String, amount: float, is_multiplier: bool = false) -> void:
# 2. Modifier Stacking Logic
if not stats.has(stat_name): return
var stat = stats[stat_name]
if is_multiplier:
stat.modifiers_mult += amount
else:
stat.modifiers_add += amount
# 3. Reactive Stat Updates
# Notify UI components ONLY when specific stats change.
stat_changed.emit(stat_name, stat.get_total())
func get_stat(stat_name: String) -> float:
return stats[stat_name].get_total() if stats.has(stat_name) else 0.0
## EXPERT NOTE:
## Use 'Exponential XP Formulas': For leveling, use
## 'floor(100 * pow(level, 1.5))' to create a smooth difficulty curve.
## For 'rpg-stats', implement 'Derived Stat Calculation':
## 'attack_power = get_stat("strength") * 1.5 + get_stat("agility")'.
## Hook this into the 'stat_changed' signal to update child stats
## whenever a parent stat is modified.
## NEVER allow direct modification of 'max_health' variable;
## always use the 'add_modifier' protocol to prevent 'Value Leakage'.
# persistent_character_stats.gd
# Saving and loading character progression
extends Node
@export var stats: CharacterStats # Custom Resource
func save_stats():
ResourceSaver.save(stats, "user://player_stats.tres")
func load_stats():
if ResourceLoader.exists("user://player_stats.tres"):
stats = load("user://player_stats.tres")
# resource_stat_inheritance.gd
# Specialized stat containers (e.g., Elemental Resistances)
class_name ElementalStats extends BaseStats
@export var fire_res: float = 0.0
@export var ice_res: float = 0.0
func get_res(element: String) -> float:
return get(element + "_res")
# stat_modifier_stacking.gd
# Preventing modifier bloat and conflicts
extends Node
# EXPERT NOTE: Use a unique ID or Name check if you don't
# want the same buff to stack multiple times.
func apply_unique_buff(stats: StatsComponent, mod: StatusEffectData):
for existing in stats.modifiers:
if existing.name == mod.name:
# Refresh duration instead of adding new
return
stats.apply_modifier(mod)
# skills/rpg-stats/scripts/stat_resource.gd
extends Resource
## Stat Resource Expert Pattern
## Modular stat system with modifier stacks, dirty flags, and derived stat support.
class_name StatResource
signal value_changed(new_value: float, old_value: float)
signal modifier_added(modifier: StatModifier)
signal modifier_removed(modifier: StatModifier)
@export var base_value: float = 10.0:
set(v):
var old = base_value
base_value = v
_dirty = true
value_changed.emit(value, get_value_from_base(old))
var _modifiers: Array[StatModifier] = []
var _cached_value: float = 0.0
var _dirty: bool = true
var value: float:
get:
if _dirty:
_recalculate()
return _cached_value
func add_modifier(mod: StatModifier) -> void:
_modifiers.append(mod)
mod.changed.connect(_on_modifier_changed)
_dirty = true
modifier_added.emit(mod)
value_changed.emit(value, _cached_value) # Value updates lazily, but signal needs current
func remove_modifier(mod: StatModifier) -> void:
if mod in _modifiers:
_modifiers.erase(mod)
mod.changed.disconnect(_on_modifier_changed)
_dirty = true
modifier_removed.emit(mod)
value_changed.emit(value, _cached_value)
func get_value_from_base(base: float) -> float:
# Recalculate hypothetical value
var v = base
for mod in _modifiers:
if mod.type == StatModifier.Type.ADD:
v += mod.value
for mod in _modifiers:
if mod.type == StatModifier.Type.MULTIPLY:
v *= mod.value
return v
func _recalculate() -> void:
_cached_value = get_value_from_base(base_value)
_dirty = false
func _on_modifier_changed() -> void:
_dirty = true
value_changed.emit(value, _cached_value)
# Inner class for easy portability
class StatModifier extends Resource:
enum Type { ADD, MULTIPLY }
signal changed
@export var type: Type = Type.ADD:
set(v): type = v; changed.emit()
@export var value: float = 0.0:
set(v): value = v; changed.emit()
@export var source: Variant # Optional reference to source weapon/buff
## EXPERT USAGE:
## @export var strength: StatResource
## strength.add_modifier(sword_mod)
## print(strength.value)
# stats_component_reactive.gd
# Orchestrator for dynamic stat calculation
class_name StatsComponent extends Node
# EXPERT NOTE: The final value is calculated "Just-In-Time"
# to ensure all active modifiers are correctly applied.
signal stats_recalculated
signal hp_changed(current: int, max: int)
@export var base: BaseStats
var modifiers: Array[StatusEffectData] = []
var current_hp: int = 0
func _ready():
if base:
current_hp = base.get_max_hp()
func get_attribute(attr_name: String) -> float:
var val = base.get(attr_name) as float
# Apply additive modifiers first
for mod in modifiers:
if mod.attribute == attr_name and mod.type == StatusEffectData.Type.ADDITIVE:
val += mod.value
# Then apply multipliers
for mod in modifiers:
if mod.attribute == attr_name and mod.type == StatusEffectData.Type.MULTIPLICATIVE:
val *= mod.value
return val
func apply_modifier(mod: StatusEffectData):
modifiers.append(mod)
stats_recalculated.emit()
if mod.duration > 0:
get_tree().create_timer(mod.duration).timeout.connect(
func(): remove_modifier(mod)
)
func remove_modifier(mod: StatusEffectData):
modifiers.erase(mod)
stats_recalculated.emit()
# status_effect_data.gd
# Data definition for buffs and debuffs
class_name StatusEffectData extends Resource
# EXPERT NOTE: Defining effects as Resources makes them
# strictly data-driven and easy to serialize/save.
enum Type { ADDITIVE, MULTIPLICATIVE, OVERRIDE }
@export var name: String = "Effect"
@export var type: Type = Type.ADDITIVE
@export var attribute: String = "strength"
@export var value: float = 0.0
@export var duration: float = 5.0
@export var icon: Texture2D