RPG Quest
Part 2: Monsters & Combat
Your world gets dangerous. Add a Monster class, roaming enemies, and a full turn-based battle system, attack, defend, flee, plus XP and level-ups when you win.
Your world gets dangerous. Add a Monster class, roaming enemies, and a full turn-based battle system, attack, defend, flee, plus XP and level-ups when you win.
Create a Monster class and a factory method that builds different enemy types.
public 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);
}
return create("slime");
}
public void takeDamage(int amount) { hp = Math.max(0, hp - amount); }
public boolean isAlive() { return hp > 0; }
}public 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);
}
return create("slime");
}
public void takeDamage(int amount) { hp = Math.max(0, hp - amount); }
public boolean isAlive() { return hp > 0; }
}
Trigger battles as the hero explores, with smart odds that feel fair.
String state = "EXPLORE";
Monster foe;
int safeSteps = 0;
// after a successful move in keyPressed:
if (state.equals("EXPLORE") && map[heroRow][heroCol] == 0) {
if (safeSteps > 0) safeSteps--;
else if (Math.random() < 0.15) {
double roll = Math.random();
String type = roll < 0.5 ? "slime"
: roll < 0.85 ? "wolf" : "goblin";
foe = Monster.create(type);
state = "BATTLE";
}
} String state = "EXPLORE";
Monster foe;
int safeSteps = 0;
// after a successful move in keyPressed:
if (state.equals("EXPLORE") && map[heroRow][heroCol] == 0) {
if (safeSteps > 0) safeSteps--;
else if (Math.random() < 0.15) {
double roll = Math.random();
String type = roll < 0.5 ? "slime"
: roll < 0.85 ? "wolf" : "goblin";
foe = Monster.create(type);
state = "BATTLE";
}
}
String type = roll < String type = roll < ? "slime"
: roll < ? "slime"
: roll < ? "wolf" : "goblin"; ? "wolf" : "goblin";
When a battle starts, replace the map with a proper battle screen.
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.drawString(battleLog, 60, 280);
g.drawString("[A]ttack [D]efend [P]otion [F]lee", 60, 500);
return; // skip drawing the map
}
void drawBar(Graphics g, int x, int y, int val, int max) {
g.setColor(Color.RED); g.fillRect(x, y, 150, 14);
g.setColor(new Color(6,214,160)); g.fillRect(x, y, 150 * val / max, 14);
} 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.drawString(battleLog, 60, 280);
g.drawString("[A]ttack [D]efend [P]otion [F]lee", 60, 500);
return; // skip drawing the map
}
void drawBar(Graphics g, int x, int y, int val, int max) {
g.setColor(Color.RED); g.fillRect(x, y, 150, 14);
g.setColor(new Color(6,214,160)); g.fillRect(x, y, 150 * val / max, 14);
}
Implement the complete turn: player action, then the monster hits back.
boolean defending = false;
void battleAction(char key) {
defending = false;
if (key == 'a') {
int dmg = hero.getAttack() + (int)(Math.random()*4) - 1;
foe.takeDamage(dmg);
battleLog = "You hit " + foe.name + " for " + dmg + "!";
} else if (key == 'd') {
defending = true;
battleLog = "You brace behind your shield.";
} else if (key == 'p') {
battleLog = hero.useItem("Potion") ? "Glug! Potion heals 12." : "No potions left!";
} else if (key == 'f') {
if (Math.random() < 0.6) { state = "EXPLORE"; safeSteps = 3;
battleLog = ""; return; }
battleLog = "Could not escape!";
} else return;
if (foe.isAlive()) {
int dmg = foe.attack + (int)(Math.random()*3);
if (defending) dmg /= 2;
hero.takeDamage(dmg);
battleLog += " " + foe.name + " hits for " + dmg + "!";
if (!hero.isAlive()) state = "GAME_OVER";
}
} boolean defending = false;
void battleAction(char key) {
defending = false;
if (key == 'a') {
int dmg = hero.getAttack() + (int)(Math.random()*4) - 1;
foe.takeDamage(dmg);
battleLog = "You hit " + foe.name + " for " + dmg + "!";
} else if (key == 'd') {
defending = true;
battleLog = "You brace behind your shield.";
} else if (key == 'p') {
battleLog = hero.useItem("Potion") ? "Glug! Potion heals 12." : "No potions left!";
} else if (key == 'f') {
if (Math.random() < 0.6) { state = "EXPLORE"; safeSteps = 3;
battleLog = ""; return; }
battleLog = "Could not escape!";
} else return;
if (foe.isAlive()) {
int dmg = foe.attack + (int)(Math.random()*3);
if (defending) dmg /= 2;
hero.takeDamage(dmg);
battleLog += " " + foe.name + " hits for " + dmg + "!";
if (!hero.isAlive()) state = "GAME_OVER";
}
}
int dmg = foe.attack + (int)(Math.random()*3); if (int dmg = foe.attack + (int)(Math.random()*3); if () dmg ) dmg 2; hero.takeDamage(dmg); 2; hero.takeDamage(dmg);
Reward victory with XP and gold, and level the hero up at thresholds.
public void gainXp(int amount) {
xp += amount;
while (xp >= level * 25) {
xp -= level * 25;
level++;
maxHp += 6;
attack += 2;
hp = maxHp; // level-up full heal!
}
}
public int getLevel() { return level; }
public void addGold(int g) { gold += g; } public void gainXp(int amount) {
xp += amount;
while (xp >= level * 25) {
xp -= level * 25;
level++;
maxHp += 6;
attack += 2;
hp = maxHp; // level-up full heal!
}
}
public int getLevel() { return level; }
public void addGold(int g) { gold += g; }
Handle losing gracefully and tune combat until it feels fair but tense.
if (state.equals("GAME_OVER") && e.getKeyChar() == 'r') {
heroRow = 1; heroCol = 1;
hero.heal(hero.getMaxHp() / 2); // half HP respawn
state = "EXPLORE";
safeSteps = 5;
} if (state.equals("GAME_OVER") && e.getKeyChar() == 'r') {
heroRow = 1; heroCol = 1;
hero.heal(hero.getMaxHp() / 2); // half HP respawn
state = "EXPLORE";
safeSteps = 5;
}
heroRow = 1; heroCol = heroRow = 1; heroCol = ; // back to the start hero.heal(hero.getMaxHp() / ; // back to the start hero.heal(hero.getMaxHp() / ); // half health safeSteps = ); // half health safeSteps = ; // breathing room; // breathing room
Part 2 adds the Monster class, random encounters and the turn-based battle screen. This is the full three-part RPG it all leads to , combat, a quest and a dragon boss, all in one runnable Main.javaMain.java. Run javac Main.java && java Mainjavac Main.java && java Main. Compare your battle logic against this.
// 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 = "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("WIN") || 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("WIN") || state.equals("GAME_OVER")) {
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 = "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("WIN") || 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("WIN") || state.equals("GAME_OVER")) {
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);
}
}