Saltearse al contenido

Tiendas de NPCs y Comercio

Objetivo

Crear un Mercader Feran Longtooth — un NPC pasivo que ofrece intercambios de trueque, cambiando Enchanted Fruit por Enchanted Saplings y bloques Crystal Glow. Construirás la definición de la tienda, configurarás el rol del NPC con lógica de interacción, y conectarás todo para que los jugadores puedan hacer clic derecho en el NPC para comerciar.

Lo que Aprenderás

  • Cómo las definiciones de tiendas de trueque controlan los inventarios de comercio de NPCs
  • Cómo los espacios de comercio Fixed ofrecen intercambios siempre disponibles
  • Cómo los espacios de comercio Pool crean stock rotativo aleatorio
  • Cómo InteractionInstruction con OpenBarterShop conecta la tienda a un NPC
  • Cómo Stock, RefreshInterval y RestockHour gestionan los resets de inventario

Requisitos Previos

Repositorio del mod complementario: hytale-mods-custom-shop


Descripción General de la Tienda de Trueque

Las tiendas de trueque se encuentran en Server/BarterShops/ y definen lo que un NPC mercader vende. Hytale usa un sistema de trueque — los jugadores intercambian objetos directamente por otros objetos, no hay moneda.

Cada intercambio tiene un Input (lo que el jugador paga) y un Output (lo que el jugador recibe). El juego vanilla incluye dos mercaderes:

  • Kweebec Merchant — 3 intercambios fijos + 2 grupos pool con stock rotativo, se reabastece cada 3 días
  • Klops Merchant — 1 intercambio fijo, se reabastece diariamente

Paso 1: Configurar la Estructura de Archivos del Mod

NPCShopsAndTrading/
├── manifest.json
├── Server/
│ ├── BarterShops/
│ │ └── Feran_Enchanted_Merchant.json
│ ├── NPC/
│ │ └── Roles/
│ │ └── Feran_Enchanted_Merchant.json
│ └── Languages/
│ ├── en-US/
│ │ └── server.lang
│ ├── es/
│ │ └── server.lang
│ └── pt-BR/
│ └── server.lang

manifest.json

{
"Group": "HytaleModdingManual",
"Name": "NPCShopsAndTrading",
"Version": "1.0.0",
"Description": "Feran Longtooth merchant that trades Enchanted Fruit for Enchanted Saplings",
"Authors": [
{
"Name": "HytaleModdingManual"
}
],
"Dependencies": {},
"OptionalDependencies": {},
"IncludesAssetPack": false,
"TargetServerVersion": "2026.02.19-1a311a592"
}

Ten en cuenta que IncludesAssetPack es false — este mod solo agrega archivos JSON del lado del servidor. El modelo Feran Longtooth ya existe en el juego vanilla, así que no necesitamos una carpeta Common/.


Paso 2: Crear la Definición de la Tienda de Trueque

La definición de la tienda controla qué intercambios aparecen en la interfaz cuando el jugador interactúa con el mercader.

Crea Server/BarterShops/Feran_Enchanted_Merchant.json:

{
"DisplayNameKey": "server.barter.feran_enchanted_merchant.title",
"RefreshInterval": {
"Days": 2
},
"RestockHour": 6,
"TradeSlots": [
{
"Type": "Fixed",
"Trade": {
"Output": { "ItemId": "Plant_Sapling_Enchanted", "Quantity": 1 },
"Input": [{ "ItemId": "Plant_Fruit_Enchanted", "Quantity": 3 }],
"Stock": 5
}
},
{
"Type": "Fixed",
"Trade": {
"Output": { "ItemId": "Ore_Crystal_Glow", "Quantity": 1 },
"Input": [{ "ItemId": "Plant_Fruit_Enchanted", "Quantity": 10 }],
"Stock": 3
}
}
]
}

Campos de la Tienda

CampoPropósito
DisplayNameKeyClave de traducción para el título de la tienda mostrado en la interfaz de comercio
RefreshInterval.DaysNúmero de días del juego entre reabastecimientos de stock
RestockHourHora del día (0-24) cuando ocurre el reabastecimiento. 6 = 6 AM
TradeSlotsArray de definiciones de espacios de comercio (Fixed o Pool)

Esta tienda tiene dos intercambios fijos:

IntercambioInputOutputStock
Retoño3 Enchanted Fruit1 Enchanted Sapling5 por reabastecimiento
Cristal10 Enchanted Fruit1 Crystal Glow block3 por reabastecimiento

El bloque Crystal Glow es más caro (10 frutas vs 3) y tiene menor stock, haciéndolo un intercambio premium.


Paso 3: Entendiendo los Tipos de Espacios de Comercio

Espacios Fijos

Los espacios fijos siempre aparecen en la tienda y ofrecen el mismo intercambio:

