Funpix Kit
Inicio / Componentes / fpx · inventory

fpx · inventory

Inventario por huecos con stacking y tope, paperdoll sobre EquipmentLayout declarativo e InventoryOps (equipar/usar/comprar/vender/trade todo-o-nada).

capa 2 v0.2.0 items ↓ stats ↓

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

Qué es

Inventario por huecos fijos con stacking hasta max_stack (el excedente abre hueco nuevo; add_item devuelve cuántas unidades ENTRARON — 0 = lleno y el llamador no pierde el ítem), ítems únicos, normalize() para curar saves, consume() sin mutación parcial. Equipment: paperdoll sobre un EquipmentLayout (huecos, qué admite cada uno, exclusiones declarativas), motivos de rechazo tipados y agregado gear_bonus() hacia stats. InventoryOps (FK-3): las operaciones compartidas cliente/servidor del ejemplar sobre (Inventory, Equipment) — equipar desde inventario con reversión, desequipar, usar consumible, destruir, comprar/vender, canje P2P todo-o-nada — con el gate de requisitos inyectado.

Instalación

  1. Descarga fpx-inventory-v0.2.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/
│       ├── inventory/
│       ├── 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/inventory.

Ejemplo de uso

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

inventory · ejemplo
# Mochila por huecos con stacking (capacidad paramétrica) + paperdoll declarativo.
var inv := Inventory.new(30)
var eq := Equipment.new()  # sin argumentos = los 9 huecos del ejemplar

var espada := ItemFixtures.equipment(&"espada", &"weapon", {&"weapon_atk": 40.0})
var pocion := ItemFixtures.consumable(&"pocion", 10, &"heal", 50)
inv.add_item(espada)
var entraron := inv.add_item(pocion, 25)  # 25 uds → 3 huecos (tope 10)
# add_item devuelve cuántas unidades ENTRARON; 0 = lleno y NO pierdes el ítem.

# Equipar desde el inventario. El gate de requisitos se INYECTA (tu regla de
# nivel/clase); lo reemplazado vuelve a la mochila y si no cabe se REVIERTE todo.
var can_equip := func(it: ItemDefinition) -> StringName:
	return InventoryOps.OK if it.required_level <= 10 else &"low_level"
var res := InventoryOps.equip_from_inventory(inv, eq, espada, &"", can_equip)
if res["reason"] == InventoryOps.OK:
	var gear: GearBonus = eq.gear_bonus()  # → StatSheet.set_source(&"equipment", gear)

# Usar un consumible: gasta UNA unidad y aplica el efecto registrado.
var used := InventoryOps.use_consumable(inv, pocion, ConsumableEffects.with_defaults(), hero)

# Comprar/vender: InventoryOps mueve SOLO ítems; el ORO lo mueve el llamador
# cuando reason == OK (patrón P-49: mismo código en cliente y servidor).
var buy := InventoryOps.buy_item(inv, pocion, hero_gold)
if buy["reason"] == InventoryOps.OK:
	hero_gold -= buy["cost"]

# El paperdoll es DATOS: un juego con 3 huecos define su propio layout.
var layout := EquipmentLayout.new()
layout.slots = [&"weapon", &"armor", &"ring"]
layout.accepts = {&"weapon": [&"weapon"], &"armor": [&"armor"], &"ring": [&"ring"]}
var eq_mini := Equipment.new(layout)

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
Inventorynew(capacity = 100) · add_item(item, n) -> entraron · consume(id, n) -> bool · count_of · is_full · used_slots · normalize · capacity
EquipmentLayout (Resource)default_layout() (los 9 huecos del ejemplar) · slots · accepts · exclusions · candidates_for(item_slot) · slot_accepts · exclusion_reason
Equipmentnew(layout = default) · equip(item, target = &"") -> {reason, slot, replaced} · unequip(slot) · slot_for · accepts · gear_bonus() · equipped_ids()
InventoryOpsequip_from_inventory(inv, eq, item, target, can_equip) · unequip_to_inventory · use_consumable(inv, item, effects, target) · destroy_item · sell_price · buy_item(inv, item, gold) · sell_item · trade_apply(a, gold_a, offer_a, b, gold_b, offer_b)

Patrón P-49 del ejemplar: InventoryOps mueve SOLO inventario/equipo; el oro lo mueve el llamador cuando reason == OK.

Firmas completas (del código)

Equipmentextends RefCountedequipment.gd

Slots de equipo del GDD (E1-INV-02): arma + escudo o 2 armas, armadura, casco, botas, necklace, 2 anillos y belt. Los 6 slots de Astralitas son stub VISUAL en la UI (deshabilitados) y ningún sistema los lee. Núcleo puro (A5): los requisitos §8 los valida CharacterProgress.can_equip y el flujo con inventario/oro vive en GameState.

