Astral Realms Documentation Help

Loot & Rewards

Rewards are never handed to the player directly inside a dungeon. Mob kills and reward pots drop owner-locked items that carry their payoff (commands) in the item's PDC; picking one up queues that payoff onto the player's DungeonPlayerData instead of running it. Experience works the same way — it just accumulates on DungeonPlayerData 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%.

Loot tables

A DungeonLootTable (DungeonLootTable) is the shape used by both mobs-loot-tables and 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 RandomCollection. 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() (a single weighted pick). The results are collected into a Set<Entry>, so if the same entry is drawn more than once in one generateLoot() call, it only yields one drop — the actual number of items dropped can be lower than the roll count when the table has few high-weight entries.

mobs-loot-tables: skeleton-archer: 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 & experience

Both mob-experiences and mobs-loot-tables on the blueprint are keyed by mob blueprint id (the id registered with AstralMobs, not the entity type). MobListener#onMobDeath handles EntityDeathEvent: it only acts when the killer (getDamageSource().getCausingEntity()) is a Player, the mob died inside a dungeon world the player participates in, and that instance's state 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 and skipped (no XP, no loot).

  2. The configured experience value is added to the killer's DungeonPlayerData.experience.

  3. If mobs-loot-tables has an entry for that mob id, generateLoot() is rolled and each resulting Entry is spawned with RewardService.spawnItem(killer, itemStack, entity.getLocation(), entry.commands()) (see Picking up rewards) — i.e. at the mob's death location, the corpse. The item's display is resolved through the killer's placeholder container (AstralPaperAPI.createPlaceholderContainer(player)), so name/lore can reference player placeholders. A failure spawning one entry is caught and logged per-entry; it doesn't stop the rest of the loot table.

A mob with no mob-experiences entry drops no XP and is logged as a warning; a mob with no mobs-loot-tables entry simply drops no loot (silent, no warning). Every drop is owner-locked to the killer (Item#setOwner) with setCanMobPickup(false), so mobs at the corpse can't pick items back up — see Picking up rewards.

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. PlayerInteractEvent (main hand only) 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 from the pots table with generateSingleLoot() — the table's minimum/maximum are not used here; a click always yields exactly one entry (or nothing, if generateSingleLoot() 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 at the pot's location with a small random upward/outward velocity (a parabolic toss), trailing Particle.SMALL_GUST each tick until it lands or dies.

  6. Removes the block from the instance's alivePots() tracking set.

  7. If the counter reached 0, breaks the pot: sets the block to AIR and plays Sound.BLOCK_DECORATED_POT_SHATTER.

So each pot yields up to 3 rewards (one per click) before it shatters, regardless of how the pots loot table's minimum/maximum are configured. Unlike mob loot, a pot's display is resolved with no placeholder container (ItemStackWrapper#get(), no-arg).

Picking up rewards

Every reward item — from a mob kill or a pot — is dropped 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 only that player (not mobs, and not other players) can pick it 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:

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

  • If the picked-up item carries a astraldungeons:command PDC list, the event is cancelled and the item entity removed; its commands are added to playerData.commands(), a CHALLENGE-type toast is shown ("You received a reward!" / "Check your rewards with /dungeon rewards", with the item as the toast icon), and minecraft:entity.player.levelup plays at the player's location. The vanilla pickup (into the player's inventory) never happens — the reward only exists as queued commands from this point on.

  • An item with no commands PDC (i.e. not one of these reward drops) is picked up normally.

Experience

mob-experiences values are added straight onto DungeonPlayerData.experience (a running double) on each qualifying kill — there's no separate leveling or currency conversion inside the dungeon itself. While a player is inside an instance, their current total is exposed live as %dungeons_experience% (DungeonsPlaceholders, namespace dungeons); see Placeholders.

Cross-server reward claim

Rewards accumulate per-player on DungeonPlayerData (experience + a Set<String> commands) and are only turned into effects once the player is back on the lobby:

  1. On the dungeon serverPlayerDataService (paper) keeps an in-memory Map<UUID, DungeonPlayerData>. add(player) seeds an empty entry on join (when the player's party has an active instance); MobListener/RewardListener mutate it in place while the player is inside. On quit, flush(playerId) removes it from the cache and persists it to the shared cache under key dungeons:players_data:<uuid> (DungeonPlayerDataRepository, JSON via Gson).

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

  3. claimRewards reads dungeons:players_data:<uuid> from the cache (DungeonPlayerDataRepository#get). If the read fails it logs an error and stops; if there's no cached data for that player it silently does nothing (nothing was ever earned, or it was already claimed).

  4. Otherwise it builds the player's placeholder container and registers the DungeonPlayerData onto it (namespace data, exposing %data_experience%), then runs the configured reward-actions — a PaperActionList — against that player and container.

  5. It then dispatches every queued command from console, substituting %player_name% for the player's name, on the main thread.

  6. Finally the cached entry is deleted (DungeonPlayerDataRepository#delete). Any exception while running reward actions or deleting is caught and logged; a failure at this stage does not re-queue the claim — the player would need another PlayerDataLoadedEvent (e.g. a relog) to retry if the delete failed partway.

reward-actions lives in the bridge config.yml:

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

Authoring notes

  • Loot and experience are defined per blueprint, on the dungeon (paper) server — mobs-loot-tables, components-loot-tables, and mob-experiences are all blueprint fields, loaded from plugins/AstralDungeons/blueprints/. See Dungeon & Room Blueprints.

  • 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.

  • commands on a loot Entry 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 per entry.

  • chance is a relative weight, not a percentage — pad or shrink the table by adding/removing entries or adjusting other entries' weights, not by trying to make every table sum to a fixed total.

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

Last modified: 25 July 2026