Compare commits

...

9 Commits

Author SHA1 Message Date
f3e0b73931 Merge remote-tracking branch 'origin/master' into dev-arena 2022-06-11 03:24:36 +03:00
lopar
6509a796f6 Админка. 2022-02-13 01:50:04 +02:00
lopar
fdaadf69e6 Заявки на поединки. Начало. 2022-02-13 01:46:59 +02:00
lopar
1bf7a40fe9 Merge remote-tracking branch 'origin/dev-arena' into dev-arena 2022-02-12 20:17:51 +02:00
lopar
b652c242c4 Merge branch 'master' into dev-arena 2022-02-12 14:41:36 +02:00
Igor Barkov (iwork)
54da589f45 Класс рассчёта нанесения физичестого урона. #2 2022-01-27 17:16:01 +02:00
Igor Barkov (iwork)
7596d115fa Merge branch 'master' into dev-arena 2022-01-27 15:39:21 +02:00
lopar
d8ccb13873 немного логики 2022-01-27 00:56:03 +02:00
Igor Barkov (iwork)
e4cce2e96e Костыли и велосипеды! 2022-01-25 19:59:18 +02:00
6 changed files with 442 additions and 16 deletions

51
arena.php Normal file
View File

@ -0,0 +1,51 @@
<?php
/**
* Арена. Принцип: https://src.liks.pw/lopar/battles-game-test/issues/3
*/
use Battles\Arena;
use Battles\Template;
if (isset($_POST['startTime']) && isset($_POST['teamMembersQuantity'])) {
Arena::fight()->addNew((int)$_POST['teamMembersQuantity'], 2, (int)$_POST['startTime']);
}
if (isset($_POST['fight_id']) && isset($_POST['team_id'])) {
Arena::fight()->join((int)$_POST['fight_id'], (int)$_POST['team_id']);
}
Template::header('Арена');
?>
<?php if(Arena::fight()->hasNoPendingFights()): ?>
<form method='post' id='newbattle'></form>
<H3>Подать заявку на поединок</H3>
<label for='startTime'>Начало боя</label>
<select name='startTime' id='startTime' form='newbattle'>
<option value=1 selected>через 1 минуту</option>
<option value=3>через 3 минуты</option>
<option value=5>через 5 минут</option>
<option value=10>через 10 минут</option>
</select>
<br><br>
<label>Размер команды (1-20)
<input type='number' min='1' max='20' name='teamMembersQuantity' form='newbattle' value='5'>
</label>
<br><br>
<input type='submit' value='Подать заявку' form='newbattle'>
<?php endif; ?>
<?= Arena::fight()->getPendingList() ?>
<?php foreach (Arena::fight()->getPendingList() as $row): ?>
<!-- !!PLACEHOLDER!! -->
<div>
User1, User2, User3
<form method='post' style='display:inline'>
<input type='hidden' name='teamId' value='1'>
<input type='submit' value='Я за этих'>
</form>
<em>против</em>
User4, User5, User6
<form method='post' style='display:inline'>
<input type='hidden' name='teamId' value='2'>
<input type='submit' value='Я за этих'>
</form>
<? endforeach; ?>
</div>

117
classes/Battles/Arena.php Normal file
View File

