Ideally you would write a custom property editor if you want full control over how to modify the data but you can do a lot with Object._get()
, Object._set()
and Object._get_property_list()
This is a somewhat convoluted and semi-working example. I ran out of time to fix a few issues but it can give you an idea:
@tool
class_name EnemyParty extends Resource
# This can come from another file
const ENEMY_LIST = ["Goblin", "Skeleton", "Bandit", "Orc", "Troll", "Spider"]
# A flag to find out if a property is stored to disk and it's a variable in a script
# this find the @export variables in a Object
const VAR_STORAGE = (PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_SCRIPT_VARIABLE)
# The party
var party:Dictionary = {}
# The size
var size:int = 0
func _get(property: StringName) -> Variant:
if property == "Party Size":
return size
# If it start with Party/Enemy then it's a new enemy so return an empty string
if property.begins_with("Party/Enemy "):
return ""
# If it start with Party/ then is an enemy, split the property path and get the stat value
if property.begins_with("Party/"):
var splits = property.split("/")
var key = splits[1]
var field = splits[2]
return party[key].get(field)
return null
func _set(property: StringName, value: Variant) -> bool:
if property == "Party Size":
size = value
if size == 0:
party.clear()
notify_property_list_changed()
return false
# if we are setting a value for the property that begins with Party/Enemy
# then it's a new enemy so create a CharStats with random stats and
# add it to the dictionary
if property.begins_with("Party/Enemy "):
if not party.find_key(value):
var stats = CharStats.new()
stats.strength = randi_range(5, 40)
stats.dexterity = randi_range(5, 40)
stats.luck = randi_range(5, 40)
stats.magic = randi_range(5, 40)
stats.spirit = randi_range(5, 40)
stats.vitality = randi_range(5, 40)
party[value] = stats
notify_property_list_changed()
return true
# If it begins with party then it's an enemy, set the value to the stat
if property.begins_with("Party/"):
var splits = property.split("/")
var key = splits[1]
var field = splits[2]
party[key].set(field, value)
notify_property_list_changed()
return true
return false
func _get_property_list() -> Array[Dictionary]:
var result:Array[Dictionary] = []
# The party variable, don't show it in the inspector and save it to disk
result.push_back({
"name": "party",
"type": TYPE_DICTIONARY,
"usage": PROPERTY_USAGE_NO_EDITOR,
})
# The size variable, don't show it in the inspector and save it to disk
result.push_back({
"name": "size",
"type": TYPE_INT,
"usage": PROPERTY_USAGE_NO_EDITOR,
})
# I'm using this property as a proxy to the size variable
result.push_back({
"name": "Party Size",
"type": TYPE_INT,
"hint_string": "0,15,or_greater,suffix:enemies",
"usage": PROPERTY_USAGE_EDITOR
})
# iterate over the size
for i in size:
# if i is less than the party size
if i < party.size():
# get the key from the party
var key = party.keys()[i]
# get the properties that we need to show
var properties = party[key].get_property_list().filter(func(p): return p.usage & VAR_STORAGE == VAR_STORAGE)
# for each property
for prop in properties:
# Add a property Party/<key>/<stat_name>
result.push_back({
"name": "Party/%s/%s" % [key, prop.name],
"type": prop.type,
"usage": PROPERTY_USAGE_EDITOR
})
else:
# else i is bigger than the party size so we need to show a new enemy selection
# Fiter the enemy list removing the ones that where added before
var list = ENEMY_LIST.filter(func(n): return not party.has(n))
# Push a new property Party/Enemy <index>
result.push_back({
"name": "Party/Enemy %s" % (i+1),
"type": TYPE_STRING,
"hint": PROPERTY_HINT_ENUM_SUGGESTION,
"hint_string": ",".join(list),
"usage": PROPERTY_USAGE_EDITOR
})
return result
And the CharStats
script:
class_name CharStats extends Resource
@export var strength := 0
@export var dexterity := 0
@export var vitality := 0
@export var magic := 0
@export var spirit := 0
@export var luck := 0
Result: