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


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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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.