diff --git a/src/Battle.php b/src/Battle.php new file mode 100644 index 0000000..bed2e67 --- /dev/null +++ b/src/Battle.php @@ -0,0 +1,164 @@ +player = $player; + $this->monster = $monster; + $this->diceBag = new DiceBag(); + } + + public function getActions() + { + + } + + public function selectAction() + { + + } + + /** + * Fights the number of rounds given by the parameter $n and returns the number + * of actual rounds fought. + * @param int $n + * @param bool $firstDamageRound Which damage rounds are calculated. Cannot be 0. + * @return int Number of fights fought. + */ + public function fightNRounds(int $n = 1, int $firstDamageRound = self::DAMAGEROUND_BOTH): int + { + if ($firstDamageRound === 0) { + throw new ArgumentException('$firstDamageRound must not be 0.'); + } + + for ($count = 0; $count < $n; $count++) { + if ($this->player->isAlive() > 0 && $this->monster->isAlive()) { + $this->fightOneRound($firstDamageRound); + $isSurprised = self::DAMAGEROUND_BOTH; + } else { + break; + } + } + + return $count; + } + + /** + * Fights exactly 1 round + * @param int $firstDamageRound + */ + protected function fightOneRound(int $firstDamageRound) + { + // playerDamage is the damage done to the player, to the monster. + list($playerDamage, $monsterDamage, $playerAttack) = $this->calculateDamage(); + + // Player does damage to the monster + if ($firstDamageRound & self::DAMAGEROUND_PLAYER + && $this->player->isAlive() + && $this->monster->isAlive() + ) { + if ($monsterDamage < 0) { + // The damage done to the monster is negative. + // This means that the monster conters the player's attack + $this->player->damage(0 - $monsterDamage); + } elseif ($monsterDamage > 0) { + // The damage done to the monster is positive. + // This means that this is a normal attack + $this->monster->damage($monsterDamage); + } else { + // The damage done to the monster is 0. + // We interpretate this as a miss. + } + } + + // Monster does damage to the player + if ($firstDamageRound & self::DAMAGEROUND_MONSTER + && $this->player->isAlive() + && $this->monster->isAlive() + ) { + if ($playerDamage > 0) { + // The damage done to the player is negative + // THis means that the player conters the monster's attack + $this->monster->damage(0 - $playerDamage); + } elseif($playerDamage > 0) { + // The damage done to the player is positive. + // This means that this is a normal attack + $this->player->damage($playerDamage); + } + else { + // The damage done to the player is 0. + // We interpretate this as a miss. + } + } + } + + /** + * Returns the damage done to the player and to the monster. + * @return array [playerDamage, monsterDamage, playerAttack] + */ + protected function calculateDamage(): array + { + $monsterDefense = $this->monster->getDefense(); + $monsterAttack = $this->monster->getAttack(); + $playerDefense = $this->player->getDefense(); + $playerAttack = $this->player->getAttack(); + + $monsterDamage = 0; + $playerDamage = 0; + + while ($monsterDamage === 0 && $playerDamage === 0) { + $atk = $playerAttack; + + // Critical hit probablity is derived from the old e_rand() function. + // e_rand(1, 3) == 3 has a probablity of ~25%. + if ($this->diceBag->chance(0.25)) { + $atk *= 3; + } + + // Calculate damage done to the monster + $playerAtkRoll = $this->diceBag->normal(0, $atk); + $monsterDefRoll = $this->diceBag->normal(0, $monsterDefense); + $monsterDamage = $playerAtkRoll - $monsterDefRoll; + + if ($monsterDamage < 0) { + $monsterDamage /= 2; + } + + // Calculate damage done to the player + $playerDefRoll = $this->diceBag->normal(0, $playerDefense); + $monsterAtkRoll = $this->diceBag->normal(0, $monsterAttack); + $playerDamage = $monsterAtkRoll - $playerDefRoll; + + if ($playerDamage < 0) { + $playerDamage /= 2; + } + } + + return [ + (int)round($playerDamage, 0), + (int)round($monsterDamage, 0), + $atk + ]; + } +} diff --git a/src/Models/BasicEnemy.php b/src/Models/BasicEnemy.php new file mode 100644 index 0000000..0955839 --- /dev/null +++ b/src/Models/BasicEnemy.php @@ -0,0 +1,110 @@ +id; + } + + /** + * Returns the enemy's name + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * Returns the enemy's display name - this is the same than the name. + * @return string + */ + public function getDisplayName(): string + { + return $this->name; + } + + /** + * Returns the enemy's level. + * @return int + */ + public function getLevel(): int + { + return $this->level; + } + + /** + * Returns the enemy's current health + * @return int + */ + public function getHealth(): int + { + if ($this->health === null) { + $this->health = $this->getMaxHealth(); + } + + return $this->health; + } + + /** + * Does damage to the entity. + * @param int $damage + */ + public function damage(int $damage) + { + $this->health -= $damage; + + if ($this->health < 0) { + $this->health = 0; + } + } + + /** + * Heals the enemy + * @param int $heal + * @param type $overheal True if healing bigger than maxhealth is desired. + */ + public function heal(int $heal, bool $overheal = false) + { + $this->health += $heal; + + if ($this->health > $this->getMaxHealth() && $overheal === false) { + $this->health = $this->getMaxHealth(); + } + } + + /** + * Returns true if the enemy is alive. + * @return bool + */ + public function isAlive(): bool + { + if ($this->getHealth() > 0) { + return true; + } else { + return false; + } + } +} \ No newline at end of file diff --git a/src/Models/Buff.php b/src/Models/Buff.php new file mode 100644 index 0000000..c5f2414 --- /dev/null +++ b/src/Models/Buff.php @@ -0,0 +1,12 @@ +health; } + /** + * Does damage to the entity. + * @param int $damage + */ + public function damage(int $damage) + { + $this->health -= $damage; + + if ($this->health < 0) { + $this->health = 0; + } + } + + /** + * Heals the enemy + * @param int $heal + * @param type $overheal True if healing bigger than maxhealth is desired. + */ + public function heal(int $heal, bool $overheal = false) + { + $this->health += $heal; + + if ($this->health > $this->getMaxHealth() && $overheal === false) { + $this->health = $this->getMaxHealth(); + } + } + + /** + * Returns true if the character is alive. + * @return bool + */ + public function isAlive(): bool + { + return $this->getHealth() > 0; + } + + /** + * Returns the character's level + * @return int + */ + public function getLevel(): int + { + return $this->level; + } + + /** + * Returns the character's virtual attribute "attack" + */ + public function getAttack(): int + { + return $this->level; + } + + /** + * Returns the character's virtual attribute "defense" + */ + public function getDefense(): int + { + return $this->level; + } + + /** + * Sets the character's level + * @param int $level + */ + public function setLevel(int $level) + { + $this->level = $level; + } + /** * Returns the current character scene and creates one if it is non-existant * @return \LotGD\Core\Models\CharacterViewpoint diff --git a/src/Models/CharacterInterface.php b/src/Models/CharacterInterface.php index bab633d..03006c2 100644 --- a/src/Models/CharacterInterface.php +++ b/src/Models/CharacterInterface.php @@ -3,12 +3,10 @@ declare(strict_types=1); namespace LotGD\Core\Models; -# use LotGD\Core\Tools\Optional\Optional; - /** * Interface for the character model and all objects that mimick such a model. */ -interface CharacterInterface +interface CharacterInterface extends FighterInterface { public function getId(): int; public function getName(): string; diff --git a/src/Models/FighterInterface.php b/src/Models/FighterInterface.php new file mode 100644 index 0000000..dd0b7ad --- /dev/null +++ b/src/Models/FighterInterface.php @@ -0,0 +1,20 @@ +getLevel(); + return ($level * 10) + (int)ceil(($level + 1) / 2) - 1; + } + + /** + * Returns the attack value based on the fighter's level + * @return int + */ + public function getAttack(): int + { + $level = $this->getLevel(); + return (int)$level * 2 - 1; + } + + /** + * Returns the defense value based on the fighter's level + * @return int + */ + public function getDefense(): int + { + $level = $this->getlevel(); + return (int)floor($level*1.45); + } +} diff --git a/src/Tools/Model/MockCharacter.php b/src/Tools/Model/MockCharacter.php index 9b373d4..68418cb 100644 --- a/src/Tools/Model/MockCharacter.php +++ b/src/Tools/Model/MockCharacter.php @@ -36,11 +36,41 @@ trait MockCharacter throw new IsNullException(); } + public function damage(int $damage) + { + throw new IsNullException(); + } + + public function heal(int $heal, bool $overheal = false) + { + throw new IsNullException(); + } + public function getMaxHealth(): int { throw new IsNullException(); } + public function getLevel(): int + { + throw new IsNullException(); + } + + public function isAlive(): bool + { + throw new IsNullException(); + } + + public function getAttack(): int + { + throw new IsNullException(); + } + + public function getDefense(): int + { + throw new IsNullException(); + } + public function getCharacterViewpoint(): CharacterViewpoint { throw new IsNullException(); diff --git a/tests/Models/BattleTest.php b/tests/Models/BattleTest.php new file mode 100644 index 0000000..8d238c2 --- /dev/null +++ b/tests/Models/BattleTest.php @@ -0,0 +1,60 @@ +getEntityManager(); + + $monster = $em->getRepository(Monster::class)->find(1); + + $this->assertSame(5, $monster->getLevel()); + $this->assertSame(52, $monster->getMaxHealth()); + $this->assertSame(9, $monster->getAttack()); + $this->assertSame(7, $monster->getDefense()); + $this->assertSame($monster->getMaxHealth(), $monster->getHealth()); + } + + public function testBattle() + { + $em = $this->getEntityManager(); + + $character = $em->getRepository(Character::class)->find(1); + $monster = $em->getRepository(Monster::class)->find(1); + + $battle = new Battle($character, $monster); + + for ($n = 0; $n < 99; $n++) { + $oldPlayerHealth = $character->getHealth(); + $oldMonsterHealth = $monster->getHealth(); + + $battle->fightNRounds(1); + + $this->assertLessThanOrEqual($oldPlayerHealth, $character->getHealth()); + $this->assertLessThanOrEqual($oldMonsterHealth, $monster->getHealth()); + + if ($character->isAlive() === false && $monster->isAlive() === false) { + break; + } + } + + $this->assertTrue($character->isAlive() xor $monster->isAlive()); + } +} diff --git a/tests/datasets/battle.yml b/tests/datasets/battle.yml new file mode 100644 index 0000000..ff66b0f --- /dev/null +++ b/tests/datasets/battle.yml @@ -0,0 +1,13 @@ +characters: + - + id: 1 + name: "Player" + displayName: "The Player" + health: 100 + maxhealth: 100 + level: 10 +monsters: + - + id: 1 + name: "Evil Monster" + level: 5 \ No newline at end of file