Quest Board Plugin
Minecraft Plugin · Spigot / Paper API · Java
The Idea
Most Minecraft gameplay is self-directed — you decide what to do and when. But what if the server gave you goals? A quest board is a classic RPG mechanic: a place where players pick up tasks, go out and complete them, and return for a reward.
You'll build a quest system for a Minecraft server. Players walk up to a board — a sign, a chest GUI, an NPC, whatever you design — browse available quests, accept one, and go do it. Maybe they need to kill a certain number of skeletons. Maybe they need to bring back 32 iron ore. Maybe they need to reach a specific location. When they're done, they hand it in and collect their reward.
The quest types above are just a starting point. You decide what kinds of quests exist, what the rewards look like, how many quests a player can hold at once, and what the board looks like in-game. The key constraint is that all quest types work through the same system — adding a new quest type should never require changing the code that handles checking and rewarding quests.
What You Must Build by the End of the Project
- At least three distinct quest types, each with different completion logic
- A per-player quest tracker — players can hold active quests and complete them independently
- A completion and reward system that works the same way regardless of quest type
- A HUD element (boss bar or action bar) showing progress on trackable quests
- A quest board UI where players can browse, accept, and hand in quests
- At least five distinct quests spread across your quest types
- A server-side admin dashboard built with Java Swing, following the MVC pattern
OOP Requirements
This project must clearly demonstrate all four OOP topics as well as three design patterns. Here is exactly what is required for each.
Classes & Subclasses
An abstract base class Quest must be defined. It holds everything common to all quests: a title, a description, a list of reward strategies, and a unique ID. The method that checks whether a quest is complete — isComplete(Player p) — must be declared abstract. A concrete reward(Player p) method that all subclasses share lives here, and it works by iterating over the quest's reward strategies and calling apply(player) on each one.
At minimum, three concrete subclasses must extend Quest:
- KillQuest — tracks how many of a specific mob type the player has killed. Requires a target mob type and a kill count goal. Stores
int killsAchievedinternally and updates it when kill events are received. - GatherQuest — checks whether the player has a required item and quantity in their inventory. Requires a target material and an amount.
- ExploreQuest — checks whether the player has visited a specific location or region. Requires a target coordinate and a radius.
Each subclass must hold the fields specific to its own logic and override isComplete() with meaningfully different logic.
Inheritance & Polymorphism
The quest tracker must never check the type of a quest when evaluating completion. The loop that checks and rewards quests must look like this — or something equivalent:
for (Quest q : activeQuests.get(player.getUniqueId())) {
if (q.isComplete(player)) {
q.reward(player);
completed.add(q.getId());
}
}
No instanceof, no type casting in the completion loop. Each subclass's isComplete() contains all the logic needed to evaluate itself. If you find yourself writing if (q instanceof KillQuest) anywhere in the tracker, that logic belongs inside KillQuest instead.
Containers
Two containers are required:
HashMap<UUID, List<Quest>> activeQuests— maps each player's UUID to their list of currently active quests. Must support adding a quest on acceptance and removing it on completion or abandonment.HashMap<UUID, Set<String>> completedQuestIds— maps each player's UUID to the set of quest IDs they have already finished. Used to prevent re-accepting completed quests. ASetis the right choice here because order doesn't matter and duplicate checking is instant.
Interfaces
A Trackable interface must be defined and implemented by at least two of your quest subclasses:
interface Trackable {
int getProgress(Player p);
int getGoal();
String getProgressLabel();
}
The HUD code checks if (q instanceof Trackable) and displays a progress bar only for quests that implement it.
Why an interface and not an abstract class? Not all quests are trackable — ExploreQuest for example might simply be complete or not, with no meaningful intermediate progress. Putting getProgress() in the Quest abstract class would force every subclass to implement it even when it makes no sense. An interface lets only the quests that can show progress opt in to that contract, while the rest are unaffected. Note that this instanceof check is legitimate — it is checking for a capability, not a type, which is exactly what interfaces are for.
Design Pattern Requirements
Observer — Quest Event Routing
When a game event fires — a player kills a mob, picks up an item, moves to a new location — the right quest objects need to find out. The naive solution is to check quest types in the event handler, but this is the same problem the polymorphism requirement already forbids in the completion loop. The solution is the Observer pattern.
Define a separate listener interface for each event category:
interface KillEventListener { void onKill(Player p, EntityType mob); }
interface GatherEventListener { void onItemGather(Player p, Material mat, int amount); }
interface MoveEventListener { void onMove(Player p, Location loc); }
A central QuestEventDispatcher registers as a Bukkit listener and maintains a separate list for each interface. When an event fires, it fans the call out to all registered listeners of the relevant type. Each quest subclass implements only the interfaces relevant to its logic — KillQuest implements KillEventListener, GatherQuest implements GatherEventListener, and so on.
Registration is tied to the quest lifecycle: when a player accepts a quest it is registered with the dispatcher, and when it is completed or abandoned it is unregistered. This ensures only active quests are notified.
The instanceof checks inside register() are legitimate here — the dispatcher is checking for capabilities (does this quest care about kill events?), not branching on quest type to perform quest-specific logic:
void register(Quest q) {
if (q instanceof KillEventListener k) killListeners.add(k);
if (q instanceof GatherEventListener g) gatherListeners.add(g);
if (q instanceof MoveEventListener m) moveListeners.add(m);
}
Strategy — Rewards
The reward system must use the Strategy pattern. Rather than hardcoding reward logic into quest subclasses, the Quest base class holds a List<RewardStrategy> and its reward(Player p) method simply calls apply(player) on each one. This means reward types are fully interchangeable and composable — a single quest can give experience, an item, and run a server command simultaneously.
The following implementations are required:
- ExperienceReward — grants the player a configurable amount of XP
- ItemReward — adds a configurable item and quantity to the player's inventory
- CommandReward — runs a server command with the player's name substituted in
CommandReward deserves special attention: by running a command like /eco give {player} 500, it allows integration with any plugin that exposes commands — economy plugins, rank plugins, custom systems — without your quest plugin taking a hard dependency on any of them. This is the Strategy pattern's open/closed principle in practice: new reward types can be added without touching the quest hierarchy.
MVC — Admin Dashboard
The Swing admin dashboard (Part 5) must follow the Model-View-Controller pattern. The boundaries between layers must be strict:
- Model — plain Java objects holding the shared state: active quests per player, progress data, the event log. No Bukkit imports, no Swing imports. Could run in a unit test.
- View — the Swing window and all its components. Reads from the model to render, fires events to the controller when the operator interacts. No Bukkit imports.
- Controller — the only layer that imports both Bukkit and Swing. Receives Bukkit game events and updates the model; receives Swing UI events and schedules Bukkit tasks in response. It is the bridge.
If you find a Bukkit import in the View or a Swing import in the Model, something has leaked across the boundary.
Part by Part
Part 1 — Build Your Quest Hierarchy
This part is about modelling what a quest is in code, before worrying about how it gets triggered or tracked in-game.
Build the Quest abstract class and at least three concrete subclasses. Each subclass must hold its own fields — a KillQuest needs a target mob type and a kill count goal, a GatherQuest needs a material and an amount. The isComplete() method doesn't need real logic yet — a stub returning false is fine for now.
Add a simple /acceptquest command that assigns a hardcoded quest to the player and stores it in the HashMap. That's enough for part 1 — the objects should exist and be assignable, even if they don't do anything yet.
What you decide in this part:
- What quest types you want to support and what fields each needs
- What reward strategies to implement and how quests are constructed with them
- Whether there's a quest limit per player and how you'll represent that
Checkpoint: Plugin loads. A player can run /acceptquest and have a quest assigned to them and stored in the HashMap.
Part 2 — Quests That Can Be Completed
In this part quests become real. Your goal is a working end-to-end quest loop: a player accepts a quest, goes and does the thing, the system detects completion, and the reward fires.
isComplete() needs real logic in each subclass. Somewhere — on a timer, on an event, your choice — the plugin iterates over the player's active quests and calls isComplete() on each one.
This part is also where you implement the Observer pattern for event routing. Define the listener interfaces (KillEventListener, GatherEventListener, MoveEventListener) and build the QuestEventDispatcher. Wire up registration so that accepting a quest registers it and completing or abandoning it unregisters it. Each quest subclass should implement the relevant listener interface and update its internal state when notified.
What you decide in this part:
- Which listener interfaces each quest type needs to implement
- Whether
isComplete()is a pure read-only check or whether it also updates internal state - What happens to quest progress if a player logs off mid-quest
Checkpoint: At least one quest of each type is completable end-to-end through normal gameplay. Completed quests cannot be re-accepted.
Part 3 — The Trackable Interface and HUD
This part introduces the Trackable interface and wires up a HUD so players can see their progress.
Define the Trackable interface and implement it on at least two of your quest subclasses — KillQuest and GatherQuest are natural fits since both have a numeric current/goal relationship. ExploreQuest might not implement it if "visited or not" is the only meaningful state.
In your quest display code, check if (q instanceof Trackable) and show a boss bar or action bar with the progress label for quests that support it. Quests that don't implement Trackable simply show no progress bar.
What you decide in this part:
- What
getProgressLabel()returns for each trackable quest type - Whether the progress bar updates in real time or only when the player does something
- How you handle a player with multiple active trackable quests — one bar, multiple bars, or a toggle?
Checkpoint: Players with an active trackable quest see a progress bar. The bar updates as they make progress. Non-trackable quests show no bar.
Part 4 — Quest Board UI and Rewards
This part you build the interface players use to browse and accept quests, and fully implement the reward system.
The quest board should be accessible in-game — a chest GUI triggered by right-clicking a sign, an NPC interaction, a command, or whatever fits your server's feel. Players should be able to see available quests, read their descriptions, accept one, and later hand it in for the reward.
This is also where the RewardStrategy implementations come together. Each quest should be constructed with a list of reward strategies, and handing in a completed quest should trigger all of them. Make sure at least one quest uses multiple strategies simultaneously — for example giving both XP and an item — to demonstrate that the system composes correctly.
Make sure at least five distinct quests are available across your quest types. The board should not show quests the player has already completed, and should not allow accepting a quest they already have active.
What you decide in this part:
- How the board is accessed in-game
- How quests are handed in — a separate sign, the same board, a command
- How the board looks — item icons, lore text, colour coding by type
- What reward strategies each quest uses
- Any polish you want to add: quest log command, completion messages, sound effects
Checkpoint: A player can open the quest board, browse available quests, accept one, complete it, and hand it in — all without using commands. Rewards fire correctly. At least five distinct quests exist across all types.
Part 5 — Admin Dashboard (Swing + MVC)
This part adds a server-side admin dashboard: a Java Swing window that runs on the server machine, giving the operator a live view of all active quests and a log of quest completions.
The threading problem
Running Swing from within a Spigot plugin means dealing with three distinct threads:
- The Bukkit main thread — where all game logic runs
- The Swing Event Dispatch Thread (EDT) — where all Swing rendering and interaction must happen
- The shared model in between — data structures that both threads read and write
You cannot safely call Swing from the Bukkit thread, and you cannot safely call Bukkit from the Swing EDT. The solution is to keep all shared state in thread-safe data structures, use SwingUtilities.invokeLater() to push updates to the Swing side, and use Bukkit.getScheduler().runTask() to push changes back to the Bukkit side.
What to build
Live quest table: A JTable showing all online players, their active quests, and current progress. The table should refresh periodically using a javax.swing.Timer. Since KillQuest and GatherQuest already implement Trackable, you can call getProgress() and getGoal() to populate the progress column — the interface you built in Part 3 drives the dashboard directly.
Event log: A scrolling JTextArea that appends a timestamped line each time a quest is completed — for example [14:32] Steve completed Skeleton Slayer (+200 XP). Completion events are fired on the Bukkit thread and must be handed off safely to the Swing EDT before appending.
MVC structure
The dashboard must follow the MVC pattern as described in the Design Patterns section. Concretely:
- The Model holds a
ConcurrentHashMapof player quest data and a thread-safe queue for log events. It has no knowledge of how the data is displayed. - The View reads from the model on a timer and repaints the table and log. It fires action events to the controller when the operator interacts.
- The Controller listens to Bukkit events and writes to the model; it also listens to Swing actions and schedules Bukkit tasks in response.
Lifecycle
The dashboard window is launched in onEnable() in a new thread:
new Thread(() -> {
SwingUtilities.invokeLater(() -> new DashboardView(model, controller).setVisible(true));
}).start();
It must shut down cleanly in onDisable() — close the window and stop any running timers.
What you decide in this part:
- How the table is laid out and what columns it shows
- How frequently the table refreshes
- Any additional operator controls you want to add
Checkpoint: The dashboard window launches when the server starts. It shows all online players and their active quest progress in real time. The event log records quest completions as they happen. The window closes cleanly when the server stops.