Pet Effects
Effects are the passive bonuses a pet blueprint grants its owner. Each blueprint carries an effects: map — <effect-id>: { type: "...", ... } — deserialized entry-by-entry into a WrappedPetEffect by WrappedPetEffectTypeSerializer. The type key selects a PetEffect implementation by its registered string id in PetEffectsRegistry; everything else in the entry deserializes straight into that implementation's own @ConfigSerializable record.
Pet#applyEffect/Pet#removeEffect iterate a pet's blueprint().effects() and call PetEffect#apply/#remove on each wrapped effect. This runs on PetEquipEvent/PetUnequipEvent, on PlayerDataLoadedEvent, and after every level-up — see Passive effects. Some effect types (money, storage, rideable, no-fall) have no-op apply/remove bodies and are instead read directly off the blueprint at the point of use — each per-effect section below notes which.
Common config keys
Key | Type | Description |
|---|---|---|
| String | The registered effect id (see the summary table below). Selects which |
| Component (MiniMessage) | What the effect is called to a player — "Vente Lianes", "Revenus". The only display text a blueprint still owns; how it is coloured and laid out belongs to a transformer. |
| String | How its value should read: |
|
| Level → incremental amount granted at that level (not a running total). |
The map key an effect is written under (shop-modifier:, jobs_farmer_experience:…) becomes the effect's id, which is what tells two effects of the same type on one pet apart.
WrappedPetEffectTypeSerializer builds WrappedPetEffect#cumulativeValues by walking values in level order and summing as it goes, into a sorted map — so a blueprint may list its levels out of order, and totalValue(level) answers by looking down to the nearest level the blueprint names rather than by an exact lookup. That is what lets a table name only the levels that change something: an effect gaining at 5 and next at 10 reports the level-5 total throughout 6-9. Independently, every PetEffect implementation exposes the same arithmetic itself through the interface default value(level) (values().entrySet().stream().filter(key <= level).sum()), which is what implementations call from their own apply(). maxLevel is the highest key present in values; maxValue is the cumulative total at that key; unlockLevel is the first level whose entry is non-zero (0 when the table grants nothing at any level, which counts as "nothing to unlock" rather than permanently locked).
Effects backed by an optional plugin (shop-modifier, jobs-modifier, rotating-shop-modifier) are only registered once the matching hook succeeded. On a server without that plugin, a blueprint using one loads with that effect skipped and a warning — Skipping effect '<type>' of pet blueprint '<id>': <plugin> is not installed. — instead of failing outright.
Effects run asynchronously by default (PetEffect#async() defaults to true, so Pet#applyEffect/#removeEffect dispatch them via Bukkit.getScheduler().runTaskAsynchronously) — potion is the only implementation that overrides this to false, since Player#addPotionEffect/#removePotionEffect must run on the main thread.
Rendering an effect
An effect's display lines are built from configuration, not Java. Two pieces make that possible.
PetEffectView — one effect bound to the level a pet is actually at. It is what %pet_effects% hands out, so a line can ask whether the effect is locked or maxed without the level being threaded back in. Its keys resolve under the effect namespace; see Effect view sub-keys.
Transformers — plugins/AstralPets/transformers/*.yml, plugin-scoped AstralCore transformers that a blueprint's lore calls with $apply-transformer(<id>). The shipped set:
Transformer | Type | What it draws |
|---|---|---|
|
| One line per effect, over |
|
| One effect's line — picks between locked, flag, level, maxed and active forms by priority. |
|
| The effect's current total, formatted by its |
|
| The same for the total at the effect's last level. |
|
| The whole "next level" block, header included — and an empty branch at max level, so the block removes itself. |
|
| One line per effect that gains next level, over |
|
| One effect's next-level line. |
|
| The XP bar, filled from |
They are declared global: false, so they resolve from AstralPets' own configuration and do not collide with another plugin's transformer of the same name.
Adding a new unit is therefore a config change: give the effect unit: "blocks" and add a blocks case to pet-effect-value/pet-effect-max-value/pet-effect-next-value.
Summary
Registered id | Class | Target system | Async |
|---|---|---|---|
| AstralStats | Yes | |
| Core economy ( | Yes (no-op | |
| Bukkit | No | |
| Pet's own | Yes (no-op | |
| Pet mounting ( | Yes (no-op | |
| Fall damage ( | Yes (no-op | |
| Nothing — display only | Yes (no-op | |
| AstralShop ( | Yes | |
| AstralJobs ( | Yes | |
| AstralRotatingShop ( | Yes |
stats
Adds an AstralStats StatModifier to the owner for as long as the pet is equipped. StatPetEffect.apply() calls StatsAPI.addModifier(player, stat, new StatModifier(key, stat, ModifierSource.OTHER, EquipmentSlot.SADDLE, value(level), modifierType)); remove() calls StatsAPI.removeModifier. The modifier's key is modifier-key suffixed with the pet's UUID (PetUtils#modifyKey), so multiple pets — even of the same blueprint — never collide on the same modifier key.
Field | Type | Description |
|---|---|---|
| Key | AstralStats stat key the modifier applies to (e.g. |
| Key | Base key for the registered |
|
|
|
|
| Level → incremental modifier amount. |
money
Periodic passive income. MoneyPetEffect.apply()/remove() are no-ops — the payout is driven entirely by EconomyEffectTask, an async repeating task (runTaskTimerAsynchronously, 10s initial delay / 30s period) registered in AstralPets#onEnable. Every run, for every online player's active pets, it finds each MoneyPetEffect on the blueprint and pays out when all of the following hold:
moneyEffect.value(pet.level())(the cumulative amount) is non-zero.pet.equippedSince()is non-zero (the pet is actually equipped).at least
intervalhas elapsed since bothequippedSinceand the pet'slastMoneyGainTime.
Payouts from every eligible pet on a player are summed into one economyService.deposit(...) call, then each contributing pet's individual %amount% is reported separately via the pet-generated-money message (%pet_name% + %amount%, see messages.yml), and that pet's lastMoneyGainTime is updated.
Field | Type | Description |
|---|---|---|
|
| Level → incremental currency amount paid per interval. |
| Duration | Minimum time between payouts for this effect, parsed by |
potion
Applies an infinite-duration Bukkit PotionEffect to the owner while equipped. PotionPetEffect.apply() computes amplifier = (int) value(level) and, if greater than 0, calls effect.createEffect(PotionEffect.INFINITE_DURATION, amplifier - 1) — so values should be keyed with the potion level (1 = the vanilla "I" tier), not the raw Bukkit amplifier; the effect subtracts 1 internally. remove() calls player.removePotionEffect(effect). Runs synchronously (async() returns false).
Field | Type | Description |
|---|---|---|
|
| Registry key from |
|
| Level → incremental potion-level amount; cumulative total minus 1 is the applied amplifier. |
storage
Sizes the pet's own PetInventory. StoragePetEffect.apply()/remove() are no-ops — the size isn't tied to the equip lifecycle at all. Pet#build() sets the initial size from getEffectLevel(StoragePetEffect.class, 1), and Pet#handleLevelUps() recomputes it (getEffectLevel(StoragePetEffect.class, level)) after every level-up, resizing the backing array if it changed. Because of this, storage size (and therefore whether [open-pet-container] opens anything) depends only on the pet's own level, not on whether it's currently active/equipped.
Field | Type | Description |
|---|---|---|
|
| Level → incremental inventory slots. Cumulative total is the pet's current storage size. |
rideable
Gates whether the pet can be mounted. RideablePetEffect.apply()/remove() are no-ops; EntityService#ride checks blueprint().getEffectLevel(RideablePetEffect.class, pet.level()) >= 1 directly. Riding additionally requires the blueprint's own entity.rideable: true — the effect only unlocks it once the pet's level also grants a cumulative value of at least 1; below that, [ride-pet]/ interacting with the spawned pet reports pet-cannot-be-rode with the level at which riding unlocks (PetBlueprint#levelUntilValue).
Field | Type | Description |
|---|---|---|
|
| Level → incremental value; riding unlocks once the cumulative total reaches |
no-fall
Cancels fall damage for the owner. NoFallPetEffect.apply()/remove() are no-ops; EffectListener's EntityDamageEvent handler checks, for every one of the player's active pets, blueprint().getEffectLevel(NoFallPetEffect.class, pet.level()) > 0 and cancels the event if any active pet qualifies — the pet does not need to be spawned, only equipped.
Field | Type | Description |
|---|---|---|
|
| Level → incremental value; fall damage is cancelled once the cumulative total is greater than |
dummy
Grants nothing. apply()/remove() are empty and no system reads it — it exists purely so a blueprint can declare a display-only line that scales with level like any other effect: a bonus granted somewhere else entirely, a flavour perk, or a placeholder for an effect not built yet.
Because it is a normal effect entry it still gets a label, a unit, a level table and therefore an unlock level, a maxed state and a next-level value — so it renders through the same transformers as everything else and needs no special-casing in lore.
Field | Type | Description |
|---|---|---|
|
| Level → incremental value. Used only for display and for the unlock/max arithmetic. |
shop-modifier
Adds an AstralShop price ShopModifier for the owner. ShopModifierPetEffect.apply() divides the cumulative value by 100 and registers an item-scoped ShopItemModifier (if item-id is set), a category-scoped ShopCategoryModifier (if category-id is set instead), or a global ShopModifier otherwise — via ShopAPI.addModifier. The registered key is key suffixed with the pet's UUID and, when present, the item-id/category-id (PetUtils#modifyKey).
Field | Type | Description |
|---|---|---|
| Key | Base key for the registered |
|
|
|
|
|
|
|
| Level → incremental amount; applied as |
| String? | Optional. Scopes the modifier to one shop item instead of the whole shop. |
| String? | Optional (ignored if |
jobs-modifier
Adds an AstralJobs JobModifier for the owner. JobModifierPetEffect.apply() registers the modifier via JobsAPI.addModifier using the raw cumulative value (unlike shop-modifier/rotating-shop-modifier, it is not divided by 100). The registered key is modifier-key suffixed with the pet's UUID and the modifier scope name.
Field | Type | Description |
|---|---|---|
| Key | Base key for the registered |
|
|
|
|
|
|
| String? | Optional. Scopes the modifier to one job; omitted/ |
|
| Level → incremental amount, used as-is (not divided by |
rotating-shop-modifier
Adds an AstralRotatingShop price modifier for the owner. RotatingShopModifierPetEffect.apply() calls RotatingShopAPI.addModifier(playerId, key, value(level) / 100); remove() calls RotatingShopAPI.removeModifier. The registered key is modifier-key suffixed with the pet's UUID.
Field | Type | Description |
|---|---|---|
| Key | Base key for the registered rotating-shop modifier. |
|
| Level → incremental amount; applied as |
See Pet Blueprints for the surrounding blueprint shape, Passive effects for the equip/unequip lifecycle that drives most of these, and Placeholders for everything a display line can ask about one effect.