☕ My First Java Game â€ĸ Episode 7 of 7 â€ĸ See All Episodes
🎮 Episode 7 â€ĸ Big Project â€ĸ RPG Part 3 of 3 â€ĸ Series Finale

RPG Quest
Part 3: The Dragon

The finale. Add a talking NPC with a real quest, a locked dragon gate, a multi-phase boss battle, and the victory ending your hero deserves. Everything from all seven episodes comes together here.

🎮 Ages 11+ 🎮 ~2 Hours ☕ Java ✓ Free
? NPC dialogue ? Quest tracking 🎮 Locked gates ? Boss phases ? Endings ? Bringing it together
⭐
0 XP
Level 1
đŸ”Ĩ0
Your Progress 0 / 6 steps
Build it in our Java Studio too: lay out your game, add real Java methods and export a runnable Swing project in the browser, with no install. Open the Java Mod Studio →
đŸ“Ļ
Reference project: download the finished code files used in this workshop: âŦ‡ java-rpg-part3-workshop-reference.zip. Every code block also has a Copy button.
đŸ’ģ
How to run: Write Java code in each step. The canvas shows what your code does, a live preview that maps Java concepts to a visual game. Full setup + offline compile guide stays below as reference

Build Java RPG Part 3 Step by Step

Each step adds a feature. Write the code, watch it come alive in the canvas.

Step 1 / 6
Click the NPC to start a conversation
1
🎮
The Village Elder (NPC)
A talkable character with multi-line dialogue
Active
🎮
Goal for this step

Add an NPC on the map you can talk to, with dialogue that advances.

  • 1New tile type 4 = NPC. Place the Elder near the start and draw them purple.
  • 2Talking: pressing E while standing NEXT to (not on) the NPC sets state = "DIALOGUE".
  • 3"Next to" means the row differs by 1 OR the column differs by 1: Math.abs(heroRow-npcRow) + Math.abs(heroCol-npcCol) == 1, that sum is called Manhattan distance.
  • 4Dialogue is a String[] lines plus an index; E advances a line; after the last line, back to EXPLORE.
  • 5Draw a text box along the bottom with the current line, classic RPG style.
GamePanel.java
    int npcRow = 2, npcCol = 3;
    String[] dialogue = {
        "Elder: Ah, " + "Aria" + "... the Dragon stirs again.",
        "Elder: Its gate only opens to the DRAGON KEY.",
        "Elder: Three goblins guard its pieces. Slay them.",
        "Elder: Return victorious, and the realm is saved!"
    };
    int dialogueLine = 0;

    boolean nextToNpc() {
        return Math.abs(heroRow - npcRow)
             + Math.abs(heroCol - npcCol) == 1;
    }

    // in keyPressed (EXPLORE):
    if (e.getKeyChar() == 'e' && nextToNpc()) {
        state = "DIALOGUE";
        dialogueLine = 0;
    }
    // in keyPressed (DIALOGUE):
    if (e.getKeyChar() == 'e') {
        dialogueLine++;
        if (dialogueLine >= dialogue.length) state = "EXPLORE";
    }
🎮 The base template (continues your Part 2 RPG)

Part 3 continues your Part 2 project (hero, world, monsters and combat). If you need the base Swing template it is here; otherwise open your Part 2 files and keep building.

Main.java
// The starting template every game in this series uses.
// Save as Main.java, then run:  javac Main.java   and   java Main
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

public class Main {
    public static void main(String[] args) {
        JFrame frame = new JFrame("My Java Game");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(new GamePanel());
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
}

class GamePanel extends JPanel implements ActionListener {
    Timer timer = new Timer(16, this);         // ~60 FPS
    public GamePanel() {
        setPreferredSize(new Dimension(700, 560));
        setBackground(new Color(25, 30, 60));   // night sky
        timer.start();
    }
    public void actionPerformed(ActionEvent e) {
        update();                               // move everything
        repaint();                              // then draw everything
    }
    void update() { /* movement goes here */ }
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        // drawing goes here
    }
}
🎮
Fill in the Blanks
+15 XP
The distance check |?row| + |?col| == 1 is called distance. Dialogue advances with the key until the line index reaches dialogue..
🧠
Knowledge Check
+15 XP
Why talk to an NPC you are NEXT TO rather than standing on?
AStanding on people is rude (and looks broken), the NPC tile stays solid like a wall
BJava cannot detect same-tile overlap
CIt saves a tile type
2
🎮
The Quest System
Track goblin kills; the Elder reacts to your progress
Locked
🎮
Goal for this step

