Authoring Shops & Currencies

The four Token Shop pack folders, offer fields, rotating pools, and currency authoring

Content pack assetServer/MMOSkillTree/{Shops,ShopPools,ShopEntries,ShopTemplates,Currencies}/*.json

Parsed via TokenShopConfig (Shop/ShopPool/ShopEntry) and CurrencyConfig (Currency). The Token Shop engine is jar-shipped but dormant with no ShopEntries content - it ships in the standalone MMO Skill Bounty Pack.

defaults<pack<owner

The Token Shop is the bounty-token sink: players spend currency at a storefront for XP, boosts, and conversions. Four raw-Payload asset types cooperate, all lowerCamelCase in the body, PascalCase envelope keys - see the Content Types & Merging casing rule.

content-packs/bounty-contracts-pack/Server/MMOSkillTree/
├── Shops/            # storefront groupings (ShopBoard) - referenced by an entry/pool's "shop" field
├── ShopPools/        # rotating offer pools (RotationSpec/SelectionSpec/PoolSlot/RerollCost)
├── ShopEntries/      # individual purchasable offers - the only type that requires a cost object
├── ShopTemplates/    # extends/params skeletons shared by ShopEntry AND ShopPool
└── Currencies/       # item-backed or counter-backed balances

Shop (storefront)

Folder Shops/*.json. Filename (lowercased) is the runtime shop id, referenced by an entry or pool's shop field. Parsed via TokenShopConfig.parseShopBoard.

FieldTypeDefaultDescription
titleKey / descriptionKeystringnone
iconstringnone
enabledbooltrue
orderint0Default-shop tiebreak.
requirementsobjectungatedShallow Requirements form: features/skills/minCombatLevel/permission.
layoutstring"flat"The other shipped value is a skill-grouped layout. A skill-grouped layout renders static entries only - it hides pooled/rotating offers, so use flat whenever the shop has a rotating pool.
* required
Server/MMOSkillTree/Shops/XpExchange.json
{
  "Name": "XpExchange",
  "Payload": {
    "titleKey": "shop.xpexchange.title",
    "descriptionKey": "shop.xpexchange.desc",
    "icon": "Tool_Pickaxe_Crude",
    "order": 1,
    "enabled": true
  }
}

ShopPool

Folder ShopPools/*.json. Filename (lowercased) is the runtime pool id. Same rotating-pool shape as a Bounty Board's rotation schedule, plus a shop field to group the pool under a storefront.

FieldTypeDefaultDescription
titleKey / descriptionKey / iconstringnone
enabledbooltrue
orderint0
rotationRotationSpecdaily intervalSee below.
selectionSelectionSpecweighted_random / period seedSee below.
slotsPoolSlot[][] (draws 4 offers per period)See below. With no slots authored, a pool draws a default of 4 offers per period.
rerollCostRerollCostnone (no paid reroll)See below.
requirementsobjectungatedGates the whole pool.
shopstringnull (default shop)Groups this pool under a named storefront.
* required

rotation / selection / slots / rerollCost

This shape is shared with the Bounty Board rotation engine (rotation/) - the same fields work the same way for both offer pools and bounty pools.

FieldTypeDefaultDescription
rotation.type"interval""interval"The only shipped cadence type.
rotation.periodstring"daily""daily", "weekly", or "<N>s" (e.g. "7200s").
rotation.anchorstring"utc""utc" or "utc_monday" (shifts weekly boundaries to land on Monday 00:00 UTC).
rotation.offsetMinutesint0
selection.type"weighted_random""weighted_random"The only shipped selection type.
selection.seed"period""period"The only shipped seed strategy - draws are deterministic per (poolId, period).
slots[].filter.tier / filter.tagstringnoneA flat tier/tag directly on the slot object (no nested filter) is also accepted. tier on a ShopPool slot matches an entry's own tier field.
slots[].countint1 (clamped 1-64)
slots[].optionalboolfalseAn unfillable optional slot is skipped silently instead of failing the draw.
rerollCost.currencystringnoneCurrency id spent on a paid manual reroll.
rerollCost.amountlong0
rerollCost.maxPerPeriodint0 (unlimited)
* required

ShopEntry

Folder ShopEntries/*.json. Filename does not drive the id - the runtime id is the inner Payload.id (falls back to the asset key, lowercased, if id is absent). Parsed via TokenShopConfig.parseEntry.

FieldTypeDefaultDescription
idstringfilename lowercasedLowercased regardless of source.
titleKey / descriptionKeystringnoneNo by-convention fallback for shop entries - always author these explicitly.
displayName / descriptionstringnoneDeprecated raw fallbacks. Prefer the *Key fields.
cost*Cost object-No scalar fallback. An entry with no cost object is warned and dropped entirely at load.
iconstringresolvedUnset on a concrete-skill XP reward entry auto-resolves to that skill's registry icon.
categorystring"items"Known values: boosts, items, conversion, featured. Unknown values warn in the audit but still work.
orderint0
limitPerDayint0 (unlimited)Per-player, server-tracked.
limitTotalint0 (unlimited)Per-player, server-tracked, lifetime.
poolstringnull (static, always listed)Set to enroll in a ShopPool id - the entry only surfaces when drawn by that pool's rotation.
tierstringnoneMatches a pool slot's tier filter.
weightdouble1.0Selection weight within its pool.
shopstringnull (default shop)Groups this entry into a named Shop storefront.
groupKeystringnoneUI grouping hint, e.g. fanned XP-packet tiers share a groupKey per skill.
requirementsobjectungatedShallow Requirements form.
rewardsReward[][]Queue-for-offline-delivery defaults true for shop rewards - a purchase must never vanish.
* required

Cost is always an object

There is no scalar cost+currencyId shorthand for a ShopEntry (that draft form was removed pre-release). Always author "cost": { "currencies": {...}, "items": [...] }.

ShopTemplate

Folder ShopTemplates/*.json. One store serves both ShopEntry and ShopPool - a concrete entry or pool extends a template id from this shared registry. The only reserved keys the resolver strips are extends/params; everything else (including forEachSkill, read after template resolution) overlays through. Full DSL mechanics on Template Extension.

forEachSkill: true fans the resolved template out to one ShopEntry per active skill, substituting {{SKILL}} (skill id) and {{SKILL_NAME}} (display name) into every string value, never a key. That is why requireSelfLevel exists as a special scalar the fan-out converts into requirements.requiresSkills.<skill>=N in Java, instead of a templated map key. The id pattern (containing {{SKILL}}) is what the generated per-skill entries key off, so several tiers can fan out to distinct ids without colliding. An explicitly authored entry with the same generated id always wins over the fan-out.

ShopTemplates/Xp_Packet_Base.json
{
  "Name": "Xp_Packet_Base",
  "Payload": {
    "forEachSkill": true,
    "category": "conversion", "shop": "xpexchange", "pool": "xpexchange",
    "groupKey": "{{SKILL}}", "displayName": "{{SKILL_NAME}} {{TIER_NAME}}",
    "cost": { "currencies": { "bounty_token": "{{TOKENS}}", "life_essence": "{{ESSENCE}}" } },
    "rewards": [ { "type": "XP", "skill": "{{SKILL}}", "amount": "{{XP}}" } ]
  }
}
ShopEntries/XP_Packet_Greater.json
{
  "Name": "XP_Packet_Greater",
  "Payload": {
    "extends": "xp_packet_base",
    "params": { "TIER_NAME": "Training", "TOKENS": "165", "ESSENCE": "65", "XP": "7500" },
    "id": "shop_xp_greater_{{SKILL}}", "tier": "greater", "order": 42,
    "requireSelfLevel": 30, "limitPerDay": 3
  }
}

Reserved param names

params must never define SKILL/SKILL_NAME - the fan-out pass reserves them and runs AFTER template {{param}} resolution, which is what lets the two passes compose (unknown tokens survive pass 1 by design).

Minimal ShopEntry example

json
{
  "Name": "Simple_Offer",
  "Payload": {
    "id": "shop_simple",
    "cost": { "currencies": { "bounty_token": 50 } },
    "rewards": [ { "type": "XP", "skill": "MINING", "amount": 500 } ]
  }
}

Exhaustive ShopEntry example

Server/MMOSkillTree/ShopEntries/Boost_Mining.json
{
  "Name": "Boost_Mining",
  "Payload": {
    "id": "shop_boost_mining",
    "titleKey": "shop.boost_mining.title",
    "descriptionKey": "shop.boost_mining.desc",
    "cost": { "currencies": { "bounty_token": 150 } },
    "icon": "Tool_Pickaxe_Crude",
    "category": "boosts",
    "order": 20,
    "shop": "general",
    "limitPerDay": 3,
    "requirements": { "requiresSkills": { "MINING": 1 } },
    "rewards": [
      { "type": "BOOST_TOKEN", "skill": "MINING", "multiplier": 3.0, "durationMinutes": 20 }
    ]
  }
}

Currency authoring

Folder Currencies/*.json. Filename (lowercased) is the runtime currency id. Parsed via CurrencyConfig's override-merge logic, shared by both the pack and owner layers.

FieldTypeDefaultDescription
displayNamestringechoes idDeprecated raw fallback.
nameKeystringnoneExplicit localization key. Convention (counter-backed only): currency.<id>.name. An item-backed currency needs no name key - it derives from the backing item's native lang key.
hytaleItemIdstringnoneSetting this makes the currency item-backed (balance = inventory count of this item id; the player can trade/drop/lose it on death per vanilla rules). Omit for counter-backed (balance lives in SkillComponent.currencies, no inventory representation).
iconstringnoneTexture-path icon, rarely used directly.
iconItemIdstringnoneDisplay-only item id for chip/UI icons, independent of hytaleItemId backing - a counter-backed currency typically sets only this. Falls back to hytaleItemId if unset.
colorstring (hex)"#ffffff"
caplong0 (uncapped)
lossOnDeathPercentdouble [0,1]0Clamped to [0,1].
decayPerDayPercentdouble [0,1]0Clamped; compounds over offline days.
showOnSidebarbooltrue
showOnMasteryPagebooltrue
visibilityobjectnone (always visible)Deep PrerequisiteGroup form. Gates whether the player even sees this currency.
sources.xpConversionPercentdouble0Nested under a sources object. Passive XP -> currency conversion rate.
* required

Minimal example (item-backed)

Server/MMOSkillTree/Currencies/Life_Essence.json
{ "Name": "Life_Essence", "Payload": { "hytaleItemId": "Ingredient_Life_Essence" } }

Exhaustive example (counter-backed)

Server/MMOSkillTree/Currencies/Mastery_Point.json
{
  "Name": "Mastery_Point",
  "Payload": {
    "iconItemId": "Ingredient_Crystal_Purple",
    "color": "#c08aff",
    "cap": 0,
    "lossOnDeathPercent": 0,
    "decayPerDayPercent": 0,
    "showOnSidebar": true,
    "showOnMasteryPage": true,
    "sources": { "xpConversionPercent": 0 }
  }
}

Per-player purchase limits

A ShopEntry's limitPerDay/limitTotal counters live as appended SkillComponent fields (no migration needed). They track per-player, server-side, and reset on the shop's own daily boundary, independent of any ShopPool rotation the entry may also be enrolled in.

The Token Shop is gated launch: a MMO_Token_Trader block, the mmo_token_shop_open interaction, and /mmoshopui open it in-game; /mmoshop is the admin command; /mmoshop validate runs the ShopValidator content audit. Shop and currency content ship in the standalone MMO Skill Bounty Pack; the engine, UI, and Java stay in the jar.