Operators and math
Arithmetic, comparisons, logic and random numbers, with game-style examples.
Plugins calculate all the time: damage after armor, a 2% chance to find a diamond, how many ticks are in 30 seconds, how far a player is from spawn. On this page you will learn Java's math and logic operators, the traps that catch every beginner, the Math class and random numbers, and you will build a plugin with lucky mining, double XP and a /journey command.
Arithmetic: + - * / %
An operator is a symbol that does something with values, like + adds two numbers. The values it works on are called operands. Java has five arithmetic operators:
| Operator | Meaning | Example | Result |
|---|---|---|---|
+ | Add | 7 + 2 | 9 |
- | Subtract | 7 - 2 | 5 |
* | Multiply | 7 * 2 | 14 |
/ | Divide | 7 / 2 | 3 (yes, really, see below) |
% | Remainder: what is left after dividing | 7 % 2 | 1 |
1void main() {2 int diamonds = 7;3 int players = 2;4 5 IO.println("7 + 2 = " + (diamonds + players));6 IO.println("7 - 2 = " + (diamonds - players));7 IO.println("7 * 2 = " + (diamonds * players));8 IO.println("7 / 2 = " + (diamonds / players));9 IO.println("7 % 2 = " + (diamonds % players));10 IO.println("7.0 / 2 = " + (7.0 / 2));11}- Two
intvariables to calculate with. - The parentheses around
(diamonds + players)matter: they make Java add the numbers before gluing the answer onto the text. Without them you would get"7 + 2 = 72". - Both values are
int, so the answer is aninttoo. 3.5 does not fit in anint, so Java drops the .5 and gives 3. - The remainder: 2 goes into 7 three times (6), and 1 is left over.
- One side is a
double, so the answer is adouble: 3.5.
In plugins, these operators show up wherever numbers change. Two lines you could find in real plugins:
1player.setFoodLevel(player.getFoodLevel() - 2);2event.setAmount(event.getAmount() * 2);- Reads the hunger bar, subtracts 2, and sets the result: the player loses one drumstick.
- In a
PlayerExpChangeEvent: doubles the XP the player is about to receive. The plugin later on this page does exactly this.
The integer division trap
When both sides of / are whole numbers, Java does integer division: it divides and throws away the decimals. It does not round. Think of sharing diamonds: you cannot cut a diamond in half.
This is the bug that bites every beginner, usually when calculating a percentage. Run this:
1void main() {2 int health = 15;3 int maxHealth = 20;4 5 int wrong = health / maxHealth * 100;6 IO.println("Wrong: " + wrong + "%");7 8 int better = health * 100 / maxHealth;9 IO.println("Whole numbers, multiply first: " + better + "%");10 11 double exact = (double) health / maxHealth * 100;12 IO.println("With a double: " + exact + "%");13}- Java works left to right:
15 / 20is 0.75, but with integers that becomes 0. Then0 * 100is still 0. The player at 75% health sees "0%". - Fix 1: multiply first.
1500 / 20is exactly 75, so nothing is lost. - Fix 2: cast one side to
double. As soon as one side is adouble, Java does decimal division: 0.75, then 75.0.
The rule to remember: if both sides are whole numbers, the result is a whole number. If either side is a double (or a float), the result has decimals. Writing 100.0 instead of 100, or 2.0 instead of 2, is often enough to switch to decimal math.
Dividing a whole number by zero crashes with an ArithmeticException, as you saw in How code reads. Dividing a double by zero does not crash; it gives Infinity or NaN ("not a number"), which then spreads into every calculation that uses it. Either way, make sure you never divide by something that can be zero.
The remainder operator
% (also called modulo) gives what is left over after dividing. It sounds obscure, but it is one of the most useful operators in plugins. Two classic uses are splitting things into groups and doing something every Nth time.
Stacks and inventory slots
1void main() {2 int cobblestone = 150;3 int fullStacks = cobblestone / 64;4 int leftOver = cobblestone % 64;5 IO.println(cobblestone + " cobblestone = " + fullStacks + " stacks and " + leftOver + " more");6 7 int slot = 23;8 int row = slot / 9;9 int column = slot % 9;10 IO.println("Chest slot " + slot + " is in row " + row + ", column " + column);11}- How many full stacks: 150 / 64 is 2.
- What is left after the full stacks: 150 - 128 is 22.
- Chest slots are numbered from 0, nine per row.
- The row (counting from 0): 23 / 9 is 2, the third row.
- The column (counting from 0): 23 % 9 is 5, the sixth column.
You will use the slot math when building menus: Inventories and GUI menus number every slot this way.
Every Nth time
A number divides evenly by N exactly when the remainder is 0. So count % 10 == 0 is true on the 10th, 20th, 30th time, and so on:
1void main() {2 int blocksMined = 30;3 IO.println(blocksMined + " blocks: milestone? " + (blocksMined % 10 == 0));4 5 blocksMined = 31;6 IO.println(blocksMined + " blocks: milestone? " + (blocksMined % 10 == 0));7 8 int wave = 6;9 IO.println("Wave " + wave + " is even? " + (wave % 2 == 0));10 IO.println("Wave " + wave + " is a boss wave (every 5th)? " + (wave % 5 == 0));11}- 30 % 10 is 0, so 30 is a milestone. The
==compares and givestrueorfalse; comparisons are explained further down. - The classic even-number check: even numbers have no remainder when divided by 2.
- A boss on every 5th wave of a minigame.
The example-events template uses exactly this to celebrate every 10th diamond ore a player mines:
1if (total % MILESTONE_EVERY == 0) {2 new DiamondMilestoneEvent(player, total).callEvent();3}- With
MILESTONE_EVERYset to 10, this is true at 10, 20, 30 diamond ores.
Changing a variable: ++, -- and +=
Adding to a variable is so common that Java has shortcuts for it. These are called compound assignment operators:
| Shortcut | Means the same as | Plugin example |
|---|---|---|
kills++ | kills = kills + 1 | Count a kill |
lives-- | lives = lives - 1 | Lose a life in a minigame |
coins += 25 | coins = coins + 25 | Pay a quest reward |
coins -= 40 | coins = coins - 40 | Charge for a shop item |
multiplier *= 2 | multiplier = multiplier * 2 | Double an XP multiplier |
price /= 2 | price = price / 2 | Half-price sale |
1void main() {2 int kills = 0;3 kills++;4 kills++;5 IO.println("Kills: " + kills);6 7 int lives = 3;8 lives--;9 IO.println("Lives: " + lives);10 11 int coins = 100;12 coins += 25;13 IO.println("Coins after reward: " + coins);14 coins -= 40;15 IO.println("Coins after buying bread: " + coins);16 17 double health = 20.0;18 health -= 4.5;19 IO.println("Health after a creeper: " + health);20 21 int xpMultiplier = 1;22 xpMultiplier *= 2;23 IO.println("XP multiplier on weekends: " + xpMultiplier);24}- Adds one. Two of these take
killsfrom 0 to 2. - Subtracts one.
- Adds 25 to whatever is already there.
- Takes 40 away.
- Works with decimals too.
- Multiplies what is there by 2.
++ can also go in front: ++count. On its own line, count++ and ++count do the same thing. Inside a bigger expression they differ, which confuses everyone:
1void main() {2 int count = 5;3 IO.println("count++ shows " + count++ + ", then count is " + count);4 5 count = 5;6 IO.println("++count shows " + ++count + ", then count is " + count);7}- After the name: Java uses the old value (5) first, then adds one.
- Before the name: Java adds one first, then uses the new value (6).
Comparisons: true or false
A comparison operator compares two values and gives a boolean: true or false. Every decision a plugin makes ("is the player low on health?", "is this block diamond ore?") starts with one.
| Operator | Question it asks | Example |
|---|---|---|
== | Equal? | foodLevel == 20 |
!= | Not equal? | foodLevel != 20 |
< | Less than? | health < 6.0 |
<= | Less than or equal? | ping <= 80 |
> | Greater than? | coins > price |
>= | Greater than or equal? | level >= 30 |
1void main() {2 double health = 5.5;3 int level = 30;4 int foodLevel = 20;5 6 IO.println("Low health? " + (health < 6.0));7 IO.println("Can enchant at max level? " + (level >= 30));8 IO.println("Food is full? " + (foodLevel == 20));9 IO.println("Food is not full? " + (foodLevel != 20));10 11 boolean needsWarning = health < 6.0;12 IO.println("Show a warning: " + needsWarning);13}- 5.5 is less than 6.0, so this is
true. Six health points is three hearts. - Greater than or equal, so exactly 30 counts.
- Two equals signs: "are these equal?".
- The answer of a comparison is a normal
booleanvalue, so you can store it in a variable with a good name.
In plugins, comparisons usually sit inside an if, which runs code only when the answer is true. Making decisions covers if fully, but you can already read this:
1if (player.getHealth() < 6.0) {2 player.sendRichMessage("<red>Careful, you are low on health!");3}- The comparison. The message is only sent when it is
true.
Logic: and, or, not
Logical operators combine true and false values:
&&(and): true only when both sides are true. "Level 10 or higher and not in combat."||(or): true when at least one side is true. "Is an operator or has the fly permission." The|key is Shift+\.!(not): flips true to false and false to true.!isInCombatreads "not in combat".
These tables, called truth tables, list every possible combination:
a | b | a && b | a || b |
|---|---|---|---|
true | true | true | true |
true | false | false | true |
false | true | false | true |
false | false | false | false |
a | !a |
|---|---|
true | false |
false | true |
1void main() {2 boolean isOperator = false;3 boolean hasFlyPermission = true;4 int level = 12;5 boolean isInCombat = true;6 7 boolean canFly = isOperator || hasFlyPermission;8 IO.println("Can fly: " + canFly);9 10 boolean canEnterArena = level >= 10 && !isInCombat;11 IO.println("Can enter the arena: " + canEnterArena);12 13 boolean canUseSpawnShop = level >= 5 && (isOperator || !isInCombat);14 IO.println("Can use the spawn shop: " + canUseSpawnShop);15}- Only one of them needs to be true. The player is not an operator but has the permission, so they can fly.
- Both parts must be true. Level 12 is fine, but the player is in combat, so
!isInCombatis false and the whole answer is false. - Parentheses group the "or" so it is worked out first, like in math. Read it as: level 5 or more, and also (an operator, or not fighting).
The exemplar on Events and listeners protects diamond ore with exactly this kind of logic:
1boolean isDiamondOre = block.getType() == Material.DIAMOND_ORE2 || block.getType() == Material.DEEPSLATE_DIAMOND_ORE;3if (isDiamondOre && !player.hasPermission("eventsdemo.mine.diamonds")) {4 event.setCancelled(true);5}- Either kind of diamond ore counts.
- Cancel only when it is diamond ore and the player does not have the permission.
Short-circuiting: Java stops early
&& and || are lazy in a useful way. If the left side of && is false, the whole thing must be false, so Java does not even look at the right side. If the left side of || is true, Java skips the right side too. This is called short-circuiting, and it lets you check for null safely:
1void main() {2 String nickname = null;3 boolean hasLongNickname = nickname != null && nickname.length() > 10;4 IO.println("Long nickname? " + hasLongNickname);5}- The left side is false (the nickname is
null), so Java never runsnickname.length(). No crash.
Swap the two sides, and the program crashes, because now length() runs first on a null value:
1void main() {2 String nickname = null;3 boolean hasLongNickname = nickname.length() > 10 && nickname != null;4 IO.println("Long nickname? " + hasLongNickname);5}- Runs first and crashes with a
NullPointerException. In the error,<local1>is Java's name for the variablenicknamewhen it runs a file this way.
In plugins, put the safety check on the left: killer != null && killer.hasPermission("...").
Order of operations
Java follows the same order as school math: multiplication and division before addition and subtraction. The full order is called operator precedence. These are the levels you need, from first to last:
| Order | Operators | Example |
|---|---|---|
| 1 (first) | Parentheses ( ) | (2 + 3) * 4 is 20 |
| 2 | !, ++, --, casts like (int) | !isInCombat |
| 3 | * / % | 2 + 3 * 4 is 14 |
| 4 | + - | 20 - 5 - 3 is 12 (left to right) |
| 5 | < <= > >= | coins - price >= 0 |
| 6 | == != | slot % 9 == 0 |
| 7 | && | |
| 8 | || | |
| 9 (last) | = += -= and friends | The answer is stored at the very end |
1void main() {2 IO.println("2 + 3 * 4 = " + (2 + 3 * 4));3 IO.println("(2 + 3) * 4 = " + ((2 + 3) * 4));4 IO.println("20 - 5 - 3 = " + (20 - 5 - 3));5 6 int swordPrice = 30;7 int applePrice = 4;8 int apples = 5;9 int total = swordPrice + applePrice * apples;10 IO.println("A sword and 5 apples cost " + total);11 12 double average = (12 + 7 + 20) / 3.0;13 IO.println("Average kills: " + average);14}- Multiplication first: 3 * 4 is 12, plus 2 is 14.
- Parentheses first: 5 * 4 is 20.
- Same level, so left to right: 15 - 3 is 12.
- The apples are multiplied first: 30 + 20 is 50, which is what a shop should charge.
- The parentheses add the kills first. Dividing by
3.0instead of3keeps the decimals.
The Math class
Java comes with a Math class full of ready-made calculations. You use them like the methods you already know: Math.max(12, 30) means "Math, give me the bigger of 12 and 30".
| Method | What it gives back | Plugin use |
|---|---|---|
Math.max(a, b) | The bigger value | Health never below 0: Math.max(health - damage, 0.0) |
Math.min(a, b) | The smaller value | Never over the max: Math.min(health + 4, 20.0) |
Math.clamp(value, min, max) | The value, kept between min and max | Food from 0 to 20 in one step |
Math.abs(x) | The value without its minus sign | How far apart two numbers are |
Math.round(x) | Nearest whole number, as a long | Showing "152 blocks" instead of "152.38" |
Math.floor(x) | Rounds down, as a double | Block coordinate from an exact position |
Math.ceil(x) | Rounds up, as a double | Hearts shown for 13.5 health |
Math.sqrt(x) | Square root | Distances |
Math.pow(a, b) | a to the power b | Level costs that grow quickly |
1void main() {2 IO.println("Math.max(12, 30) = " + Math.max(12, 30));3 IO.println("Math.min(64, 100) = " + Math.min(64, 100));4 IO.println("Math.abs(-8.5) = " + Math.abs(-8.5));5 IO.println("Math.round(7.5) = " + Math.round(7.5));6 IO.println("Math.round(7.49) = " + Math.round(7.49));7 IO.println("Math.floor(7.9) = " + Math.floor(7.9));8 IO.println("Math.ceil(7.1) = " + Math.ceil(7.1));9 IO.println("Math.floor(-0.5) = " + Math.floor(-0.5));10 IO.println("Math.sqrt(144) = " + Math.sqrt(144));11 IO.println("Math.pow(2, 10) = " + Math.pow(2, 10));12 IO.println("Math.clamp(26.0, 0.0, 20.0) = " + Math.clamp(26.0, 0.0, 20.0));13 14 double health = 13.456;15 double oneDecimal = Math.round(health * 10) / 10.0;16 IO.println(health + " rounded to one decimal: " + oneDecimal);17}- 7.5 rounds up to 8; 7.49 rounds down to 7.
- Always down, and the answer is a
double(7.0), not anint. - Always up.
- Down means toward minus infinity, so -0.5 becomes -1.0. A cast
(int) -0.5would give 0 instead. That difference matters for negative coordinates. - 2 multiplied by itself 10 times. The result is a
double. - 26 is above the maximum, so you get 20.
- A common trick to keep one decimal: 13.456 times 10 is 134.56, rounded is 135, divided by 10.0 is 13.5.
Here they all work together in a damage calculation, the kind a custom weapon or a PvP plugin would do:
1static final double MAX_HEALTH = 20.0;2 3void main() {4 double baseDamage = 7.0;5 double critMultiplier = 1.5;6 int armorPercent = 50;7 8 double critDamage = baseDamage * critMultiplier;9 double afterArmor = critDamage * (100 - armorPercent) / 100.0;10 IO.println("Crit hit: " + critDamage + ", after " + armorPercent + "% armor: " + afterArmor);11 12 double health = 12.0;13 double newHealth = Math.max(health - afterArmor, 0.0);14 IO.println("Health left: " + newHealth);15 16 double healed = Math.min(newHealth + 6.5, MAX_HEALTH);17 IO.println("After a golden apple: " + healed);18 19 int heartsShown = (int) Math.ceil(healed / 2);20 IO.println("Hearts on the health bar: " + heartsShown);21}- A critical hit does 1.5 times the damage: 10.5.
- 50% armor lets 50% of the damage through. The
100.0keeps the math in decimals. - Health must never go below 0, even if the hit is bigger than the health left.
- Healing must never go above 20.
- 13.25 health is 6.625 hearts. Minecraft shows a partly filled heart as a heart, so we round up and cast to
int.
Ticks and time
A Minecraft server runs 20 ticks every second, one tick every 50 milliseconds. Paper measures most times in ticks: delays for tasks, potion effect lengths, how long a title stays on screen, fire. So you constantly convert between seconds and ticks:
- Seconds to ticks: multiply by 20. 30 seconds is 600 ticks.
- Ticks to seconds: divide by 20.0 (with the
.0if you want the decimals). 50 ticks is 2.5 seconds. - Ticks to minutes and seconds: divide by 20 for total seconds, then
/ 60for minutes and% 60for the seconds left over.
1static final int TICKS_PER_SECOND = 20;2 3void main() {4 int cooldownSeconds = 30;5 long cooldownTicks = cooldownSeconds * TICKS_PER_SECOND;6 IO.println(cooldownSeconds + " seconds = " + cooldownTicks + " ticks");7 8 int ticks = 50;9 double seconds = ticks / (double) TICKS_PER_SECOND;10 IO.println(ticks + " ticks = " + seconds + " seconds");11 12 int ticksPlayed = 8_000;13 int totalSeconds = ticksPlayed / TICKS_PER_SECOND;14 int minutes = totalSeconds / 60;15 int secondsLeft = totalSeconds % 60;16 IO.println(ticksPlayed + " ticks = " + minutes + " min " + secondsLeft + " s");17}- A named constant instead of a bare 20 everywhere.
- The result is stored as a
long, because Paper's scheduler wants delays aslongvalues. Anintfits into alongautomatically. - The cast makes this decimal division: 2.5, not 2.
- 400 seconds is 6 whole minutes...
- ...and 40 seconds left over.
In a plugin, that math goes straight into the scheduler. This line from the plugin below waits 5 seconds before writing to the console:
Where this file livesjava-operators-math-demosrcmainjavacomexamplejavaoperatorsmathdemoOperatorsMathPlugin.java
The package com.example.javaoperatorsmathdemo is the folder path com/example/javaoperatorsmathdemo inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- java-operators-math-demo/
- src/main/
- java/com/example/javaoperatorsmathdemo/Package com.example.javaoperatorsmathdemo
- DoubleXpListener.javaListener: reacts to events
- JourneyCommand.javaCommand (BasicCommand)
- LuckyMiningListener.javaListener: reacts to events
- OperatorsMathPlugin.javayou are hereMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/javaoperatorsmathdemo/Package com.example.javaoperatorsmathdemo
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
19long delayTicks = READY_MESSAGE_DELAY_SECONDS * TICKS_PER_SECOND;- 5 seconds times 20 ticks per second is 100 ticks. The scheduler page explains delayed and repeating tasks.
The Tick Calculator converts between seconds, minutes and ticks for you when you just need a number.
Distance between two points
Every position in a Minecraft world has three coordinates: x (east and west), y (up and down) and z (north and south). The straight-line distance between two positions uses the Pythagorean formula: subtract the coordinates, square each difference, add them up, take the square root.
1void main() {2 double playerX = 110.0;3 double playerY = 64.0;4 double playerZ = -30.0;5 6 double spawnX = 100.0;7 double spawnY = 64.0;8 double spawnZ = -10.0;9 10 double dx = playerX - spawnX;11 double dy = playerY - spawnY;12 double dz = playerZ - spawnZ;13 14 double distance = Math.sqrt(dx * dx + dy * dy + dz * dz);15 IO.println("Distance to spawn: " + distance);16 IO.println("Rounded: " + Math.round(distance) + " blocks");17 18 double radius = 25.0;19 double distanceSquared = dx * dx + dy * dy + dz * dz;20 IO.println("Inside the spawn area? " + (distanceSquared <= radius * radius));21}- How far apart the two points are on each axis: 10, 0 and -20.
- Squaring removes the minus signs. 100 + 0 + 400 is 500, and the square root of 500 is about 22.36.
- Players do not need 15 decimals. Rounded, it is 22 blocks.
- To check "within 25 blocks", compare the squared distance with 25 squared. Square roots are slow to calculate, and this skips the root entirely while giving the same answer.
Paper does this math for you. A Location has distance(other) and distanceSquared(other), and the plugin below uses both. The Worlds, locations and movement page covers coordinates in depth.
Random numbers
Loot drops, critical hits, random teleports and lucky blocks all need randomness. The easiest good choice in Java is ThreadLocalRandom:
1import java.util.concurrent.ThreadLocalRandom;2 3void main() {4 ThreadLocalRandom random = ThreadLocalRandom.current();5 6 int dice = random.nextInt(1, 7);7 IO.println("You rolled a " + dice);8 9 int bonusArrows = random.nextInt(2, 6);10 IO.println("The skeleton dropped " + bonusArrows + " arrows");11 12 int roll = random.nextInt(100);13 boolean dropsSaddle = roll < 25;14 IO.println("Roll " + roll + " out of 0-99, saddle drops (25% chance)? " + dropsSaddle);15 16 boolean dropsDiamond = random.nextDouble() < 0.05;17 IO.println("Rare diamond drop (5% chance)? " + dropsDiamond);18}ThreadLocalRandomlives in a package that is not loaded automatically, so it needs an import line.- Gets the random number generator. It is fast and safe to use anywhere in a plugin.
- A whole number from 1 up to, but not including, 7. So 1 to 6, like a die. The second number is always one past the highest value you want.
- 2, 3, 4 or 5 arrows.
- A number from 0 to 99: one hundred possible values.
- 25 of the 100 possible values (0 to 24) are below 25, so this is true 25% of the time. This is the standard way to write "a 25% chance".
nextDouble()gives a decimal from 0.0 up to (not including) 1.0. Below 0.05 happens 5% of the time. Handy for chances like 0.5%.
Press Run a few times: the output changes every time, so yours will not match the recorded output. That is the point.
A plugin built on math
This plugin puts the whole page to work. Mining stone has a 2% chance to drop a bonus diamond, all XP is doubled, and /journey tells you how far you are from spawn and how long you have been alive.
Lucky mining
Where this file livesjava-operators-math-demosrcmainjavacomexamplejavaoperatorsmathdemoLuckyMiningListener.java
The package com.example.javaoperatorsmathdemo is the folder path com/example/javaoperatorsmathdemo inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- java-operators-math-demo/
- src/main/
- java/com/example/javaoperatorsmathdemo/Package com.example.javaoperatorsmathdemo
- DoubleXpListener.javaListener: reacts to events
- JourneyCommand.javaCommand (BasicCommand)
- LuckyMiningListener.javayou are hereListener: reacts to events
- OperatorsMathPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/javaoperatorsmathdemo/Package com.example.javaoperatorsmathdemo
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.javaoperatorsmathdemo;2 3import java.util.concurrent.ThreadLocalRandom;4import net.kyori.adventure.text.Component;5import net.kyori.adventure.text.format.NamedTextColor;6import org.bukkit.Material;7import org.bukkit.block.Block;8import org.bukkit.event.EventHandler;9import org.bukkit.event.Listener;10import org.bukkit.event.block.BlockBreakEvent;11import org.bukkit.inventory.ItemStack;12 13public final class LuckyMiningListener implements Listener {14 15 private static final int BONUS_CHANCE_PERCENT = 2;16 17 @EventHandler(ignoreCancelled = true)18 public void onBreak(BlockBreakEvent event) {19 Block block = event.getBlock();20 boolean isStone = block.getType() == Material.STONE || block.getType() == Material.DEEPSLATE;21 if (!isStone) {22 return;23 }24 25 int roll = ThreadLocalRandom.current().nextInt(100);26 boolean isLucky = roll < BONUS_CHANCE_PERCENT;27 if (isLucky) {28 block.getWorld().dropItemNaturally(block.getLocation(), ItemStack.of(Material.DIAMOND));29 event.getPlayer().sendActionBar(Component.text("Lucky find! A diamond popped out.", NamedTextColor.AQUA));30 }31 }32}- The chance as a named constant. Change one number to make the server luckier.
- Skips blocks that another plugin already protected. Explained in Event priorities and canceling.
- An "or" with two comparisons: normal stone or deepslate both count.
- "Not stone": leave the method right away with
return. Nothing else happens for dirt, logs or ores. - A roll from 0 to 99.
- Only 0 and 1 are below 2, so this is true 2 times in 100.
- Drops a diamond at the block's position, as if it fell out of the stone.
- Shows the text above the hotbar instead of in chat.
Double XP
Where this file livesjava-operators-math-demosrcmainjavacomexamplejavaoperatorsmathdemoDoubleXpListener.java
The package com.example.javaoperatorsmathdemo is the folder path com/example/javaoperatorsmathdemo inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- java-operators-math-demo/
- src/main/
- java/com/example/javaoperatorsmathdemo/Package com.example.javaoperatorsmathdemo
- DoubleXpListener.javayou are hereListener: reacts to events
- JourneyCommand.javaCommand (BasicCommand)
- LuckyMiningListener.javaListener: reacts to events
- OperatorsMathPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/javaoperatorsmathdemo/Package com.example.javaoperatorsmathdemo
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.javaoperatorsmathdemo;2 3import org.bukkit.event.EventHandler;4import org.bukkit.event.Listener;5import org.bukkit.event.player.PlayerExpChangeEvent;6 7public final class DoubleXpListener implements Listener {8 9 private static final int XP_MULTIPLIER = 2;10 11 @EventHandler12 public void onExpChange(PlayerExpChangeEvent event) {13 int boosted = event.getAmount() * XP_MULTIPLIER;14 event.setAmount(boosted);15 }16}- Fires when a player is about to receive experience, for example from an XP orb.
- The original amount times 2. Because both are
int, the result is anint, which is exactly whatsetAmountwants. - Changes how much XP the player actually gets.
The /journey command
Where this file livesjava-operators-math-demosrcmainjavacomexamplejavaoperatorsmathdemoJourneyCommand.java
The package com.example.javaoperatorsmathdemo is the folder path com/example/javaoperatorsmathdemo inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- java-operators-math-demo/
- src/main/
- java/com/example/javaoperatorsmathdemo/Package com.example.javaoperatorsmathdemo
- DoubleXpListener.javaListener: reacts to events
- JourneyCommand.javayou are hereCommand (BasicCommand)
- LuckyMiningListener.javaListener: reacts to events
- OperatorsMathPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/javaoperatorsmathdemo/Package com.example.javaoperatorsmathdemo
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.javaoperatorsmathdemo;2 3import io.papermc.paper.command.brigadier.BasicCommand;4import io.papermc.paper.command.brigadier.CommandSourceStack;5import org.bukkit.Location;6import org.bukkit.entity.Player;7 8public final class JourneyCommand implements BasicCommand {9 10 private static final int TICKS_PER_SECOND = 20;11 private static final double SPAWN_AREA_RADIUS = 50.0;12 13 @Override14 public void execute(CommandSourceStack source, String[] args) {15 if (!(source.getExecutor() instanceof Player player)) {16 source.getSender().sendRichMessage("<red>Only players can use /journey.");17 return;18 }19 20 Location here = player.getLocation();21 Location spawn = player.getWorld().getSpawnLocation();22 double distance = here.distance(spawn);23 long roundedDistance = Math.round(distance);24 boolean nearSpawn = here.distanceSquared(spawn) <= SPAWN_AREA_RADIUS * SPAWN_AREA_RADIUS;25 26 int ticksAlive = player.getTicksLived();27 int totalSeconds = ticksAlive / TICKS_PER_SECOND;28 int minutes = totalSeconds / 60;29 int seconds = totalSeconds % 60;30 31 player.sendRichMessage("<gold>You are " + roundedDistance + " blocks from spawn.");32 player.sendRichMessage("<gray>Inside the spawn area: <white>" + nearSpawn);33 player.sendRichMessage("<gray>Alive for <white>" + minutes + " min " + seconds + " s <gray>(" + ticksAlive + " ticks)");34 }35}- Makes sure a player ran the command. Making decisions explains this line.
- The player's current position.
- The spawn point of the world the player is in.
- Paper does the Pythagorean formula for you.
- Stored as a
long, because that is whatMath.roundreturns for adouble. - The squared-distance trick: is the player within 50 blocks? 50 squared is 2500.
- How many ticks this player entity has existed in the world, which is roughly how long since you joined.
- Ticks to seconds, with integer division because we only want whole seconds.
- The seconds left over after the full minutes.
Inside the spawn area: false
Alive for 6 min 40 s (8000 ticks)
The main class
Where this file livesjava-operators-math-demosrcmainjavacomexamplejavaoperatorsmathdemoOperatorsMathPlugin.java
The package com.example.javaoperatorsmathdemo is the folder path com/example/javaoperatorsmathdemo inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- java-operators-math-demo/
- src/main/
- java/com/example/javaoperatorsmathdemo/Package com.example.javaoperatorsmathdemo
- DoubleXpListener.javaListener: reacts to events
- JourneyCommand.javaCommand (BasicCommand)
- LuckyMiningListener.javaListener: reacts to events
- OperatorsMathPlugin.javayou are hereMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/javaoperatorsmathdemo/Package com.example.javaoperatorsmathdemo
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.javaoperatorsmathdemo;2 3import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;4import org.bukkit.plugin.java.JavaPlugin;5 6public final class OperatorsMathPlugin extends JavaPlugin {7 8 private static final long TICKS_PER_SECOND = 20;9 private static final long READY_MESSAGE_DELAY_SECONDS = 5;10 11 @Override12 public void onEnable() {13 getServer().getPluginManager().registerEvents(new LuckyMiningListener(), this);14 getServer().getPluginManager().registerEvents(new DoubleXpListener(), this);15 getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, event ->16 event.registrar().register("journey", "Shows how far you are from spawn and how long you have been alive",17 new JourneyCommand()));18 19 long delayTicks = READY_MESSAGE_DELAY_SECONDS * TICKS_PER_SECOND;20 getServer().getScheduler().runTaskLater(this,21 () -> getLogger().info(READY_MESSAGE_DELAY_SECONDS + " seconds (" + delayTicks + " ticks) have passed."),22 delayTicks);23 }24}- A
longconstant, so the multiplication below produces thelongthe scheduler wants. - Registers both listeners, as on the Events and listeners page.
- Registers
/journey. Commands, part 1 explains this pattern. - 5 times 20: 100 ticks.
- Runs the code after
delayTicksticks. The() ->part is a small piece of code passed along to run later, called a lambda (see Lambdas).
[16:20:01 INFO]: [OperatorsMath] Enabling OperatorsMath v1.0.0[16:20:04 INFO]: Done (3.112s)! For help, type "help"[16:20:06 INFO]: [OperatorsMath] 5 seconds (100 ticks) have passed.Download this plugin as a Gradle project, run it, mine some stone and type /journey.
Practice
Daily reward calculator
A server gives a daily login reward of 100 coins plus 50 coins for every day of the player's streak, but never more than 500. Every 7th streak day is a bonus day. After claiming, the player must wait 90 seconds before claiming again (short for testing).
Extend this program so it prints the reward, whether today is a bonus day, and the cooldown both as "1 min 30 s" and in ticks.
1void main() {2 int streakDays = 9;3 IO.println("Login streak: " + streakDays + " days");4}Hint 1
Use Math.min(..., 500) to cap the reward. Precedence means 100 + 50 * streakDays multiplies first, which is what you want.
Hint 2
"Every 7th" is a job for the remainder operator: streakDays % 7 == 0.
Hint 3
Minutes are seconds / 60, the leftover seconds are seconds % 60, and ticks are seconds * 20.
Show the solution
With a 9-day streak, 100 + 450 would be 550, so the cap brings it down to 500. 9 % 7 is 2, so it is not a bonus day.
1static final int TICKS_PER_SECOND = 20;2static final int MAX_REWARD = 500;3 4void main() {5 int streakDays = 9;6 IO.println("Login streak: " + streakDays + " days");7 8 int reward = Math.min(100 + 50 * streakDays, MAX_REWARD);9 IO.println("Reward for today: " + reward + " coins");10 11 boolean isBonusDay = streakDays % 7 == 0;12 IO.println("Weekly bonus day? " + isBonusDay);13 14 int cooldownSeconds = 90;15 long cooldownTicks = cooldownSeconds * TICKS_PER_SECOND;16 IO.println("Next claim in " + cooldownSeconds / 60 + " min " + cooldownSeconds % 60 + " s ("17 + cooldownTicks + " ticks)");18}- The smaller of the calculated reward and the cap.
- Inside the text, the parentheses are not needed here because
/and%run before+. Adding them would still be clearer.
Luckier mining
Change LuckyMiningListener so the chance is 5%, and a lucky find drops 1 to 3 diamonds instead of always one.
Hint 1
The chance is a single constant. For the amount, remember that the second number of nextInt(min, max) is not included.
Hint 2
ItemStack.of(Material.DIAMOND, amount) makes a stack with that many diamonds.
Show the solution
Set BONUS_CHANCE_PERCENT to 5, and replace the drop line inside the if with these two lines:
1int amount = ThreadLocalRandom.current().nextInt(1, 4);2block.getWorld().dropItemNaturally(block.getLocation(), ItemStack.of(Material.DIAMOND, amount));nextInt(1, 4) gives 1, 2 or 3. Writing nextInt(1, 3) is the classic mistake: it never gives 3.
The Practice Arena has more math challenges with instant test results.
Recap
+ - * /work as in math, and%gives the remainder. Use%for "every Nth time", stacks and slot rows.- Whole number divided by whole number gives a whole number:
7 / 2is 3. Make one side adoubleto keep decimals. count++,coins += 25and friends change a variable in place. Keep++on its own line.- Comparisons (
== != < <= > >=) givetrueorfalse.=stores,==compares. &&needs both sides,||needs one,!flips. Put null checks on the left so short-circuiting protects you.- When in doubt about order, use parentheses.
Math.max,min,clamp,round(gives along),floor,ceil,sqrtandpowdo common calculations.- 20 ticks make one second.
ThreadLocalRandom.current().nextInt(100) < 25is a 25% chance.
Quick quiz
What does
int percent = 15 / 20 * 100;store?15 / 20is integer division, which gives 0, and 0 times 100 is 0. Writing15 * 100 / 20gives 75.A minigame should spawn a boss on every 5th wave. Which check is right?
The remainder is 0 exactly on waves 5, 10, 15 and so on.wave / 5 == 0is only true for waves 0 to 4, andwave == 5only for wave 5.Which numbers can
ThreadLocalRandom.current().nextInt(1, 4)give?The first number is included and the second is not. To include 4, writenextInt(1, 5).Why is
killer != null && killer.isOp()safe, whilekiller.isOp() && killer != nullcan crash?Short-circuiting: when the left side of&&is false, the answer is already known. In the second version,isOp()runs first, and calling a method onnullthrows aNullPointerException.How many ticks should you pass to the scheduler to wait 3 seconds?
The server runs 20 ticks per second, so 3 seconds is 3 times 20. 3000 is the number of milliseconds, which the scheduler does not use.
Next steps
- Working with text: build, compare and format the messages your plugins send.
- Making decisions: put your comparisons to work with
ifandswitch. - Tick Calculator: convert times to ticks without doing the math.