Turn the Elder's words into a real, trackable quest with a HUD counter.

  • 1Quest state is small: boolean questActive; int goblinsSlain;, big systems, small variables.
  • 2Finishing the Elder's dialogue the first time sets questActive = true.
  • 3In battleAction, when a defeated foe's name equals "Goblin" and the quest is active: goblinsSlain++.
  • 4HUD shows 🎮 Goblins: 2/3 while the quest runs.
  • 5Make the Elder's dialogue CHANGE with progress, quest given, quest in progress, quest done. Three small String arrays and a pick.
GamePanel.java
    boolean questActive = false;
    int goblinsSlain = 0;

    // when a monster dies in battleAction:
    if (!foe.isAlive()) {
        hero.gainXp(foe.xpReward);
        hero.addGold(foe.goldReward);
        if (questActive && foe.name.equals("Goblin"))
            goblinsSlain++;
        state = "EXPLORE";
    }

    // choosing what the Elder says:
    String[] currentDialogue() {
        if (!questActive)          return dialogue;        // gives quest
        if (goblinsSlain < 3)      return new String[]{
            "Elder: " + goblinsSlain + " of 3 goblins slain. Fight on!" };
        return new String[]{
            "Elder: Incredible! Take the DRAGON KEY.",
            "Elder: The gate in the north-east awaits. Good luck, hero." };
    }
🎮
Code Challenge
+20 XP
Count quest kills only when they should count:
GamePanel.java
if ( && foe.name.equals())
    goblinsSlain;
🎮 Hint: Two conditions: the quest must be running, and the dead monster must be the right species. Then add one.
🧠
Knowledge Check
+15 XP
The Elder's dialogue changes based on quest progress. Why does that matter?
AIt uses less memory than one long speech
BThe world reacts to the player's actions, that reactivity is what makes RPGs feel alive
CNPCs must have exactly three speeches
3
🎮
The Dragon Key & Gate
A locked tile that consumes the key item to open
Locked
🎮
Goal for this step

Gate the boss behind the quest reward, classic lock-and-key design.

  • 1When the Elder's "quest done" dialogue finishes, grant the key once: hero.pickUp(new Item("Dragon Key","key",0)), guard it with a boolean so it cannot be granted twice!
  • 2New tile type 5 = the Gate. Place it walling off a boss chamber in the north-east.
  • 3In walkable(): tile 5 is walkable ONLY if the hero carries the key. Give Hero a hasItem(String) helper.
  • 4Stepping through consumes the key and swings the gate: set the tile to 0 and log "The gate grinds open!".
  • 5Lock-and-key is the oldest design pattern in games, you have now built it from scratch.
Hero.java + GamePanel.java
// Hero:
public boolean hasItem(String itemName) {
    for (Item it : inventory)
        if (it.name.equals(itemName)) return true;
    return false;
}

// GamePanel.walkable():
if (map[r][c] == 5)
    return hero.hasItem("Dragon Key");

// after moving onto the gate tile:
if (map[heroRow][heroCol] == 5) {
    hero.useItem("Dragon Key");      // consumed!
    map[heroRow][heroCol] = 0;
}
🎮
Fill in the Blanks
+15 XP
Tile type is the gate. It is only walkable when hero.("Dragon Key") returns true, and stepping through s the key so it is consumed.
🧠
Knowledge Check
+15 XP
What stops the player reaching the Dragon at level 1 and getting flattened?
AThe Dragon is friendly at first
BThe gate needs the key, the key needs 3 goblin kills, the quest chain forces you to level up first
CThe map is too big
4
🎮
The Dragon: A Boss With Phases
A two-phase boss that changes tactics at half health
Locked
🎮
Goal for this step