const OK: StringName = &""
const SLOTS: Array[StringName] = …
const SLOT_ACCEPTS: Dictionary[StringName, Array] = …

Qué TIPO de pieza (ItemDefinition.slot) admite cada hueco del paperdoll: armas a las dos manos (2 armas es legal, GDD), escudo SOLO a la secundaria, anillos a cualquiera de sus dos huecos, el resto al suyo.

var equipped: Dictionary[StringName, ItemDefinition] = {}
var layout: EquipmentLayout = null

DELTA kit-first (FK-3, plan §6.4 acople 2): el paperdoll es DATOS. Sin argumento, EquipmentLayout.default_layout() == las const SLOTS/SLOT_ACCEPTS de arriba (que se conservan como referencia y para slot_accepts estático): comportamiento idéntico a Stremus. Anotado en SYNC.md.

Equipment.new(paperdoll: EquipmentLayout = null)

Constructor.

static func slot_accepts(slot: StringName, item: ItemDefinition) -> bool

true si la pieza puede vivir en ESE hueco del layout POR DEFECTO (v6: los drops del paperdoll validan el destino — el arma no entra al casco). Para un layout propio usa accepts(slot, item) de la instancia.

func accepts(slot: StringName, item: ItemDefinition) -> bool

slot_accepts sobre el layout de ESTA instancia.

func slot_for(item: ItemDefinition) -> StringName

Slot destino para un ítem: el primer hueco VACÍO que lo admite, en el orden del layout; si todos están llenos, el primero (reemplaza). Con el layout por defecto: armas a la mano libre (principal primero), escudo SOLO a off_hand, anillos al primer hueco; el resto a su slot — idéntico al ejemplar.

func equip(item: ItemDefinition, target: StringName = &"") -> Dictionary

Equipa y devuelve {"reason": OK|motivo, "slot": StringName, "replaced": ItemDefinition|null}. target fija el hueco EXACTO (drop del paperdoll, v6) y se valida con slot_accepts; vacío = el natural de slot_for. Regla GDD (E1-INV-02): un escudo con DOS armas equipadas se rechaza con motivo — o arma+escudo o 2 armas.

func unequip(slot: StringName) -> ItemDefinition

Desequipa el slot y devuelve el ítem (null si estaba vacío).

func gear_bonus() -> GearBonus

Agregado de bonos de TODO el equipo (E1-INV-03): mismo vocabulario que las pasivas del árbol; entra al pipeline §3 como gear.

func equipped_ids() -> Array[StringName]

IDs de las piezas equipadas (MOT-3): las lee el motor de conjuntos para contar cuántas piezas de cada SetDefinition están puestas.

EquipmentLayoutextends Resourceequipment_layout.gd

FK-3 · El paperdoll como DATOS (plan §6.4): qué huecos hay, qué tipo de pieza (ItemDefinition.slot) admite cada uno y las reglas de exclusión declarativas. En Stremus esto son las const SLOTS/SLOT_ACCEPTS y la regla «arma+escudo o 2 armas» codificada en Equipment.equip. Los defaults del esquema (default_layout()) SON los 9 huecos de producción de Stremus. Destino natural de una pieza (candidates_for): los huecos que la admiten, EN EL ORDEN de slots — el primero vacío gana; si todos están llenos, el primero (reemplaza). Con el layout de Stremus reproduce exactamente a Equipment.slot_for (arma → principal, luego secundaria; anillo → ring_1, luego ring_2). Exclusión: {"item_slot": tipo de la pieza que se rechaza, "when_filled_with": {hueco: tipo, …} (TODOS deben cumplirse), "reason": motivo tipado}.

const LAYOUT_PATH := "res://data/inventory/equipment_layout.tres"

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

@export var slots: Array[StringName] = []

Huecos del paperdoll, en orden de preferencia.

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

hueco → tipos de pieza (ItemDefinition.slot) que admite.

@export var exclusions: Array[Dictionary] = []

Reglas de exclusión declarativas (ver cabecera).

static func default_layout() -> EquipmentLayout

Los 9 huecos de Stremus: armas a las dos manos (2 armas es legal), escudo SOLO a la secundaria, anillos a cualquiera de sus dos huecos, el resto al suyo; escudo rechazado con DOS armas puestas (&"two_weapons").

func has_slot(slot: StringName) -> bool
func slot_accepts(slot: StringName, item: ItemDefinition) -> bool
func candidates_for(item_slot: StringName) -> Array[StringName]

Huecos que admiten el tipo de pieza, en el orden de slots.

func exclusion_reason(item: ItemDefinition, equipped: Dictionary) -> StringName

Motivo de exclusión para equipar item con equipped puesto, o OK (&"").

Inventoryextends RefCountedinventory.gd