{
"Type": "Fixed",
"Trade": {
"Output": { "ItemId": "Plant_Sapling_Enchanted", "Quantity": 1 },
"Input": [{ "ItemId": "Plant_Fruit_Enchanted", "Quantity": 3 }],
"Stock": 5
}
}
CampoPropósito
Trade.OutputEl objeto y cantidad que el jugador recibe
Trade.InputArray de objetos que el jugador debe pagar. Múltiples entradas requieren todos los objetos
Trade.StockNúmero de veces que este intercambio puede completarse antes del reabastecimiento

Espacios Pool

Los espacios pool seleccionan aleatoriamente intercambios de un pool más grande en cada reabastecimiento, creando stock rotativo. El Kweebec_Merchant vanilla usa este patrón:

{
"Type": "Pool",
"SlotCount": 3,
"Trades": [
{
"Weight": 50,
"Output": { "ItemId": "Food_Salad_Fruit", "Quantity": 2 },
"Input": [{ "ItemId": "Ingredient_Life_Essence", "Quantity": 20 }],
"Stock": [4, 8]
},
{
"Weight": 20,
"Output": { "ItemId": "Recipe_Food_Pie_Apple", "Quantity": 1 },
"Input": [{ "ItemId": "Ingredient_Life_Essence_Concentrated", "Quantity": 2 }],
"Stock": [1]
}
]
}
CampoPropósito
SlotCountNúmero de intercambios seleccionados aleatoriamente del pool en cada reabastecimiento
Trades[].WeightProbabilidad relativa de que este intercambio aparezca. Mayor = más probable
Trades[].StockFormato array: [fijo] para stock exacto o [min, max] para rango aleatorio

La diferencia con los espacios fijos: el Stock de pool usa un array ([4, 8] significa 4-8 unidades), mientras que el Stock fijo usa un número (5 significa exactamente 5).


Paso 4: Crear el Rol de NPC Mercader

Este es el paso más importante. Los mercaderes vanilla usan un NPC Type: "Generic" con InteractionInstruction que abre la tienda de trueque cuando el jugador hace clic derecho. Esto es muy diferente de los NPCs de combate que usan Variant + Reference.

Crea Server/NPC/Roles/Feran_Enchanted_Merchant.json:

{
"Type": "Generic",
"Parameters": {
"NameTranslationKey": {
"Value": "server.npcRoles.Feran_Enchanted_Merchant.name",
"Description": "Translation key for NPC name display"
}
},
"StartState": "Idle",
"DefaultNPCAttitude": "Ignore",
"DefaultPlayerAttitude": "Neutral",
"Appearance": "Feran_Longtooth",
"MaxHealth": 100,
"KnockbackScale": 0.5,
"IsMemory": true,
"MemoriesCategory": "Feran",
"BusyStates": ["$Interaction"],
"MotionControllerList": [
{
"Type": "Walk",
"MaxWalkSpeed": 3,
"Gravity": 10,
"RunThreshold": 0.3,
"MaxFallSpeed": 15,
"MaxRotationSpeed": 360,
"Acceleration": 10
}
],
"Instructions": [
{
"Instructions": [
{
"$Comment": "Idle state - no player nearby",
"Sensor": { "Type": "State", "State": "Idle" },
"Instructions": [
{
"$Comment": "Watch player when they approach",
"Sensor": { "Type": "Player", "Range": 8 },
"Actions": [
{ "Type": "State", "State": "Watching" }
]
},
{
"Sensor": { "Type": "Any" },
"BodyMotion": { "Type": "Nothing" }
}
]
},
{
"$Comment": "Watching state - player is nearby",
"Sensor": { "Type": "State", "State": "Watching" },
"Instructions": [
{
"Continue": true,
"Sensor": { "Type": "Player", "Range": 12 },
"HeadMotion": { "Type": "Watch" }
},
{
"$Comment": "Return to Idle when player leaves",
"Sensor": {
"Type": "Not",
"Sensor": { "Type": "Player", "Range": 12 }
},
"Actions": [
{ "Type": "State", "State": "Idle" }
]
},
{
"Sensor": { "Type": "Any" },
"BodyMotion": { "Type": "Nothing" }
}
]
},
{
"$Comment": "Interaction state - look at player while shop is open",
"Sensor": { "Type": "State", "State": "$Interaction" },
"Instructions": [
{
"Continue": true,
"Sensor": { "Type": "Target", "Range": 10 },
"HeadMotion": { "Type": "Watch" }
},
{
"Sensor": { "Type": "Any" },
"Actions": [
{
"Type": "Timeout",
"Delay": [1, 1],
"Action": {
"Type": "Sequence",
"Actions": [
{ "Type": "ReleaseTarget" },
{ "Type": "State", "State": "Watching" }
]
}
}
]
}
]
}
]
}
],
"InteractionInstruction": {
"Instructions": [
{
"Sensor": {
"Type": "Not",
"Sensor": { "Type": "CanInteract", "ViewSector": 180 }
},
"Actions": [
{ "Type": "SetInteractable", "Interactable": false }
]
},
{
"Continue": true,
"Sensor": { "Type": "Any" },
"Actions": [
{
"Type": "SetInteractable",
"Interactable": true,
"Hint": "server.interactionHints.trade"
}
]
},
{
"Sensor": { "Type": "HasInteracted" },
"Instructions": [
{
"Sensor": {
"Type": "Not",
"Sensor": { "Type": "State", "State": "$Interaction" }
},
"Actions": [
{ "Type": "LockOnInteractionTarget" },
{ "Type": "OpenBarterShop", "Shop": "Feran_Enchanted_Merchant" },
{ "Type": "State", "State": "$Interaction" }
]
}
]
}
]
},
"NameTranslationKey": { "Compute": "NameTranslationKey" }
}