Build a boss fight that feels different from every normal battle.

  • 1The Dragon is a Monster with big numbers: 80 HP, 9 attack, 100 XP, 150 gold.
  • 2New tile type 6 inside the chamber triggers the fight directly (no random roll), bosses are placed, not random.
  • 3Phase 2 at half health: attack +4 and the log roars "The Dragon is ENRAGED!", draw its HP bar orange.
  • 4Enrage exactly once: flip a boolean when hp = maxHp/2 the first time.
  • 5Losing to the Dragon respawns you OUTSIDE the chamber with the gate closed but quest complete, retry without redoing everything.
GamePanel.java
    boolean enraged = false;

    // stepping on tile 6:
    if (map[heroRow][heroCol] == 6) {
        foe = new Monster("Dragon", 80, 9, 100, 150);
        enraged = false;
        state = "BATTLE";
    }

    // in battleAction, after damaging the boss:
    if (foe.name.equals("Dragon") && !enraged
            && foe.hp <= foe.maxHp / 2) {
        enraged = true;
        foe.attack += 4;
        battleLog = "The Dragon is ENRAGED!";
    }
🎮
Code Challenge
+20 XP
Trigger the enrage exactly once, at half health:
GamePanel.java
if (foe.name.equals("Dragon") && !
        && foe.hp <= foe.maxHp / ) {
    enraged = ;
    foe.attack += 4;
}
🎮 Hint: The boolean guard stops it re-triggering every frame; half health is maxHp divided by two; then latch the flag.
🧠
Knowledge Check
+15 XP
Why do bosses change behaviour mid-fight?
ATo fix bugs in phase one
BA long fight with one pattern gets boring, phases force the player to adapt and create a story arc
CJava switches require it
5
🎮
The Ending
Victory state, credits and the hero's final stats
Locked
🎮
Goal for this step

Defeating the Dragon triggers a proper ending screen worthy of the journey.

  • 1When the defeated foe is the Dragon: state = "VICTORY" instead of EXPLORE.
  • 2The VICTORY screen is your credits: golden "THE REALM IS SAVED!", the hero's final level, gold, goblins slain, and your name as the developer. You earned that line.
  • 3Let the ending breathe: accept R only after a 2-second delay (a small frame counter).
  • 4Add a play-again path that resets the world, or leave the hero basking forever. Your call, developer.
GamePanel.java (paintComponent)
    if (state.equals("VICTORY")) {
        g.setColor(new Color(15, 10, 25));
        g.fillRect(0, 0, 700, 560);
        g.setColor(new Color(255, 209, 102));
        g.setFont(new Font("Arial", Font.BOLD, 40));
        g.drawString("THE REALM IS SAVED!", 110, 200);
        g.setColor(Color.WHITE);
        g.setFont(new Font("Arial", Font.PLAIN, 20));
        g.drawString(hero.getName() + ", Level " + hero.getLevel(), 110, 260);
        g.drawString("Goblins slain: " + goblinsSlain + " â€ĸ Dragon: DEFEATED", 110, 290);
        g.drawString("A game by YOU", 110, 350);
        return;
    }
🎮
Fill in the Blanks
+15 XP
Beating the Dragon sets state to rather than EXPLORE. The ending screen shows the hero's final and, most importantly, credits as the developer.
🧠
Knowledge Check
+15 XP
Why delay the restart key on the ending screen?
AJava needs time to garbage-collect
BSo players cannot accidentally skip the ending they spent seven episodes earning
CTimers cannot stop instantly
6
🎮
Series Wrap: What You Actually Learned
Review the architecture and where to go next
Locked
🎮
Goal for this step

Step back and see the professional patterns hiding inside your seven games.

  • 1Look at what you used: game loop (every episode), state machines (Pong, RPG), encapsulation (Hero), factories (Monster.create), tile maps (RPG), ArrayLists + iterators (Space Shooter), lock-and-key design (the gate).
  • 2Those are not "beginner tricks", they are the same patterns inside Minecraft (Java!), Stardew Valley and Undertale.
  • 3Stretch goals if you want more: save/load with a file, sound effects with javax.sound, sprites with ImageIO instead of rectangles, or a second dungeon map.
  • 4Or go bigger: this series used pure Java so you learned the real thing, engines like libGDX will now feel easy.
  • 5Finish the quiz below to complete the entire Java series. GG, developer. ?
