Funpix Kit
Inicio / Componentes / fpx · items

fpx · items

Esquema de ítem (equipo/consumible/material/quest), conjuntos con bonos por umbral, ItemCatalog por convención, RarityCatalog como datos y ConsumableEffects.

capa 1 v0.3.0 stats ↓

El ZIP incluye: addons/fpx/items, addons/fpx/stats + sus tests + data/balance/default.tres.

Qué es

El esquema de ítem como datos y sus catálogos. ItemDefinition: familias por item_type (&"equipment" ocupa slot y aporta modifiers al pipeline de stats · &"consumable" con effect/effect_amount/effect_quest · &"material" · &"quest"), stacking con tope, requisitos genéricos de RPG (nivel/tier/clase), presentación (icono, rareza, flavor, status — líneas de la sección ESTADO de la ficha —, warning — aviso libre del banner —, precio), características de arma para la ficha (weapon_ap_cost/range/uses_per_turn/crit_pct/crit_bonus + daño weapon_damage_min/max/element, solo slot &"weapon"), economía (bind/único/vendible). SetDefinition: conjuntos con bonos por umbral de piezas equipadas, acumulativos, en vocabulario GearBonus; SetCatalog los descubre por convención. FK-2 añade RarityCatalog (las rarezas como Resource), ConsumableEffects (registro abierto id → Callable) e ItemCatalog (by_id tolerante sobre SEARCH_DIRS).

Instalación

  1. Descarga fpx-items-v0.3.0.zip (botón de arriba) y extráelo en la raíz de tu proyecto Godot — deja la estructura tal cual:
text
tu-juego/
├── project.godot
├── addons/
│   └── fpx/
│       ├── items/
│       └── stats/
└── tests/          ← opcional: bórralo si no quieres los tests
  1. Abre el proyecto (o corre --import headless). Sin plugins ni autoloads que activar: las clases quedan disponibles en cualquier script.
  2. Opcional — verifica con la suite (necesita GdUnit4, ver la guía): -a tests/items.

Ejemplo de uso

Usa las fixtures incluidas en el componente para probar en segundos; en tu juego, sustitúyelas por tus datos.

items · ejemplo
# Ítems como Resource: defínelos en .tres (ItemCatalog los descubre por
# convención en res://data/items) o constrúyelos en código/fixtures.
var pocion := ItemFixtures.consumable(&"pocion", 10, &"heal", 50)
var espada := ItemFixtures.equipment(&"espada", &"weapon", {&"weapon_atk": 40.0})

# Catálogo tolerante: id ausente → null + warning único (no crashea).
var item := ItemCatalog.by_id(&"pocion")

# Efectos de consumible: registro abierto id → Callable (&"heal" viene de serie:
# cura effect_amount sin pasar de max_hp del target).
var effects := ConsumableEffects.with_defaults()
effects.register(&"antidoto", func(_it: ItemDefinition, target: Object) -> Dictionary:
	target.set("poisoned", false)
	return {"applied": true})
var result := effects.apply(pocion, hero)  # hero necesita hp / max_hp

# La ficha del ítem nace del ESQUEMA (mockup 2026-08-31): estado, aviso y los
# DOS bloques de arma —características y efectos del arma— son solo datos.
espada.status = ["Ninguna forjamagia futura"]  # sección ESTADO de la ficha
espada.warning = "Se destruye al cambiar de zona"  # banner de aviso
espada.weapon_ap_cost = 3        # características: Coste/Alcance/Usos/Crítico
espada.weapon_crit_pct = 20
espada.weapon_damage_min = 26    # EFECTOS DEL ARMA: rango + elemento
espada.weapon_damage_max = 32
espada.weapon_damage_element = &"agua"
espada.rarity = &"raro"          # la rareza SOLO tiñe el borde de la ficha

# Conjuntos con bonos por umbral: 2 piezas → +30 HP · 4 → +8 DEF, +2 STR.
var set_def := ItemFixtures.golden_set()
var equipped: Array[StringName] = [&"yelmo_dorado", &"espada_dorada"]
var bonus: GearBonus = set_def.bonus_for(set_def.pieces_equipped(equipped))

