โ˜• My First Java Game ยท Episode 5 of 7 ยท See All Episodes
๐Ÿ—ก๏ธ Episode 5 ยท Big Project ยท RPG Part 1 of 3

RPG Quest
Part 1: The Hero

The three-part finale begins! Design your hero with real class design, stats, inventory, levelling, and build a tile-map world to walk around. This part is all about the architecture that makes big games possible.

๐Ÿ‘ถ Ages 11+ โฑ๏ธ ~2 Hours โ˜• Java โœ“ Free
๐Ÿ—๏ธ Class design ๐Ÿ“Š Stats & levelling ๐ŸŽ’ Inventory ๐Ÿ—บ๏ธ Tile maps ๐Ÿšถ Grid movement ๐Ÿงฑ Wall collision
โญ
0 XP
Level 1
๐Ÿ”ฅ0
Your Progress 0 / 6 steps
1
๐Ÿ—๏ธ
Design Before Code
Plan the classes: Hero, Item, GamePanel, on paper first
Active
๐ŸŽฏ
Goal for this step

Sketch the architecture so the next three workshops slot together perfectly.

  • 1Big games die without a plan. Ours: Hero (stats + inventory), Item (things you carry), GamePanel (world + loop), and later Monster and Battle.
  • 2Each class has ONE job. Hero knows nothing about drawing; GamePanel knows nothing about damage maths. This is called separation of concerns.
  • 3Create the project folder RpgQuest with Main.java + GamePanel.java (700ร—560 window, the usual template).
  • 4Create two empty files ready for the next steps: Hero.java and Item.java.
  • 5Seriously, grab paper and draw three boxes with arrows before continuing. Two minutes now saves hours later.
๐Ÿ’ก
Professional studios draw this exact diagram (a class diagram) before writing any code on a new game.
๐Ÿ“ฆ Full starter template , the Swing window + game loop

The Main.java launcher plus the GamePanel game loop. Use 700ร—560 for the RPG in setPreferredSizesetPreferredSize. Part 1 splits Hero and Item into their own files; the finished code below shows everything in one runnable file.

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
    }
}// 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
Giving each class ONE clear job is called separation of . Our hero data lives in .java, while drawing and the game loop live in GamePanel.java.
๐Ÿง 
Knowledge Check
+15 XP
Why should the Hero class NOT contain drawing code?
ADrawing is too slow for heroes
BOne job per class, Hero manages stats/inventory, GamePanel draws. Each can change without breaking the other
CJava forbids Graphics outside panels
2
๐Ÿฆธ
The Hero Class
Name, HP, attack, gold, XP, with constructor and methods
Locked
๐ŸŽฏ
Goal for this step

Write a Hero class holding all the character data, plus safe methods to change it.

  • 1Fields: name, maxHp, hp, attack, gold, xp, level, all private (data protection!).
  • 2A constructor sets the starting values from the name you pass in.
  • 3Methods instead of raw field access: takeDamage(int) clamps HP at 0; heal(int) clamps at maxHp; isAlive() returns hp > 0.
  • 4This is encapsulation, the Hero controls its own rules, so HP can never go negative by accident.
Hero.java
public class Hero {
    private String name;
    private int maxHp = 30, hp = 30;
    private int attack = 5;
    private int gold = 0, xp = 0, level = 1;

    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 int getHp() { return hp; }
    public int getMaxHp() { return maxHp; }
    public int getAttack() { return attack; }
    public String getName() { return name; }
}public class Hero {
    private String name;
    private int maxHp = 30, hp = 30;
    private int attack = 5;
    private int gold = 0, xp = 0, level = 1;

    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 int getHp() { return hp; }
    public int getMaxHp() { return maxHp; }
    public int getAttack() { return attack; }
    public String getName() { return name; }
}
โŒจ๏ธ
Code Challenge
+20 XP
Write the two clamped methods that protect the heroโ€™s HP:
Hero.java
public void takeDamage(int amount) {
    hp = Math.public void takeDamage(int amount) {
    hp = Math.(0, hp - amount);
}
public void heal(int amount) {
    hp = Math.(0, hp - amount);
}
public void heal(int amount) {
    hp = Math.(maxHp, hp + amount);
}(maxHp, hp + amount);
}
๐Ÿ’ก Hint: Damage must never push HP below zero; healing must never push it above maxHp.
๐Ÿง 
Knowledge Check
+15 XP
The fields are private and changed only through methods. What does this prevent?
AOther classes setting hp = -50 or gold = 999999 directly, bypassing the game rules
BThe hero being drawn twice
CSlow compilation
3
๐ŸŽ’
Items & Inventory
An Item class and the heroโ€™s ArrayList of loot
Locked
๐ŸŽฏ
Goal for this step

