Astral Realms Documentation Help

Loot & Rewards

Rewards are never handed to the player directly inside a dungeon. Mob drops and reward pots produce owner-locked item entities that carry their payoff (commands) in the item's PDC; picking one up queues that payoff onto every alive participant's DungeonPlayerData instead of running it. Experience works the same way — it just accumulates as a number. Everything queued is only actually paid out once the player leaves the instance and reconnects to the lobby, where the bridge module claims it. See Dungeon & Room Blueprints for how blueprints are authored and loaded, and Placeholders for %dungeons_experience%.

The two loot sources are configured in different places:

Source

Configured on

Page section

Reward pots

The dungeon blueprint's components-loot-tables.pots

Reward pots

Mob drops

The AstralMobs blueprint's own drops, tagged with a persistent-data payload

Mob loot

Loot tables

A DungeonLootTable is the shape used by components-loot-tables on a dungeon blueprint:

Field

Type

Description

entries

Map<String, Entry>

Map of entry id → Entry. The id is free-form and only used as the map key.

minimum

int

Minimum number of loot rolls per generateLoot() call.

maximum

int

Maximum number of loot rolls (inclusive) per generateLoot() call.

Each Entry is:

Field

Type

Description

display

ItemStackWrapper

The dropped item's appearance. See item-stack for the field reference.

commands

Set<String>

Commands queued for the player once they pick up this reward and later claim it (see Picking up rewards).

chance

double

Relative weight for this entry, fed into a PercentageRandomCollection. Weights are not required to sum to 1 or 100 — an entry's odds are its weight divided by the table's total weight.

generateLoot() rolls a random count in [minimum, maximum], then draws that many entries one at a time via generateSingleLoot(). The results are collected into a Set<Entry>, so if the same entry is drawn more than once in one call it only yields one drop — the actual number of items can be lower than the roll count when the table has few high-weight entries. A misconfigured table (minimum > maximum, or negatives) is clamped rather than throwing: minimum floors at 0 and maximum at the clamped minimum.

generateSingleLoot() has one short-circuit worth knowing: a table with exactly one entry whose chance is exactly 100 returns that entry directly, bypassing the weighted draw. Any other single-entry table still goes through the draw and returns that entry anyway, so the effect is the same — it only matters if you were relying on a sub-100 chance in a one-entry table to sometimes yield nothing. It won't.

components-loot-tables: pots: minimum: 1 maximum: 2 entries: bone-shard: display: material: "BONE" name: "<gray>Splintered Bone" commands: - "eco give %player_name% 5" chance: 10.0 rare-drop: display: material: "ARROW" name: "<gold>Hunter's Arrow" commands: - "give %player_name% arrow 1" chance: 1.0

Mob loot

MobListener#onMobDrop handles EntityDeathEvent at MONITOR priority, only when the direct damaging entity is a Player. For every item in event.getDrops():

  1. Air and meta-less stacks are skipped.

  2. The stack's persistent data container is read for the key configured as mobs.loot-persistent-data-key (default astraldungeons:loot), as a STRING. A drop without that key, or with a blank value, is logged as a warning and discarded.

  3. A drop that has it is respawned through RewardService.spawnItem at the death location, owned by the killer, carrying that string as its single queued command.

Then event.getDrops() is cleared — so nothing lands as an ordinary drop. In practice: a mob's drop only reaches a player if its AstralMobs item carries the loot key, and the string stored under that key is the console command the player receives on claim.

The stack the player sees is the AstralMobs drop itself, unchanged; there is no separate display to author.

Mob experience

mob-experiences on the dungeon blueprint is keyed by mob blueprint id (the id registered with AstralMobs, not the entity type). MobListener#onMobDeath handles EntityDeathEvent at LOW priority: it only acts when the direct damaging entity is a Player, the mob died inside a dungeon world that player participates in, and that instance is ACTIVE.