# Rarezas como datos (8 de serie = las del ejemplar; tu juego puede cargar
# su propio res://data/items/rarities.tres).
var rarities := RarityCatalog.active()
print(rarities.item_label(espada), " · rango ", rarities.item_rank(espada))
print(rarities.ids_by_rank())  # de común a única, para ordenar tooltips

Demo en vivo

El componente REAL corriendo en tu navegador — Godot 4.7 exportado a Web sobre las fixtures del kit. La primera carga baja el motor (~10 MB comprimido) y se comparte entre todas las demos.

abrir a pantalla completa ↗

API pública

ClaseQué da
ItemDefinition (Resource)campos exportados + is_equipment()/is_consumable()/is_quest_item()/is_weapon()
SetDefinition (Resource)total_pieces · pieces_equipped(ids) · active_thresholds(n) · bonus_for(n) -> GearBonus
SetCatalogload_all() desde SETS_DIR
RarityCatalog (Resource)defaults() (las 8 del ejemplar) · active()/set_active() · label_of/color_key_of/rank_of · ids_by_rank() · item_label/item_rank
ConsumableEffectswith_defaults() (heal) · register(id, Callable) · apply(item, target) -> {effect, applied, …}
ItemCatalogby_id(id) (null + warning único si falta) · load_all() · set_search_dirs() para sandbox

Firmas completas (del código)

ConsumableEffectsextends RefCountedconsumable_effects.gd

FK-2 · Registro de efectos de consumible id → Callable (plan §6.3). En Stremus el efecto se resuelve con un match item.effect dentro de UseItemAction.execute (solo heal; el resto pass). Aquí el vocabulario es ABIERTO: el kit trae heal de serie y el juego registra los suyos (grant_quest, teleport, …) sin tocar el componente. Catálogo TOLERANTE: un efecto desconocido avisa UNA vez por id (push_warning) y es no-op (applied == false) — nunca rompe. Firma del handler: func(item: ItemDefinition, target: Object) -> Dictionary. El resultado SIEMPRE lleva {"effect": StringName, "applied": bool} y lo que el handler añada (p. ej. "amount"). El target es lo que el juego quiera (una unidad de combate, el personaje, …): heal solo exige que exponga hp y max_hp (duck typing vía get/set), igual que UnitState en el ejemplar.

const OK := &""
static func with_defaults() -> ConsumableEffects

Registro con el efecto heal de serie.

func register(id: StringName, handler: Callable) -> void
func unregister(id: StringName) -> void
func has(id: StringName) -> bool
func ids() -> Array[StringName]
func apply(item: ItemDefinition, target: Object) -> Dictionary

Aplica el efecto del ítem sobre target. NO gasta stock: eso es de InventoryOps.use_consumable (separación efecto / inventario).

static func heal(item: ItemDefinition, target: Object) -> Dictionary

Efecto de serie: cura effect_amount PV sin pasar de max_hp (misma regla que UseItemAction del ejemplar). Devuelve {"amount": PV curados de verdad}.

ItemCatalogextends RefCounteditem_catalog.gd

FK-2 · Resolución de ítems por id con SEARCH_DIRS (plan §6.3, FW-4). En Stremus la ruta está clonada en GameState.ITEMS_DIR y CharCodec.ITEMS_DIR; aquí vive UNA vez, en una const de cabecera (guardia FK-0), y by_id es TOLERANTE: id desconocido → null + warning una sola vez, nunca un crash. Convención: <dir>/<id>.tres en el primer directorio de SEARCH_DIRS que lo tenga (el juego puede añadir carpetas — p. ej. un pack de DLC — sin tocar el componente). Se deja como clase aparte (y no como estático de ItemDefinition, como decía el plan) para no tocar el esquema copiado sin delta de Stremus (SYNC.md).

const SEARCH_DIRS: Array[String] = ["res://data/items"]
static func search_dirs() -> Array[String]
static func set_search_dirs(dirs: Array[String]) -> void

Para tests/sandbox: buscar en otras carpetas sin tocar la const.

static func reset_search_dirs() -> void
static func clear_cache() -> void
static func path_for(id: StringName) -> String
static func by_id(id: StringName) -> ItemDefinition
static func load_all() -> Array[ItemDefinition]

Todos los ítems de SEARCH_DIRS (orden alfabético por archivo → determinista).

ItemDefinitionextends Resourceitem_definition.gd