@ -0,0 +1,117 @@
<?php
# Date: 12.02.2022 (20:33)
namespace Battles;
use Battles\Database\Db;
class Arena
{
private int $fight_id;
private int $team_id;
function addNew(int $membersLimit = 5, int $groupsLimit = 2, int $startTime = 3)
{
if (
$this->isOnArena() &&
$this->hasNoPendingFights() &&
$this->hasNoActiveFights() &&
in_array($startTime, [1,3,5,10])
) {
$query1 = 'insert into fights_pending (fight_id, start_time, members_limit, groups_limit) VALUES (?,?,?,?)';
$query2 = 'insert into fights_pending_users (fight_id, user_id, team_id) values (?,?,?)';
$startTime = strtotime("+{$startTime}minutes");
$uid = User::getInstance()->getId();
//последовательность важна!
Db::getInstance()->execute($query1, [$uid, $startTime, $membersLimit, $groupsLimit]);
Db::getInstance()->execute($query2, [$uid, $uid, 1]);
}
}
function join(int $fight_id, int $team_id)
{
$this->fight_id = $fight_id;
$this->team_id = $team_id;
if (
$this->hasNoClanEnemies() &&
$this->hasFreeSpace() &&
$this->isOnArena() &&
$this->hasNoPendingFights() &&
$this->hasNoActiveFights()
) {
$query = 'insert into fights_pending_users (fight_id, user_id, team_id) values (?,?,?)';
Db::getInstance()->execute($query, [$fight_id, User::getInstance()->getId(), $team_id]);
}
}
function leave()
{
// чтобы не вылететь из заявки в момент начала поединка
if (
$this->hasNoActiveFights() &&
!$this->isFightStarter()
) {
Db::getInstance()->execute('delete from fights_pending_users where user_id = ?', User::getInstance()->getId());
}
}
function getPendingList(): object
{
return new \stdClass();
/** !!PLACEHOLDER!! */
}
public static function fight(): self
{
return new self();
}
// проверка на соклана
private function hasNoClanEnemies(): bool
{
$query = 'select user_id from fights_pending_users where fight_id = ? and team_id = ?';
$enemies = Db::getInstance()->ofetchAll($query, [$this->fight_id, $this->team_id]);
foreach ($enemies as $enemy) {
if (User::getInstance()->getClan() && User::getInstance()->getClan() === User::getInstance($enemy->user_id)->getClan()) {
return false;
}
}
return true;
}
// проверка на переполнение
private function hasFreeSpace(): bool
{
$query = 'select members_limit, groups_limit from fights_pending where fight_id = ?';
$query2 = 'select count(*) from fights_pending_users where fight_id = ? and team_id = ?';
$limits = Db::getInstance()->ofetch($query, $this->fight_id);
$currentUsers = Db::getInstance()->fetchColumn($query2, [$this->fight_id, $this->team_id]);
return $limits->members_limit > $currentUsers && $limits->groups_limit >= $this->team_id;
}
// проверка на нахождение в комнате (1 = арена)
private function isOnArena(): bool
{
return User::getInstance()->getRoom() === 1;
}
// проверка на нахождение в другой заявке
public function hasNoPendingFights(): bool
{
$query = 'select count(*) from fights_pending_users where user_id = ?';
return Db::getInstance()->fetchColumn($query, User::getInstance()->getId()) > 0;
}
// проверка на нахождение в поединке
public function hasNoActiveFights(): bool
{
$query = 'select count(*) from fighters where user_id = ?';
return Db::getInstance()->fetchColumn($query, User::getInstance()->getId()) > 0;
}
// проверка на создателя поединка
private function isFightStarter(): bool
{
$query = 'select count(*) from fights_pending_users where user_id = fight_id and user_id = ?';
return Db::getInstance()->fetchColumn($query, User::getInstance()->getId()) > 0;
}
}

View File

@ -74,7 +74,7 @@ class Db
// Allows the user to retrieve results using a // Allows the user to retrieve results using a
// column from the results as a key for the array // column from the results as a key for the array
if (!is_null($key) && $results[0][$key]) { if (!is_null($key) && $results[0][$key]) {
$keyed_results = array(); $keyed_results = [];
foreach ($results as $result) { foreach ($results as $result) {
$keyed_results[$result[$key]] = $result; $keyed_results[$result[$key]] = $result;
} }
@ -94,7 +94,7 @@ class Db
return $stmt->fetch(PDO::FETCH_OBJ); return $stmt->fetch(PDO::FETCH_OBJ);
} }
public function ofetchAll($query, $values = null, $key = null): object public function ofetchAll($query, $values = null)
{ {
if (is_null($values)) { if (is_null($values)) {
$values = []; $values = [];
@ -102,18 +102,7 @@ class Db
$values = [$values]; $values = [$values];
} }
$stmt = $this->execute($query, $values); $stmt = $this->execute($query, $values);
$results = $stmt->fetchAll(PDO::FETCH_OBJ); return $stmt->fetchAll(PDO::FETCH_OBJ);
// Allows the user to retrieve results using a
// column from the results as a key for the array
if (!is_null($key) && $results[0][$key]) {
$keyed_results = (object)[];
foreach ($results as $result) {
$keyed_results->$result[$key] = $result;
}
$results = $keyed_results;
}
return $results;
} }
public function lastInsertId() public function lastInsertId()

142
classes/Battles/Fight.php Normal file
View File