🎮
The best next project is one you invent. Pick a small idea you love, and you now have every tool to build it.
🎮
Fill in the Blanks
+15 XP
The Monster.create method is an example of the pattern, the Hero's private fields demonstrate , and MENU/PLAYING/GAME_OVER is a state .
🧠
Knowledge Check
+15 XP
Seven games later, what is the single most important habit you practised in every episode?
ATyping fast
BBuild one small piece, run it, test it, THEN add the next piece
CUsing as many classes as possible
✅ See the complete finished RPG , all three parts assembled

The whole game in one runnable file: hero, tile world and loot (Part 1), monsters and turn-based combat (Part 2), plus an NPC with dialogue, a 3-goblin quest, a key-gated boss chamber and a Dragon boss (Part 3). Save as Main.java, run javac Main.java && java Main, and play it start to finish. Stuck? Compare your file against this.

Main.java
// Complete Java RPG (the full game you finish by the end of Part 3).
// All classes are in one file so it runs with:  javac Main.java  then  java Main
// In the workshops you split Hero, Item and Monster into their own files, which also works.
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.List;
import java.util.ArrayList;

public class Main {
    public static void main(String[] args) {
        JFrame frame = new JFrame("RPG Quest");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(new GamePanel());
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
}

class Item {
    String name, type;
    int power;
    public Item(String name, String type, int power) {
        this.name = name; this.type = type; this.power = power;
    }
}

class Hero {
    private String name;
    private int maxHp = 30, hp = 30, attack = 5, gold = 0, xp = 0, level = 1;
    private List<Item> inventory = new ArrayList<>();

    public Hero(String name) { this.name = name; }

    public void takeDamage(int amount) { hp = Math.max(0, hp - amount); }
    public void heal(int amount)       { hp = Math.min(maxHp, hp + amount); }
    public boolean isAlive()           { return hp > 0; }

    public void pickUp(Item item) { inventory.add(item); }
    public boolean hasItem(String itemName) {
        for (Item it : inventory) if (it.name.equals(itemName)) return true;
        return false;
    }
    public boolean useItem(String itemName) {
        for (Item it : inventory) {
            if (it.name.equals(itemName)) {
                if (it.type.equals("potion")) heal(it.power);
                if (it.type.equals("weapon")) attack += it.power;
                inventory.remove(it);
                return true;
            }
        }
        return false;
    }
    public void gainXp(int amount) {
        xp += amount;
        while (xp >= level * 20) {          // level up
            xp -= level * 20;
            level++; maxHp += 10; hp = maxHp; attack += 2;
        }
    }
    public void addGold(int amount) { gold += amount; }

    public String getName() { return name; }
    public int getHp()      { return hp; }
    public int getMaxHp()   { return maxHp; }
    public int getAttack()  { return attack; }
    public int getGold()    { return gold; }
    public int getLevel()   { return level; }
    public int inventorySize() { return inventory.size(); }
}

class Monster {
    String name;
    int hp, maxHp, attack, xpReward, goldReward;
    public Monster(String name, int hp, int attack, int xp, int gold) {
        this.name = name; this.maxHp = hp; this.hp = hp;
        this.attack = attack; this.xpReward = xp; this.goldReward = gold;
    }
    public static Monster create(String type) {
        switch (type) {
            case "slime":  return new Monster("Slime", 12, 3, 8, 5);
            case "wolf":   return new Monster("Wolf", 20, 5, 15, 12);
            case "goblin": return new Monster("Goblin", 28, 7, 25, 20);
            case "dragon": return new Monster("Dragon", 80, 9, 100, 150);
            default:       return new Monster("Slime", 12, 3, 8, 5);
        }
    }
    public void takeDamage(int amount) { hp = Math.max(0, hp - amount); }
    public boolean isAlive() { return hp > 0; }
}

class GamePanel extends JPanel implements ActionListener, KeyListener {
    Timer timer = new Timer(16, this);
    int TILE = 40;