Ítem como datos (E1-INV, A10): añadir un ítem es crear un .tres en data/items/ — cero código. Tres familias por item_type: &"equipment" (ocupa slot y aporta modifiers al StatBlock, E1-INV-02/03), &"consumable" (efecto por 1 PA en combate, E1-INV-04) y &"material" (crafting E3+, solo ocupa inventario).

@export var id: StringName
@export var display_name: String = ""
@export var item_type: StringName = &"material"

&"equipment" | &"consumable" | &"material" | &"quest" (ED-1: objeto de misión — ocupa inventario como un material, NO es usable ni equipable; su ficha lo etiqueta "Objeto de misión" y, por DATOS, suele ir sellable=false). El vocabulario vive en el validador/UI: aquí es solo documentación.

@export var stackable: bool = true
@export var max_stack: int = 99

Tope de unidades por slot para stackeables (MOT-2, P-56 §3.3): al apilar, el excedente por encima de este tope abre un slot nuevo (Inventory.add_item). No aplica al equipo (stackable=false → 1 por hueco). Valores <=1 se tratan como 1. FLAG(MOT-2): reinstaura el tope que v7 (2026-07-10) había dejado como metadato muerto por pedido del diseñador; si economía prefiere stacks sin tope, poner max_stack muy alto por ítem — no requiere código.

@export var slot: StringName = &""

Slot del GDD: &"weapon" (a la mano libre; 2 armas o arma+escudo), &"shield" (solo off_hand), &"armor", &"helmet", &"boots", &"necklace", &"ring" (2 slots), &"belt". Las Astralitas son stub visual (E1-INV-02).

@export var modifiers: Dictionary[StringName, float] = {}

Bonos del ítem: claves del catálogo §3 (GearBonus.FIELD_KEYS) o atributos §2 — el mismo vocabulario que las pasivas del árbol (E1-INV-03).

@export var required_level: int = 1

Requisitos §8: el rebirth nunca te "des-equipa" — tier de job + nivel DENTRO del job, sin ambigüedad de nivel total.

@export var required_job_tier: int = 0
@export var required_class: StringName = &""

Clase exigida por ID (P-56 §3.2): &"" (default) = cualquier clase, como hoy; poblado (p.ej. &"swordsman") exige que la clase ACTIVA coincida por id para equipar (CharacterProgress.can_equip). El rebirth no des-equipa: el gate mira la clase activa, no el árbol acumulado — decisión de diseño futura.

@export var item_level: int = 0

Nivel del ítem (ilvl) RESERVADO (P-56 §3.2, prioridad baja): sin lógica aún; metadato para escalar precio/loot a futuro. 0 = sin definir.

@export var weapon_ap_cost: int = 0

Solo slot &"weapon"; 0 = la línea no se muestra (el bloque colapsa si todas están a 0). Coste en PA, alcance en celdas, usos por turno, crítico en % y bonus de daño al crítico (columna derecha del bloque).

@export var weapon_range: int = 0
@export var weapon_uses_per_turn: int = 0
@export var weapon_crit_pct: int = 0
@export var weapon_crit_bonus: int = 0
@export var weapon_damage_min: int = 0

Sección EFECTOS DEL ARMA de la ficha: rango min–max + elemento del vocabulario {neutral|fuego|agua|aire|tierra} (ItemFormat.WEAPON_ELEMENTS; desconocido cae a etiqueta genérica). max <= 0 = sin sección.

@export var weapon_damage_max: int = 0
@export var weapon_damage_element: StringName = &"neutral"
@export var effect: StringName = &"heal"

v1: &"heal" (curación plana) · &"grant_quest" (Questlines, plan §E2): usarlo FUERA de combate otorga la quest effect_quest; si no se puede aceptar (prerequisitos, ya activa/completada) el uso se rechaza SIN consumir stock — misma regla que la poción a vida llena (P-58). En combate los consumibles siguen vetados por la UI (P-27). Más efectos según F3.

@export var effect_amount: int = 0
@export var effect_quest: StringName = &""

Quest otorgada por &"grant_quest" (id contra data/quests).

@export var icon: Texture2D

Icono para inventario y tooltips (P-34); null = placeholder con iniciales.

@export var rarity: StringName = &"comun"