@ -0,0 +1,142 @@
<?php
namespace Battles;
use Battles\Database\Db;
class Fight
{
public static Fight $current;
private $db;
private \DateTimeImmutable $timer;
public const MELEE_ATTACK = 1;
public const RANGED_ATTACK = 2;
public const USE_MAGIC = 3;
public const MOVE = 4;
public const FLEE = 5;
public const PASS = 0;
private int $turn_timeout;
public function init($fighters)
{
$defaultRow = 2;
$defaultTimer = $this->timer->format('U');
$query = 'insert into fighters (
user_id,
strength,
dexterity,
intuition,
endurance,
intelligence,
wisdom,
accuracy,
evasion,
criticals,
health,
max_health,
mana,
max_mana,
melee_min,
melee_max,
battle_id,
team_id,
row_id,
turn_timeout
) values (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)';
$i = 1;
$mergedOptions = [];
foreach ($fighters as $fighter) {
$statsObj = new UserStats($fighter->user_id);
$stats = $statsObj->getFullStats();
$options[$i] = [
$fighter->user_id,
$stats->strength,
$stats->dexterity,
$stats->intuition,
$stats->endurance,
$stats->intelligence,
$stats->wisdom,
$stats->accuracy,
$stats->evasion,
$stats->criticals,
$statsObj->getHealth(),
$statsObj->getMaxHealth(),
$statsObj->getMana(),
$statsObj->getMaxMana(),
$stats->min_physical_damage,
$stats->max_physical_damage,
$fighter->fight_id,
$fighter->team_id,
$defaultRow,
$defaultTimer,
];
if ($i > 1) {
$query .= ', (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)';
$mergedOptions = array_merge_recursive($mergedOptions, $options[$i]);
}
$i++;
}
Db::getInstance()->execute($query, $mergedOptions);
}
public function playerTurn(int $action, int $uid): void
{
// Перед ходом проверить, а жив ли ты вообще?
if (empty($this->turn_timeout)) {
$this->turn_timeout = $this->db->querySingle('select turn_timeout from fighters where uid = ' . $uid);
}
$now = date('U');
/* select from last_turn_time and look at $now_plus_3_minutes - if ok, continue, if no, do nothing */
if ($now > $this->turn_timeout && !in_array($action, [self::MELEE_ATTACK, self::RANGED_ATTACK, self::USE_MAGIC, self::MOVE, self::PASS, self::FLEE])) {
$action = self::PASS;
$stmt_update_timer = $this->db->prepare('update fighters set last_turn_time = ? where uid = ?');
$stmt_update_timer->bindValue(1, date('U', strtotime('+3 minute')));
$stmt_update_timer->bindValue(2, $uid);
}
if ($action === self::MELEE_ATTACK) {
//Выполнимо только с клетки 1, только по вражеской клетке 1.
//Выполнимо по клетке 2, если клетка 1 пуста _у всех сторон_;
//Выполнимо по клетке 3, если клетка 2 пуста _у всех сторон_;
//Стоя на клетке 2 при пустой клетке 1 - атака невозможна!
echo 'Melee!';
}
if ($action === self::RANGED_ATTACK) {
//С клетки 1 атака на вражеские клетки 1 и 2;
//С клетки 2 атака на свою клетку 1 и вражескую клетку 1;
//С клетки 2 атака на вражескую 2, только если пустая клетка 1, либо нет клеток 1 _ни у одной из сторон_.
echo 'Ranged!';
}
if ($action === self::USE_MAGIC) {
//Достаёт кого угодно откуда угодно в любых обстоятельствах.
//ОЧЕНЬ внимательно проверять цель. Случайный хил трупа вызовёт апокалипсис в логике.
echo '!MAGIC!';
}
if ($action === self::MOVE) {
//клетка 1 - ближний ряд, только шаг назад
//клетка 2 - средний ряд, вперёд или назад
//клетка 3 - тыл, только вперёд
//В момент хода при соблюдении условий удара может прилететь неблокируемая атака на расстоянии.
//Перемещение - это ручной гарантированный уворот от всех летящих физических атак.
//Перемещение на пустующую клетку 1 с клетки 2 - это ручной гарантированный уворот всех стоящих на клетке 2 от всех летящих немагических атак по всей клетке.
echo 'I have legs!!';
}
if ($action === self::FLEE) {
//побег из боя, только с клетки 3.
echo 'Help me, mommy!';
}
if ($action === self::PASS) {
//Пропуск хода.
echo 'I pass this turn.';
}
// ПИСАТЬ РЕЗУЛЬТАТ ХОДА ТОЛЬКО ПОСЛЕ ПОВТОРНОЙ ПРОВЕРКИ НА НАЛИЧИЕ ПРОТИВНИКА - ОН МОГ УСПЕТЬ ОТОЙТИ!
// !!ИЛИ УМЕРЕТЬ!!
$stmt_update_timer->execute();
}
}

View File

