Updated BattleClass to use BattleEvents

Implemented BattleEvents from @austenmc's old branch.

Added an empty Buff-Model as well, as well as a BuffList.

@ToDo: Add events, and add buff calculations to the battle class.

This PR will, however, only introduce the basic battle class, buffs will
come in another patch.
This commit is contained in:
Basilius Sauter
2016-05-25 11:30:57 +02:00
parent fb2c764e15
commit e04d963633
13 changed files with 360 additions and 129 deletions
+90 -100
View File
@@ -3,6 +3,8 @@ declare(strict_types=1);
namespace LotGD\Core;
use Doctrine\Common\Collections\ArrayCollection;
use LotGD\Core\{
DiceBag,
Exceptions\ArgumentException,
@@ -10,6 +12,11 @@ use LotGD\Core\{
Exceptions\BattleNotOverException,
Models\FighterInterface
};
use LotGD\Core\Models\BattleEvents\{
CriticalHitEvent,
DamageEvent,
DeathEvent
};
/**
* Class for managing and running battles between 2 participants.
@@ -22,18 +29,23 @@ class Battle
const DAMAGEROUND_MONSTER = 0b10;
const DAMAGEROUND_BOTH = 0b11;
const RESULT_UNDECIDED = 0;
const RESULT_PLAYERDEATH = 1;
const RESULT_MONSTERDEATH = 2;
protected $player;
protected $monster;
protected $diceBag;
protected $isOver = false;
protected $winner;
protected $looser;
protected $game;
protected $events;
protected $result = 0;
protected $round = 0;
public function __construct(FighterInterface $player, FighterInterface $monster)
public function __construct(Game $game, FighterInterface $player, FighterInterface $monster)
{
$this->game = $game;
$this->player = $player;
$this->monster = $monster;
$this->diceBag = new DiceBag();
$this->events = new ArrayCollection();
}
public function getActions()
@@ -52,7 +64,7 @@ class Battle
*/
public function isOver()
{
return $this->isOver;
return $this->result !== self::RESULT_UNDECIDED;
}
/**
@@ -61,22 +73,24 @@ class Battle
*/
public function getWinner(): FighterInterface
{
if (is_null($this->winner)) {
if ($this->isOver() === false) {
throw new BattleNotOverException('There is no winner yet.');
}
return $this->winner;
return $this->result === self::RESULT_PLAYERDEATH ? $this->monster : $this->player;
}
/**
* Returns the looser of this fight
* @return FighterInterface
*/
public function getLooser(): FighterInterface
public function getLoser(): FighterInterface
{
if (is_null($this->looser)) {
throw new BattleNotOverException('There is no looser yet.');
if ($this->isOver() === false) {
throw new BattleNotOverException('There is no winner yet.');
}
return $this->looser;
return $this->result === self::RESULT_PLAYERDEATH ? $this->player : $this->monster;
}
/**
@@ -92,20 +106,15 @@ class Battle
throw new ArgumentException('$firstDamageRound must not be 0.');
}
if ($this->isOver === true) {
if ($this->isOver()) {
throw new BattleIsOverException('This battle has already ended. You cannot fight anymore rounds.');
}
for ($count = 0; $count < $n; $count++) {
$this->fightOneRound($firstDamageRound);
$isSurprised = self::DAMAGEROUND_BOTH;
$firstDamageRound = self::DAMAGEROUND_BOTH;
// If one of the participants is dead, abort.
if ($this->player->isAlive() === false || $this->monster->isAlive() === false) {
$this->isOver = true;
$this->winner = $this->player->isAlive() ? $this->player : $this->monster;
$this->looser = $this->player->isAlive() ? $this->monster : $this->player;
if ($this->isOver()) {
break;
}
}
@@ -119,97 +128,78 @@ class Battle
*/
protected function fightOneRound(int $firstDamageRound)
{
// playerDamage is the damage done to the player, to the monster.
list($playerDamage, $monsterDamage, $playerAttack) = $this->calculateDamage();
$damageHasBeenDone = false;
// 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 counters 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.
do {
$offenseTurnEvents = $firstDamageRound & self::DAMAGEROUND_PLAYER ? $this->turn($this->player, $this->monster) : new ArrayCollection();
$defenseTurnEvents = $firstDamageRound & self::DAMAGEROUND_MONSTER ? $this->turn($this->monster, $this->player) : new ArrayCollection();
$events = new ArrayCollection(array_merge($offenseTurnEvents->toArray(), $defenseTurnEvents->toArray()));
$eventsToAdd = new ArrayCollection();
foreach($events as $event) {
$event->apply();
if ($event instanceof DamageEvent && $event->getDamage() !== 0) {
$damageHasBeenDone = true;
}
$eventsToAdd->add($event);
if ($this->player->getHealth() <= 0) {
$this->events->add(new DeathEvent($this->player));
$this->result = self::RESULT_PLAYERDEATH;
break;
}
if ($this->monster->getHealth() <= 0) {
$this->events->add(new DeathEvent($this->monster));
$this->result = self::RESULT_MONSTERDEATH;
break;
}
}
}
} while($damageHasBeenDone === false);
// 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 counters 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.
}
}
$this->round++;
$this->events = new ArrayCollection(array_merge($this->events->toArray(), $eventsToAdd->toArray()));
}
/**
* Returns the damage done to the player and to the monster.
* @return array [playerDamage, monsterDamage, playerAttack]
* Runs one turn.
* @param FighterInterface $attacker
* @param FighterInterface $defender
*/
protected function calculateDamage(): array
protected function turn(FighterInterface $attacker, FighterInterface $defender): ArrayCollection
{
$monsterDefense = $this->monster->getDefense();
$monsterAttack = $this->monster->getAttack();
$playerDefense = $this->player->getDefense();
$playerAttack = $this->player->getAttack();
$events = new ArrayCollection();
$monsterDamage = 0;
$playerDamage = 0;
$attackersAttack = $attacker->getAttack($this->game);
$defendersDefense = $defender->getDefense($this->game);
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) {
// Counter attack is only half as hard
$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) {
// Counter attack is only half as hard
$playerDamage /= 2;
if ($attacker === $this->game->getCharacter()) {
// Players can land critical hits
if ($this->game->getDiceBag()->chance(0.25)) {
$attackersAttack *= 3;
}
}
return [
(int)round($playerDamage, 0),
(int)round($monsterDamage, 0),
$atk
];
$attackersAtkRoll = $this->game->getDiceBag()->normal(0, $attackersAttack);
$defendersDefRoll = $this->game->getDiceBag()->normal(0, $defendersDefense);
$damage = $attackersAtkRoll - $defendersDefRoll;
if ($attackersAttack > $attacker->getAttack($this->game, true)) {
$events->add(new CriticalHitEvent($attacker, $attackersAttack));
}
if ($damage < 0) {
// RIPOSTE are only half as damaging than normal attacks
$damage /= 2;
}
$damage = (int)round($damage, 0);
$events->add(new DamageEvent($attacker, $defender, $damage));
return $events;
}
}
+21
View File
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace LotGD\Core;
use Doctrine\Common\Collections\Collection;
use LotGD\Core\Models\Buff;
use LotGD\Core\Models\Character;
/**
* Description of BuffList
*/
class BuffList
{
private $buffs;
public function __construct(Collection $buffs) {
$this->buffs = $buffs;
}
}
@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace LotGD\Core\Exceptions;
/**
* Exception if a specific, required argument is missing
*/
class BuffSlotOccupiedException extends CoreException
{
}
+20
View File
@@ -5,6 +5,8 @@ namespace LotGD\Core;
use Doctrine\ORM\EntityManagerInterface;
use LotGD\Core\Models\Character;
class Game
{
private $entityManager;
@@ -35,4 +37,22 @@ class Game
{
return $this->eventManager;
}
/**
* Returns the game's dice bag.
* @return DiceBag
*/
public function getDiceBag(): DiceBag
{
return $this->diceBag;
}
/**
* Returns the active character for this game run
* @return Character
*/
public function getCharacter(): Character
{
return $this->character;
}
}
+20
View File
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace LotGD\Core\Models\BattleEvents;
/**
* BattleEvent
*/
class BattleEvent
{
public function apply()
{
}
public function decorate(Game $game): string
{
return "";
}
}
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace LotGD\Core\Models\BattleEvents;
use LotGD\Core\Models\FighterInterface;
/**
* Description of CriticalHitEvent
*/
class CriticalHitEvent extends BattleEvent
{
/** @var FighterInstance */
protected $attacker;
/** @var int */
protected $criticalAttackValue;
public function __construct(FighterInterface $attacker, int $criticalAttackValue)
{
$this->attacker = $attacker;
$this->criticalAttackValue = $criticalAttackValue;
}
public function decorate(Game $game): string
{
$pureAttackersAttack = $this->attacker->getAttack($game, true);
if ($this->criticalAttackValue > $pureAttackersAttack * 4) {
return "You execute a MEGA power move!!!";
} elseif ($this->criticalAttackValue > $pureAttackersAttack * 3) {
return "You execute a DOUBLE power move!!!";
} elseif ($this->criticalAttackValue > $pureAttackersAttack * 2) {
return "You execute a power move!!!";
} elseif ($this->criticalAttackValue > $pureAttackersAttack * 1.25) {
return "You execute a minor power move!";
}
}
}
+69
View File
@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace LotGD\Core\Models\BattleEvents;
use LotGD\Core\Models\FighterInterface;
/**
* BattleEvent
*/
class DamageEvent extends BattleEvent
{
/** @var FighterInstance */
protected $attacker;
/** @var FighterInstance */
protected $defender;
/** @var int Damage applied */
protected $damage;
public function __construct(FighterInterface $attacker, FighterInterface $defender, int $damage)
{
$this->attacker = $attacker;
$this->defender = $defender;
$this->damage = $damage;
}
public function getDamage(): int
{
return $this->damage;
}
public function apply()
{
if ($this->damage !== 0) {
// Only damage the victim if there is an actual effect
$victim = $this->damage > 0 ? $this->defender : $this->attacker;
$victim->damage(abs($this->damage));
}
}
public function decorate(Game $game): string
{
$attackersName = $this->attacker->getDisplayName();
$defendersName = $this->defender->getDisplayName();
if ($this->damage === 0) {
if ($this->attacker === $game->getCharacter()) {
return "You try to hit {$defendersName} but MISS!";
}
else {
return "{$attackersName} tries to hit you but they MISS!";
}
} elseif ($this->damage > 0) {
if ($this->attacker === $game->getCharacter()) {
return "You hit {$defendersName} for {$this->damage} points of damage!";
}
else {
return "{$attackersName} hits you for {$this->damage} points of damage!";
}
} else {
if ($this->attacker === $game->getCharacter()) {
return "You try to hit {$defendersName} but are RIPOSTED for {$this->damage} points of damage";
}
else {
return "{$attackersName} tries to hit you but you RIPOSTE for {$this->damage} points of damage";
}
}
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace LotGD\Core\Models\BattleEvents;
use LotGD\Core\Models\FighterInterface;
/**
* BattleEvent
*/
class DeathEvent extends BattleEvent
{
protected $victim;
public function __construct(FighterInterface $victim)
{
$this->victim = $victim;
}
public function apply()
{
}
public function decorate(Game $game): string
{
return "";
}
}
+25 -14
View File
@@ -10,6 +10,8 @@ use Doctrine\Common\Collections\{
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\Table;
use LotGD\Core\Game;
use LotGD\Core\Tools\Exceptions\BuffSlotOccupiedException;
use LotGD\Core\Tools\Model\{
Creator,
PropertyManager,
@@ -215,7 +217,7 @@ class Character implements CharacterInterface, CreateableInterface
/**
* Returns the character's virtual attribute "attack"
*/
public function getAttack(): int
public function getAttack(Game $game, bool $ignoreBuffs = false): int
{
return $this->level * 2;
}
@@ -223,7 +225,7 @@ class Character implements CharacterInterface, CreateableInterface
/**
* Returns the character's virtual attribute "defense"
*/
public function getDefense(): int
public function getDefense(Game $game, bool $ignoreBuffs = false): int
{
return $this->level * 2;
}
@@ -251,6 +253,27 @@ class Character implements CharacterInterface, CreateableInterface
return $this->characterViewpoint->first();
}
/**
* Returns a list of buffs
*/
public function getBuffs(): BuffList
{
$this->buffList ?? new BuffList($this->buffs);
return $this->buffList;
}
/**
* Adds a buff to the buffList
*/
public function addBuff(Buff $buff, bool $override = false)
{
try {
$this->getBuffs()->add($buff);
} catch(BuffSlotOccupiedException $e) {
$this->getBuffs()->renew($buff);
}
}
/**
* Returns a list of message threads this user has created.
* @return Collection
@@ -259,16 +282,4 @@ class Character implements CharacterInterface, CreateableInterface
{
return $this->messageThreads;
}
public function sendMessageTo(Character $recipient)
{
// ToDo: implement later
throw new \LotGD\Core\Exceptions\NotImplementedException;
}
public function receiveMessageFrom(Character $author)
{
// ToDo: implement later
throw new \LotGD\Core\Exceptions\NotImplementedException;
}
}
+4 -2
View File
@@ -3,6 +3,8 @@ declare(strict_types=1);
namespace LotGD\Core\Models;
use LotGD\Core\Game;
/**
* Interface for models that should be able to participate in fights.
*/
@@ -13,8 +15,8 @@ interface FighterInterface
public function getMaxHealth(): int;
public function getHealth(): int;
public function isAlive(): bool;
public function getAttack(): int;
public function getDefense(): int;
public function getAttack(Game $game, bool $ignoreBuffs = false): int;
public function getDefense(Game $game, bool $ignoreBuffs = false): int;
public function damage(int $damage);
public function heal(int $heal);
}
+4 -2
View File
@@ -3,6 +3,8 @@ declare(strict_types=1);
namespace LotGD\Core\Tools\Model;
use LotGD\Core\Game;
/**
* Automatically calculated values based on the fighter's level
*/
@@ -22,7 +24,7 @@ trait AutoScaleFighter
* Returns the attack value based on the fighter's level
* @return int
*/
public function getAttack(): int
public function getAttack(Game $game, bool $ignoreBuffs = false): int
{
$level = $this->getLevel();
return (int)$level * 2 - 1;
@@ -32,7 +34,7 @@ trait AutoScaleFighter
* Returns the defense value based on the fighter's level
* @return int
*/
public function getDefense(): int
public function getDefense(Game $game, bool $ignoreBuffs = false): int
{
$level = $this->getlevel();
return (int)floor($level*1.45);
+3 -2
View File
@@ -4,6 +4,7 @@ declare(strict_types = 1);
namespace LotGD\Core\Tools\Model;
use LotGD\Core\Game;
use LotGD\Core\Exceptions\IsNullException;
use LotGD\Core\Models\CharacterViewpoint;
@@ -61,12 +62,12 @@ trait MockCharacter
throw new IsNullException();
}
public function getAttack(): int
public function getAttack(Game $game, bool $ignoreBuffs = false): int
{
throw new IsNullException();
}
public function getDefense(): int
public function getDefense(Game $game, bool $ignoreBuffs = false): int
{
throw new IsNullException();
}
+25 -9
View File
@@ -5,6 +5,8 @@ namespace LotGD\Core\Tests\Models;
use LotGD\Core\{
Battle,
DiceBag,
Game,
Models\Character,
Models\Monster
};
@@ -19,6 +21,19 @@ class BattleTest extends ModelTestCase
/** @var string default data set */
protected $dataset = "battle";
public function getMockGame(Character $character): Game
{
$game = $this->getMockBuilder(Game::class)
->disableOriginalConstructor()
->getMock();
$game->method('getEntityManager')->willReturn($this->getEntityManager());
$game->method('getDiceBag')->willReturn(new DiceBag());
$game->method('getCharacter')->willReturn($character);
return $game;
}
/**
* Tests basic monster functionality
*/
@@ -26,12 +41,13 @@ class BattleTest extends ModelTestCase
{
$em = $this->getEntityManager();
$character = $em->getRepository(Character::class)->find(1);
$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(9, $monster->getAttack($this->getMockGame($character)));
$this->assertSame(7, $monster->getDefense($this->getMockGame($character)));
$this->assertSame($monster->getMaxHealth(), $monster->getHealth());
}
@@ -45,7 +61,7 @@ class BattleTest extends ModelTestCase
$character = $em->getRepository(Character::class)->find(1);
$monster = $em->getRepository(Monster::class)->find(1);
$battle = new Battle($character, $monster);
$battle = new Battle($this->getMockGame($character), $character, $monster);
for ($n = 0; $n < 99; $n++) {
$oldPlayerHealth = $character->getHealth();
@@ -75,7 +91,7 @@ class BattleTest extends ModelTestCase
$highLevelPlayer = $em->getRepository(Character::class)->find(2);
$lowLevelMonster = $em->getRepository(Monster::class)->find(3);
$battle = new Battle($highLevelPlayer, $lowLevelMonster);
$battle = new Battle($this->getMockGame($highLevelPlayer), $highLevelPlayer, $lowLevelMonster);
for ($n = 0; $n < 99; $n++) {
$oldPlayerHealth = $highLevelPlayer->getHealth();
@@ -108,7 +124,7 @@ class BattleTest extends ModelTestCase
$lowLevelPlayer = $em->getRepository(Character::class)->find(3);
$highLevelMonster = $em->getRepository(Monster::class)->find(2);
$battle = new Battle($lowLevelPlayer, $highLevelMonster);
$battle = new Battle($this->getMockGame($lowLevelPlayer), $lowLevelPlayer, $highLevelMonster);
for ($n = 0; $n < 99; $n++) {
$oldPlayerHealth = $lowLevelPlayer->getHealth();
@@ -141,7 +157,7 @@ class BattleTest extends ModelTestCase
$character = $em->getRepository(Character::class)->find(1);
$monster = $em->getRepository(Monster::class)->find(1);
$battle = new Battle($character, $monster);
$battle = new Battle($this->getMockGame($character), $character, $monster);
$battle->getWinner();
}
@@ -156,9 +172,9 @@ class BattleTest extends ModelTestCase
$character = $em->getRepository(Character::class)->find(1);
$monster = $em->getRepository(Monster::class)->find(1);
$battle = new Battle($character, $monster);
$battle = new Battle($this->getMockGame($character), $character, $monster);
$battle->getLooser();
$battle->getLoser();
}
/**
@@ -172,7 +188,7 @@ class BattleTest extends ModelTestCase
$character = $em->getRepository(Character::class)->find(1);
$monster = $em->getRepository(Monster::class)->find(1);
$battle = new Battle($character, $monster);
$battle = new Battle($this->getMockGame($character), $character, $monster);
// Fighting for 99 rounds should be enough for determining a looser - and to
// throw the exception.