Create items (potions, keys, swords) and let the hero carry and use them.

  • 1Item is small: a name, a type ("potion", "key", "weapon") and a power number.
  • 2Hero gains List<Item> inventory = new ArrayList<>(); plus pickUp(Item) and useItem(String name).
  • 3useItem finds the first item with that name: a potion heals by its power and is removed; a weapon adds its power to attack and is removed.
  • 4Test in main(): create a hero, damage them 20, pick up a Potion(12), use it, print HP, should read 22/30.
Item.java + Hero.java
public class Item {
    String name, type;
    int power;
    public Item(String name, String type, int power) {
        this.name = name; this.type = type; this.power = power;
    }
}

// inside Hero:
private java.util.List<Item> inventory = new java.util.ArrayList<>();

public void pickUp(Item item) { inventory.add(item); }

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;   // did not have one
}public class Item {
    String name, type;
    int power;
    public Item(String name, String type, int power) {
        this.name = name; this.type = type; this.power = power;
    }
}

// inside Hero:
private java.util.List<Item> inventory = new java.util.ArrayList<>();

public void pickUp(Item item) { inventory.add(item); }

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;   // did not have one
}
โœ๏ธ
Fill in the Blanks
+15 XP
The inventory is a List of objects. Using a potion calls the heroโ€™s method and then s the item from the list.
๐Ÿง 
Knowledge Check
+15 XP
Why does useItem return a boolean?
AAll Java methods must return something
BSo the caller knows whether the hero actually had that item, no potion, no heal
CBooleans are needed for ArrayLists
4
๐Ÿ—บ๏ธ
The Tile-Map World
Draw a world from a 2D int array: grass, walls, water
Locked
๐ŸŽฏ
Goal for this step

Build a visible world out of tiles, the same technique as Zelda and Pokรฉmon.

  • 1The world is an int[][] map, each number is a tile type: 0 grass, 1 wall, 2 water, 3 chest.
  • 2Our map is 14 rows ร— 17 columns of 40-pixel tiles (making 680ร—560).
  • 3Write the map by hand as a literal array, you are drawing a level with numbers!
  • 4paintComponent: nested loop, pick a colour per tile type, fillRect each 40ร—40 square.
  • 5Design your own layout, put walls around the edge, a lake, and a chest room.
GamePanel.java
    int TILE = 40;
    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,1,1,0,0,0,2,2,2,0,0,1,1,0,0,1},
        {1,0,1,0,0,0,0,2,2,2,0,0,0,1,0,0,1},
        {1,0,0,0,0,1,0,0,2,0,0,0,0,0,0,0,1},
        {1,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,1},
        {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}
        // ... build yours 14 rows tall!
    };

    // paintComponent:
    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));
            }
            g.fillRect(c*TILE, r*TILE, TILE, TILE);
        }    int TILE = 40;
    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,1,1,0,0,0,2,2,2,0,0,1,1,0,0,1},
        {1,0,1,0,0,0,0,2,2,2,0,0,0,1,0,0,1},
        {1,0,0,0,0,1,0,0,2,0,0,0,0,0,0,0,1},
        {1,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,1},
        {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}
        // ... build yours 14 rows tall!
    };

    // paintComponent:
    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));
            }
            g.fillRect(c*TILE, r*TILE, TILE, TILE);
        }