Rareza (P-56 Fase B · simplificada 2026-08-31): SOLO tiñe el borde de la ficha con el color del tier. Catálogo v2, escalafón del diseñador 2026-07-19: &"comun" (default) < &"poco_comun" < &"raro" < &"epico" < &"legendario" < &"mitico" < &"angelical" < &"trascendente". &"comun" = borde neutro (el de la ficha del mockup, tal cual).

@export var status: Array[String] = []

Líneas de la sección ESTADO de la ficha (forjamagia, nivel de mascota…): texto libre por DATOS, una línea por entrada. Vacío = sin sección.

@export var price: int = 0

Valor de referencia en oro (v1 informativo: compra/venta con la Tienda). Sin campo de peso por decisión del diseñador (2026-07-10): el inventario se limita por huecos, no por carga.

@export var bind: StringName = &"none"

Ligadura del ítem (MMO, ED-1). &"none" (default) = libre: se comercia y transfiere como hoy. &"character" = ligado al PERSONAJE: no es comerciable ni transferible — JAMÁS entra a una oferta de trade. &"account" = ligado a la CUENTA: no cruza entre cuentas de la misma persona. El rechazo del canje es server-authoritative (TradeService): un ítem con bind != &"none" no puede ofrecerse. Vocabulario cerrado {none|character|account} en el validador.

@export var unique: bool = false

Único (ED-1): 1 por personaje — el inventario RECHAZA una 2ª copia (add_item devuelve 0, igual que el hueco lleno). Un ítem único no debe apilar: el validador exige unique => max_stack <= 1 (y por tanto un solo hueco).

@export var sellable: bool = true

Vendible a la tienda NPC (ED-1): true (default) = la tienda lo compra por su precio de reventa; false = la tienda NO lo compra aunque tenga price (ProgressMutations.sell_item lo rechaza con &"not_sellable"). Los objetos de misión (item_type &"quest") ponen esto en false POR DATOS, no por código.

func is_equipment() -> bool
func is_consumable() -> bool
func is_weapon() -> bool

Arma: la única familia con características propias y EFECTOS DEL ARMA.

func is_quest_item() -> bool

Objeto de misión (ED-1): se comporta como un material (ocupa inventario, NO es usable ni equipable). El vocabulario vive en el validador y la UI.

RarityCatalogextends Resourcerarity_catalog.gd

FK-2 · Catálogo de rarezas como DATOS (plan §6.3). En Stremus las 8 rarezas viven hardcodeadas en la capa UI (scripts/ui/item_format.gd:60-69, ItemFormat.RARITIES); aquí son un Resource que el juego rellena y el kit consulta: id → {label, color_key, rank}. El ORDEN de entries es el escalafón (rank ascendente). Los defaults del esquema (defaults()) SON las 8 rarezas de producción de Stremus (molde §5 regla 3). Catálogo TOLERANTE: una rareza desconocida cae al peldaño 0 con su propio id como etiqueta y la clave de color de la primera entrada — nunca rompe. color_key es una clave SEMÁNTICA (la resuelve el tema del juego, D-FW-7); el kit no conoce colores.

const RARITIES_PATH := "res://data/items/rarities.tres"

Ruta por convención del catálogo del juego (const de cabecera, guardia FK-0).

@export var entries: Dictionary[StringName, Dictionary] = {}

id → {"label": String, "color_key": StringName, "rank": int}.

static func defaults() -> RarityCatalog

Las 8 rarezas del ejemplar (escalafón del diseñador de Stremus 2026-07-19).

static func active() -> RarityCatalog

Catálogo activo: el inyectado por set_active, si no el .tres del juego en RARITIES_PATH, si no los defaults. Mismo patrón que BalanceProfileDefinition.

static func set_active(catalog: RarityCatalog) -> void
static func reset_active() -> void
func has(id: StringName) -> bool
func label_of(id: StringName) -> String
func color_key_of(id: StringName) -> StringName
func rank_of(id: StringName) -> int
func ids_by_rank() -> Array[StringName]

Ids ordenados por rank ascendente (el escalafón, para UI/validadores).

func item_label(item: ItemDefinition) -> String

Atajos sobre un ítem (equivalentes a ItemFormat.rarity_* del ejemplar).

func item_rank(item: ItemDefinition) -> int
SetCatalogextends RefCountedset_catalog.gd