Inventario con stacking (E1-INV-01, GDD; MOT-2 P-56 §3.3): los consumibles y materiales apilan hasta ItemDefinition.max_stack unidades por slot; el excedente abre un slot nuevo. El equipo no apila (1 por hueco). Capacidad 100 huecos (2 páginas de 50 en la UI). Núcleo puro (A5): la UI y el pickup viven fuera; add_item devuelve cuántas unidades ENTRARON — si es 0, el pickup falla y el ítem NO desaparece (feedback del llamador). FLAG(MOT-2): reinstaura el tope max_stack que v7 (2026-07-10) había dejado sin uso (stacks sin tope, pedido del diseñador). Un stack sin tope se logra con datos (max_stack alto) — cero código. Ratificación async del diseñador.

const CAPACITY: int = 100

Capacidad por defecto (la del ejemplar). DELTA kit-first (FK-3, plan §6.4 acople 1): la capacidad es parámetro de _init; sin argumento, idéntico a Stremus. Anotado en SYNC.md; retro-portar cuando toque.

var slots: Array[Dictionary] = []

Slots fijos: cada entrada es {} (vacío) o {"item": ItemDefinition, "count": int}.

var capacity: int = CAPACITY

Nº de huecos de ESTE inventario (== slots.size()).

Inventory.new(slot_count: int = CAPACITY)

Constructor.

func add_item(item: ItemDefinition, count: int = 1) -> int

Añade hasta count unidades respetando max_stack. Devuelve las que entraron (0 = lleno). El excedente por encima del tope de un slot abre un hueco nuevo (MOT-2, P-56 §3.3).

func normalize() -> void

Consolida stacks duplicados del mismo ítem y los reparte respetando max_stack (MOT-2): cura saves con stacks partidos y, si un save antiguo dejó un stack por encima del tope actual, lo divide en huecos nuevos. El equipo (no stackeable) conserva su slot; los liberados quedan vacíos.

func count_of(item_id: StringName) -> int
func consume(item_id: StringName, count: int = 1) -> bool

Consume count unidades (false si no hay stock suficiente; sin mutación).

func is_full() -> bool
func used_slots() -> int
InventoryOpsextends RefCountedinventory_ops.gd

FK-3 · Operaciones de inventario+equipo (plan §6.4): la mitad de INVENTARIO de ProgressMutations de Stremus (scripts/core/progression/progress_mutations.gd) portada sobre (Inventory, Equipment) en vez de CharacterProgress. Mismos motivos tipados, mismas reversiones, cero drift: el gate de requisitos §8 (nivel/job/clase) es de progression y entra INYECTADO como can_equip. Patrón P-49 del ejemplar: estas funciones mueven SOLO inventario/equipo; el ORO lo mueve el llamador cuando reason == OK (así el server asienta su ledger y el cliente su GameState con el mismo código).

const OK := &""
static func accept_all(_item: ItemDefinition) -> StringName

Gate por defecto: sin requisitos (un juego sin niveles/jobs no inyecta nada).

static func equip_from_inventory(inventory: Inventory, equipment: Equipment, item: ItemDefinition, target: StringName = &"", can_equip: Callable = Callable()) -> Dictionary

Equipa DESDE el inventario validando can_equip y las reglas del layout; lo reemplazado vuelve al inventario y si no cabe se REVIERTE la operación completa. Devuelve {"reason": OK|motivo, "slot": StringName}.

static func unequip_to_inventory(inventory: Inventory, equipment: Equipment, slot: StringName) -> StringName

Desequipa un hueco devolviendo la pieza al inventario (falla si está lleno: la pieza NO desaparece, sigue equipada).

static func destroy_item(inventory: Inventory, item: ItemDefinition) -> int

Destruye TODO el stock de un ítem. Devuelve cuántas unidades se destruyeron.

static func use_consumable(inventory: Inventory, item: ItemDefinition, effects: ConsumableEffects = null, target: Object = null) -> Dictionary

Usa un consumible fuera de combate: gasta UNA unidad y, si hay registro de efectos, lo aplica sobre target. Sin registro = solo gasta stock (v1 del ejemplar). Devuelve {"reason": OK|motivo, "result": Dictionary del efecto}.

static func sell_price(item: ItemDefinition) -> int

Precio de VENTA a NPC (P-49 v1 del ejemplar): mitad del catálogo, suelo 1; price 0 = invendible.

static func buy_item(inventory: Inventory, item: ItemDefinition, gold: int) -> Dictionary

Compra a NPC: valida precio/oro/hueco y aplica SOLO la parte de inventario. Devuelve {"reason": OK|motivo, "cost": int}.

static func sell_item(inventory: Inventory, item: ItemDefinition) -> Dictionary

Venta a NPC: consume UNA unidad. Devuelve {"reason": OK|motivo, "gain": int}.

