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.
This commit is contained in:
+164
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LotGD\Core;
|
||||
|
||||
use LotGD\Core\{
|
||||
DiceBag,
|
||||
Exceptions\ArgumentException,
|
||||
Models\FighterInterface
|
||||
};
|
||||
|
||||
/**
|
||||
* Description of Battle
|
||||
*/
|
||||
class Battle
|
||||
{
|
||||
const DAMAGEROUND_PLAYER = 0b01;
|
||||
const DAMAGEROUND_MONSTER = 0b10;
|
||||
const DAMAGEROUND_BOTH = 0b11;
|
||||
|
||||
protected $player;
|
||||
protected $monster;
|
||||
protected $diceBag;
|
||||
|
||||
public function __construct(FighterInterface $player, FighterInterface $monster)
|
||||
{
|
||||
$this->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
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LotGD\Core\Models;
|
||||
|
||||
use Doctrine\ORM\Mapping\MappedSuperclass;
|
||||
|
||||
/**
|
||||
* @MappedSuperclass
|
||||
*/
|
||||
abstract class BasicEnemy implements FighterInterface
|
||||
{
|
||||
/** @Id @Column(type="integer") @GeneratedValue */
|
||||
private $id;
|
||||
/** @Column(type="string", length=50); */
|
||||
private $name;
|
||||
/** @Column(type="integer"); */
|
||||
private $level;
|
||||
/** @var int */
|
||||
private $health;
|
||||
|
||||
/**
|
||||
* Returns the enemy's id
|
||||
* @return int
|
||||
*/
|
||||
public function getId(): int
|
||||
{
|
||||
return $this->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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LotGD\Core\Models;
|
||||
|
||||
/**
|
||||
* Description of Buff
|
||||
*/
|
||||
class Buff
|
||||
{
|
||||
|
||||
}
|
||||
@@ -33,10 +33,12 @@ class Character implements CharacterInterface, CreateableInterface
|
||||
private $name;
|
||||
/** @Column(type="text"); */
|
||||
private $displayName;
|
||||
/** @Column(type="integer", options={"default" = 10}) */
|
||||
/** @Column(type="integer", options={"default":10}) */
|
||||
private $maxHealth = 10;
|
||||
/** @Column(type="integer", options={"default" = 10}) */
|
||||
/** @Column(type="integer", options={"default":10}) */
|
||||
private $health = 10;
|
||||
/** @Column(type="integer", options={"default":1})/ */
|
||||
private $level = 1;
|
||||
/** @OneToMany(targetEntity="CharacterProperty", mappedBy="owner", cascade={"persist"}) */
|
||||
private $properties;
|
||||
/** @OneToMany(targetEntity="CharacterViewpoint", mappedBy="owner", cascade={"persist"}) */
|
||||
@@ -59,6 +61,7 @@ class Character implements CharacterInterface, CreateableInterface
|
||||
private static $fillable = [
|
||||
"name",
|
||||
"maxHealth",
|
||||
"level",
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -160,6 +163,76 @@ class Character implements CharacterInterface, CreateableInterface
|
||||
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 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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LotGD\Core\Models;
|
||||
|
||||
/**
|
||||
* Interface for models that should be able to participate in fights.
|
||||
*/
|
||||
interface FighterInterface
|
||||
{
|
||||
public function getDisplayName(): string;
|
||||
public function getLevel(): int;
|
||||
public function getMaxHealth(): int;
|
||||
public function getHealth(): int;
|
||||
public function isAlive(): bool;
|
||||
public function getAttack(): int;
|
||||
public function getDefense(): int;
|
||||
public function damage(int $damage);
|
||||
public function heal(int $heal);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LotGD\Core\Models;
|
||||
|
||||
use Doctrine\ORM\Mapping\Entity;
|
||||
use Doctrine\ORM\Mapping\Table;
|
||||
|
||||
use LotGD\Core\Tools\Model\AutoScaleFighter;
|
||||
|
||||
/**
|
||||
* The Monster entity
|
||||
*
|
||||
* @Entity
|
||||
* @Table(name="masters")
|
||||
*/
|
||||
class Master extends BasicEnemy
|
||||
{
|
||||
use AutoScaleFighter;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LotGD\Core\Models;
|
||||
|
||||
use Doctrine\ORM\Mapping\Entity;
|
||||
use Doctrine\ORM\Mapping\Table;
|
||||
|
||||
use LotGD\Core\Tools\Model\AutoScaleFighter;
|
||||
|
||||
/**
|
||||
* The Monster entity
|
||||
*
|
||||
* @Entity
|
||||
* @Table(name="monsters")
|
||||
*/
|
||||
class Monster extends BasicEnemy
|
||||
{
|
||||
use AutoScaleFighter;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LotGD\Core\Tools\Model;
|
||||
|
||||
/**
|
||||
* Automatically calculated values based on the fighter's level
|
||||
*/
|
||||
trait AutoScaleFighter
|
||||
{
|
||||
/**
|
||||
* Returns the maximum health based on the fighter's level
|
||||
* @return int
|
||||
*/
|
||||
public function getMaxHealth(): int
|
||||
{
|
||||
$level = $this->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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LotGD\Core\Tests\Models;
|
||||
|
||||
use LotGD\Core\{
|
||||
Battle,
|
||||
Models\Character,
|
||||
Models\Monster
|
||||
};
|
||||
|
||||
use LotGD\Core\Tests\ModelTestCase;
|
||||
|
||||
/**
|
||||
* Tests the management of Characters
|
||||
*/
|
||||
class BattleTest extends ModelTestCase
|
||||
{
|
||||
/** @var string default data set */
|
||||
protected $dataset = "battle";
|
||||
|
||||
public function testBasicMonster()
|
||||
{
|
||||
$em = $this->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());
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user