Cómo Funciona el NPC Mercader

Este es un NPC Type: "Generic" — a diferencia de los NPCs de combate que heredan de plantillas, los mercaderes definen su comportamiento directamente. Esto es lo que hace cada sección:

SecciónPropósito
DefaultPlayerAttitude: "Neutral"El NPC no atacará a los jugadores
BusyStates: ["$Interaction"]Evita que el NPC haga otras cosas mientras la tienda está abierta
InstructionsComportamiento de IA: inactivo, observar jugadores que se acercan, mirar al jugador durante el comercio
InteractionInstructionLógica de clic derecho: mostrar pista de comercio, abrir tienda al hacer clic

La parte crítica es el InteractionInstruction:

  1. SetInteractable con Hint: "server.interactionHints.trade" — muestra el tooltip “Comerciar” cuando el jugador mira al NPC
  2. HasInteracted sensor — se activa cuando el jugador hace clic derecho
  3. OpenBarterShop con Shop: "Feran_Enchanted_Merchant" — abre la interfaz de comercio vinculada a la definición de la tienda
  4. LockOnInteractionTarget — hace que el NPC mire al jugador durante el intercambio

Paso 5: Agregar Claves de Traducción

Crea un archivo server.lang para cada idioma:

Server/Languages/en-US/server.lang

npcRoles.Feran_Enchanted_Merchant.name = Enchanted Merchant
barter.feran_enchanted_merchant.title = Enchanted Merchant
interactionHints.trade = Trade

Server/Languages/es/server.lang

npcRoles.Feran_Enchanted_Merchant.name = Mercader Encantado
barter.feran_enchanted_merchant.title = Mercader Encantado
interactionHints.trade = Comerciar

Server/Languages/pt-BR/server.lang

npcRoles.Feran_Enchanted_Merchant.name = Mercador Encantado
barter.feran_enchanted_merchant.title = Mercador Encantado
interactionHints.trade = Negociar

Las claves .lang omiten el prefijo server. — el motor lo agrega automáticamente para archivos de idioma del lado del servidor.


Paso 6: Probar en el Juego

  1. Copia la carpeta NPCShopsAndTrading/ a %APPDATA%/Hytale/UserData/Mods/

  2. Asegúrate de que el mod CustomTreesAndSaplings también esté instalado — la tienda referencia objetos de ese mod

  3. Inicia Hytale y entra en Modo Creativo

  4. Genera el mercader y obtén algo de Enchanted Fruit para comerciar:

    /op self
    /npc spawn Feran_Enchanted_Merchant
    /spawnitem Plant_Fruit_Enchanted 9
  5. Haz clic derecho en el Feran para abrir la tienda de trueque

NPC Mercader Feran Encantado en el juego

Interfaz de la tienda de trueque mostrando ambos intercambios

  1. Verifica:
    • El título de la tienda muestra “Enchanted Merchant”
    • El intercambio muestra: 3 Enchanted Fruit → 1 Enchanted Sapling
    • Puedes completar el intercambio 5 veces (Stock: 5)
    • Después de comprar las 5, el espacio se muestra como agotado

Errores comunes y soluciones:

ErrorCausaSolución
Unknown barter shopEl valor de Shop en OpenBarterShop no coincide con el nombre del archivoAsegúrate de que "Shop": "Feran_Enchanted_Merchant" coincida con Feran_Enchanted_Merchant.json
No aparece pista de comercio al pasar el cursorInteractionInstruction falta o está malformadoVerifica que la acción SetInteractable con Hint esté presente
El NPC es hostilActitud o plantilla incorrectaAsegúrate de que DefaultPlayerAttitude sea "Neutral" y Type sea "Generic"
El intercambio muestra objetos incorrectosError en ItemIdVerifica que Plant_Fruit_Enchanted y Plant_Sapling_Enchanted coincidan con los nombres reales de los archivos de objetos
La tienda nunca se reabasteceRefreshInterval faltaAgrega "RefreshInterval": { "Days": 2 } a la definición de la tienda

Referencia de Tiendas de Trueque Vanilla

Archivo VanillaPatrónIntercambios
Kweebec_Merchant.json3 Fijos + 2 grupos PoolEspecias, Sal, Masa (fijos) + comida y recetas (rotativos)
Klops_Merchant.json1 FijoUn solo intercambio de letrero de construcción

Próximos Pasos