Introduce general buff handling and tests

This commit introduces basic buff handling: Adding buffs, removing buffs,
expiring buffs. The Battle procedure controls the buffs and activates them
every round, expires them one round per round and removes the buff if the
number of rounds left is 0.

The BattleTest suite tests for the correct sequence and the correct
messages.
This commit is contained in:
Basilius Sauter
2016-06-01 14:05:18 +02:00
parent 5280e37617
commit 4badaea249
14 changed files with 497 additions and 17 deletions
+29 -3
View File
@@ -13,6 +13,7 @@ use LotGD\Core\{
Models\FighterInterface
};
use LotGD\Core\Models\BattleEvents\{
BuffMessageEvent,
CriticalHitEvent,
DamageEvent,
DeathEvent
@@ -58,6 +59,11 @@ class Battle
}
public function getEvents()
{
return $this->events;
}
/**
* Returns true if the battle is over.
* @return type
@@ -130,6 +136,9 @@ class Battle
{
$damageHasBeenDone = false;
$playerBuffStartEvents = $this->player->getBuffs()->activate();
$monsterBuffStartEvents = $this->monster->getBuffs()->activate();
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();
@@ -147,13 +156,13 @@ class Battle
$eventsToAdd->add($event);
if ($this->player->getHealth() <= 0) {
$this->events->add(new DeathEvent($this->player));
$deathEvent = new DeathEvent($this->player);
$this->result = self::RESULT_PLAYERDEATH;
break;
}
if ($this->monster->getHealth() <= 0) {
$this->events->add(new DeathEvent($this->monster));
$deathEvent = new DeathEvent($this->monster);
$this->result = self::RESULT_MONSTERDEATH;
break;
}
@@ -161,7 +170,21 @@ class Battle
} while($damageHasBeenDone === false);
$this->round++;
$this->events = new ArrayCollection(array_merge($this->events->toArray(), $eventsToAdd->toArray()));
$playerBuffEndEvents = $this->player->getBuffs()->expireOneRound();
$monsterBuffEndEvents = $this->monster->getBuffs()->expireOneRound();
$this->events = new ArrayCollection(
array_merge(
$this->events->toArray(),
$playerBuffStartEvents->toArray(),
$monsterBuffStartEvents->toArray(),
$eventsToAdd->toArray(),
$playerBuffEndEvents->toArray(),
$monsterBuffEndEvents->toArray(),
isset($deathEvent) ? [$deathEvent] : []
)
);
}
/**
@@ -173,6 +196,9 @@ class Battle
{
$events = new ArrayCollection();
$attackersBuffs = $attacker->getBuffs();
$defendersBuffs = $defender->getBuffs();
$attackersAttack = $attacker->getAttack($this->game);
$defendersDefense = $defender->getDefense($this->game);
+127 -5
View File
@@ -3,19 +3,141 @@ declare(strict_types=1);
namespace LotGD\Core;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\{
ArrayCollection,
Collection
};
use LotGD\Core\Models\Buff;
use LotGD\Core\Models\Character;
use LotGD\Core\Models\{
Buff,
Character,
BattleEvents\BuffMessageEvent
};
/**
* Description of BuffList
*/
class BuffList
{
private $buffs;
protected $buffs;
protected $buffsBySlot;
protected $activeBuffs;
public function __construct(Collection $buffs) {
protected $activated = false;
protected $badguyInvulnurable = false;
protected $badguyDamageModifier = 1;
protected $badguyAttackModifier = 1;
protected $badguyDefenseModifier = 1;
protected $goodguyInvulnurable = false;
protected $goodguyDamageModifier = 1;
protected $goodguyAttackModifier = 1;
protected $goodguyDefenseModifier = 1;
protected $events;
protected $loaded = false;
public function __construct(Collection $buffs)
{
$this->buffs = $buffs;
$this->events = new ArrayCollection();
}
public function loadBuffs()
{
if ($this->loaded === false) {
foreach($this->buffs as $buff) {
$this->buffsBySlot[$buff->getSlot()] = $buff;
}
}
}
public function activate(): Collection
{
if ($this->activated === true) {
throw new BuffListAlreadyActivatedException("You can activate the buff list only once.");
}
$this->activeBuffs = new ArrayCollection();
$activationEvents = new ArrayCollection();
foreach ($this->buffs as $buff) {
// Only look at buffs that are activated in battle.
if ($buff->getsActivatedAt(Buff::ACTIVATE_NONE)) {
continue;
}
$this->activeBuffs->add($buff);
if ($buff->hasBeenStarted() === false) {
$activationMessage = $buff->getStartMessage();
if ($activationMessage !== "") {
$activationEvents->add(new BuffMessageEvent($activationMessage));
}
$buff->setHasBeenStarted();
}
else {
$roundMessage = $buff->getRoundMessage();
if ($roundMessage !== "") {
$activationEvents->add(new BuffMessageEvent($roundMessage));
}
}
}
return $activationEvents;
}
public function expireOneRound(): Collection
{
$endEvents = new ArrayCollection();
foreach($this->activeBuffs as $buff) {
$roundsLeft = $buff->getRounds() - 1;
$buff->setRounds($roundsLeft);
if ($roundsLeft === 0) {
$endMessage = $buff->getEndMessage();
if ($endMessage !== "") {
$endEvents->add(new BuffMessageEvent($endMessage));
}
$this->remove($buff);
}
}
return $endEvents;
}
public function remove(Buff $buff)
{
unset($this->buffsBySlot[$buff->getSlot()]);
$this->buffs->removeElement($buff);
$this->activeBuffs->removeElement($buff);
}
public function add(Buff $buff)
{
$this->loadBuffs();
$slot = $buff->getSlot();
if (isset($this->buffsBySlot[$buff->getSlot()])) {
throw new BuffSlotOccupiedException("The slot {$slot} is already occupied.");
}
$this->buffs->add($buff);
$this->buffsBySlot[$buff->getSlot()] = $buff;
}
public function renew(Buff $buff)
{
$this->loadBuffs();
$slot = $buff->getSlot();
if (isset($this->buffsBySlot[$buff->getSlot()])) {
$this->buffs->removeElement($buff);
}
$this->buffs->add($buff);
$this->buffsBySlot[$buff->getSlot()] = $buff;
}
}
+12
View File
@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace LotGD\Core\Exceptions;
/**
* Exception if a specific, required argument is missing
*/
class BattleEventException extends BattleException
{
}
@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace LotGD\Core\Exceptions;
/**
* Exception if a specific, required argument is missing
*/
class BuffListAlreadyActivatedException extends CoreException
{
}
+8
View File
@@ -3,14 +3,22 @@ declare(strict_types=1);
namespace LotGD\Core\Models\BattleEvents;
use LotGD\Core\Exceptions\BattleEventException;
/**
* BattleEvent
*/
class BattleEvent
{
private $applied = false;
public function apply()
{
if ($this->applied === true) {
throw new BattleEventException("Cannot apply an event more than once.");
}
$this->applied = true;
}
public function decorate(Game $game): string
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace LotGD\Core\Models\BattleEvents;
use LotGD\Core\Exceptions\BattleEventException;
/**
* BattleEvent
*/
class BuffMessageEvent extends BattleEvent
{
private $message = "";
public function __construct(string $message) {
$this->message = $message;
}
public function getMessage(): string
{
return $this->message;
}
public function decorate(Game $game): string
{
return $message;
}
}
+2
View File
@@ -31,6 +31,8 @@ class DamageEvent extends BattleEvent
public function apply()
{
parent::apply();
if ($this->damage !== 0) {
// Only damage the victim if there is an actual effect
$victim = $this->damage > 0 ? $this->defender : $this->attacker;
+149 -4
View File
@@ -6,6 +6,8 @@ namespace LotGD\Core\Models;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\Table;
use LotGD\Core\Exceptions\ArgumentException;
/**
* A model representing a buff used to modify the flow of the battle.
* @Entity
@@ -18,6 +20,8 @@ class Buff
const ACTIVATE_OFFENSE = 0b0100;
const ACTIVATE_DEFENSE = 0b1000;
const ACTIVATE_WHILEROUND = 0b1100;
const ACTIVATE_NONE = 0b0000;
const ACTIVATE_ANY = 0b1111;
/** @Id @Column(type="integer") @GeneratedValue */
private $id;
@@ -88,6 +92,12 @@ class Buff
* @Column(type="boolean")
*/
private $survivesNewDay = false;
/**
* True if the buff should expire if the battle ended.
* @var bool
* @Column(type="boolean")
*/
private $expiresAfterBattle = false;
/**
* The number of rounds this buff lasts.
*
@@ -210,6 +220,114 @@ class Buff
* @Column(type="boolean")
*/
private $goodguyInvulnurable = false;
/**
* True if the buff has already been started
* @var bool
* @Column(type="boolean")
*/
private $hasBeenStarted = false;
private $buffArrayTemplate = [
"slot" => "string",
"name" => "string",
"startMessage" => "string",
"roundMessage" => "string",
"endMessage" => "string",
"effectSucceedsMessage" => "string",
"effectFailsMessage" => "string",
"noEffectMessage" => "string",
"newDayMessage" => "string",
"activateAt" => "int",
"survivesNewDay" => "bool",
"expiresAfterBattle" => "bool",
"rounds" => "int",
"badguyRegeneration" => "int",
"goodguyRegeneration" => "int",
"badguyLifetap" => "float",
"goodguyLifetap" => "float",
"badguyDamageReflection" => "float",
"goodguyDamageReflection" => "float",
"numberOfMinions" => "int",
"minionMinBadguyDamage" => "int",
"minionMaxBadguyDamage" => "int",
"minionMinGoodguyDamage" => "int",
"minionMaxGoodguyDamage" => "int",
"badguyDamageModifier" => "float",
"badguyAttackModifier" => "float",
"badguyDefenseModifier" => "float",
"badguyInvulnurable" => "bool",
"goodguyDamageModifier" => "float",
"goodguyAttackModifier" => "float",
"goodguyDefenseModifier" => "float",
"goodguyInvulnurable" => "bool",
];
private $required = [
"slot",
];
/**
* Creates a new buff entity using an array
* @param array $buffArray
* @throws ArgumentException
*/
public function __construct(array $buffArray) {
foreach($buffArray as $attribute => $value) {
// Throw exception if an attribute does not exist (to prevent spelling errors)
if (!isset($this->buffArrayTemplate[$attribute])) {
throw new ArgumentException("{$attribute} is not a valid key for a buff.");
}
switch($this->buffArrayTemplate[$attribute]) {
case "string":
if (is_string($value) === false) {
throw new ArgumentException("{$attribute} needs to be a string.");
}
break;
case "int":
if (is_int($value) === false) {
throw new ArgumentException("{$attribute} needs to be a int.");
}
break;
case "float":
if (is_float($value) === false) {
throw new ArgumentException("{$attribute} needs to be a float.");
}
break;
case "boolean":
if (is_bool($value) === false) {
throw new ArgumentException("{$attribute} needs to be boolean.");
}
break;
}
$this->{$attribute} = $value;
foreach($this->required as $required) {
if (is_null($this->$required)) {
throw new ArgumentException("{$required} needs to be inside of the buffArray!");
}
}
}
}
/**
* Creates a new buff entity using another buff as the template.
* @param \LotGD\Core\Models\Buff $buff
* @return \LotGD\Core\Models\Buff
*/
public static function constructFromTemplate(Buff $buff): Buff {
$buffArray = [];
foreach($this->buffArrayTemplate as $attribute => $type) {
$buffArray[$attribute] = $buff->$attribute;
}
return new Buff($buffArray);
}
/**
* Returns the id of the buff
@@ -253,7 +371,7 @@ class Buff
*/
public function getStartMessage(): string
{
return $this->startMessage;
return $this->startMessage ?? "";
}
/**
@@ -262,7 +380,7 @@ class Buff
*/
public function getRoundMessage(): string
{
return $this->roundMessage;
return $this->roundMessage ?? "";
}
/**
@@ -271,7 +389,7 @@ class Buff
*/
public function getEndMessage(): string
{
return $this->endMessage;
return $this->endMessage ?? "";
}
/**
@@ -326,7 +444,7 @@ class Buff
*/
public function getsActivatedAt(int $flag): bool
{
return $this->activateAt & $flag;
return ($flag === self::ACTIVATE_NONE ? $this->activateAt === self::ACTIVATE_NONE : $this->activateAt & $flag);
}
/**
@@ -337,6 +455,15 @@ class Buff
{
return $this->survivesNewDay;
}
/**
* Returns true if the buff expires after the battle.
* @return bool
*/
public function expiresAfterBattle(): bool
{
return $this->expiresAfterBattle;
}
/**
* Returns the number of rounds left
@@ -539,4 +666,22 @@ class Buff
{
return $this->goodguyInvulnurable;
}
/**
* Returns true if the buff has already been started
* @return bool
*/
public function hasBeenStarted(): bool
{
return $this->hasBeenStarted;
}
/**
* Sets if the buff has been started (or not).
* @param bool $setTo
*/
public function setHasBeenStarted(bool $setTo = true)
{
$this->hasBeenStarted = $setTo;
}
}
+7 -2
View File
@@ -10,7 +10,10 @@ use Doctrine\Common\Collections\{
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\Table;
use LotGD\Core\Game;
use LotGD\Core\{
BuffList,
Game
};
use LotGD\Core\Tools\Exceptions\BuffSlotOccupiedException;
use LotGD\Core\Tools\Model\{
Creator,
@@ -61,6 +64,8 @@ class Character implements CharacterInterface, CreateableInterface
private $messageThreads;
/** @OneToMany(targetEntity="Buff", mappedBy="character", cascade={"persist"}) */
private $buffs;
/** @var BuffList */
private $buffList;
/** @var array */
private static $fillable = [
@@ -258,7 +263,7 @@ class Character implements CharacterInterface, CreateableInterface
*/
public function getBuffs(): BuffList
{
$this->buffList ?? new BuffList($this->buffs);
$this->buffList = $this->buffList ?? new BuffList($this->buffs);
return $this->buffList;
}
+5 -1
View File
@@ -3,7 +3,10 @@ declare(strict_types=1);
namespace LotGD\Core\Models;
use LotGD\Core\Game;
use LotGD\Core\{
BuffList,
Game
};
/**
* Interface for models that should be able to participate in fights.
@@ -19,4 +22,5 @@ interface FighterInterface
public function getDefense(Game $game, bool $ignoreBuffs = false): int;
public function damage(int $damage);
public function heal(int $heal);
public function getBuffs(): BuffList;
}
+16 -1
View File
@@ -3,7 +3,12 @@ declare(strict_types=1);
namespace LotGD\Core\Tools\Model;
use LotGD\Core\Game;
use Doctrine\Common\Collections\ArrayCollection;
use LotGD\Core\{
BuffList,
Game
};
/**
* Automatically calculated values based on the fighter's level
@@ -39,4 +44,14 @@ trait AutoScaleFighter
$level = $this->getlevel();
return (int)floor($level*1.45);
}
/**
* Returns an empty bufflist
* @return BuffList
*/
public function getBuffs(): BuffList
{
$this->buffList = $this->buffList ?? new BuffList(new ArrayCollection());
return $this->buffList;
}
}
+13 -1
View File
@@ -4,7 +4,10 @@ declare(strict_types = 1);
namespace LotGD\Core\Tools\Model;
use LotGD\Core\Game;
use LotGD\Core\{
BuffList,
Game
};
use LotGD\Core\Exceptions\IsNullException;
use LotGD\Core\Models\CharacterViewpoint;
@@ -81,4 +84,13 @@ trait MockCharacter
{
return $default;
}
/**
* Returns an empty bufflist
* @return BuffList
*/
public function getBuffs(): BuffList
{
throw new IsNullException();
}
}
@@ -3,13 +3,19 @@ declare(strict_types=1);
namespace LotGD\Core\Tests\Models;
use Doctrine\Common\Collections\Collection;
use LotGD\Core\{
Battle,
DiceBag,
Game,
Models\Buff,
Models\Character,
Models\Monster
};
use LotGD\Core\Models\BattleEvents\{
BuffMessageEvent
};
use LotGD\Core\Tests\ModelTestCase;
@@ -196,4 +202,80 @@ class BattleTest extends ModelTestCase
$battle->fightNRounds(1);
}
}
private function provideBuffBattleParticipants(Buff $buff): Battle
{
$em = $this->getEntityManager();
$character = $em->getRepository(Character::class)->find(4);
$monster = $em->getRepository(Monster::class)->find(3);
$character->addBuff($buff);
return new Battle($this->getMockGame($character), $character, $monster);
}
protected function assertBuffEventMessageExists(
Collection $events,
string $battleEventText,
int $timesAtLeast = 1,
int $timesAtMax = null
) {
$eventCounter = 0;
foreach($events as $event) {
if ($event instanceof BuffMessageEvent) {
if ($battleEventText === $event->getMessage()) {
$eventCounter++;
}
}
}
if ($timesAtMax === null) {
$timesAtMax = $timesAtLeast;
}
$this->assertGreaterThanOrEqual($timesAtLeast, $eventCounter);
$this->assertLessThanOrEqual($timesAtMax, $eventCounter);
}
public function testBattleBuffMessages()
{
$battle = $this->provideBuffBattleParticipants(new Buff([
"slot" => "test",
"rounds" => 3,
"startMessage" => "And this buff starts!",
"roundMessage" => "The buff is still activate",
"endMessage" => "The buff is ending.",
"activateAt" => Buff::ACTIVATE_ROUNDSTART,
]));
$battle->fightNRounds(5);
$this->assertBuffEventMessageExists($battle->getEvents(), "And this buff starts!", 1);
$this->assertBuffEventMessageExists($battle->getEvents(), "The buff is ending.", 1);
$this->assertBuffEventMessageExists($battle->getEvents(), "The buff is still activate", 1, 2);
$expectedEvents = [
BuffMessageEvent::class, // Activation round
\LotGD\Core\Models\BattleEvents\DamageEvent::class, // Round 1
\LotGD\Core\Models\BattleEvents\DamageEvent::class,
BuffMessageEvent::class, // message every round
\LotGD\Core\Models\BattleEvents\DamageEvent::class, // Round 2
\LotGD\Core\Models\BattleEvents\DamageEvent::class,
BuffMessageEvent::class, // message every round
\LotGD\Core\Models\BattleEvents\DamageEvent::class, // Round 3
\LotGD\Core\Models\BattleEvents\DamageEvent::class,
BuffMessageEvent::class, // message expires
\LotGD\Core\Models\BattleEvents\DamageEvent::class, // Round 4
\LotGD\Core\Models\BattleEvents\DamageEvent::class,
\LotGD\Core\Models\BattleEvents\DamageEvent::class, // Round 5
\LotGD\Core\Models\BattleEvents\DamageEvent::class,
];
$numOfEvents = count($battle->getEvents());
for ($i = 0; $i < $numOfEvents; $i++) {
$this->assertInstanceOf($expectedEvents[$i], $battle->getEvents()[$i]);
}
}
}
+7
View File
@@ -20,6 +20,13 @@ characters:
health: 10
maxhealth: 10
level: 1
-
id: 4
name: "Low Damage Char"
displayName: "Low Damage Char"
health: 500
maxhealth: 500
level: 0
monsters:
-
id: 1