Job + Spell merge #9

Merged
erns6604 merged 23 commits from Job into main 2025-10-27 12:18:22 +01:00
31 changed files with 701 additions and 50 deletions

12
pom.xml
View File

@@ -20,6 +20,18 @@
<version>5.8.1</version> <version>5.8.1</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
<version>2.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.12.0</version>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -1,5 +1,5 @@
package Action; package Action;
public interface Action { public interface Action {
void exectue(Actor player); void execute(Actor player);
} }

View File

@@ -0,0 +1,19 @@
package Action;
import Job.HasJob;
import Job.Wizard;
public class CastAction implements Action {
private Wizard requireWizard(Actor actor) {
if (actor instanceof HasJob hasJob && hasJob.getJob() instanceof Wizard wizard) {
return wizard;
}
throw new IllegalStateException(actor + " cannot perform this action without being a Wizard!");
}
@Override
public void execute(Actor player) {
var wizard = requireWizard(player);
wizard.castSpell(player);
}
}

View File

@@ -5,7 +5,7 @@ import Job.Miner;
public class DigAction implements Action { public class DigAction implements Action {
@Override @Override
public void exectue(Actor actor) { public void execute(Actor actor) {
Miner miner = requireMiner(actor); Miner miner = requireMiner(actor);
miner.dig(actor); miner.dig(actor);
} }

View File

@@ -0,0 +1,19 @@
package Action;
import Job.HasJob;
import Job.Wizard;
public class LearnSpellAction implements Action {
@Override
public void execute(Actor actor) {
Wizard wizard = requireWizard(actor);
wizard.learnSpell(actor);
}
private Wizard requireWizard(Actor actor) {
if (actor instanceof HasJob hasJob && hasJob.getJob() instanceof Wizard wizard) {
return wizard;
}
throw new IllegalStateException(actor + " cannot perform this action without being a Wizard!");
}
}

22
src/main/java/Attack.java Normal file
View File

@@ -0,0 +1,22 @@
public abstract class Attack {
//Namnet och bägge dessa siffror är helt lösryckta
private static final String DEFAULT_NAME = "DEFAULT_NAME";
private static final double DEFAULT_ENERGY_COST = 5d;
private static final double DEFAULT_DAMAGE = 5d;
private String name;
private double energyCost;
private double damage;
public Attack() {
name = DEFAULT_NAME;
energyCost = DEFAULT_ENERGY_COST;
damage = DEFAULT_DAMAGE;
}
public Attack(String name, double energyCost, double damage) {
this.name = name;
this.energyCost = energyCost;
this.damage = damage;
}
}

View File

@@ -0,0 +1,3 @@
public enum Biomes {
GRASSLAND, MOUNTAIN, COAST, FOREST //Är inte fäst vid dessa
}

View File

@@ -0,0 +1,77 @@
import Entity.Position;
//Vill inte göra så mycket med den här klassen, då jag vill påverka implementeringen av Player så lite som möjligt
public abstract class Character {
private static final double DEFAULT_HEALTH = 50d;
private static final double DEFAULT_LEVEL = 1.0;
private static final double DEFAULT_ENERGY = 50d; //Detta borde kanske egentligen beräknas och sättas automatiskt genom en algoritm som tar health och level i beaktskap?? Eller något sådant
private static final Position DEFAULT_POSITION = new Position(0,0); //Är detta en bra idé?? Mest för att kunna ha defaultkonstruktor
// Borde jag bara sätta allt till default direkt här????
private double health;
private double level;
private double energy; //Borde kanske beräknas genom en algoritm istället för att kunna sättas i konstruktorn... Så det alltid blir balanserat
private Position position;
// Hur många varianter på konstruktorn behövs?
public Character() {
this.health = DEFAULT_HEALTH;
this.level = DEFAULT_LEVEL;
this.energy = DEFAULT_ENERGY;
this.position = DEFAULT_POSITION;
}
// Jag antar att den som instansierar massa monster i världen ansvarar
// för att kolla att "position" har ett tillåtet värde.
// Just denna variant är bara för testning i min implementation av Monster
public Character(Position position) {
this.health = DEFAULT_HEALTH;
this.level = DEFAULT_LEVEL;
this.energy = DEFAULT_ENERGY;
this.position = position;
}
public Character(double health, double level, double energy, Position position) {
this.health = health;
this.level = level;
this.energy = energy;
this.position = position;
}
//Returnerar true om attacken gick igenom, annars false
//Tänker att positionen som skickas in är karaktärens egna
//Kommer antagligen behöva diverse hjälpmetoder i implementeringen då det är många krav som måste uppfyllas för att attacken ska gå igenom
abstract boolean attack(Position position, Character character);
public double getHealth() {
return health;
}
public void setHealth(int newHealth) {
health = newHealth;
}
public double getLevel() {
return level;
}
public void setLevel(int newLevel) {
level = newLevel;
}
public double getEnergy() {
return energy;
}
public void setEnergy(double newEnergy) {
energy = newEnergy;
}
public Position getPosition() {
return position;
}
public void setPosition(Position newPosition) {
position = newPosition;
}
}

View File

@@ -0,0 +1,7 @@
package Character;
import java.util.List;
public interface HasSpellBook {
List<String> getSpellBook();
}

View File

@@ -2,5 +2,6 @@ package Combat;
public interface HasHealth { public interface HasHealth {
void setHealth(int health); void setHealth(int health);
int getHealth();
boolean isAlive(); boolean isAlive();
} }

View File

@@ -0,0 +1,6 @@
package Combat;
public interface HasMana {
void setMana(int mana);
int getMana();
}

View File

@@ -0,0 +1,18 @@
package Combat;
import Entity.Entity;
public class OffensiveDamageSpell extends Spell {
public OffensiveDamageSpell(String spellName, int cost, int potency) {
super(spellName, cost, potency);
}
@Override
public void cast(Entity source, Entity target) {
if(target instanceof HasHealth health && source instanceof HasMana mana) {
mana.setMana(mana.getMana() - this.getCost());
health.setHealth(health.getHealth() - this.getPotency());
}
}
}

View File

@@ -0,0 +1,54 @@
package Combat;
import Entity.Entity;
public abstract class Spell {
private final String spellName;
private int cost;
private int potency;
public Spell(String spellName, int cost, int potency) {
this.spellName = spellName;
this.cost = cost;
this.potency = potency;
}
public String getSpellName() {
return spellName;
}
public int getCost() {
return cost;
}
public int getPotency() {
return potency;
}
public abstract void cast(Entity source, Entity target);
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Spell spell = (Spell) o;
if (cost != spell.cost) return false;
if (potency != spell.potency) return false;
return spellName.equals(spell.spellName);
}
@Override
public int hashCode() {
int result = spellName.hashCode();
result = 31 * result + cost;
result = 31 * result + potency;
return result;
}
@Override
public String toString() {
return spellName + " (Cost: " + cost + ", Potency: " + potency + ")";
}
}

View File

@@ -3,18 +3,24 @@ package Entity;
import Action.Action; import Action.Action;
import Action.Actor; import Action.Actor;
import Combat.HasHealth; import Combat.HasHealth;
import Combat.HasMana;
import Combat.Spell;
import Inventory.Inventory;
import Job.Job; import Job.Job;
import Job.HasJob; import Job.HasJob;
import Inventory.HasInventory; import Inventory.HasInventory;
import Inventory.HasSpellBook;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
public class Player extends Entity implements Movable, Actor, HasInventory, HasJob, HasHealth { public class Player extends Entity implements Movable, Actor, HasInventory, HasSpellBook, HasJob, HasHealth, HasMana {
protected int health; protected int health;
protected int mana;
protected Job job; protected Job job;
protected Position position; protected Position position = new Position(0,0);
protected List<String> items = new LinkedList<>(); protected Inventory inventory= new Inventory();
protected List<Spell> spells = new LinkedList<>();
public Player(String name, Job job) { public Player(String name, Job job) {
super(name); super(name);
this.job = job; this.job = job;
@@ -34,7 +40,7 @@ public class Player extends Entity implements Movable, Actor, HasInventory, HasJ
@Override @Override
public boolean canMoveTo(Position position) { public boolean canMoveTo(Position position) {
return false; return true;
} }
public Job getJob() { public Job getJob() {
return job; return job;
@@ -45,13 +51,15 @@ public class Player extends Entity implements Movable, Actor, HasInventory, HasJ
this.job = job; this.job = job;
} }
public List<String> getInventory() { public Inventory getInventory() {
return items; return inventory;
} }
public List<Spell> getSpellBook() {return spells;}
@Override @Override
public void performAction(Action action) { public void performAction(Action action) {
action.exectue(this); action.execute(this);
} }
@Override @Override
@@ -69,6 +77,15 @@ public class Player extends Entity implements Movable, Actor, HasInventory, HasJ
this.health = health; this.health = health;
} }
@Override
public int getHealth() {return health;}
@Override
public void setMana(int mana) {this.mana = mana;}
@Override
public int getMana() {return mana;}
@Override @Override
public boolean isAlive() { public boolean isAlive() {
return health > 0; return health > 0;

View File

@@ -3,5 +3,5 @@ package Inventory;
import java.util.List; import java.util.List;
public interface HasInventory { public interface HasInventory {
List<String> getInventory(); Inventory getInventory();
} }

View File

@@ -0,0 +1,8 @@
package Inventory;
import java.util.List;
import Combat.Spell;
public interface HasSpellBook {
List<Spell> getSpellBook();
}

View File

@@ -0,0 +1,24 @@
package Inventory;
import java.util.ArrayList;
import java.util.List;
public class Inventory {
List<String> items;
public Inventory() {
items = new ArrayList<>();
}
List<String> getItems() {
return items;
}
public void addItem(String item) {
items.add(item);
}
public boolean containsItem(String item) {
return items.contains(item);
}
}

View File

@@ -1,10 +1,11 @@
package Job; package Job;
import Shared.HasExperience; import Shared.ExperienceTable;
import Shared.Levelable;
import java.util.Objects; import java.util.Objects;
public abstract class Job implements HasExperience { public abstract class Job implements Levelable {
protected int level; protected int level;
protected int experience; protected int experience;
protected String name; protected String name;
@@ -18,7 +19,27 @@ public abstract class Job implements HasExperience {
return level; return level;
} }
@Override
public int remainingXpUntilLevelUp() {
return ExperienceTable.getXpForLevel(level) - experience;
}
@Override
public void gainExperience(int exp) {
experience += exp;
if (experience >= ExperienceTable.getXpForLevel(level)) {
levelUp();
}
}
@Override
public int getExperience() {
return experience;
}
@Override
public void levelUp() { public void levelUp() {
experience = experience - ExperienceTable.getXpForLevel(level);
level++; level++;
} }
@@ -38,4 +59,5 @@ public abstract class Job implements HasExperience {
return Objects.hash(name); return Objects.hash(name);
} }
} }

View File

@@ -11,17 +11,7 @@ public class Miner extends Job {
public void dig(Actor actor) { public void dig(Actor actor) {
if (actor instanceof HasInventory a) { if (actor instanceof HasInventory a) {
a.getInventory().add("Stone"); a.getInventory().addItem("Stone");
} }
} }
@Override
public int getExperience() {
return experience;
}
@Override
public void gainExperience(int exp) {
experience += exp;
}
} }

View File

@@ -0,0 +1,70 @@
package Job;
import Action.Actor;
import Combat.HasMana;
import Combat.OffensiveDamageSpell;
import Entity.Entity;
import Inventory.HasSpellBook;
import Combat.Spell;
import java.util.Scanner;
public class Wizard extends Job {
private final Scanner scanner;
public Wizard(Scanner scanner) {
super("Wizard");
this.scanner = scanner;
}
public Wizard() {
this(new Scanner(System.in));
}
public void learnSpell(Actor actor) {
if(actor instanceof HasSpellBook a ) {
OffensiveDamageSpell defaultSpell = new OffensiveDamageSpell("fireball", 20, 20);
a.getSpellBook().add(defaultSpell);
}
}
public void castSpell(Actor actor) {
if(actor instanceof HasSpellBook spellbook && actor instanceof HasMana mana && actor instanceof Entity entity) {
if(spellbook.getSpellBook().isEmpty()){
System.out.println("You haven't learned any spells");
return;
}
System.out.println("Which spell do you want to cast?");
for(int i = 0; i < spellbook.getSpellBook().size(); i++){
System.out.println((i + 1) + ". " + spellbook.getSpellBook().get(i).toString());
}
int spellIndex = scanner.nextInt();
scanner.nextLine();
if(spellIndex < 1 || spellIndex > spellbook.getSpellBook().size()) {
System.out.println("Invalid choice!");
return;
}
Spell spell = spellbook.getSpellBook().get(spellIndex - 1);
if (mana.getMana() >= spell.getCost()){
spell.cast(entity, entity);
}
else{
System.out.println("Not enough mana!");
}
}
}
@Override
public int getExperience() {
return experience;
}
@Override
public void gainExperience(int exp) {
experience += exp;
}
}

View File

@@ -0,0 +1,32 @@
import Entity.Position;
import java.util.*;
public abstract class Monster extends Character{
private final List<Biomes> habitat = new ArrayList<>();
public Monster() {
habitat.addAll(Arrays.asList(Biomes.GRASSLAND, Biomes.MOUNTAIN, Biomes.COAST, Biomes.FOREST));
}
public Monster(Position position) {
super(position);
habitat.addAll(Arrays.asList(Biomes.GRASSLAND, Biomes.MOUNTAIN, Biomes.COAST, Biomes.FOREST));
}
public Monster(double health, double level, double energy, Position position) {
super(health, level, energy, position);
habitat.addAll(Arrays.asList(Biomes.GRASSLAND, Biomes.MOUNTAIN, Biomes.COAST, Biomes.FOREST));
}
public Monster(double health, double level, double energy, Position position, List<Biomes> habitat) {
super(health, level, energy, position);
this.habitat.addAll(habitat);
}
//Är detta bra??? Med unmodifiableList dvs
public List<Biomes> getHabitat() {
return Collections.unmodifiableList(habitat);
}
}

View File

@@ -0,0 +1,16 @@
package Shared;
import java.util.Map;
public final class ExperienceTable {
private static final Map<Integer, Integer> XP_TABLE = Map.of(
1, 100,
2, 250
);
private ExperienceTable() {}
public static int getXpForLevel(int level) {
return XP_TABLE.getOrDefault(level, Integer.MAX_VALUE);
}
}

View File

@@ -1,6 +1,6 @@
package Shared; package Shared;
public interface HasExperience { public interface Levelable {
int getLevel(); int getLevel();
int getExperience(); int getExperience();
@@ -8,4 +8,6 @@ public interface HasExperience {
void gainExperience(int exp); void gainExperience(int exp);
void levelUp(); void levelUp();
int remainingXpUntilLevelUp();
} }

View File

@@ -1,18 +0,0 @@
public class Spell {
private final String spellName;
private int cost;
private int potency;
public Spell(String spellName, int cost, int potency) {
this.spellName = spellName;
this.cost = cost;
this.potency = potency;
}
public String getSpellName() {return spellName;}
public int getCost() {return cost;}
public int getPotency() {return potency;}
}

View File

@@ -0,0 +1,65 @@
import Combat.OffensiveDamageSpell;
import Entity.Player;
import Job.Wizard;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayInputStream;
import java.util.Scanner;
import static org.hamcrest.Matchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
public class InterestingTests {
private Wizard wizard;
@BeforeEach
public void setup() {
wizard = new Wizard();
}
@Test
public void testCastSpell_successfulCast() {
Scanner testScanner = new Scanner(new ByteArrayInputStream("1\n".getBytes()));
wizard = new Wizard(testScanner);
Player player = new Player("Gandalf", wizard);
player.setMana(50);
player.setHealth(100);
OffensiveDamageSpell fireball = new OffensiveDamageSpell("Fireball", 20, 15);
player.getSpellBook().add(fireball);
int initialMana = player.getMana();
int initialHealth = player.getHealth();
wizard.castSpell(player);
assertThat(player.getMana(), is(initialMana - fireball.getCost()));
assertThat(player.getHealth(), is(initialHealth - fireball.getPotency()));
}
@Test
public void testCastSpell_notEnoughMana() {
Scanner testScanner = new Scanner(new ByteArrayInputStream("1\n".getBytes()));
wizard = new Wizard(testScanner);
Player player = new Player("Merlin", wizard);
player.setMana(10);
player.setHealth(100);
OffensiveDamageSpell fireball = new OffensiveDamageSpell("Fireball", 20, 15);
player.getSpellBook().add(fireball);
int initialMana = player.getMana();
int initialHealth = player.getHealth();
wizard.castSpell(player);
assertThat(player.getMana(), is(initialMana));
assertThat(player.getHealth(),is(initialHealth));
}
}

View File

@@ -0,0 +1,61 @@
import Action.CastAction;
import Action.LearnSpellAction;
import Combat.OffensiveDamageSpell;
import Entity.Player;
import Job.Wizard;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class MagicSystemTest {
private Player defaultPlayer() {
return new Player("Alex");
}
@Test void cant_cast_spell_without_being_wizard(){
var p = defaultPlayer();
CastAction action = new CastAction();
IllegalStateException exception = assertThrows(
IllegalStateException.class,
() -> action.execute(p)
);
assertTrue(exception.getMessage().contains("cannot perform this action without being a Wizard"));
}
@Test void cant_learn_spell_without_being_wizard(){
var p = defaultPlayer();
LearnSpellAction action = new LearnSpellAction();
IllegalStateException exception = assertThrows(
IllegalStateException.class,
() -> action.execute(p)
);
assertTrue(exception.getMessage().contains("cannot perform this action without being a Wizard"));
}
@Test void damage_spell_damages_target(){
var defaultSpell = new OffensiveDamageSpell("fireball", 20, 20);
var p = defaultPlayer();
p.setMana(30);
p.setHealth(30);
defaultSpell.cast(p, p);
assertEquals(10, p.getHealth());
}
@Test void damage_spell_drains_mana_from_caster(){
var defaultSpell = new OffensiveDamageSpell("fireball", 20, 20);
var p = defaultPlayer();
p.setMana(30);
p.setHealth(30);
defaultSpell.cast(p, p);
assertEquals(10, p.getMana());
}
@Test void cant_cast_spell_with_empty_spellbook(){
var p = defaultPlayer();
var job = new Wizard();
p.learnJob(job);
p.performAction(new CastAction());
}
}

View File

@@ -6,7 +6,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
public class MinerTest { public class MinerTest {
@Test @Test
void miner_can_level_up() { void can_level_up() {
var job = new Miner(); var job = new Miner();
assertEquals(1, job.getLevel()); assertEquals(1, job.getLevel());
job.levelUp(); job.levelUp();
@@ -14,12 +14,33 @@ public class MinerTest {
} }
@Test @Test
void miner_can_gain_xp() { void can_gain_xp() {
var job = new Miner(); var job = new Miner();
job.gainExperience(25); job.gainExperience(25);
assertEquals(25, job.getExperience()); assertEquals(25, job.getExperience());
} }
@Test
void level_up_when_experience_cap_is_reached() {
var job = new Miner();
job.gainExperience(job.remainingXpUntilLevelUp());
assertEquals(2, job.getLevel());
}
@Test
void additional_xp_carries_over_on_level_up() {
var job = new Miner();
job.gainExperience(job.remainingXpUntilLevelUp() + 10);
assertEquals(10, job.getExperience());
}
@Test
void dig_on_ice_require_ice_pick() {
}
} }

View File

@@ -0,0 +1,50 @@
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
public class MonsterTest {
@Test
void monster_cannot_attack_when_dead() {
/*
Monster someMonster = new Monster();
someMonster.setHealth(-5d);
Assert(false, someMonster.attack(someMonster.getPosition(), character));
*/
}
@Test
void monster_cannot_be_attacked_when_dead() {
/*
Monster attackingMonster = new Monster();
Monster deadMonster = new Monster();
deadMonster.setHealth(-4d);
Assert(false, attackingMonster.attack(attackingMonster.getPosition(), deadMonster));
*/
}
@Test
void monster_cannot_attack_character_at_different_position() {
/*
Monster monsterAtOnePosition = new Monster(new Position(0,0));
Monster monsterAtDifferentPosition = new Monster(new Position(1,1));
Assert(false, monsterAtOnePosition.attack(monsterAtOnePosition.getPosition(), monsterAtDifferentPosition);
*/
}
@Test
void monster_is_instantiated_correctly_from_default_constructor() {
/*
AssertAll????
*/
}
@Test
void monster_is_instantiated_correctly_from_position_only_constructor() {
}
@Test
void monster_is_instantiated_correctly_from_full_constructor() {
}
}

View File

@@ -1,8 +1,11 @@
import Action.DigAction; import Action.DigAction;
import Action.LearnSpellAction;
import Combat.OffensiveDamageSpell;
import Entity.Position; import Entity.Position;
import Job.Miner; import Job.Miner;
import Entity.Player; import Entity.Player;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import Job.Wizard;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
@@ -18,6 +21,18 @@ class PlayerTest {
assertFalse(p.isAlive()); assertFalse(p.isAlive());
} }
@Test void get_health_returns_health() {
var p = defaultPlayer();
p.setHealth(10);
assertEquals(10, p.getHealth());
}
@Test void get_mana_returns_mana() {
var p = defaultPlayer();
p.setMana(10);
assertEquals(10, p.getMana());
}
@Test @Test
public void can_change_position() { public void can_change_position() {
var p = defaultPlayer(); var p = defaultPlayer();
@@ -38,9 +53,19 @@ class PlayerTest {
@Test @Test
void miner_can_dig() { void miner_can_dig() {
var p = new Player("John"); var p = new Player("John");
var job = new Miner(); p.learnJob(new Miner());
p.learnJob(job);
p.performAction(new DigAction()); p.performAction(new DigAction());
assertTrue(p.getInventory().contains("Stone")); assertTrue(p.getInventory().containsItem("Stone"));
}
@Test
void wizard_can_learn_spell() {
var p = new Player("Bob");
var job = new Wizard();
var defaultSpell = new OffensiveDamageSpell("fireball", 20, 20);
p.learnJob(job);
p.performAction(new LearnSpellAction());
System.out.println(p.getSpellBook());
assertTrue(p.getSpellBook().contains(defaultSpell));
} }
} }

View File

@@ -1,3 +1,5 @@
import Combat.OffensiveDamageSpell;
import Combat.Spell;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
@@ -5,19 +7,19 @@ import static org.junit.jupiter.api.Assertions.*;
public class SpellTest { public class SpellTest {
private Spell defaultSpell() { private Spell defaultSpell() {
return new Spell("fireball", 20, 40); return new OffensiveDamageSpell("fireball", 20, 40);
} }
@Test @Test
void setSpellNameOnCreation(){ void setSpellNameOnCreation(){
var spell = defaultSpell(); var spell = defaultSpell();
assertEquals("fireball", spell.getSpellName(), "Spell name should be set"); assertEquals("fireball", spell.getSpellName(), "Combat.Spell name should be set");
} }
@Test @Test
void setCostOnCreation() { void setCostOnCreation() {
var spell = defaultSpell(); var spell = defaultSpell();
assertEquals(20, spell.getCost(), "Spell cost should have been set"); assertEquals(20, spell.getCost(), "Combat.Spell cost should have been set");
} }
@Test @Test

View File

@@ -0,0 +1,26 @@
import Job.Wizard;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class WizardTest {
@Test
void wizard_has_level() {
var job = new Wizard();
assertEquals(1, job.getLevel());
}
@Test
void wizard_can_level_up() {
var job = new Wizard();
job.levelUp();
assertEquals(2, job.getLevel());
}
@Test
void wizard_can_gain_xp(){
var job = new Wizard();
job.gainExperience(40);
assertEquals(40, job.getExperience());
}
}