Java Knight

This commit is contained in:
2026-07-12 19:20:33 +03:30
parent 78d4bedb08
commit a527c5bcfa
67 changed files with 1742 additions and 254 deletions
+5
View File
@@ -1,6 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://repo.maven.apache.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
+2 -2
View File
@@ -9,8 +9,8 @@
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>25</maven.compiler.source>
<maven.compiler.target>25</maven.compiler.target>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
@@ -0,0 +1,345 @@
package org.project;
import org.project.entity.enemies.Dragon;
import org.project.entity.enemies.Enemy;
import org.project.entity.enemies.KeyType;
import org.project.entity.players.Assassin;
import org.project.entity.players.Knight;
import org.project.entity.players.Player;
import org.project.entity.players.Wizard;
import org.project.item.consumables.Consumable;
import org.project.location.Location;
import org.project.util.ConsoleColors;
import org.project.util.Dice;
import java.util.List;
import java.util.Scanner;
/**
* The main game controller. It owns the player, the world's locations and the
* merchant, and drives the whole flow: class selection, the exploration menu,
* turn-based combat, progression, and the final Dragon fight.
*
* All the polymorphism set up in the class hierarchy pays off here — the combat
* loop only ever calls methods on {@code Player} and {@code Enemy}, and each
* concrete class fills in its own behaviour.
*/
public class Game {
private final Scanner scanner = new Scanner(System.in);
private final List<Location> locations;
private final Merchant merchant = new Merchant(scanner);
private Player player;
private boolean running = true;
public Game(List<Location> locations) {
this.locations = locations;
}
// ------------------------------------------------------------------
// Top-level flow
// ------------------------------------------------------------------
public void start() {
printPrologue();
chooseClass();
explorationLoop();
}
private void printPrologue() {
System.out.println(ConsoleColors.bold(ConsoleColors.CYAN + "\n=== JAVA KNIGHT ===" + ConsoleColors.RESET));
System.out.println(ConsoleColors.cyan(
"For centuries, the land of Javanest lived in peace, until a magical Dragon\n"
+ "plunged it into darkness, cursing its people into monsters. It split the three\n"
+ "keys to its lair among them. Collect all three keys, grow strong, and slay the\n"
+ "Dragon to break the curse!\n"));
}
private void chooseClass() {
System.out.print("Name your hero: ");
String name = scanner.hasNextLine() ? scanner.nextLine().trim() : "Hero";
if (name.isEmpty()) {
name = "Hero";
}
System.out.println("\nChoose your class:");
System.out.println("1. " + ConsoleColors.cyan("Knight")
+ " ⚔️ Highest base damage, sturdy armor. Special: Shield Bash (stun).");
System.out.println("2. " + ConsoleColors.purple("Wizard")
+ " 🧙 Highest HP. Special: Arcane Blast (damage + self-heal).");
System.out.println("3. " + ConsoleColors.yellow("Assassin")
+ " 🗡️ Highest mana. Special: Vanish (dodge + guaranteed crit).");
int choice = readChoice(1, 3);
player = switch (choice) {
case 1 -> new Knight(name);
case 2 -> new Wizard(name);
default -> new Assassin(name);
};
System.out.println(ConsoleColors.green("\nWelcome, " + player.getName() + " the " + player.getClassName() + "!"));
}
// ------------------------------------------------------------------
// Exploration menu
// ------------------------------------------------------------------
private void explorationLoop() {
while (running && player.isAlive()) {
Location location = locations.get(Dice.between(0, locations.size() - 1));
Enemy enemy = location.spawnEnemy();
System.out.println(ConsoleColors.bold("\n────────────────────────────────────────"));
System.out.println("You arrive at " + ConsoleColors.cyan(location.getName())
+ ". A wild " + ConsoleColors.red(enemy.getName()) + " appears!");
printKeyProgress();
// Decide what to do about this particular enemy.
boolean encounterOngoing = true;
while (encounterOngoing && running && player.isAlive()) {
System.out.println("\nWhat will you do?");
System.out.println("1. Fight the " + enemy.getName());
System.out.println("2. Move to another location");
System.out.println("3. Visit the Merchant");
if (player.hasAllKeys()) {
System.out.println("4. " + ConsoleColors.bold("Go to the Castle to face the Dragon 🐉"));
}
int max = player.hasAllKeys() ? 4 : 3;
int choice = readChoice(1, max);
switch (choice) {
case 1 -> {
boolean won = combat(enemy);
if (!won) {
gameOver();
return;
}
afterVictory(enemy);
encounterOngoing = false; // move on to a new encounter
}
case 2 -> {
System.out.println("You slip away to find another path...");
encounterOngoing = false;
}
case 3 -> merchant.visit(player);
case 4 -> {
castleFight();
return;
}
}
}
}
}
// ------------------------------------------------------------------
// Combat
// ------------------------------------------------------------------
/** Runs a full turn-based battle. Returns true if the player survives. */
private boolean combat(Enemy enemy) {
System.out.println(ConsoleColors.bold("\nYou chose to FIGHT!"));
while (player.isAlive() && enemy.isAlive()) {
printCombatStatus(enemy);
playerTurn(enemy);
if (enemy.isAlive()) {
enemyTurn(enemy);
}
}
return player.isAlive();
}
private void printCombatStatus(Enemy enemy) {
System.out.println();
System.out.println(ConsoleColors.green("[" + player.getName() + " (" + player.getClassName() + ") - "
+ player.getHp() + "/" + player.getMaxHP() + " HP | "
+ player.getMp() + "/" + player.getMaxMP() + " Mana] Lv." + player.getLevel()));
System.out.println(ConsoleColors.red("[" + enemy.getName() + " - "
+ enemy.getHp() + "/" + enemy.getMaxHP() + " HP]"));
System.out.println("---");
}
/** Handle exactly one player action (loops until a valid one is chosen). */
private void playerTurn(Enemy enemy) {
System.out.println(ConsoleColors.bold("\nYour Turn:"));
System.out.println("1. Light Attack | 2. Heavy Attack " + ConsoleColors.blue("(-" + Player.HEAVY_COST + " Mana)")
+ " | 3. Defend " + ConsoleColors.blue("(-" + Player.DEFEND_COST + " Mana)")
+ " | 4. Heal " + ConsoleColors.blue("(-" + Player.HEAL_COST + " Mana)")
+ " | 5. " + player.getSpecialName() + " " + ConsoleColors.blue("(-" + Player.SPECIAL_COST + " Mana)")
+ " | 6. Use Item");
while (true) {
int choice = readChoice(1, 6);
switch (choice) {
case 1 -> {
player.lightAttack(enemy);
return;
}
case 2 -> {
if (requireMana(Player.HEAVY_COST)) {
player.heavyAttack(enemy);
return;
}
}
case 3 -> {
if (requireMana(Player.DEFEND_COST)) {
player.defend();
return;
}
}
case 4 -> {
if (requireMana(Player.HEAL_COST)) {
player.healAction();
return;
}
}
case 5 -> {
if (requireMana(Player.SPECIAL_COST)) {
player.specialAbility(enemy);
return;
}
}
case 6 -> {
if (useItemMenu()) {
return;
}
}
}
}
}
private boolean requireMana(int cost) {
if (player.canAfford(cost)) {
return true;
}
System.out.println(ConsoleColors.blue("Not enough Mana! Pick another action."));
return false;
}
/**
* Show the inventory and use a chosen consumable. Returns true if an item was
* used (which consumes the turn), false if the player had nothing / cancelled.
*/
private boolean useItemMenu() {
List<Consumable> inv = player.getInventory();
if (inv.isEmpty()) {
System.out.println("Your inventory is empty!");
return false;
}
System.out.println("Choose an item to use (0 to cancel):");
for (int i = 0; i < inv.size(); i++) {
System.out.println((i + 1) + ". " + inv.get(i).getName());
}
int choice = readChoice(0, inv.size());
if (choice == 0) {
return false;
}
player.useConsumable(choice - 1);
return true;
}
private void enemyTurn(Enemy enemy) {
System.out.println(ConsoleColors.bold("\n" + enemy.getName() + "'s Turn:"));
if (enemy.isStunned()) {
enemy.clearStun();
System.out.println(ConsoleColors.yellow("😵 " + enemy.getName() + " is stunned and skips its turn!"));
return;
}
enemy.attack(player);
}
// ------------------------------------------------------------------
// Post-combat rewards & progression
// ------------------------------------------------------------------
private void afterVictory(Enemy enemy) {
System.out.println(ConsoleColors.bold(ConsoleColors.GREEN
+ "\n🏆 The " + enemy.getName() + " is defeated!" + ConsoleColors.RESET));
player.gainXP(enemy.getXpReward());
int coins = Math.max(0, Dice.between(enemy.getCoinReward() - 4, enemy.getCoinReward() + 4));
player.addCoins(coins);
System.out.println(ConsoleColors.yellow("💰 Found " + coins + " coins (total: " + player.getCoins() + ")."));
tryKeyDrop(enemy);
player.restoreAfterBattle();
System.out.println(ConsoleColors.green("You catch your breath — HP and Mana fully restored."));
}
/** Roll for the enemy's unique key, respecting the one-key-per-species rule. */
private void tryKeyDrop(Enemy enemy) {
KeyType key = enemy.getKeyType();
if (key == null || player.hasKey(key)) {
return; // Dragon has no key, or we already own this one
}
if (Dice.chance(enemy.getKeyDropChance())) {
player.addKey(key);
System.out.println(ConsoleColors.bold(ConsoleColors.PURPLE
+ "🗝️ The " + enemy.getName() + " dropped the " + key.getDisplayName() + "!" + ConsoleColors.RESET));
printKeyProgress();
}
}
private void printKeyProgress() {
StringBuilder sb = new StringBuilder("Keys: ");
for (KeyType key : KeyType.values()) {
sb.append(player.hasKey(key) ? ConsoleColors.green("[" + key.getDisplayName() + "] ")
: ConsoleColors.red("[???] "));
}
System.out.println(sb.toString().trim());
}
// ------------------------------------------------------------------
// Endgame
// ------------------------------------------------------------------
private void castleFight() {
System.out.println(ConsoleColors.bold(ConsoleColors.RED
+ "\nYou march to the Castle. The ground trembles as the Dragon awakens...\n" + ConsoleColors.RESET));
Dragon dragon = new Dragon();
boolean won = combat(dragon);
if (won) {
System.out.println(ConsoleColors.bold(ConsoleColors.GREEN
+ "\n🎉 VICTORY! The Dragon falls, the curse breaks, and peace returns to Javanest!"
+ ConsoleColors.RESET));
System.out.println(ConsoleColors.green("You are the Java Knight. 🛡️"));
} else {
gameOver();
}
running = false;
}
private void gameOver() {
System.out.println(ConsoleColors.bold(ConsoleColors.RED
+ "\n💀 GAME OVER — " + player.getName() + " has fallen. Darkness consumes Javanest..."
+ ConsoleColors.RESET));
running = false;
}
// ------------------------------------------------------------------
// Input helper
// ------------------------------------------------------------------
/** Read an integer choice in [min, max], re-prompting on bad input. */
private int readChoice(int min, int max) {
while (true) {
System.out.print(ConsoleColors.bold("> "));
if (!scanner.hasNextLine()) {
return min; // no more input: pick the safe default so we don't loop forever
}
try {
int value = Integer.parseInt(scanner.nextLine().trim());
if (value >= min && value <= max) {
return value;
}
} catch (NumberFormatException ignored) {
// fall through to re-prompt
}
System.out.println("Please enter a number between " + min + " and " + max + ".");
}
}
}
@@ -5,11 +5,19 @@ import org.project.location.Location;
import java.util.ArrayList;
import java.util.List;
/**
* Entry point. Builds the world (a handful of locations) and starts the game.
*/
public class Main {
public static void main(String[] args) {
// TODO: ADD LOCATIONS TO YOUR GAME
List<Location> locations = new ArrayList<>();
locations.add(new Location("Whispering Woods"));
locations.add(new Location("Forgotten Crypt"));
locations.add(new Location("Bleak Marsh"));
locations.add(new Location("Ruined Village"));
locations.add(new Location("Frostpeak Pass"));
// TODO: IMPLEMENT GAMEPLAY
Game game = new Game(locations);
game.start();
}
}
@@ -0,0 +1,100 @@
package org.project;
import org.project.entity.players.Player;
import org.project.item.consumables.Flask;
import org.project.item.consumables.ManaPotion;
import org.project.item.weapons.GreatAxe;
import org.project.item.weapons.Weapon;
import org.project.util.ConsoleColors;
import java.util.Scanner;
/**
* Bonus feature: a shop where the player spends coins earned in battle on
* consumables, a stronger weapon, or armor repairs.
*/
public class Merchant {
private static final int REPAIR_COST = 15;
private final Scanner scanner;
public Merchant(Scanner scanner) {
this.scanner = scanner;
}
public void visit(Player player) {
System.out.println(ConsoleColors.yellow("\n🧙 Merchant: \"Welcome, traveler. Coin buys survival out here.\""));
boolean shopping = true;
while (shopping) {
System.out.println(ConsoleColors.bold("\n--- Merchant's Wares ---"));
System.out.println("You have " + ConsoleColors.yellow(player.getCoins() + " coins") + ".");
System.out.println("1. Health Flask (heals 25 HP) - 20 coins");
System.out.println("2. Mana Potion (restores 20 Mana) - 20 coins");
System.out.println("3. Great Axe (12 damage weapon) - 80 coins");
System.out.println("4. Repair Armor - " + REPAIR_COST + " coins");
System.out.println("5. Leave");
int choice = readChoice(1, 5);
switch (choice) {
case 1 -> buyConsumable(player, new Flask(), 20);
case 2 -> buyConsumable(player, new ManaPotion(), 20);
case 3 -> buyWeapon(player, new GreatAxe());
case 4 -> repairArmor(player);
case 5 -> shopping = false;
}
}
System.out.println(ConsoleColors.yellow("🧙 Merchant: \"Safe travels, hero.\"\n"));
}
private void buyConsumable(Player player, org.project.item.consumables.Consumable item, int price) {
if (player.spendCoins(price)) {
player.addConsumable(item);
System.out.println(ConsoleColors.green("Bought a " + item.getName() + "! Added to your inventory."));
} else {
notEnough();
}
}
private void buyWeapon(Player player, Weapon weapon) {
if (player.spendCoins(weapon.getPrice())) {
player.setWeapon(weapon);
System.out.println(ConsoleColors.green("You equip the " + weapon.getName()
+ " (" + weapon.getDamage() + " damage)!"));
} else {
notEnough();
}
}
private void repairArmor(Player player) {
if (player.spendCoins(REPAIR_COST)) {
player.getArmor().repair();
System.out.println(ConsoleColors.green("Your " + player.getArmor().getName() + " is fully repaired."));
} else {
notEnough();
}
}
private void notEnough() {
System.out.println(ConsoleColors.red("Not enough coins!"));
}
private int readChoice(int min, int max) {
while (true) {
System.out.print("> ");
if (!scanner.hasNextLine()) {
return max; // treat end-of-input as "Leave"
}
try {
int value = Integer.parseInt(scanner.nextLine().trim());
if (value >= min && value <= max) {
return value;
}
} catch (NumberFormatException ignored) {
// fall through and re-prompt
}
System.out.println("Please enter a number between " + min + " and " + max + ".");
}
}
}
@@ -1,21 +1,39 @@
package org.project.entity;
/**
* The common contract that everything able to fight in the game must follow.
*
* Both {@code Player} and {@code Enemy} implement this interface, which is what
* lets the combat loop treat a Knight and a Dragon the same way (polymorphism):
* it only ever talks to an {@code Entity}.
*/
public interface Entity {
/** Perform this entity's basic attack against a target. */
void attack(Entity target);
/** Put the entity into a defensive stance for the next incoming hit. */
void defend();
/** Restore some health (capped at the maximum). */
void heal(int health);
/** Restore some mana/stamina (capped at the maximum). */
void fillMana(int mana);
/** Apply incoming damage to this entity. */
void takeDamage(int damage);
String getName();
int getHp();
int getMaxHP();
int getMp();
int getMaxMP();
/*
TODO: ADD OTHER REQUIRED AND BONUS METHODS
*/
/** @return true while the entity still has health left. */
boolean isAlive();
}
@@ -0,0 +1,30 @@
package org.project.entity.enemies;
import org.project.entity.Entity;
import org.project.entity.players.Player;
import org.project.util.ConsoleColors;
/**
* Dragon 🐉 — the final boss. Enormous HP and damage. Its fiery breath is "true
* damage": it bypasses armor and the player's defend stance entirely by calling
* the overloaded {@link Player#takeDamage(int, boolean)} with {@code true}.
*
* Even though it is the boss, it still extends {@link Enemy} so it reuses all
* the common combat plumbing — exactly what the assignment asks for.
*/
public class Dragon extends Enemy {
public Dragon() {
super("Dragon", 160, 26, 120, 100, null, 0);
}
@Override
public void attack(Entity target) {
System.out.println(ConsoleColors.red("🐉🔥 The Dragon unleashes a torrent of fire that bypasses all defenses!"));
if (target instanceof Player player) {
player.takeDamage(damage, true); // true = ignore armor and shields
} else {
target.takeDamage(damage);
}
}
}
@@ -1,34 +1,137 @@
package org.project.entity.enemies;
import org.project.item.weapons.Weapon;
import org.project.entity.Entity;
import org.project.util.ConsoleColors;
// TODO: UPDATE IMPLEMENTATION
public abstract class Enemy {
Weapon weapon;
/**
* Abstract base for every monster, including the Dragon boss.
*
* It holds the stats and rewards shared by all enemies and provides a plain
* default attack. Each subclass overrides {@link #attack(Entity)} (and sometimes
* {@link #takeDamage(int)}) to add its signature ability — a crit, a
* resurrection, lifesteal, or armor-piercing fire.
*/
public abstract class Enemy implements Entity {
protected String name;
private int hp;
private int mp;
private final int maxHP;
protected int damage;
public Enemy(int hp, int mp, Weapon weapon) {
this.hp = hp;
this.mp = mp;
private final int xpReward;
private final int coinReward;
private final KeyType keyType; // null for the Dragon (it drops no key)
private final int keyDropChance; // percent chance to drop its key
this.weapon = weapon;
private boolean stunned = false;
protected Enemy(String name, int maxHP, int damage,
int xpReward, int coinReward, KeyType keyType, int keyDropChance) {
this.name = name;
this.maxHP = maxHP;
this.hp = maxHP;
this.damage = damage;
this.xpReward = xpReward;
this.coinReward = coinReward;
this.keyType = keyType;
this.keyDropChance = keyDropChance;
}
/** Default attack: hit the target for this enemy's damage. */
@Override
public void attack(Entity target) {
System.out.println(ConsoleColors.red("👾 " + name + " attacks!"));
target.takeDamage(damage);
}
@Override
public void takeDamage(int damage) {
hp -= damage;
if (hp < 0) {
hp = 0;
}
// Note: the attacker prints the damage/HP lines, so this stays quiet.
}
/** Enemies don't have a defensive stance in this game. */
@Override
public void defend() {
// no-op
}
@Override
public void heal(int health) {
hp += health;
if (hp > maxHP) {
hp = maxHP;
}
}
/** Enemies use no mana, so this does nothing. */
@Override
public void fillMana(int mana) {
// no-op
}
// --- Stun handling (used by the Knight's Shield Bash) ---
public void stun() {
stunned = true;
}
public boolean isStunned() {
return stunned;
}
public void clearStun() {
stunned = false;
}
// --- Getters ---
@Override
public String getName() {
return name;
}
@Override
public int getHp() {
return hp;
}
public int getMp() {
return mp;
@Override
public int getMaxHP() {
return maxHP;
}
public Weapon getWeapon() {
return weapon;
@Override
public int getMp() {
return 0;
}
@Override
public int getMaxMP() {
return 0;
}
@Override
public boolean isAlive() {
return hp > 0;
}
public int getXpReward() {
return xpReward;
}
public int getCoinReward() {
return coinReward;
}
public KeyType getKeyType() {
return keyType;
}
public int getKeyDropChance() {
return keyDropChance;
}
}
@@ -0,0 +1,31 @@
package org.project.entity.enemies;
import org.project.entity.Entity;
import org.project.util.ConsoleColors;
import org.project.util.Dice;
/**
* Goblin 👹 — low health, but a high chance to land a devastating critical hit.
* Drops the Goblin Key.
*/
public class Goblin extends Enemy {
private static final int CRIT_CHANCE = 40; // percent
public Goblin() {
super("Goblin", 30, 8, 20, 12, KeyType.GOBLIN, 25);
}
@Override
public void attack(Entity target) {
if (Dice.chance(CRIT_CHANCE)) {
int critDamage = damage * 2;
System.out.println(ConsoleColors.red("👹 Goblin used Critical Strike!"));
System.out.println(ConsoleColors.red("💥 Critical hit!"));
target.takeDamage(critDamage);
} else {
System.out.println(ConsoleColors.red("👹 Goblin lunges with a rusty blade!"));
target.takeDamage(damage);
}
}
}
@@ -0,0 +1,22 @@
package org.project.entity.enemies;
/**
* The three keys that the Dragon scattered among the cursed monsters.
* Each standard enemy species is tied to exactly one key type, and the player
* needs all three to unlock the castle.
*/
public enum KeyType {
GOBLIN("Goblin Key"),
SKELETON("Skeleton Key"),
VAMPIRE("Vampire Key");
private final String displayName;
KeyType(String displayName) {
this.displayName = displayName;
}
public String getDisplayName() {
return displayName;
}
}
@@ -1,6 +1,31 @@
package org.project.entity.enemies;
// TODO: UPDATE IMPLEMENTATION
public class Skeleton {
// TODO: DESIGN ENEMY AND IMPLEMENT THE CONSTRUCTOR
import org.project.util.ConsoleColors;
/**
* Skeleton ☠️ — a bag of bones that can pull itself back together once per
* battle, returning to 50% HP the first time it would die. Drops the Skeleton
* Key.
*/
public class Skeleton extends Enemy {
private boolean hasResurrected = false;
public Skeleton() {
super("Skeleton", 40, 10, 25, 14, KeyType.SKELETON, 25);
}
@Override
public void takeDamage(int damage) {
super.takeDamage(damage);
// First time it drops to 0, it revives at half health.
if (getHp() <= 0 && !hasResurrected) {
hasResurrected = true;
int revived = getMaxHP() / 2;
heal(revived); // hp was 0, so this sets it to 50% of max
System.out.println(ConsoleColors.purple(
"☠️ The Skeleton collapses into a pile of bones... then reassembles with " + getHp() + " HP!"));
}
}
}
@@ -0,0 +1,28 @@
package org.project.entity.enemies;
import org.project.entity.Entity;
import org.project.util.ConsoleColors;
/**
* Vampire 🦇 — heals itself for a portion of the damage it deals (lifesteal),
* making it a war of attrition. Drops the Vampire Key.
*/
public class Vampire extends Enemy {
private static final double LIFESTEAL = 0.5; // heals for 50% of damage dealt
public Vampire() {
super("Vampire", 48, 11, 30, 16, KeyType.VAMPIRE, 25);
}
@Override
public void attack(Entity target) {
System.out.println(ConsoleColors.red("🦇 Vampire sinks its fangs in!"));
target.takeDamage(damage);
int stolen = (int) (damage * LIFESTEAL);
heal(stolen);
System.out.println(ConsoleColors.green(
"🩸 Vampire drains " + stolen + " HP and now has " + getHp() + "/" + getMaxHP() + " HP."));
}
}
@@ -0,0 +1,42 @@
package org.project.entity.players;
import org.project.entity.Entity;
import org.project.item.armors.LeatherArmor;
import org.project.item.weapons.Dagger;
import org.project.util.ConsoleColors;
/**
* The Assassin 🗡️ — the nimble skirmisher. Highest max mana/stamina so it can
* chain abilities, with average damage and low HP.
*
* Special: Vanish — turns invisible, dodging the enemy's next attack entirely
* and guaranteeing a critical hit on the Assassin's next strike.
*/
public class Assassin extends Player {
public Assassin(String name) {
// Highest Mana/Stamina (55); low HP (45).
super(name, 45, 55, 6, new Dagger(), new LeatherArmor());
}
@Override
public String getClassName() {
return "Assassin";
}
@Override
public String getSpecialName() {
return "Vanish";
}
@Override
public void specialAbility(Entity target) {
spendMana(SPECIAL_COST);
// Flip the two combat-state flags the base class already knows about.
dodgeNext = true;
guaranteedCrit = true;
System.out.println(ConsoleColors.purple("🌫️ " + name + " VANISHES into the shadows! "
+ ConsoleColors.blue("(-" + SPECIAL_COST + " Mana)")));
System.out.println(ConsoleColors.purple(" The next enemy attack will miss, and the next strike is a guaranteed crit!"));
}
}
@@ -0,0 +1,24 @@
package org.project.entity.players;
import org.project.entity.Entity;
/**
* "The Rule of Five" every player class must offer exactly these five actions
* on its turn. Declaring them in an interface guarantees each class implements
* all of them, and lets the game loop drive any player class the same way.
*
* Light Attack is free; every other action costs mana/stamina, and the Special
* Ability always costs the most.
*/
public interface ICombatActions {
void lightAttack(Entity target);
void heavyAttack(Entity target);
void defend();
void healAction();
void specialAbility(Entity target);
}
@@ -1,6 +1,46 @@
package org.project.entity.players;
// TODO: UPDATE IMPLEMENTATION
public class Knight {
// TODO: DESIGN KNIGHT'S WEAPON AND ARMOR AND IMPLEMENT THE CONSTRUCTOR
import org.project.entity.Entity;
import org.project.entity.enemies.Enemy;
import org.project.item.armors.KnightArmor;
import org.project.item.weapons.Sword;
import org.project.util.ConsoleColors;
/**
* The Knight ⚔️ — the front-line bruiser. Highest base damage of the three
* classes and sturdy plate armor, but an average health and mana pool.
*
* Special: Shield Bash — deals heavy damage and stuns the enemy so it skips its
* next turn.
*/
public class Knight extends Player {
public Knight(String name) {
// Highest base damage (8); average HP/Mana.
super(name, 50, 40, 8, new Sword(), new KnightArmor());
}
@Override
public String getClassName() {
return "Knight";
}
@Override
public String getSpecialName() {
return "Shield Bash";
}
@Override
public void specialAbility(Entity target) {
spendMana(SPECIAL_COST);
System.out.println(ConsoleColors.purple("🛡️💥 " + name + " performs a mighty SHIELD BASH! "
+ ConsoleColors.blue("(-" + SPECIAL_COST + " Mana)")));
dealDamage(target, (int) (getAttackPower() * 2.2));
// Stun only makes sense against an actual enemy.
if (target instanceof Enemy enemy) {
enemy.stun();
System.out.println(ConsoleColors.yellow("😵 " + enemy.getName() + " is stunned and will skip its next turn!"));
}
}
}
@@ -1,42 +1,203 @@
package org.project.entity.players;
import org.project.entity.Entity;
import org.project.entity.enemies.KeyType;
import org.project.item.armors.Armor;
import org.project.item.consumables.Consumable;
import org.project.item.weapons.Weapon;
import org.project.util.ConsoleColors;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
/**
* Abstract base for all playable heroes (Knight, Wizard, Assassin).
*
* This class holds everything the three classes share: health/mana bars, the
* weapon and armor, the level/XP system, coins, the inventory of consumables,
* and the collected keys. It also implements four of the five combat actions,
* because Light/Heavy Attack, Defend and Heal work the same for every class.
* Only {@link #specialAbility(Entity)} is left abstract so each subclass can
* provide its own unique ultimate (polymorphism).
*/
public abstract class Player implements Entity, ICombatActions {
// Mana/stamina cost of each action. Special is deliberately the highest.
public static final int HEAVY_COST = 8;
public static final int DEFEND_COST = 6;
public static final int HEAL_COST = 12;
public static final int SPECIAL_COST = 15;
// TODO: UPDATE IMPLEMENTATION
public abstract class Player {
protected String name;
Weapon weapon;
Armor armor;
private int hp;
private int maxHP;
private int mp;
private int maxMP;
private int baseDamage;
public Player(String name, int hp, int mp, Weapon weapon, Armor armor) {
protected Weapon weapon;
protected Armor armor;
// Progression
private int level = 1;
private int xp = 0;
private int xpToNext = 50;
private int coins = 0;
private final List<Consumable> inventory = new ArrayList<>();
private final Set<KeyType> keys = EnumSet.noneOf(KeyType.class);
// Temporary combat states, all triggered by actions and consumed on the
// next relevant hit. Kept in the base class so takeDamage/dealDamage stay
// generic while subclasses just flip the flags.
protected boolean defending = false;
protected boolean dodgeNext = false;
protected boolean guaranteedCrit = false;
protected Player(String name, int maxHP, int maxMP, int baseDamage, Weapon weapon, Armor armor) {
this.name = name;
this.hp = hp;
this.mp = mp;
this.maxHP = maxHP;
this.hp = maxHP;
this.maxMP = maxMP;
this.mp = maxMP;
this.baseDamage = baseDamage;
this.weapon = weapon;
this.armor = armor;
}
/** The class name shown in menus, e.g. "Knight". */
public abstract String getClassName();
/** The display name of the special ability, used to build the turn menu. */
public abstract String getSpecialName();
// ------------------------------------------------------------------
// Combat actions (ICombatActions)
// ------------------------------------------------------------------
/** Total raw damage of a basic hit = class base damage + weapon damage. */
public int getAttackPower() {
return baseDamage + weapon.getDamage();
}
/** The generic Entity attack maps to the free Light Attack. */
@Override
public void attack(Entity target) {
target.takeDamage(weapon.getDamage());
lightAttack(target);
}
@Override
public void lightAttack(Entity target) {
System.out.println(ConsoleColors.cyan("⚔️ " + name + " (" + getClassName() + ") used Light Attack! (0 Mana)"));
dealDamage(target, getAttackPower());
}
@Override
public void heavyAttack(Entity target) {
spendMana(HEAVY_COST);
System.out.println(ConsoleColors.cyan("⚔️ " + name + " used Heavy Attack! " + manaTag(HEAVY_COST)));
dealDamage(target, getAttackPower() * 2);
}
@Override
public void defend() {
// TODO
spendMana(DEFEND_COST);
defending = true;
System.out.println(ConsoleColors.yellow("🛡️ " + name + " braces for the next hit! " + manaTag(DEFEND_COST)));
}
@Override
public void healAction() {
spendMana(HEAL_COST);
int amount = (int) (maxHP * 0.4);
heal(amount);
System.out.println(ConsoleColors.green("" + name + " heals for " + amount + " HP! " + manaTag(HEAL_COST)));
System.out.println(ConsoleColors.green(name + " now has " + hp + "/" + maxHP + " HP."));
}
// specialAbility is abstract -> each class supplies its own ultimate.
/**
* Shared damage routine for the player's attacks. Applies a guaranteed crit
* if the Assassin set one up, then reports the result.
*/
protected void dealDamage(Entity target, int amount) {
if (guaranteedCrit) {
amount *= 2;
guaranteedCrit = false;
System.out.println(ConsoleColors.purple("🌙 Critical hit from the shadows!"));
}
target.takeDamage(amount);
System.out.println(ConsoleColors.red(target.getName() + " took " + amount + " damage!"));
System.out.println(target.getName() + " has " + target.getHp() + "/" + target.getMaxHP() + " HP remaining.");
System.out.println(ConsoleColors.blue(name + " Mana: " + mp + "/" + maxMP));
}
// ------------------------------------------------------------------
// Taking damage (with overloading for the Dragon's true damage)
// ------------------------------------------------------------------
@Override
public void takeDamage(int damage) {
hp -= damage - armor.getDefense();
// Normal hit: goes through armor and any defensive stance.
takeDamage(damage, false);
}
/**
* Overloaded version. When {@code ignoreDefenses} is true the hit ignores
* armor and the defend stance this is how the Dragon's fiery breath
* bypasses shields.
*/
public void takeDamage(int damage, boolean ignoreDefenses) {
if (dodgeNext) {
dodgeNext = false;
System.out.println(ConsoleColors.purple("💨 " + name + " was invisible and dodged the attack completely!"));
return;
}
int incoming = damage;
if (!ignoreDefenses) {
if (defending) {
defending = false;
incoming = incoming / 4; // brace reduces the blow to 25%
System.out.println(ConsoleColors.yellow("🛡️ " + name + " blocks most of the blow!"));
}
incoming -= armor.getDefense();
armor.absorbHit();
}
if (incoming < 1) {
incoming = 1; // a hit always stings at least a little
}
hp -= incoming;
if (hp < 0) {
hp = 0;
}
System.out.println(ConsoleColors.red(name + " took " + incoming + " damage!"));
System.out.println(name + " has " + hp + "/" + maxHP + " HP remaining.");
}
// ------------------------------------------------------------------
// Resource helpers
// ------------------------------------------------------------------
/** Spend mana if affordable. Returns false when there is not enough. */
protected boolean spendMana(int cost) {
if (mp < cost) {
return false;
}
mp -= cost;
return true;
}
public boolean canAfford(int cost) {
return mp >= cost;
}
private String manaTag(int cost) {
return ConsoleColors.blue("(-" + cost + " Mana)");
}
@Override
@@ -55,11 +216,100 @@ public abstract class Player {
}
}
/** Refill HP and mana fully after a won battle. */
public void restoreAfterBattle() {
hp = maxHP;
mp = maxMP;
}
// ------------------------------------------------------------------
// Leveling
// ------------------------------------------------------------------
/** Award XP and level up as many times as the total allows. */
public void gainXP(int amount) {
xp += amount;
System.out.println(ConsoleColors.purple("✨ Gained " + amount + " XP! (" + xp + "/" + xpToNext + ")"));
while (xp >= xpToNext) {
xp -= xpToNext;
levelUp();
}
}
private void levelUp() {
level++;
xpToNext = (int) (xpToNext * 1.5);
maxHP += 10;
maxMP += 5;
baseDamage += 2;
hp = maxHP;
mp = maxMP;
System.out.println(ConsoleColors.bold(ConsoleColors.GREEN
+ "⬆️ LEVEL UP! " + name + " is now level " + level + "!" + ConsoleColors.RESET));
System.out.println(ConsoleColors.green(
" Max HP -> " + maxHP + " | Max Mana -> " + maxMP + " | Base Damage -> " + baseDamage));
}
// ------------------------------------------------------------------
// Inventory, coins and keys
// ------------------------------------------------------------------
public void addConsumable(Consumable c) {
inventory.add(c);
}
public List<Consumable> getInventory() {
return inventory;
}
/** Use (and remove) the consumable at the given index on this player. */
public void useConsumable(int index) {
Consumable c = inventory.remove(index);
c.use(this);
}
public void addCoins(int amount) {
coins += amount;
}
public boolean spendCoins(int amount) {
if (coins < amount) {
return false;
}
coins -= amount;
return true;
}
public int getCoins() {
return coins;
}
public void addKey(KeyType key) {
keys.add(key);
}
public boolean hasKey(KeyType key) {
return keys.contains(key);
}
public boolean hasAllKeys() {
return keys.size() == KeyType.values().length;
}
public Set<KeyType> getKeys() {
return keys;
}
// ------------------------------------------------------------------
// Simple getters
// ------------------------------------------------------------------
@Override
public String getName() {
return name;
}
@Override
public int getHp() {
return hp;
}
@@ -69,6 +319,7 @@ public abstract class Player {
return maxHP;
}
@Override
public int getMp() {
return mp;
}
@@ -78,12 +329,24 @@ public abstract class Player {
return maxMP;
}
@Override
public boolean isAlive() {
return hp > 0;
}
public int getLevel() {
return level;
}
public Weapon getWeapon() {
return weapon;
}
public void setWeapon(Weapon weapon) {
this.weapon = weapon;
}
public Armor getArmor() {
return armor;
}
}
@@ -0,0 +1,44 @@
package org.project.entity.players;
import org.project.entity.Entity;
import org.project.item.armors.Robe;
import org.project.item.weapons.Staff;
import org.project.util.ConsoleColors;
/**
* The Wizard 🧙 — the glass cannon caster. Highest max HP so it can survive long
* fights, but weak basic attacks and flimsy armor.
*
* Special: Arcane Blast — blasts the enemy for heavy damage AND heals the Wizard
* at the same time.
*/
public class Wizard extends Player {
public Wizard(String name) {
// Highest HP (65); low base damage (5).
super(name, 65, 45, 5, new Staff(), new Robe());
}
@Override
public String getClassName() {
return "Wizard";
}
@Override
public String getSpecialName() {
return "Arcane Blast";
}
@Override
public void specialAbility(Entity target) {
spendMana(SPECIAL_COST);
System.out.println(ConsoleColors.purple("🔮 " + name + " channels an ARCANE BLAST! "
+ ConsoleColors.blue("(-" + SPECIAL_COST + " Mana)")));
dealDamage(target, (int) (getAttackPower() * 2.5));
int lifeReturn = (int) (getMaxHP() * 0.25);
heal(lifeReturn);
System.out.println(ConsoleColors.green("🌿 The spell's energy restores " + lifeReturn + " HP to " + name
+ " (" + getHp() + "/" + getMaxHP() + ")."));
}
}
@@ -2,10 +2,19 @@ package org.project.item;
import org.project.entity.Entity;
/**
* Anything that can live in the player's inventory or be sold by the merchant.
*
* Weapons, Armor and Consumables are the three families of items; they all
* implement this interface so the merchant can list and sell them uniformly.
*/
public interface Item {
/** Use / apply the item on a target (deal damage, heal, equip, ...). */
void use(Entity target);
/*
TODO: ADD OTHER REQUIRED AND BONUS METHODS
*/
String getName();
/** Cost in coins at the merchant. */
int getPrice();
}
@@ -1,17 +1,48 @@
package org.project.item.armors;
// TODO: UPDATE IMPLEMENTATION
public abstract class Armor {
import org.project.entity.Entity;
import org.project.item.Item;
/**
* Base class for every piece of armor. Armor lowers the damage the wearer takes
* through its {@code defense} value, and slowly wears out through
* {@code durability}: once durability hits zero the armor breaks and stops
* protecting until it is repaired at the merchant.
*/
public abstract class Armor implements Item {
private final String name;
private final int price;
private int defense;
private int maxDefense;
private final int maxDefense;
private int durability;
private int maxDurability;
private final int maxDurability;
private boolean isBroke;
public Armor(int defense, int durability) {
protected Armor(String name, int defense, int durability, int price) {
this.name = name;
this.defense = defense;
this.maxDefense = defense;
this.durability = durability;
this.maxDurability = durability;
this.price = price;
}
/** Equipping armor has no direct effect on a target, so this is a no-op. */
@Override
public void use(Entity target) {
// Armor is worn, not "used" on someone. Nothing to do here.
}
/** Every hit chips away one point of durability; may break the armor. */
public void absorbHit() {
if (isBroke) {
return;
}
durability--;
checkBreak();
}
public void checkBreak() {
@@ -21,13 +52,23 @@ public abstract class Armor {
}
}
// TODO: (BONUS) UPDATE THE REPAIR METHOD
/** Bonus: merchants can repair worn/broken armor back to full. */
public void repair() {
isBroke = false;
defense = maxDefense;
durability = maxDurability;
}
@Override
public String getName() {
return name;
}
@Override
public int getPrice() {
return price;
}
public int getDefense() {
return defense;
}
@@ -1,6 +1,12 @@
package org.project.item.armors;
// TODO: UPDATE IMPLEMENTATION
public class KnightArmor {
// TODO: DESIGN ARMOR'S ATTRIBUTES IMPLEMENT THE CONSTRUCTOR
/**
* Heavy plate armor worn by the Knight. High defense to match the Knight's
* front-line, tanky role.
*/
public class KnightArmor extends Armor {
public KnightArmor() {
super("Knight Plate", 4, 25, 40);
}
}
@@ -0,0 +1,12 @@
package org.project.item.armors;
/**
* Light armor worn by the Assassin. Modest defense, but does not slow the
* nimble assassin down.
*/
public class LeatherArmor extends Armor {
public LeatherArmor() {
super("Leather Vest", 2, 20, 30);
}
}
@@ -0,0 +1,12 @@
package org.project.item.armors;
/**
* A simple cloth robe worn by the Wizard. Very little protection — the Wizard is
* squishy and relies on high HP and spells instead.
*/
public class Robe extends Armor {
public Robe() {
super("Mage Robe", 1, 18, 25);
}
}
@@ -1,8 +1,37 @@
package org.project.item.consumables;
// TODO: UPDATE IMPLEMENTATION
public abstract class Consumable {
/*
TODO: ADD OTHER REQUIRED AND BONUS METHODS
*/
import org.project.entity.Entity;
import org.project.item.Item;
/**
* Base class for one-shot items the player can carry and use in battle
* (potions, flasks, ...). Each consumable knows its name, its shop price and how
* potent its effect is; the actual effect is defined by each subclass in
* {@link #use(Entity)}.
*/
public abstract class Consumable implements Item {
private final String name;
private final int price;
protected final int potency;
protected Consumable(String name, int potency, int price) {
this.name = name;
this.potency = potency;
this.price = price;
}
/** Subclasses decide what the consumable actually does. */
@Override
public abstract void use(Entity target);
@Override
public String getName() {
return name;
}
@Override
public int getPrice() {
return price;
}
}
@@ -1,16 +1,21 @@
package org.project.item.consumables;
import org.project.entity.Entity;
import org.project.util.ConsoleColors;
// TODO: UPDATE IMPLEMENTATION
public class Flask {
/*
THIS IS AN EXAMPLE OF A CONSUMABLE DESIGN.
*/
/**
* A healing flask. Restores a flat amount of HP to whoever drinks it.
*/
public class Flask extends Consumable {
public Flask() {
super("Health Flask", 25, 20);
}
// TODO: UPDATE USE METHOD
@Override
public void use(Entity target) {
target.heal(target.getMaxHP() / 10);
target.heal(potency);
System.out.println(ConsoleColors.green(
"🧪 " + target.getName() + " drinks a Health Flask and recovers " + potency + " HP!"));
}
}
@@ -0,0 +1,22 @@
package org.project.item.consumables;
import org.project.entity.Entity;
import org.project.util.ConsoleColors;
/**
* A mana/stamina potion. Restores a flat amount of the resource the player uses
* to power their abilities.
*/
public class ManaPotion extends Consumable {
public ManaPotion() {
super("Mana Potion", 20, 20);
}
@Override
public void use(Entity target) {
target.fillMana(potency);
System.out.println(ConsoleColors.blue(
"🔷 " + target.getName() + " drinks a Mana Potion and restores " + potency + " Mana!"));
}
}
@@ -0,0 +1,12 @@
package org.project.item.weapons;
/**
* The Assassin's starting weapon. Lower raw damage than the sword but cheap on
* stamina, which fits the Assassin's fast, resource-hungry play style.
*/
public class Dagger extends Weapon {
public Dagger() {
super("Twin Dagger", 6, 6, 30);
}
}
@@ -0,0 +1,12 @@
package org.project.item.weapons;
/**
* A heavy weapon only available from the merchant. Big damage for players who
* have saved up enough coins.
*/
public class GreatAxe extends Weapon {
public GreatAxe() {
super("Great Axe", 12, 10, 80);
}
}
@@ -0,0 +1,12 @@
package org.project.item.weapons;
/**
* The Wizard's starting weapon. Weak on basic hits, but the Wizard makes up for
* it with a powerful special ability.
*/
public class Staff extends Weapon {
public Staff() {
super("Oak Staff", 5, 10, 30);
}
}
@@ -2,25 +2,32 @@ package org.project.item.weapons;
import org.project.entity.Entity;
import java.util.ArrayList;
import java.util.List;
// TODO: UPDATE IMPLEMENTATION
public class Sword {
/*
THIS IS AN EXAMPLE OF A WEAPON DESIGN.
*/
/**
* The Knight's starting weapon. A reliable, high-damage blade.
*
* Also keeps the example "unique ability" idea from the skeleton: a sweeping
* slash that can hit several targets at once (useful in the bonus party mode).
*/
public class Sword extends Weapon {
int abilityCharge;
private int abilityCharge;
public Sword() {
// TODO: DESIGN SWORD'S ATTRIBUTES IMPLEMENT THE CONSTRUCTOR
super("Iron Sword", 6, 8, 30);
}
// TODO: (BONUS) UPDATE THE UNIQUE ABILITY
public void uniqueAbility(ArrayList<Entity> targets) {
/** Constructor overloading: lets the merchant sell fancier swords. */
public Sword(String name, int damage, int price) {
super(name, damage, 8, price);
}
/** Bonus: a charged sweep that damages every target passed in. */
public void uniqueAbility(List<Entity> targets) {
abilityCharge += 2;
for (Entity target : targets) {
target.takeDamage(getDamage());
target.takeDamage(getDamage() + abilityCharge);
}
}
}
@@ -1,26 +1,43 @@
package org.project.item.weapons;
import org.project.entity.Entity;
import org.project.item.Item;
// TODO: UPDATE IMPLEMENTATION
public abstract class Weapon {
private int damage;
private int manaCost;
/**
* Base class shared by every weapon. It holds the numbers common to all weapons
* (damage, the mana it takes to swing hard with it, and its shop price) so the
* concrete weapons only have to fill in those values in their constructor.
*/
public abstract class Weapon implements Item {
/*
TODO: ADD OTHER REQUIRED AND BONUS ATTRIBUTES
*/
private final String name;
private final int damage;
private final int manaCost;
private final int price;
public Weapon(int damage, int manaCost) {
protected Weapon(String name, int damage, int manaCost, int price) {
this.name = name;
this.damage = damage;
this.manaCost = manaCost;
this.price = price;
}
/** Default weapon use = strike the target for the weapon's damage. */
@Override
public void use(Entity target) {
target.takeDamage(damage);
}
@Override
public String getName() {
return name;
}
@Override
public int getPrice() {
return price;
}
public int getDamage() {
return damage;
}
@@ -28,8 +45,4 @@ public abstract class Weapon {
public int getManaCost() {
return manaCost;
}
/*
TODO: ADD OTHER REQUIRED AND BONUS METHODS
*/
}
@@ -1,28 +1,33 @@
package org.project.location;
import org.project.entity.enemies.Enemy;
import org.project.entity.enemies.Goblin;
import org.project.entity.enemies.Skeleton;
import org.project.entity.enemies.Vampire;
import org.project.util.Dice;
import java.util.ArrayList;
/**
* A place in Javanest the player can wander into. Each visit to a location
* spawns a fresh, random standard enemy (Goblin, Skeleton or Vampire).
*/
public class Location {
private String name;
private ArrayList<Enemy> enemies;
private final String name;
public Location(ArrayList<Location> locations, ArrayList<Enemy> enemies) {
this.locations = locations;
this.enemies = enemies;
public Location(String name) {
this.name = name;
}
public String getName() {
return name;
}
public ArrayList<Location> getLocations() {
return locations;
}
public ArrayList<Enemy> getEnemies() {
return enemies;
/** Spawn one of the three standard enemies at random. */
public Enemy spawnEnemy() {
return switch (Dice.between(1, 3)) {
case 1 -> new Goblin();
case 2 -> new Skeleton();
default -> new Vampire();
};
}
}
@@ -0,0 +1,55 @@
package org.project.util;
/**
* Small helper that wraps text in ANSI escape codes so the combat log is
* colourful in the terminal. Keeping all the codes in one place means the rest
* of the game never has to remember the raw "[..m" strings.
*
* Colour convention used across the game:
* red -> damage taken / dealt
* blue -> mana / stamina usage
* green -> healing and good news
*/
public final class ConsoleColors {
private ConsoleColors() {
// utility class, never instantiated
}
public static final String RESET = "";
public static final String RED = "";
public static final String GREEN = "";
public static final String YELLOW = "";
public static final String BLUE = "";
public static final String PURPLE = "";
public static final String CYAN = "";
public static final String BOLD = "";
public static String red(String text) {
return RED + text + RESET;
}
public static String green(String text) {
return GREEN + text + RESET;
}
public static String blue(String text) {
return BLUE + text + RESET;
}
public static String yellow(String text) {
return YELLOW + text + RESET;
}
public static String purple(String text) {
return PURPLE + text + RESET;
}
public static String cyan(String text) {
return CYAN + text + RESET;
}
public static String bold(String text) {
return BOLD + text + RESET;
}
}
@@ -0,0 +1,26 @@
package org.project.util;
import java.util.concurrent.ThreadLocalRandom;
/**
* Tiny wrapper around the random number generator so the rest of the code reads
* nicely (e.g. {@code Dice.chance(20)} instead of raw Random calls everywhere).
*/
public final class Dice {
private Dice() {
}
/**
* @param percent a value from 0 to 100
* @return true with the given percentage probability
*/
public static boolean chance(int percent) {
return ThreadLocalRandom.current().nextInt(100) < percent;
}
/** @return a random integer in the inclusive range [min, max]. */
public static int between(int min, int max) {
return ThreadLocalRandom.current().nextInt(min, max + 1);
}
}
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,31 @@
org/project/Game.class
org/project/entity/enemies/Enemy.class
org/project/entity/enemies/Vampire.class
org/project/item/armors/Robe.class
org/project/entity/players/ICombatActions.class
org/project/item/consumables/Flask.class
org/project/item/weapons/Dagger.class
org/project/Main.class
org/project/entity/players/Wizard.class
org/project/Merchant.class
org/project/entity/players/Player.class
org/project/location/Location.class
org/project/entity/enemies/Dragon.class
org/project/entity/enemies/Goblin.class
org/project/util/ConsoleColors.class
org/project/item/weapons/Staff.class
org/project/entity/enemies/Skeleton.class
org/project/entity/Entity.class
org/project/item/armors/LeatherArmor.class
org/project/item/weapons/Sword.class
org/project/entity/players/Assassin.class
org/project/util/Dice.class
org/project/item/armors/KnightArmor.class
org/project/entity/enemies/KeyType.class
org/project/item/weapons/Weapon.class
org/project/item/weapons/GreatAxe.class
org/project/item/armors/Armor.class
org/project/entity/players/Knight.class
org/project/item/consumables/Consumable.class
org/project/item/consumables/ManaPotion.class
org/project/item/Item.class
@@ -0,0 +1,31 @@
/Users/faraz/Documents/SBU/Term 6/AP/HW-04-JAVA-KNIGHT/Java-Knight/src/main/java/org/project/Game.java
/Users/faraz/Documents/SBU/Term 6/AP/HW-04-JAVA-KNIGHT/Java-Knight/src/main/java/org/project/Main.java
/Users/faraz/Documents/SBU/Term 6/AP/HW-04-JAVA-KNIGHT/Java-Knight/src/main/java/org/project/Merchant.java
/Users/faraz/Documents/SBU/Term 6/AP/HW-04-JAVA-KNIGHT/Java-Knight/src/main/java/org/project/entity/Entity.java
/Users/faraz/Documents/SBU/Term 6/AP/HW-04-JAVA-KNIGHT/Java-Knight/src/main/java/org/project/entity/enemies/Dragon.java
/Users/faraz/Documents/SBU/Term 6/AP/HW-04-JAVA-KNIGHT/Java-Knight/src/main/java/org/project/entity/enemies/Enemy.java
/Users/faraz/Documents/SBU/Term 6/AP/HW-04-JAVA-KNIGHT/Java-Knight/src/main/java/org/project/entity/enemies/Goblin.java
/Users/faraz/Documents/SBU/Term 6/AP/HW-04-JAVA-KNIGHT/Java-Knight/src/main/java/org/project/entity/enemies/KeyType.java
/Users/faraz/Documents/SBU/Term 6/AP/HW-04-JAVA-KNIGHT/Java-Knight/src/main/java/org/project/entity/enemies/Skeleton.java
/Users/faraz/Documents/SBU/Term 6/AP/HW-04-JAVA-KNIGHT/Java-Knight/src/main/java/org/project/entity/enemies/Vampire.java
/Users/faraz/Documents/SBU/Term 6/AP/HW-04-JAVA-KNIGHT/Java-Knight/src/main/java/org/project/entity/players/Assassin.java
/Users/faraz/Documents/SBU/Term 6/AP/HW-04-JAVA-KNIGHT/Java-Knight/src/main/java/org/project/entity/players/ICombatActions.java
/Users/faraz/Documents/SBU/Term 6/AP/HW-04-JAVA-KNIGHT/Java-Knight/src/main/java/org/project/entity/players/Knight.java
/Users/faraz/Documents/SBU/Term 6/AP/HW-04-JAVA-KNIGHT/Java-Knight/src/main/java/org/project/entity/players/Player.java
/Users/faraz/Documents/SBU/Term 6/AP/HW-04-JAVA-KNIGHT/Java-Knight/src/main/java/org/project/entity/players/Wizard.java
/Users/faraz/Documents/SBU/Term 6/AP/HW-04-JAVA-KNIGHT/Java-Knight/src/main/java/org/project/item/Item.java
/Users/faraz/Documents/SBU/Term 6/AP/HW-04-JAVA-KNIGHT/Java-Knight/src/main/java/org/project/item/armors/Armor.java
/Users/faraz/Documents/SBU/Term 6/AP/HW-04-JAVA-KNIGHT/Java-Knight/src/main/java/org/project/item/armors/KnightArmor.java
/Users/faraz/Documents/SBU/Term 6/AP/HW-04-JAVA-KNIGHT/Java-Knight/src/main/java/org/project/item/armors/LeatherArmor.java
/Users/faraz/Documents/SBU/Term 6/AP/HW-04-JAVA-KNIGHT/Java-Knight/src/main/java/org/project/item/armors/Robe.java
/Users/faraz/Documents/SBU/Term 6/AP/HW-04-JAVA-KNIGHT/Java-Knight/src/main/java/org/project/item/consumables/Consumable.java
/Users/faraz/Documents/SBU/Term 6/AP/HW-04-JAVA-KNIGHT/Java-Knight/src/main/java/org/project/item/consumables/Flask.java
/Users/faraz/Documents/SBU/Term 6/AP/HW-04-JAVA-KNIGHT/Java-Knight/src/main/java/org/project/item/consumables/ManaPotion.java
/Users/faraz/Documents/SBU/Term 6/AP/HW-04-JAVA-KNIGHT/Java-Knight/src/main/java/org/project/item/weapons/Dagger.java
/Users/faraz/Documents/SBU/Term 6/AP/HW-04-JAVA-KNIGHT/Java-Knight/src/main/java/org/project/item/weapons/GreatAxe.java
/Users/faraz/Documents/SBU/Term 6/AP/HW-04-JAVA-KNIGHT/Java-Knight/src/main/java/org/project/item/weapons/Staff.java
/Users/faraz/Documents/SBU/Term 6/AP/HW-04-JAVA-KNIGHT/Java-Knight/src/main/java/org/project/item/weapons/Sword.java
/Users/faraz/Documents/SBU/Term 6/AP/HW-04-JAVA-KNIGHT/Java-Knight/src/main/java/org/project/item/weapons/Weapon.java
/Users/faraz/Documents/SBU/Term 6/AP/HW-04-JAVA-KNIGHT/Java-Knight/src/main/java/org/project/location/Location.java
/Users/faraz/Documents/SBU/Term 6/AP/HW-04-JAVA-KNIGHT/Java-Knight/src/main/java/org/project/util/ConsoleColors.java
/Users/faraz/Documents/SBU/Term 6/AP/HW-04-JAVA-KNIGHT/Java-Knight/src/main/java/org/project/util/Dice.java
+174 -154
View File
@@ -1,175 +1,195 @@
# Fourth Assignment - Java Knight ⚔️
A turn-based RPG with Roguelike elements which can be run in the terminal.
# Java Knight ⚔️
### **Prologue: The Legend of Javanest**
*For centuries, the land of Javanest lived in peace, until a magical Dragon attacked, plunging the realm into absolute darkness. With a wicked curse, the Dragon transformed the innocent people into horrific monsters: Goblins, Skeletons, and Vampires. Retreating to its impenetrable Castle, the Dragon divided the three keys to its lair and hid them among these cursed creatures. Now, it is your duty to step up and save the land. You must battle these monsters, recover the unique key from each monster type, and finally slay the Dragon to break the curse and restore peace to Javanest!*
A turn-based, roguelike-inspired RPG that runs entirely in the terminal, written
in Java for the fourth Advanced Programming assignment.
### **Introduction**
Welcome to **Java knight**, a turn-based RPG inspired by Roguelike games! In this assignment, you will develop a **text-based role-playing game (RPG)**. This project is designed to rigorously test your understanding of **Object-Oriented Programming (OOP) principles**.
⚠️ **REQUIREMENT:** You **must** utilize all the OOP concepts you have learned so far—including *Inheritance, Interfaces, Abstract Classes, Encapsulation, Polymorphism, Overloading, and Overriding*. It is extremely important that you use everything in its right place. Your design and architecture will be graded based on how well you apply these principles to avoid code duplication and maintain a clean structure.
🎯 **Your goal is not just to complete the assignment but to learn and apply OOP effectively!**
### **What is a Turn-Based Game?**
In this combat system, two sides - which are usually the player's side and the enemy's side - attack each other in turns. The side which is not attacking can perform actions to avoid or deflect the enemy's attack.
### **Core Mechanics:**
- **Turn-based combat** Players and monsters take turns attacking each other.
- **Character classes with Unique Traits** Players can choose from archetypes like **Knight, Assassin, or Wizard**, each starting with distinctly different base stats.
- **Unified Mana/Stamina System** All player classes use a unified resource (Mana/Stamina) to perform actions.
- **Standardized Action Set** Every player character has exactly 5 specific actions available during their turn.
- **Experience & Leveling System** Earn XP based on enemy strength to automatically level up and increase your base stats.
- **Progression System** You cannot fight the Dragon immediately. You must farm enemies for a chance to drop their specific key, collect all three, and grow stronger first.
> *For centuries, the land of Javanest lived in peace, until a magical Dragon
> plunged it into darkness, cursing its people into monsters and scattering the
> three keys to its lair among them. Battle the cursed creatures, recover the
> Goblin, Skeleton and Vampire keys, grow strong, and slay the Dragon to break
> the curse and restore peace to Javanest!*
---
## Tasks 📝
### 1️⃣ Step 1: Fork & Setup 🍴
1. **Fork** this repository and clone it to your local machine.
```bash
git clone https://git.meshcomp.ir/AdvancedProgramming1404/HW-04-JAVA-KNIGHT.git
```
2. Create a new branch named `develop` and switch to it.
```bash
git checkout -b develop
```
### 2️⃣ Step 2: Implement the Class Hierarchy 🌲
A well-structured OOP hierarchy is crucial. Avoid duplicating code by placing shared logic in abstract classes.
- **Entities & Locations:** You have `Entity`, `Item`(Bonus) , and `Location`.
- **Players:** `Player` is an abstract class implementing `Entity`. Subclasses: `Wizard`, `Knight`, `Assassin`.
- **Base Stat Differences:** Each class must have distinct starting stats. For example:
- **Knight:** Highest Base Damage.
- **Wizard:** Highest Max Health (HP).
- **Assassin:** Highest Max Stamina/Mana.
- **Enemies:** `Enemy` is an abstract class implementing `Entity`. Subclasses: `Skeleton`, `Goblin`, `Vampire`, and **`Dragon`**.
- **The Boss:** Even though `Dragon` is the final boss, it **must** be a subclass of `Enemy` to inherit common combat properties, while possessing extremely high stats and unique mechanics.
- **Item (Bonus):** `Consumable`, `Armor`, `Weapon` are abstract classes implementing `Item`. example :
- KnightArmor extends Armor - you can add more subclasses of Armor for extra score
- Sword extends Weapon - you can add more subclasses of Weapon for extra score
- Flask extends Consumable - you can add more subclasses of Consumable for extra score
![structure](Readme_Pictures/structure.png)
### 3️⃣ Step 3: Implement Player & Monster Methods 🏹
**Player Actions (The Rule of Five):**
Every player class **must** implement exactly the following 5 actions (You can use an interface like `ICombatActions`). Every action (except Light Attack) consumes a specific amount of Mana/Stamina. The **Special Ability** must consume the *highest* amount of Mana compared to the others.
1. **Light Attack:** Deals moderate damage and costs **NO Mana**. (Note: If the player runs out of Mana/Stamina, this is the ONLY action they can perform.)
2. **Heavy Attack:** Deals high damage, medium Mana cost.
3. **Defend:** Completely blocks or significantly reduces the damage of the enemy's *next* strike. Medium Mana cost.
4. **Heal:** Restores a portion of the player's HP. Medium-high Mana cost.
5. **Special Ability:** A unique class-based ultimate move (Highest Mana cost):
- **Wizard** 🧙‍♂️: Casts a devastating spell that damages the enemy while simultaneously replenishing some HP.
- **Assassin** 🗡️: Turns invisible, dodging the next incoming attack completely and guaranteeing a *Critical Hit* on their next turn.
- **Knight** ⚔️: Performs a shield bash that stuns the enemy, forcing them to skip their next turn while dealing heavy damage.
**Monster Abilities:**
- **Goblin** 👹: High critical hit chance but low health.
- **Skeleton** ☠️: Can resurrect once per battle with 50% HP.
- **Vampire** 🦇: Lifesteal ability a portion of the damage it deals to the player is added back to its own health.
- **Dragon (Final Boss)** 🐉: Immune to normal defense. Its fiery breath bypasses shields and deals massive damage.
🔹 Make sure each entity **prints messages** when performing actions. example output (while in combat) :
```bash
You chose to FIGHT!
[Ser Duncan - 45/45 HP | 40/40 Mana]
[Goblin - 30/30 HP]
## Table of Contents
1. [How to Compile & Run](#how-to-compile--run)
2. [How to Play](#how-to-play)
3. [Classes, Enemies & Stats](#classes-enemies--stats)
4. [Project Structure](#project-structure)
5. [OOP Principles Used](#oop-principles-used)
6. [Bonus Features](#bonus-features)
---
Your Turn:
1. Light Attack | 2. Heavy Attack (-8 Mana) | 3. Defend (-6 Mana) | 4. Heal (-12 Mana) | 5. Shield Bash (-15 Mana)
```
## How to Compile & Run
The project is a standard Maven project. You need **JDK 21+** installed.
### Using Maven (recommended)
```bash
→ Chose: Light Attack
⚔️ Ser Duncan (Knight) used Light Attack! (0 Mana)
Goblin took 10 damage!
Goblin has 20/30 HP remaining.
Ser Duncan Mana: 40/40
```
```
Goblin's Turn :
👹 Goblin used Critical Strike!
💥 Critical hit! Ser Duncan took 20 damage!
Ser Duncan has 25/45 HP remaining.
```
🔹 *Narrative Console:* Use ANSI escape codes to print colorful narrative logs (e.g., Red for damage, Blue for Mana usage, Green for healing).
### 4️⃣ Step 4: Implement the Game Loop & Progression 🎮
1. **The Core Loop:** The game starts with the player entering a location. A random standard enemy (`Goblin`, `Skeleton`, or `Vampire`) spawns immediately.
2. **Player Choices:** Before engaging, the player is presented with the following options:
- **1. Fight the enemy:** Enter the turn-based combat sequence.
- **2. Move to another location:** Skip the current enemy and spawn a new random one.
- **3. Go to the Castle to fight the Dragon:** *(Note: This option must remain strictly hidden or locked until the player has successfully collected all 3 keys).*
3. **The Key Drop Logic (RNG Gatekeeping):**
- When the player defeats an enemy, there is a **specific percentage chance (e.g., 20%)** that it will drop the unique key associated with its species (Goblin Key, Skeleton Key, Vampire Key).
- **One Key Per Species:** Once a player obtains a specific key (e.g., Goblin Key), subsequent enemies of that same type (other Goblins) will **never** drop a key again.
- The player **must collect all 3 distinct keys** to unlock Option 3 and enter the Castle.
4. **Post-Combat Recovery:** After each successful battle, the player's HP and Mana bars must automatically replenish (either fully or partially) to their base amounts so they are ready for the next encounter.
5. **Experience & Leveling System:**
- Defeating an enemy grants **XP**. The amount of XP must scale proportionally to the enemy's power level.
- Upon reaching an XP threshold, the player levels up. **Leveling up must automatically increase the player's Max HP and Max Stamina/Mana**, making them strong enough to eventually face the Dragon.
6. **Final Boss Fight:** Once the 3 Keys are obtained and the player chooses to go to the Castle, they will face the Dragon. Defeating the Dragon breaks the curse, resulting in **Victory**. Dying at any point results in **Game Over**.
🔹 Example game loop structure:
```java
while (player.isAlive() && enemy.isAlive())
player.attack(enemy);
if (enemy.isAlive()) {
enemy.attack(player);
}
}
cd Java-Knight
mvn compile
mvn exec:java -Dexec.mainClass=org.project.Main
```
### Using plain `javac` / `java`
```bash
cd Java-Knight
# compile every source file into the target/classes folder
find src/main/java -name "*.java" > sources.txt
javac -d target/classes @sources.txt
### 5️⃣ Step 5: Extra Features & Bonus Tasks ⭐
*(Optional for extra credit)*
# run it
java -cp target/classes org.project.Main
```
**Dynamic Economy & Merchant System:** Implement coins that drop from enemies. Add a "Visit Merchant" option to the main loop where players can spend coins to buy specific weapons, armors, or consumables.
**Multiple Weapons & Inventory:** Players can buy, store, and swap between multiple weapons or use consumables mid-combat.
**Multiplayer/Party Mode:** Allow multiple players to team up and fight multiple enemies together. The Dragon's breath attack will damage the entire party simultaneously.
**PvP Mode:** Implement a **Player vs. Player** combat system.
### 6️⃣ Step 6: Write a Comprehensive README 📄
As the final mandatory step of your development, you must replace the default `README.md` with your own comprehensive documentation. Your README should include:
- A brief introduction to the game.
- How to compile and run your project from the terminal.
- An explanation of the classes, design patterns, and OOP principles you used.
- A brief guide on how to play (controls, stats, classes).
> The game uses ANSI colour codes, so run it in a real terminal (macOS Terminal,
> Linux shell, Windows Terminal) for the intended colourful output.
---
## Evaluation Criteria ⚖
## How to Play
| **Criteria** | **Points** |
|-------------------------------------------------------------|------------|
| Proper use of OOP principles | **50** |
| Working combat mechanics, Leveling System & Enemy abilities | **20** |
| Clear and Comprehensive `README.md` | **20** |
| Code readability, documentation, and comments | **10** |
| Meaningful, interactive, & colored console outputs | **10** |
| Inventory (item) and Merchant System (bonus) | **20** |
| Other Extra features (bonus tasks) | **20** |
| **Total Score** | **150** |
1. **Name your hero** and **pick a class** (Knight, Wizard, or Assassin).
2. You wander into a random **location** and a random standard enemy appears.
For each encounter you choose:
- **1. Fight** enter turn-based combat.
- **2. Move** skip this enemy and travel somewhere new.
- **3. Visit the Merchant** spend coins on potions, a weapon, or repairs.
- **4. Go to the Castle** *only appears once you hold all 3 keys* fight
the Dragon.
3. **Combat is turn-based.** On your turn you pick one of five actions. Only
**Light Attack** is free; the rest cost Mana/Stamina, and the class
**Special** costs the most. If you run out of Mana, you can still Light
Attack.
## Tips 🚀
- **Follow OOP principles**: Avoid redundant code by using inheritance properly. Think carefully about what belongs in an abstract class vs. a specific subclass. Make sure you use overriding and overloading correctly.
- **Test your code**: Run different scenarios (fighting, running out of mana, leveling up, dying) to ensure everything works as expected.
- **Ask for help**: If you're stuck, reach out to your classmates or mentors.
| # | Action | Cost | Effect |
|---|---------------|-----------|--------|
| 1 | Light Attack | 0 | Moderate damage |
| 2 | Heavy Attack | 8 | Double damage |
| 3 | Defend | 6 | Blocks ~75% of the next hit |
| 4 | Heal | 12 | Restores 40% of max HP |
| 5 | Special | 15 | Unique class ultimate |
| 6 | Use Item | | Drink a potion from your inventory |
## Submission ⌛
- **Deadline**: Submit your assignment before **21 Ordibehesht (May 11th, 2026)**.
- **Submission Format**: Push your code to your forked repository, create a PR, and ensure your comprehensive `README.md` is included in the root directory.
4. **Winning a fight** grants **XP** (scaled to the enemy's power), **coins**,
and a chance to drop that species' **key**. Your HP and Mana are then fully
restored.
5. **Leveling up** automatically raises your Max HP, Max Mana and base damage.
6. Collect **all 3 keys**, go to the Castle, and defeat the **Dragon** to win.
Dying at any point is **Game Over**.
![cover](Readme_Pictures/image.png)
###### - Born of God and Void. You shall seal the blinding light that plagues their dreams. You are the Vessel. You are the Java Knight.
---
## Classes, Enemies & Stats
### Player Classes
Each class has deliberately different starting stats, so they play differently.
| Class | HP | Mana | Base Dmg | Weapon | Special ability |
|------------|----|------|----------|-------------|-----------------|
| **Knight** ⚔️ | 50 | 40 | **8 (highest)** | Iron Sword | **Shield Bash** heavy damage + stuns the enemy (skips its next turn) |
| **Wizard** 🧙 | **65 (highest)** | 45 | 5 | Oak Staff | **Arcane Blast** heavy damage AND heals the caster |
| **Assassin** 🗡️ | 45 | **55 (highest)** | 6 | Twin Dagger | **Vanish** dodges the next attack + guarantees a crit on the next strike |
### Enemies
| Enemy | HP | Ability | Key |
|------------|-----|---------|-----|
| **Goblin** 👹 | 30 | 40% chance to land a **critical hit** (double damage) | Goblin Key |
| **Skeleton** ☠️ | 40 | **Resurrects once** per battle at 50% HP | Skeleton Key |
| **Vampire** 🦇 | 48 | **Lifesteal** heals for 50% of the damage it deals | Vampire Key |
| **Dragon** 🐉 | 160 | **True damage** fiery breath ignores armor *and* the Defend stance | — |
Standard enemies each have a **25% chance** to drop their key, and only **one key
per species** can ever drop (once you have the Goblin Key, no other Goblin will
drop one).
---
## Project Structure
```
org.project
├── Main.java // entry point builds the world, starts the game
├── Game.java // the game controller / main loop
├── Merchant.java // bonus shop system
├── entity
│ ├── Entity.java // interface every combatant implements
│ ├── players
│ │ ├── ICombatActions.java // interface: the 5 required actions
│ │ ├── Player.java // abstract base for all heroes
│ │ ├── Knight.java / Wizard.java / Assassin.java
│ └── enemies
│ ├── Enemy.java // abstract base for all monsters
│ ├── KeyType.java // enum of the three keys
│ ├── Goblin.java / Skeleton.java / Vampire.java / Dragon.java
├── item
│ ├── Item.java // interface for anything ownable/sellable
│ ├── weapons (Weapon abstract → Sword, Dagger, Staff, GreatAxe)
│ ├── armors (Armor abstract → KnightArmor, LeatherArmor, Robe)
│ └── consumables (Consumable abstract → Flask, ManaPotion)
├── location
│ └── Location.java // a place that spawns random enemies
└── util
├── ConsoleColors.java // ANSI colour helpers
└── Dice.java // random-number helpers
```
---
## OOP Principles Used
This project was designed around the seven principles the assignment asks for:
- **Encapsulation** all fields are `private`/`protected` with getters. HP, Mana
and XP can only be changed through methods like `takeDamage`, `heal` and
`gainXP`, which enforce their own rules (clamping to max, capping at zero,
leveling up).
- **Inheritance** shared logic lives in abstract base classes so it isn't
duplicated. `Player` holds the HP/Mana bars, leveling, inventory and four of
the five actions; `Knight`, `Wizard` and `Assassin` only add what's unique.
Same idea for `Enemy` → its four monster subclasses, and for
`Weapon`/`Armor`/`Consumable` → their concrete items.
- **Interfaces** `Entity` is the contract every combatant obeys, which is what
lets the combat loop treat a Knight and a Dragon identically. `ICombatActions`
guarantees every player class implements exactly the five required actions.
`Item` unifies weapons, armor and consumables so the merchant can sell any of
them.
- **Abstract classes** `Player`, `Enemy`, `Weapon`, `Armor` and `Consumable`
are all abstract: they provide shared state/behaviour but can't be instantiated
on their own, forcing subclasses to fill in the specifics (e.g.
`Player.specialAbility` is abstract).
- **Polymorphism** the whole game loop is polymorphic. `Game` only ever calls
`Player`/`Enemy`/`Entity` methods; the correct `specialAbility`, `attack` or
`takeDamage` runs depending on the real object. Each enemy overrides `attack`
to add its signature move.
- **Overriding** subclasses override base behaviour, e.g. `Skeleton` overrides
`takeDamage` to resurrect, `Vampire`/`Goblin`/`Dragon` override `attack`, and
every player class overrides `specialAbility`.
- **Overloading** `takeDamage(int)` vs `takeDamage(int, boolean)` in `Player`.
The second, overloaded version ignores defenses and is how the Dragon's breath
bypasses armor and shields. The `Sword` also overloads its constructor.
---
## Bonus Features
- **Coins & Merchant system** enemies drop coins; the Merchant sells Health
Flasks, Mana Potions, a stronger **Great Axe**, and can **repair** armor.
- **Inventory & consumables** potions can be bought, stored and used mid-combat
(action 6).
- **Durable armor** armor loses durability as it absorbs hits and eventually
breaks, until repaired at the merchant.
- **Colourful narrative console** ANSI colours (red for damage, blue for mana,
green for healing) make the combat log easy to read.
---
*Born of God and Void. You shall seal the blinding light that plagues their
dreams. You are the Vessel. You are the Java Knight.*