From 38068dd0a5f2ad6444837c6c6712a161945f8ed2 Mon Sep 17 00:00:00 2001 From: Basilius Sauter Date: Mon, 23 May 2016 16:29:15 +0200 Subject: [PATCH] Completed basic battle system. The class Battle takes two participants (player and monster) that both need to implement the FighterInterface. Right now, rounds are completed by the fightNRounds method. Since all enemies in the old code follow the same default scaling, monsters and masters use a AutoScaleFighter trait for now. --- src/Battle.php | 164 +++++++++++++++++++++++++++ src/Models/BasicEnemy.php | 110 ++++++++++++++++++ src/Models/Buff.php | 12 ++ src/Models/Character.php | 77 ++++++++++++- src/Models/CharacterInterface.php | 4 +- src/Models/FighterInterface.php | 20 ++++ src/Models/Master.php | 20 ++++ src/Models/Monster.php | 20 ++++ src/Tools/Model/AutoScaleFighter.php | 40 +++++++ src/Tools/Model/MockCharacter.php | 30 +++++ tests/Models/BattleTest.php | 60 ++++++++++ tests/datasets/battle.yml | 13 +++ 12 files changed, 565 insertions(+), 5 deletions(-) create mode 100644 src/Battle.php create mode 100644 src/Models/BasicEnemy.php create mode 100644 src/Models/Buff.php create mode 100644 src/Models/FighterInterface.php create mode 100644 src/Models/Master.php create mode 100644 src/Models/Monster.php create mode 100644 src/Tools/Model/AutoScaleFighter.php create mode 100644 tests/Models/BattleTest.php create mode 100644 tests/datasets/battle.yml 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