@ -0,0 +1,127 @@
<?php
namespace Battles\Fight;
class PhysicalDamage
{
const BASE_STAT = 10;
private function rangeCheck(int $var, int $min, int $max = null): bool
{
if (is_null($max)) {
if ($var > $min) {
return true;
}
} else {
if ($var > $min && $var <= $max) {
return true;
}
}
return false;
}
/**
* Бонус 1 очко выше 10: +5% профильного урона.
* Штраф 1 очко ниже 10: -10% профильного урона.
* @param int $stat
* @return int
*/
private function statDamageModificator(int $stat): int
{
$bonus = ($stat - self::BASE_STAT) * 5;
if ($bonus < 0) {
$bonus *= 2;
}
return $bonus;
}
/**
* Бонус 1 очко выше 10: +1% шанса крита.
* Штраф 1 очко ниже 10: -2% шанса крита.
* @param int $stat
* @return int
*/
private function statCriticalsModificator(int $stat): int
{
$bonus = ($stat - self::BASE_STAT);
if ($bonus < 0) {
$bonus *= 2;
}
return $bonus;
}
/**
* Рассчёт типа удара в зависимости от параметров.
* Значения проверок смещаются в зависимости от разницы двух параметров.
* @param int $player_accuracy
* @param int $enemy_evasion
* @param int $player_criticals
* @return string
*/
protected function attack(int $player_accuracy, int $enemy_evasion, int $player_criticals): string
{
$result = '';
$d100 = mt_rand(0, 100);
$mf = $player_accuracy - $enemy_evasion;
if ($this->rangeCheck($d100, min(1, 0 - $mf), 15 - $mf)) {
$result = 'selfharm';
}
if ($this->rangeCheck($d100, min(1, 16 - $mf), 50 - $mf)) {
$result = 'miss';
}
if ($this->rangeCheck($d100, 51 - $mf, 100 - $mf - $player_criticals)) {
$result = 'hit';
}
if ($this->rangeCheck($d100, 101 - $mf - $player_criticals)) {
$result = 'crit';
}
return $result;
}
/**
* Рассчёт базового урона до применения модификаторов.
* Если урон меньше равно нулю, получается удар в 20% от минималки.
* Учитывая удар кулака 1-2, значения прибавляются к $min и $max в формуле.
* @param int $min минимальный урон
* @param int $max максимальный урон
* @param int $enemyDefence защита противника
* @return int
*/
protected function baseDamage(int $min, int $max, int $enemyDefence): int
{
return max(mt_rand(1 + $min, 2 + $max) - $enemyDefence, intval(round((1 + $min) * (20 / 100))));
}
/**
* @param object $attacker [str,dex,intu,accuracy,crit,min_dmg,max_dmg]
* @param object $defender [evasion,defence]
* @param int $type тип удара: ближний бой (1), дальний бой (2).
* @return object
*/
public function hit(object $attacker, object $defender, int $type): object
{
$result = (object)[];
$attackType = $this->attack($attacker->accuracy, $defender->evasion, $attacker->criticals + $this->statCriticalsModificator($attacker->intuition));
if ($attackType === 'miss') {
$result->damage = 0;
$result->attackType = $attackType;
return $result;
}
$damage = $this->baseDamage($attacker->min_damage, $attacker->max_damage, $defender->defence);
$critDamageMultiplier = $attackType === 'crit' ? 2 : 1;
if ($type === 1) {
$damage = $damage * ($this->statDamageModificator($attacker->strength) / 100);
}
if ($type === 2) {
$damage = $damage * ($this->statDamageModificator($attacker->dexterity) / 100);
}
$damage = $damage * $critDamageMultiplier;
$result->damage = $damage;
$result->attackType = $attackType;
return $result;
}
}

View File

@ -28,7 +28,6 @@ class User
// Пока несуществующие, для совместимости. // Пока несуществующие, для совместимости.
protected int $experience = 0; protected int $experience = 0;
protected int $battle = 0;
protected int $in_tower = 0; // Скорее башню похороним чем запустим... protected int $in_tower = 0; // Скорее башню похороним чем запустим...
protected int $zayavka = 0; protected int $zayavka = 0;
@ -77,6 +76,7 @@ class User
{ {
if (Db::getInstance()->fetchColumn('SELECT 1 FROM users_effects WHERE owner_id = ? AND type = ?', [$userId, $type])) { if (Db::getInstance()->fetchColumn('SELECT 1 FROM users_effects WHERE owner_id = ? AND type = ?', [$userId, $type])) {
Db::getInstance()->execute('DELETE FROM users_effects WHERE owner_id = ? AND type = ?', [$userId, $type]); Db::getInstance()->execute('DELETE FROM users_effects WHERE owner_id = ? AND type = ?', [$userId, $type]);
return true;
} }
return false; return false;
} }
@ -223,7 +223,7 @@ class User
public function getBattle(): int public function getBattle(): int
{ {
return $this->battle; return Arena::fight()->hasNoActiveFights();
} }
public function getInTower(): int public function getInTower(): int