On a qualifying kill:

  1. The mob's blueprint id is resolved via MobsAPI.getBlueprint(entity.getUniqueId()). If it isn't found, or mob-experiences has no entry for that id, the kill is logged as a warning and skipped — no XP.

  2. The configured experience value is added to the DungeonPlayerData.experience of every alive participant, not just the killer. A participant with no player data is logged and skipped.

  3. The killer's killedMobs counter is incremented — this is the only per-killer bookkeeping, and it is what feeds the mobs leaderboard.

Experience is a running double; there is no leveling or currency conversion inside the dungeon. While a player is inside an instance their current total is exposed live as %dungeons_experience% — see Placeholders.

Reward pots

components-loot-tables is a general per-blueprint map, but only one key is currently consumed: pots. PotListener reads blueprint().componentsLootTables().get("pots") — any other key in that map is inert.

Reward pots are placed as vanilla DecoratedPot blocks inside room schematics. A right-click with the main hand on such a block, while the interacting player is alive and participating in that pot's dungeon instance, is always cancelled and instead:

  1. Draws one entry with generateSingleLoot() — the table's minimum/maximum are not used here; a click always yields exactly one entry (or nothing, if the draw returns null).

  2. Reads the remaining-reward counter from the pot block's PDC, key astral-dungeons:pot_rewards (PersistentDataType.INTEGER), defaulting to 3 when absent — so each pot starts with 3 rewards.

  3. Wobbles the pot (DecoratedPot.startWobble, randomly POSITIVE or NEGATIVE) and spawns 5 Particle.CLOUD particles above it.

  4. Decrements and writes back the counter, plays Sound.BLOCK_DECORATED_POT_INSERT, and updates the block state.

  5. Spawns the reward via RewardService.spawnItem, tossed toward the player (see below), trailing Particle.SMALL_GUST each tick until it lands or dies.

  6. If the counter reached 0, removes the block from the instance's alivePots() tracking set, sets it to AIR and plays Sound.BLOCK_DECORATED_POT_SHATTER.

