Authoring Shops & Currencies
The four Token Shop pack folders, offer fields, rotating pools, and currency authoring
Server/MMOSkillTree/{Shops,ShopPools,ShopEntries,ShopTemplates,Currencies}/*.jsonParsed 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.
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 balancesShop (storefront)
Folder Shops/*.json. Filename (lowercased) is the runtime shop id, referenced by an entry or pool's shop field. Parsed via TokenShopConfig.parseShopBoard.
| Field | Type | Default | Description |
|---|---|---|---|
titleKey / descriptionKey | string | none | |
icon | string | none | |
enabled | bool | true | |
order | int | 0 | Default-shop tiebreak. |
requirements | object | ungated | Shallow Requirements form: features/skills/minCombatLevel/permission. |
layout | string | "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. |
{
"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.
| Field | Type | Default | Description |
|---|---|---|---|
titleKey / descriptionKey / icon | string | none | |
enabled | bool | true | |
order | int | 0 | |
rotation | RotationSpec | daily interval | See below. |
selection | SelectionSpec | weighted_random / period seed | See below. |
slots | PoolSlot[] | [] (draws 4 offers per period) | See below. With no slots authored, a pool draws a default of 4 offers per period. |
rerollCost | RerollCost | none (no paid reroll) | See below. |
requirements | object | ungated | Gates the whole pool. |
shop | string | null (default shop) | Groups this pool under a named storefront. |
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.
| Field | Type | Default | Description |
|---|---|---|---|
rotation.type | "interval" | "interval" | The only shipped cadence type. |
rotation.period | string | "daily" | "daily", "weekly", or "<N>s" (e.g. "7200s"). |
rotation.anchor | string | "utc" | "utc" or "utc_monday" (shifts weekly boundaries to land on Monday 00:00 UTC). |
rotation.offsetMinutes | int | 0 | |
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.tag | string | none | A 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[].count | int | 1 (clamped 1-64) | |
slots[].optional | bool | false | An unfillable optional slot is skipped silently instead of failing the draw. |
rerollCost.currency | string | none | Currency id spent on a paid manual reroll. |
rerollCost.amount | long | 0 | |
rerollCost.maxPerPeriod | int | 0 (unlimited) |
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.
| Field | Type | Default | Description |
|---|---|---|---|
id | string | filename lowercased | Lowercased regardless of source. |
titleKey / descriptionKey | string | none | No by-convention fallback for shop entries - always author these explicitly. |
displayName / description | string | none | Deprecated 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. |
icon | string | resolved | Unset on a concrete-skill XP reward entry auto-resolves to that skill's registry icon. |
category | string | "items" | Known values: boosts, items, conversion, featured. Unknown values warn in the audit but still work. |
order | int | 0 | |
limitPerDay | int | 0 (unlimited) | Per-player, server-tracked. |
limitTotal | int | 0 (unlimited) | Per-player, server-tracked, lifetime. |
pool | string | null (static, always listed) | Set to enroll in a ShopPool id - the entry only surfaces when drawn by that pool's rotation. |
tier | string | none | Matches a pool slot's tier filter. |
weight | double | 1.0 | Selection weight within its pool. |
shop | string | null (default shop) | Groups this entry into a named Shop storefront. |
groupKey | string | none | UI grouping hint, e.g. fanned XP-packet tiers share a groupKey per skill. |
requirements | object | ungated | Shallow Requirements form. |
rewards | Reward[] | [] | Queue-for-offline-delivery defaults true for shop rewards - a purchase must never vanish. |
Cost is always an object
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.
{
"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}}" } ]
}
}{
"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
{
"Name": "Simple_Offer",
"Payload": {
"id": "shop_simple",
"cost": { "currencies": { "bounty_token": 50 } },
"rewards": [ { "type": "XP", "skill": "MINING", "amount": 500 } ]
}
}Exhaustive ShopEntry example
{
"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.
| Field | Type | Default | Description |
|---|---|---|---|
displayName | string | echoes id | Deprecated raw fallback. |
nameKey | string | none | Explicit 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. |
hytaleItemId | string | none | Setting 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). |
icon | string | none | Texture-path icon, rarely used directly. |
iconItemId | string | none | Display-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. |
color | string (hex) | "#ffffff" | |
cap | long | 0 (uncapped) | |
lossOnDeathPercent | double [0,1] | 0 | Clamped to [0,1]. |
decayPerDayPercent | double [0,1] | 0 | Clamped; compounds over offline days. |
showOnSidebar | bool | true | |
showOnMasteryPage | bool | true | |
visibility | object | none (always visible) | Deep PrerequisiteGroup form. Gates whether the player even sees this currency. |
sources.xpConversionPercent | double | 0 | Nested under a sources object. Passive XP -> currency conversion rate. |
Minimal example (item-backed)
{ "Name": "Life_Essence", "Payload": { "hytaleItemId": "Ingredient_Life_Essence" } }Exhaustive example (counter-backed)
{
"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
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.