โŒจ๏ธ
Code Challenge
+20 XP
Convert a tileโ€™s row/column into pixels on screen:
GamePanel.java
g.fillRect(g.fillRect(*TILE, *TILE, *TILE, TILE, TILE);*TILE, TILE, TILE);
๐Ÿ’ก Hint: Columns run across (x direction); rows run down (y direction).
๐Ÿง 
Knowledge Check
+15 XP
In a tile map, what does map[3][7] == 2 mean?
AThere are 2 tiles at that spot
BThe tile at row 3, column 7 is water (type 2)
CThe map is 3ร—7 tiles
5
๐Ÿšถ
Walking the World
Grid movement that respects walls and water
Locked
๐ŸŽฏ
Goal for this step

Move the hero tile-by-tile with arrow keys, blocked by walls and water.

  • 1The heroโ€™s position is now a grid position: heroRow, heroCol, not pixels!
  • 2On keyPressed (not held, RPGs step once per press), work out the target tile.
  • 3Only move if the target tile is walkable: type 0 or 3. Walls (1) and water (2) block you.
  • 4Draw the hero as a red square at heroCol*TILE, heroRow*TILE, slightly smaller than the tile so the grass peeks out.
  • 5Walk around your world. Blocked by your own lake? That is level design working!
GamePanel.java
    int heroRow = 1, heroCol = 1;
    Hero hero = new Hero("Aria");

    boolean walkable(int r, int c) {
        if (r < 0 || r >= map.length || c < 0 || c >= map[0].length)
            return false;
        return map[r][c] == 0 || map[r][c] == 3;
    }

    public void keyPressed(KeyEvent e) {
        int r = heroRow, c = heroCol;
        if (e.getKeyCode() == KeyEvent.VK_UP)    r--;
        if (e.getKeyCode() == KeyEvent.VK_DOWN)  r++;
        if (e.getKeyCode() == KeyEvent.VK_LEFT)  c--;
        if (e.getKeyCode() == KeyEvent.VK_RIGHT) c++;
        if (walkable(r, c)) { heroRow = r; heroCol = c; }
        repaint();
    }    int heroRow = 1, heroCol = 1;
    Hero hero = new Hero("Aria");

    boolean walkable(int r, int c) {
        if (r < 0 || r >= map.length || c < 0 || c >= map[0].length)
            return false;
        return map[r][c] == 0 || map[r][c] == 3;
    }

    public void keyPressed(KeyEvent e) {
        int r = heroRow, c = heroCol;
        if (e.getKeyCode() == KeyEvent.VK_UP)    r--;
        if (e.getKeyCode() == KeyEvent.VK_DOWN)  r++;
        if (e.getKeyCode() == KeyEvent.VK_LEFT)  c--;
        if (e.getKeyCode() == KeyEvent.VK_RIGHT) c++;
        if (walkable(r, c)) { heroRow = r; heroCol = c; }
        repaint();
    }
โœ๏ธ
Fill in the Blanks
+15 XP
RPG movement is stored as a grid position: heroRow and . Before stepping we check (r, c), which returns false for walls and .
๐Ÿง 
Knowledge Check
+15 XP
Why check r < 0 || r >= map.length first in walkable()?
ATo make water solid
BReading outside the array bounds crashes Java, check the edges before reading the tile
CTo speed up walking
6
๐Ÿ’ฐ
Chests, HUD & Saving the Hero
Open chests for loot and show hero stats on screen
Locked
๐ŸŽฏ
Goal for this step

Loot chests into your inventory and display a proper RPG HUD.

  • 1Standing on a chest tile (3): give the hero loot, hero.pickUp(new Item("Potion","potion",12)); gold += 25;, then turn the tile to grass (0) so it cannot be looted twice.
  • 2Draw the HUD in a dark strip: name, HP bar (red background, green foreground scaled by hp/maxHp), gold and inventory count.
  • 3An HP bar beats an HP number, players read bars instantly.
  • 4Congratulations, Part 1 complete! You have a hero, a world, and loot. Part 2 fills the world with monsters and turn-based combat.