Descubridor del catálogo de conjuntos (MOT-3): escanea data/sets/ y devuelve todos los SetDefinition presentes, para que agregar un set sea SOLO soltar un .tres ahí — cero código (metodología §0 del plan de contenido). Compartido por el arranque local y el online (StarterProfile). Con el directorio vacío o ausente devuelve [] → el motor de sets queda inerte sin romper nada.

const SETS_DIR: String = "res://data/sets"
static func load_all() -> Array[SetDefinition]

Carga TODOS los SetDefinition de data/sets/ (orden alfabético por nombre de archivo → determinista, §18). Ignora rutas fuera de res:// y extensiones que no sean .tres (endurecimiento de load dinámico, godot-master).

SetDefinitionextends Resourceset_definition.gd

Conjunto de equipo como datos (MOT-3, P-56 §3.2): un set agrupa varias piezas por su id de ItemDefinition y otorga bonos escalonados según CUÁNTAS de sus piezas están equipadas a la vez (estilo RO/Dofus). Añadir un set = crear un .tres en data/sets/ — CERO código; el motor lo descubre solo (SetCatalog) y los bonos entran al mismo pipeline §3 que el equipo y el árbol. FLAG(MOT-3): DEFAULT ACUMULATIVO — con N piezas se aplican TODOS los umbrales <= N (p.ej. 2pc Y 4pc si tienes 4), no solo el mayor. Es el modelo de Ragnarok/Dofus. Si el diseñador prefiere "solo el umbral mayor", ese es el único cambio: filtrar a max(umbrales alcanzados) en bonus_for.

@export var id: StringName
@export var display_name: String = ""
@export var piece_ids: Array[StringName] = []

IDs de las piezas (ItemDefinition.id) que componen el conjunto. El motor cuenta cuántas de ESTAS están equipadas para elegir los umbrales activos. Los duplicados y los ids vacíos se ignoran al contar (piezas únicas).

@export var threshold_bonuses: Dictionary[int, Dictionary] = {}

Umbral (nº de piezas) -> modificadores a sumar, con el MISMO vocabulario que los ItemDefinition y las pasivas del árbol (GearBonus.FIELD_KEYS o atributos §2). Ej.: {2: {&"hp_flat": 30}, 4: {&"armor_def": 8, &"str": 2}}. Un umbral se activa cuando las piezas equipadas del set son >= a su clave.

func total_pieces() -> int

Nº total de piezas ÚNICAS declaradas (para el progreso de la UI).

func pieces_equipped(equipped_ids: Array[StringName]) -> int

Cuántas piezas ÚNICAS del set hay entre los ids equipados (una pieza cuenta una sola vez aunque el set la liste dos veces).

func active_thresholds(count: int) -> Array[int]

Umbrales ALCANZADOS con count piezas, ORDENADOS ascendente (para la UI).

func bonus_for(count: int) -> GearBonus

Bono AGREGADO del set con count piezas equipadas (acumulativo, ver FLAG): suma los modificadores de todos los umbrales alcanzados. Sin piezas -> bono neutro. Determinista: el orden de suma no altera el resultado (aditivo, §3).

Qué configura el juego