    // 0 grass, 1 wall, 2 water, 3 chest, 4 NPC, 5 gate, 6 dragon lair
    int[][] map = {
        {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
        {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,1},
        {1,0,0,4,0,0,0,2,2,2,0,0,0,0,0,0,1},
        {1,0,0,0,0,0,0,2,2,2,0,0,0,0,0,0,1},
        {1,0,0,0,0,1,1,0,0,0,1,1,0,0,0,0,1},
        {1,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,1},
        {1,0,0,0,0,1,0,0,3,0,0,1,0,0,0,0,1},
        {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
        {1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1},
        {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
        {1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
        {1,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,1},
        {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,6,1},
        {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}
    };

    Hero hero = new Hero("Aria");
    int heroRow = 1, heroCol = 1;
    int npcRow = 2, npcCol = 3;

    String state = "EXPLORE";       // EXPLORE / BATTLE / DIALOGUE / WIN / GAMEOVER
    Monster foe;
    boolean isDragon = false, defending = false;
    int safeSteps = 3;
    String battleLog = "";

    int dialogueLine = 0;
    boolean questActive = false, keyGranted = false;
    int goblinsSlain = 0;

    public GamePanel() {
        setPreferredSize(new Dimension(700, 560));
        setBackground(new Color(20, 24, 30));
        addKeyListener(this);
        setFocusable(true);
        timer.start();
    }

    public void actionPerformed(ActionEvent e) { repaint(); }   // turn-based: redraw only

    boolean walkable(int r, int c) {
        if (r < 0 || r >= map.length || c < 0 || c >= map[0].length) return false;
        int t = map[r][c];
        if (t == 5) return hero.hasItem("Dragon Key");          // gate needs the key
        return t == 0 || t == 3 || t == 6;                      // grass, chest, dragon lair
    }

    boolean nextToNpc() {
        return Math.abs(heroRow - npcRow) + Math.abs(heroCol - npcCol) == 1;
    }

    String[] elderLines() {
        if (!questActive)      return new String[]{
            "Elder: Aria... the Dragon stirs in the north-east.",
            "Elder: Slay 3 goblins to prove yourself, then seek me again." };
        if (goblinsSlain < 3)  return new String[]{
            "Elder: The goblins still roam. " + goblinsSlain + " of 3 felled." };
        return new String[]{
            "Elder: You are ready. Take this Dragon Key.",
            "Elder: The gate in the north-east awaits. Good luck, hero." };
    }

    void tryMove(int dr, int dc) {
        int r = heroRow + dr, c = heroCol + dc;
        if (!walkable(r, c)) return;

        if (map[r][c] == 5) { hero.useItem("Dragon Key"); map[r][c] = 0; battleLog = "The gate grinds open..."; }
        heroRow = r; heroCol = c;

        if (map[r][c] == 3) { hero.pickUp(new Item("Potion", "potion", 12)); hero.addGold(25); map[r][c] = 0; }

        if (map[r][c] == 6) {                    // stepped into the lair
            foe = Monster.create("dragon"); isDragon = true; defending = false;
            battleLog = "The Dragon roars!"; state = "BATTLE"; return;
        }

        // random encounter on grass
        if (map[r][c] == 0) {
            if (safeSteps > 0) safeSteps--;
            else if (Math.random() < 0.18) {
                double roll = Math.random();
                String type = roll < 0.55 ? "slime" : (roll < 0.85 ? "wolf" : "goblin");
                foe = Monster.create(type); isDragon = false; defending = false;
                battleLog = "A wild " + foe.name + " appears!";
                state = "BATTLE";
            }
        }
    }

    void doBattle(char a) {
        boolean acted = true;
        if (a == 'a') {
            int dmg = hero.getAttack();
            foe.takeDamage(dmg);
            battleLog = hero.getName() + " hits " + foe.name + " for " + dmg + "!";
        } else if (a == 'd') {
            defending = true;
            battleLog = hero.getName() + " braces for the blow.";
        } else if (a == 'p') {
            battleLog = hero.useItem("Potion") ? "Aria drinks a Potion!" : "No potions left!";
        } else if (a == 'f') {
            if (isDragon) { battleLog = "The Dragon blocks your escape!"; }
            else { state = "EXPLORE"; safeSteps = 6; battleLog = "You fled to safety."; return; }
        } else { acted = false; }
        if (!acted) return;

        if (!foe.isAlive()) {                    // monster defeated
            hero.gainXp(foe.xpReward);
            hero.addGold(foe.goldReward);
            if (isDragon) { state = "VICTORY"; return; }
            if (questActive && foe.name.equals("Goblin")) goblinsSlain++;
            state = "EXPLORE"; safeSteps = 4;
            return;
        }

        int dmg = foe.attack;                    // monster retaliates
        if (defending) { dmg = dmg / 2; defending = false; }
        hero.takeDamage(dmg);
        battleLog += "  " + foe.name + " hits back for " + dmg + "!";
        if (!hero.isAlive()) state = "GAME_OVER";
    }

    void restart() {
        // rebuild everything for a fresh run
        map[1][15] = 3; map[6][8] = 3; map[11][14] = 5; map[12][15] = 6;
        hero = new Hero("Aria");
        heroRow = 1; heroCol = 1; state = "EXPLORE"; safeSteps = 3;
        questActive = false; keyGranted = false; goblinsSlain = 0; dialogueLine = 0;
        battleLog = "";
    }

    public void keyPressed(KeyEvent e) {
        int k = e.getKeyCode();
        char ch = Character.toLowerCase(e.getKeyChar());

        if (state.equals("VICTORY") || state.equals("GAME_OVER")) {
            if (k == KeyEvent.VK_R) { restart(); repaint(); }
            return;
        }
        if (state.equals("DIALOGUE")) {
            if (k == KeyEvent.VK_E || k == KeyEvent.VK_ENTER) {
                dialogueLine++;
                String[] lines = elderLines();
                if (dialogueLine >= lines.length) {
                    if (!questActive) questActive = true;
                    else if (goblinsSlain >= 3 && !keyGranted) {
                        hero.pickUp(new Item("Dragon Key", "key", 0));
                        keyGranted = true;
                    }
                    state = "EXPLORE";
                }
            }
            repaint(); return;
        }
        if (state.equals("BATTLE")) {
            if (k == KeyEvent.VK_A) doBattle('a');
            if (k == KeyEvent.VK_D) doBattle('d');
            if (k == KeyEvent.VK_P) doBattle('p');
            if (k == KeyEvent.VK_F) doBattle('f');
            repaint(); return;
        }
        // EXPLORE
        if (ch == 'e' && nextToNpc()) { state = "DIALOGUE"; dialogueLine = 0; repaint(); return; }
        if (k == KeyEvent.VK_UP)    tryMove(-1, 0);
        if (k == KeyEvent.VK_DOWN)  tryMove(1, 0);
        if (k == KeyEvent.VK_LEFT)  tryMove(0, -1);
        if (k == KeyEvent.VK_RIGHT) tryMove(0, 1);
        repaint();
    }
    public void keyReleased(KeyEvent e) {}
    public void keyTyped(KeyEvent e) {}

    void drawBar(Graphics g, int x, int y, int val, int max) {
        g.setColor(Color.RED);
        g.fillRect(x, y, 120, 14);
        g.setColor(new Color(6, 214, 160));
        g.fillRect(x, y, Math.max(0, 120 * val / max), 14);
    }

    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        g.setFont(new Font("Arial", Font.BOLD, 18));

        if (state.equals("BATTLE")) {
            g.setColor(new Color(20, 12, 28));
            g.fillRect(0, 0, 700, 560);
            g.setColor(Color.WHITE);
            g.setFont(new Font("Arial", Font.BOLD, 22));
            g.drawString(hero.getName(), 60, 380);
            g.drawString(foe.name, 470, 140);
            drawBar(g, 60, 392, hero.getHp(), hero.getMaxHp());
            drawBar(g, 470, 152, foe.hp, foe.maxHp);
            g.setFont(new Font("Arial", Font.PLAIN, 18));
            g.drawString(battleLog, 60, 280);
            g.drawString("[A]ttack   [D]efend   [P]otion   [F]lee", 60, 500);
            return;
        }

        if (state.equals("DIALOGUE")) {
            drawWorld(g);
            g.setColor(new Color(0, 0, 0, 200));
            g.fillRect(40, 420, 620, 110);
            g.setColor(Color.WHITE);
            String[] lines = elderLines();
            g.drawString(lines[Math.min(dialogueLine, lines.length - 1)], 60, 470);
            g.setFont(new Font("Arial", Font.PLAIN, 14));
            g.drawString("Press E to continue", 60, 510);
            return;
        }

        drawWorld(g);

        if (state.equals("VICTORY") || state.equals("GAME_OVER")) {
            g.setColor(new Color(0, 0, 0, 190));
            g.fillRect(0, 0, 700, 560);
            g.setColor(state.equals("VICTORY") ? new Color(255, 209, 102) : new Color(248, 113, 113));
            g.setFont(new Font("Arial", Font.BOLD, 46));
            g.drawString(state.equals("VICTORY") ? "THE REALM IS SAVED!" : "YOU HAVE FALLEN", 90, 270);
            g.setColor(Color.WHITE);
            g.setFont(new Font("Arial", Font.PLAIN, 18));
            g.drawString("Press R to play again", 260, 310);
        }
    }

    void drawWorld(Graphics g) {
        for (int r = 0; r < map.length; r++)
            for (int c = 0; c < map[0].length; c++) {
                switch (map[r][c]) {
                    case 0 -> g.setColor(new Color(52, 120, 62));
                    case 1 -> g.setColor(new Color(90, 90, 100));
                    case 2 -> g.setColor(new Color(50, 90, 180));
                    case 3 -> g.setColor(new Color(212, 175, 55));
                    case 4 -> g.setColor(new Color(167, 139, 250));
                    case 5 -> g.setColor(new Color(120, 80, 40));
                    case 6 -> g.setColor(new Color(180, 40, 40));
                }
                g.fillRect(c * TILE, r * TILE, TILE, TILE);
            }
        // hero
        g.setColor(new Color(230, 70, 60));
        g.fillRect(heroCol * TILE + 6, heroRow * TILE + 6, TILE - 12, TILE - 12);

        // HUD strip
        g.setColor(new Color(0, 0, 0, 170));
        g.fillRect(0, 0, 700, 34);
        drawBar(g, 90, 10, hero.getHp(), hero.getMaxHp());
        g.setColor(Color.WHITE);
        g.setFont(new Font("Arial", Font.BOLD, 16));
        g.drawString(hero.getName(), 12, 24);
        g.drawString("Lv" + hero.getLevel() + "  Gold " + hero.getGold()
                + "  Goblins " + goblinsSlain + "/3", 230, 24);
    }
}
🎮
What you just learned:
  • Adding a talkable NPC with multi-line dialogue and Manhattan distance detection
  • Quest tracking with boolean flags and kill counters that update HUD display
  • Lock-and-key design: a gated tile that only opens when the player has a specific item
  • Multi-phase boss fights with enrage at half health and escalating difficulty
  • Victory screen with credits, hero stats, and a series-spanning architecture review
🎮🎮🎮🎮
Workshop Complete!
Seven episodes. One complete RPG with dialogue, quests and a dragon. You are not learning Java any more, you are a Java game developer. Take a bow. 🎮
0
Total XP
1
Level
0
Best Streak
0%
Accuracy
⭐ View My Progress & Certificates

This workshop was free and took many hours to build. If it helped you learn something new, consider supporting the project.

☕ Support on Ko-fi

🧩 Java Game Loop Challenge

Drag the Java game steps into the correct order:

Call repaint() to refresh the screen
Check collisions and boundaries
Update positions in actionPerformed()
Override paintComponent(Graphics g)
Set up the Timer for the game loop
Extend JPanel and implement KeyListener