static func trade_apply(a: Inventory, gold_a: int, offer_a: Dictionary, b: Inventory, gold_b: int, offer_b: Dictionary) -> Dictionary

Canje P2P (escrow): valida TODO (oro/stock/hueco de AMBOS lados) y aplica el intercambio COMPLETO o nada. offer_x = {"gold": int, "items": [{"item": ItemDefinition, "count": int}]}. Devuelve {"reason", "side": &"a"|&"b"|&""}.

Qué configura el juego

La capacidad (Inventory.new(n)), su data/inventory/equipment_layout.tres (EquipmentLayout.LAYOUT_PATH) o un layout en memoria, el gate can_equip: Callable (nivel/job/clase: es de progression), y el registro ConsumableEffects.

Fixture mínima

testing/inventory_fixtures.gd — InventoryFixtures.starter_bag(capacity), canonical_paperdoll() (arma +40 → atk_melee == 150), golden_set_paperdoll(), three_slot_layout() (arma/armadura/anillo).

inventory_fixtures.gdcódigo completo
inventory_fixtures.gd
# res://addons/fpx/inventory/testing/inventory_fixtures.gd
class_name InventoryFixtures
extends RefCounted
## Fixture mínima del componente `inventory`: una mochila de inicio y un
## paperdoll equipado, ambos EN MEMORIA sobre los ítems de `ItemFixtures`, más
## el layout de 3 huecos del plan (arma/armadura/anillo) para probar que el
## paperdoll es datos. El paperdoll canónico reproduce el ejemplo de `stats`
## (arma +40) para verificar equipo → GearBonus → DerivedStats de punta a punta.


## Mochila de inicio: 25 pociones (3 huecos a tope 10) + 5 hierbas.
static func starter_bag(capacity: int = Inventory.CAPACITY) -> Inventory:
	var inv := Inventory.new(capacity)
	inv.add_item(ItemFixtures.consumable(&"pocion", 10, &"heal", 50), 25)
	inv.add_item(ItemFixtures.material(&"hierba"), 5)
	return inv


## Paperdoll (layout por defecto) con espada (+40 ATK de arma) y peto (+4 DEF).
static func canonical_paperdoll() -> Equipment:
	var eq := Equipment.new()
	eq.equip(ItemFixtures.equipment(&"espada", &"weapon", {&"weapon_atk": 40.0}))
	eq.equip(ItemFixtures.equipment(&"peto", &"armor", {&"armor_def": 4.0}))
	return eq


## Paperdoll con las 4 piezas del conjunto dorado puestas.
static func golden_set_paperdoll() -> Equipment:
	var eq := Equipment.new()
	for piece: ItemDefinition in ItemFixtures.golden_set_pieces():
		eq.equip(piece)
	return eq


## Layout mínimo del plan §6.4: 3 huecos (arma / armadura / anillo), sin exclusiones.
static func three_slot_layout() -> EquipmentLayout:
	var layout := EquipmentLayout.new()
	layout.slots = [&"weapon", &"armor", &"ring"]
	layout.accepts = {
		&"weapon": [&"weapon"],
		&"armor": [&"armor"],
		&"ring": [&"ring"],
	}
	return layout

Cómo se prueba

tests/inventory/ — inventory_kit_test (stacking, único, slots, gear → stats), equipment_layout_test (el default == ejemplar; layout de 3 huecos; exclusiones), inventory_ops_test (stock, gate inyectado, reemplazo con inventario lleno, desequipar sin perder, consumible + efecto, compra/venta, trade todo-o-nada y reversión).

Ejemplar oro (Stremus)

scripts/core/progression/{inventory,equipment}.gd y la mitad de inventario de progress_mutations.gd (main@0be95b4). Los 9 huecos y la regla «arma+escudo o 2 armas» de Equipment.SLOTS/SLOT_ACCEPTS/equip = EquipmentLayout.default_layout(). Decisión 2026-07-10: se limita por huecos, no por peso.

Deuda declarada

  • FLAG-INV-WEIGHT: sin peso; un juego con peso añade campo al ítem + regla en InventoryOps.
  • FLAG-INV-DROPS-LOST (P-30): drops que no caben se pierden — política del juego, la aplica su RewardFlow.
  • FLAG-INV-UNIQUE-EQUIPPED (ED-1): una copia equipada es la excepción aceptada del «único».
  • Deltas kit-first anotados en SYNC.md (Inventory._init(capacity), Equipment sobre layout): retro-portar a Stremus.
  • Tests que se quedan en Stremus: batería completa de inventory_test (CharacterProgress), trade_mutations_test, shop_mutations_test, hotbar_test.
Generado el 2026-08-31 · Funpix Kit — componentes extraídos de Stremus: El Tercer Despertar (ejemplar oro) · Funpixart