Sus .tres de ítems (ItemCatalog.SEARCH_DIRS, por defecto res://data/items) y sets (SetCatalog.SETS_DIR); su data/items/rarities.tres o nada (defaults = Stremus); sus efectos de consumible (effects.register(&"grant_quest", …)). Los vocabularios item_type/slot/rarezas los cierra el validador del consumidor; color_key es semántica (la resuelve su tema).

Fixture mínima

testing/item_fixtures.gd — ItemFixtures.consumable()/material()/equipment()/quest_item() y el conjunto dorado (golden_set(), 4 piezas, umbrales 2 y 4 con la forma del ejemplo documentado en SetDefinition).

item_fixtures.gdcódigo completo
item_fixtures.gd
# res://addons/fpx/items/testing/item_fixtures.gd
class_name ItemFixtures
extends RefCounted
## Fixture mínima del componente `items`: constructores EN MEMORIA de ítems por
## familia y un conjunto dorado con la forma del ejemplo documentado en
## `SetDefinition.threshold_bonuses` ({2: hp_flat 30, 4: armor_def 8 + str 2}).
## Cero `res://data`: los `.tres` reales de Stremus son el ejemplar de contenido.


static func consumable(id: StringName, max_stack: int = 10,
		effect: StringName = &"heal", amount: int = 0) -> ItemDefinition:
	var item := ItemDefinition.new()
	item.id = id
	item.display_name = String(id).capitalize()
	item.item_type = &"consumable"
	item.stackable = true
	item.max_stack = max_stack
	item.effect = effect
	item.effect_amount = amount
	return item


static func material(id: StringName, max_stack: int = 99) -> ItemDefinition:
	var item := ItemDefinition.new()
	item.id = id
	item.display_name = String(id).capitalize()
	item.item_type = &"material"
	item.stackable = true
	item.max_stack = max_stack
	return item


static func equipment(id: StringName, slot: StringName,
		modifiers: Dictionary[StringName, float] = {}) -> ItemDefinition:
	var item := ItemDefinition.new()
	item.id = id
	item.display_name = String(id).capitalize()
	item.item_type = &"equipment"
	item.stackable = false
	item.max_stack = 1
	item.slot = slot
	item.modifiers = modifiers
	return item


static func quest_item(id: StringName) -> ItemDefinition:
	var item := ItemDefinition.new()
	item.id = id
	item.display_name = String(id).capitalize()
	item.item_type = &"quest"
	item.stackable = false
	item.max_stack = 1
	item.sellable = false
	item.bind = &"pickup"
	return item


## Las 4 piezas del conjunto dorado (ids estables: son contrato del fixture).
const GOLDEN_SET_PIECES: Array[StringName] = [
	&"yelmo_dorado", &"peto_dorado", &"botas_doradas", &"espada_dorada",
]


static func golden_set_pieces() -> Array[ItemDefinition]:
	return [
		equipment(&"yelmo_dorado", &"helmet", {&"armor_def": 2.0}),
		equipment(&"peto_dorado", &"armor", {&"armor_def": 4.0}),
		equipment(&"botas_doradas", &"boots", {&"agi": 1.0}),
		equipment(&"espada_dorada", &"weapon", {&"weapon_atk": 12.0}),
	]


## Conjunto dorado: 4 piezas, umbrales 2 (+30 HP) y 4 (+8 DEF, +2 STR).
static func golden_set() -> SetDefinition:
	var set_def := SetDefinition.new()
	set_def.id = &"conjunto_dorado"
	set_def.display_name = "Conjunto dorado"
	set_def.piece_ids = GOLDEN_SET_PIECES.duplicate()
	set_def.threshold_bonuses = {
		2: {&"hp_flat": 30},
		4: {&"armor_def": 8, &"str": 2},
	}
	return set_def

Cómo se prueba

tests/items/ — item_definition_test (familias, motor de conjuntos), rarity_catalog_test (defaults = 8 rarezas, tolerancia, catálogo inyectado), consumable_effects_test (heal con tope, desconocido no-op, registro del juego), item_catalog_test (id ausente sin romper, dirs inyectables).

Ejemplar oro (Stremus)

scripts/resources/{item_definition,set_definition,set_catalog}.gd (main@0be95b4). Rarezas: scripts/ui/item_format.gd:60-69 (ItemFormat.RARITIES, escalafón del diseñador 2026-07-19) → RarityCatalog.defaults(). Efecto heal: scripts/core/combat/actions/use_item_action.gd:41-49. Rutas de ítems clonadas en GameState.ITEMS_DIR/CharCodec.ITEMS_DIR → ItemCatalog.SEARCH_DIRS.

Deuda declarada

  • FLAG-ITEMS-REQS: required_level/job_tier/class se conservan como requisitos genéricos; un juego sin jobs deja required_job_tier=0.
  • FLAG-ITEMS-BIND: bind/unique/sellable son semántica MMO con defaults neutros.
  • FLAG-I18N (D-FW-8): etiquetas en español en los defaults del catálogo de rarezas.
  • Retro-porte pendiente a Stremus: ItemFormat leyendo RarityCatalog, UseItemAction usando ConsumableEffects, ITEMS_DIR → ItemCatalog.SEARCH_DIRS.
  • Tests que se quedan en Stremus: set_definition_test (atado a CharacterProgress), patron_oro_test, validadores de contenido.
Generado el 2026-08-31 · Funpix Kit — componentes extraídos de Stremus: El Tercer Despertar (ejemplar oro) · Funpixart