Each pot therefore yields up to 3 rewards (one per right-click) before it shatters, regardless of the table's minimum/maximum. Unlike the mob path, a pot's display is resolved with no placeholder container (ItemStackWrapper#get(), no-arg), so %placeholder% values in it resolve against the root container rather than per-player.

Only the last click removes the pot from alivePots(); while rewards remain it keeps its particle hint from PotParticleTask.

Where the item lands

Pots are often built into niches, against walls, or under shelves, where a naive upward toss flings the item into geometry or spawns it inside a block. The spawn point and velocity are therefore derived from the pot's surroundings:

  • Top is passable → spawn just above the pot (+1.05), with an upward speed of 0.35–0.50.

  • Top is blocked → spawn out of the face the player clicked (or, for a top/bottom click, out of the face opposite the player's facing), offset 0.85 blocks — falling back to the player's own location if that face is blocked too. Upward speed drops to 0.10–0.18.

Horizontal velocity always aims at the player rather than a random direction, at 0.06–0.14 blocks/tick.

Picking up rewards

Every reward item — mob drop or pot — is spawned through the same RewardService.spawnItem(owner, itemStack, location, commands, …):

  • The dropped Item entity's owner is set to the player it belongs to (Item#setOwner) and setCanMobPickup(false), so mobs at the corpse can't pick items back up.

  • The entry's commands are written to the item entity's PDC under astraldungeons:command as a PersistentDataType.LIST.strings() (RewardService.getCommands reads it back).

RewardListener#onPickup handles EntityPickupItemEvent at MONITOR priority:

  • An item with no astraldungeons:command PDC list is not picked up — the event is cancelled. In a dungeon world, ordinary item pickup does not happen at all.

  • If the picking-up player isn't currently tracked in a dungeon instance, or has no DungeonPlayerData, the pickup is cancelled and a warning logged (defensive guard — should not happen in normal play).

  • Otherwise the event is cancelled, the item entity removed, and for every alive participant in the instance: the entry's commands are appended to their queued commands once per item in the stack (a stack of 3 queues each command three times), a CHALLENGE-type toast showing the item is sent, and minecraft:entity.player.levelup plays.

The reward is therefore shared, not competed for: whoever walks over a drop banks it for the whole surviving party. The vanilla pickup never happens — the reward only exists as queued commands from this point on.

Dead players cannot trigger any of this: PlayerAttemptPickupItemEvent is cancelled outright for a participant who is no longer alive.

Cross-server reward claim

Rewards accumulate per-player on DungeonPlayerData (blueprintId, experience, killedMobs, duration, completed, and a List<String> commands) and are only turned into effects once the player is back on the lobby:

  1. On the dungeon server — PlayerDataService (paper) keeps an in-memory Map<UUID, DungeonPlayerData>. add(player, blueprint) seeds an entry on join (when the player's party has an active instance), stamped with the blueprint id and the current time; MobListener/RewardListener mutate it in place while the player is inside; killing the boss sets completed(true) on every alive participant. On quit, flush(playerId) stamps duration (now minus the entry's creation time), removes it from the cache and persists it to dungeons:players_data:<uuid> (DungeonPlayerDataRepository, JSON via Gson).

  2. On the lobby — the bridge's PlayerConnectionListener listens for AstralSync's PlayerDataLoadedEvent and calls the bridge's PlayerDataService#claimRewards(player).

  3. claimRewards reads the cache entry asynchronously, then hops back onto the main thread — inventory access and event dispatch are main-thread-only. A read failure is logged and stops; no cached data silently does nothing (nothing was earned, or it was already claimed).

  4. The key is consumed. The player's inventory is scanned for a key matching the run's blueprint and exactly one is removed (the stack is decremented if it holds more). A blueprint that no longer resolves locally skips this step and step 5.

  5. DungeonCompletedEvent is fired, carrying the blueprint and the run's stats. A listener may rewrite experience and mutate the commands list; the event's toPlayerData() is what the rest of the claim uses. See Developer API.

  6. The player's placeholder container gets the (possibly rewritten) DungeonPlayerData registered onto it (namespace data), and the configured reward-actions run against it.

  7. Every queued command is dispatched from console, substituting %player_name%.

  8. The cached entry is deleted; a delete failure is logged but does not abort.

  9. A dungeon_completed analytics event is logged (blueprint_id, duration, completed, mobs_killed), and the three leaderboards.yml boards are updated.

Any exception across steps 4–9 is caught and logged. A failure does not re-queue the claim — if the delete never ran, the player would need another PlayerDataLoadedEvent (a relog) to retry, and would then be paid twice.

reward-actions lives in the bridge config.yml:

reward-actions: - "[message] Tu as win %data_experience% points d'expérience chef !"

Authoring notes

  • Pot loot and experience are defined per dungeon blueprint, on the dungeon (paper) server — components-loot-tables and mob-experiences are blueprint fields loaded from plugins/AstralDungeons/blueprints/. Mob loot is not: it lives on the AstralMobs blueprint.

  • The payoff for experience is not decided on the dungeon server at all: a kill only adds a number to DungeonPlayerData.experience. What that number actually does (a chat message, a currency grant, an item, …) is entirely up to the bridge's reward-actions, evaluated once, on the lobby, with %data_experience% available — or to a DungeonCompletedEvent listener.

  • commands on a loot Entry, and the loot-key payload on a mob drop, are plain console command strings (no placeholder substitution beyond %player_name% at claim time) — build any dynamic values (amounts, item ids) directly into the string.

  • chance is a relative weight, not a percentage — adjust the table by changing entries' weights relative to each other, not by trying to make every table sum to a fixed total.

  • Because loot and experience go to every alive participant, a larger party is strictly better off per drop. Balance the tables for the party sizes the blueprint's minimum-players/maximum-players allow.

  • The reward item's PDC command key (astraldungeons:command), the pot's remaining-reward key (astral-dungeons:pot_rewards) and the key stamp on a dungeon key (dungeons:blueprint) use three different namespace spellings. This is a genuine inconsistency in the source, not a typo on this page; the keys are unrelated to each other so it has no functional effect.

Last modified: 25 September 2026