GamePanel.java
    // after a successful move:
    if (map[heroRow][heroCol] == 3) {
        hero.pickUp(new Item("Potion", "potion", 12));
        map[heroRow][heroCol] = 0;    // chest opened
    }

    // HUD in paintComponent:
    g.setColor(new Color(0, 0, 0, 170));
    g.fillRect(0, 0, 700, 34);
    g.setColor(Color.RED);
    g.fillRect(90, 10, 120, 14);
    g.setColor(new Color(6, 214, 160));
    g.fillRect(90, 10, 120 * hero.getHp() / hero.getMaxHp(), 14);
    g.setColor(Color.WHITE);
    g.drawString(hero.getName(), 12, 22);    // after a successful move:
    if (map[heroRow][heroCol] == 3) {
        hero.pickUp(new Item("Potion", "potion", 12));
        map[heroRow][heroCol] = 0;    // chest opened
    }

    // HUD in paintComponent:
    g.setColor(new Color(0, 0, 0, 170));
    g.fillRect(0, 0, 700, 34);
    g.setColor(Color.RED);
    g.fillRect(90, 10, 120, 14);
    g.setColor(new Color(6, 214, 160));
    g.fillRect(90, 10, 120 * hero.getHp() / hero.getMaxHp(), 14);
    g.setColor(Color.WHITE);
    g.drawString(hero.getName(), 12, 22);
โŒจ๏ธ
Code Challenge
+20 XP
Scale the HP bar to the heroโ€™s current health:
GamePanel.java
g.fillRect(90, 10, 120 * hero.g.fillRect(90, 10, 120 * hero.() / hero.() / hero.(), 14);(), 14);
๐Ÿ’ก Hint: Full width 120 times current HP divided by maximum HP. The fields are private, use the getter methods.
๐Ÿง 
Knowledge Check
+15 XP
After looting, we set the chest tile to 0. What bug does this prevent?
AThe hero drowning
BStanding on the chest again and looting infinite potions
CThe map failing to draw
โœ… See the complete RPG (the full game you finish by end of Part 3)

Part 1 builds the hero, the tile world and loot , the foundation. This is where it all leads: the full three-part RPG with monsters, turn-based combat, a quest and a dragon boss, all classes in one runnable Main.javaMain.java. Run javac Main.java && java Mainjavac Main.java && java Main. Use it to see how your Part 1 pieces fit the bigger picture.

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 = "WIN"; 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 = "GAMEOVER";
    }

    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("WIN") || state.equals("GAMEOVER")) {
            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("WIN") || state.equals("GAMEOVER")) {
            g.setColor(new Color(0, 0, 0, 190));
            g.fillRect(0, 0, 700, 560);
            g.setColor(state.equals("WIN") ? new Color(255, 209, 102) : new Color(248, 113, 113));
            g.setFont(new Font("Arial", Font.BOLD, 46));
            g.drawString(state.equals("WIN") ? "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);
    }
}// 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 = "WIN"; 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 = "GAMEOVER";
    }

    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("WIN") || state.equals("GAMEOVER")) {
            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("WIN") || state.equals("GAMEOVER")) {
            g.setColor(new Color(0, 0, 0, 190));
            g.fillRect(0, 0, 700, 560);
            g.setColor(state.equals("WIN") ? new Color(255, 209, 102) : new Color(248, 113, 113));
            g.setFont(new Font("Arial", Font.BOLD, 46));
            g.drawString(state.equals("WIN") ? "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);
    }
}
๐ŸŽ‰๐Ÿ†๐ŸŽฎโœจ๐ŸŽ‰
Workshop Complete!
A hero with stats, an inventory, and a world with walls, the skeleton of every RPG ever made. Part 2 adds monsters and combat!
0
Total XP
1
Level
0
Best Streak
0%
Accuracy
โ–ถ Part 2: Monsters & Combat โ†’
โญ 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