hy-farm/muchang/common/services/StaminaService.php
2026-05-30 20:23:54 +08:00

251 lines
8.0 KiB
PHP

<?php
/**
* Player stamina storage and recovery.
*/
namespace common\services;
use common\models\User;
use common\utils\RedisKeyUtil;
use RedisLock\RedisLock;
use Yii;
use yii\base\Object;
class StaminaService extends Object
{
const MAX_STAMINA = 20;
const STEAL_COST = 10;
const RECOVER_AMOUNT = 2;
const RECOVER_INTERVAL = 3600;
public static function getClientData($userID)
{
$userDbData = User::findById($userID, false);
if (!$userDbData) {
return self::defaultClientData();
}
self::recover($userDbData, time());
self::save($userDbData);
return self::formatClientData($userDbData);
}
public static function consumeSteal($userID)
{
return self::withStaminaLock($userID, function ($userID) {
return self::consumeStealUnlocked($userID);
});
}
public static function withStaminaLock($userID, callable $callback)
{
$lock = new RedisLock(Yii::$app->redis, RedisKeyUtil::lockUserStamina($userID), RedisLock::FLAG_CATCH_EXCEPTIONS);
if (!$lock->acquire(10)) {
$userDbData = User::findById($userID, false);
return [
'ok' => false,
'status' => ResultStatusServices::RS_SAME_TIME_OPERATION,
'data' => $userDbData ? self::formatClientData($userDbData) : self::defaultClientData(),
];
}
try {
return call_user_func($callback, $userID);
} finally {
$lock->release();
}
}
public static function consumeStealUnlocked($userID)
{
$check = self::checkStealUnlocked($userID);
if (empty($check['ok'])) {
return $check;
}
$userDbData = User::findById($userID, false);
$now = time();
$wasFull = (int)$userDbData->stamina >= (int)$userDbData->stamina_max;
$userDbData->stamina -= self::STEAL_COST;
if ($wasFull) {
$userDbData->stamina_recover_time = $now;
}
self::save($userDbData);
return [
'ok' => true,
'status' => ResultStatusServices::RS_OPERATION_SUCCESS,
'data' => self::formatClientData($userDbData),
];
}
public static function checkStealUnlocked($userID)
{
$userDbData = User::findById($userID, false);
if (!$userDbData) {
return [
'ok' => false,
'status' => ResultStatusServices::RS_NOT_FIND_USER,
'data' => self::defaultClientData(),
];
}
$now = time();
self::recover($userDbData, $now);
if ((int)$userDbData->stamina < self::STEAL_COST) {
self::save($userDbData);
return [
'ok' => false,
'status' => ResultStatusServices::RS_STAMINA_NOT_ENOUGH,
'data' => self::formatClientData($userDbData),
];
}
self::save($userDbData);
return [
'ok' => true,
'status' => ResultStatusServices::RS_OPERATION_SUCCESS,
'data' => self::formatClientData($userDbData),
];
}
public static function increaseMax($userID, $amount, $fillAddedCapacity = true)
{
if ($amount <= 0) {
return [
'ok' => false,
'status' => ResultStatusServices::RS_PARAM_ERROR,
'data' => self::getClientData($userID),
];
}
return self::setMax($userID, self::currentMax($userID) + (int)$amount, $fillAddedCapacity);
}
public static function setMax($userID, $maxStamina, $fillAddedCapacity = true)
{
$lock = new RedisLock(Yii::$app->redis, RedisKeyUtil::lockUserStamina($userID), RedisLock::FLAG_CATCH_EXCEPTIONS);
if (!$lock->acquire(10)) {
return [
'ok' => false,
'status' => ResultStatusServices::RS_SAME_TIME_OPERATION,
'data' => self::getClientData($userID),
];
}
try {
$userDbData = User::findById($userID, false);
if (!$userDbData) {
return [
'ok' => false,
'status' => ResultStatusServices::RS_NOT_FIND_USER,
'data' => self::defaultClientData(),
];
}
self::recover($userDbData, time());
$oldMax = self::normalizeMax((int)$userDbData->stamina_max);
$newMax = self::normalizeMax((int)$maxStamina);
$userDbData->stamina_max = $newMax;
if ($fillAddedCapacity && $newMax > $oldMax) {
$userDbData->stamina = min($newMax, (int)$userDbData->stamina + ($newMax - $oldMax));
} else {
$userDbData->stamina = min((int)$userDbData->stamina, $newMax);
}
if ((int)$userDbData->stamina >= $newMax) {
$userDbData->stamina_recover_time = time();
}
self::save($userDbData);
return [
'ok' => true,
'status' => ResultStatusServices::RS_OPERATION_SUCCESS,
'data' => self::formatClientData($userDbData),
];
} finally {
$lock->release();
}
}
private static function currentMax($userID)
{
$userDbData = User::findById($userID, false);
if (!$userDbData) {
return self::MAX_STAMINA;
}
return self::normalizeMax((int)$userDbData->stamina_max);
}
private static function recover(User $userDbData, $now)
{
$maxStamina = self::normalizeMax((int)$userDbData->stamina_max);
$userDbData->stamina_max = $maxStamina;
$userDbData->stamina = max(0, min($maxStamina, (int)$userDbData->stamina));
if ((int)$userDbData->stamina >= $maxStamina) {
$userDbData->stamina = $maxStamina;
$userDbData->stamina_recover_time = $now;
return;
}
if ((int)$userDbData->stamina_recover_time <= 0) {
$userDbData->stamina_recover_time = $now;
return;
}
$elapsed = $now - (int)$userDbData->stamina_recover_time;
if ($elapsed < self::RECOVER_INTERVAL) {
return;
}
$recoverTimes = (int)floor($elapsed / self::RECOVER_INTERVAL);
$userDbData->stamina = min($maxStamina, (int)$userDbData->stamina + $recoverTimes * self::RECOVER_AMOUNT);
if ((int)$userDbData->stamina >= $maxStamina) {
$userDbData->stamina_recover_time = $now;
} else {
$userDbData->stamina_recover_time += $recoverTimes * self::RECOVER_INTERVAL;
}
}
private static function save(User $userDbData)
{
return $userDbData->save(true, ['stamina', 'stamina_max', 'stamina_recover_time']);
}
private static function formatClientData(User $userDbData)
{
$maxStamina = self::normalizeMax((int)$userDbData->stamina_max);
$stamina = max(0, min($maxStamina, (int)$userDbData->stamina));
$nextRecoverTime = 0;
if ($stamina < $maxStamina) {
$nextRecoverTime = (int)$userDbData->stamina_recover_time + self::RECOVER_INTERVAL;
}
return [
'stamina' => $stamina,
'stamina_max' => $maxStamina,
'stamina_next_recover_time' => $nextRecoverTime,
'stamina_recover_amount' => self::RECOVER_AMOUNT,
'stamina_recover_interval' => self::RECOVER_INTERVAL,
'stamina_cost_steal' => self::STEAL_COST,
];
}
private static function defaultClientData()
{
return [
'stamina' => self::MAX_STAMINA,
'stamina_max' => self::MAX_STAMINA,
'stamina_next_recover_time' => 0,
'stamina_recover_amount' => self::RECOVER_AMOUNT,
'stamina_recover_interval' => self::RECOVER_INTERVAL,
'stamina_cost_steal' => self::STEAL_COST,
];
}
private static function normalizeMax($maxStamina)
{
return max(1, (int)$maxStamina);
}
}