What's On the Map already covered CRewardableObject — the map-object instance a Pandora's Box or Seer's Hut actually is. This is what that class is built on: a config-time reader, a pair of near-mirror-image data structures for “what you need” and “what you get,” and a second, differently-shaped answer to a question Bonus System already solved once.
Three different jobs, three different files, none of them the object a hero actually visits.
Rewardable::Info is the config-time reader — it holds a raw JsonNode and, given a hero and a randomizer, configureObject() turns that JSON into a filled-in Rewardable::Configuration. Configuration and its members (Limiter, Reward, Variables, VisitInfo, ResetInfo) are pure data — no behavior, just what a specific object's rewards actually are. Rewardable::Interface is the behavior: doHeroVisit(), getAvailableRewards(), grantReward(). None of this is the map object itself — CRewardableObject holds a Rewardable::Interface and forwards onHeroVisit() into it, the same connective pattern What's On the Map already traced for the object model generally.
Limiter and Reward are two flat structs in the same namespace, and they ask nearly the same questions in opposite directions.
Limiter tests, a Reward can grant, in the same units — experience, mana, movement, resources, both skill categories. A Seer's Hut asking for three Sulfur and handing back 2000 experience is one Configuration using both halves of the same vocabulary in opposite directions. getAvailableRewards() is where they actually meet: it walks every configured VisitInfo, tests its Limiter against the visiting hero, and returns just the indices that pass.This atlas already solved “how do you combine several eligibility checks into one” once, in Bonus System. Here it's solved again, differently.
separate classes, ANDed by default
A bonus's limiter list is implicitly ANDed; getting OR logic means reaching for a distinct composite class — AnyOfLimiter, AllOfLimiter, NoneOfLimiter — that itself holds a list of other limiters.
one struct, three baked-in fields
Rewardable::Limiter doesn't need a separate class for boolean composition — allOf, anyOf, and noneOf are LimitersList members sitting right next to heroLevel and resources on the same struct. Every field already set on a Limiter is implicitly ANDed (the same default as bonuses), but reaching OR or NOT logic is filling in a field, not choosing a different type.
Two solutions to the same problem, in the same codebase, because they were designed for different shapes of caller: bonuses attach to arbitrary DAG nodes and need real polymorphism to test against; a reward's limiter only ever answers one question, for one hero, so a flat struct with three optional sub-lists does the job without a class hierarchy.
Interface::doHeroVisit() is the function What's On the Map's onHeroVisit() dispatch actually lands in for anything rewardable.
It calls getAvailableRewards() to filter down to what this hero currently qualifies for. Zero results and there's nothing to do; one result and grantRewardWithMessage() applies it immediately; more than one and selectRewardWithMessage() builds a BlockingDialog pack and calls gameEvents.showBlockingDialog() — which, on CGameHandler, opens a CBlockingDialogQuery: a sibling of the VisitQuery What's On the Map already traced, not the same class, but the identical CQuery machinery underneath — queries->addQuery(), a queryID stamped on the pack, the player blocked until an answer comes back. A Pandora's Box with two possible rewards and a Seer's Hut with one aren't running different code, just landing on different branches of the same count check. Once a reward is chosen, granting it doesn't attach Reward::heroBonuses directly — for every bonus in the list it builds a GiveBonus gb(GiveBonus::ETarget::OBJECT, hero->id, *bonus) and calls gameEvents.giveHeroBonus(&gb). That's the exact pack The Apply Layer already opened — a reward doesn't have a private shortcut into Bonus System's DAG, it goes through the same visitGiveBonus() four-way target switch as everything else that hands out a bonus.
One detail worth naming:Interfacekeeps its ownspells::ExternalCaster, markedno serializein its own comment. A reward that casts a spell on the visiting hero (a shrine teaching a spell, a scholar exchanging one) needs aCasterto attribute that cast to — and Spell System's own caster survey already namedExternalCasteras “a rebindable stand-in for casts triggered from outside the normal flow,” citing scripts and the map editor's test tools. A rewardable object granting a spell is a third: the same stand-in, one more outside trigger.