feat(wip): 경매 입찰 기능
This commit is contained in:
@@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace sammo\API\Auction;
|
||||||
|
|
||||||
|
use sammo\Session;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use sammo\Betting;
|
||||||
|
use sammo\DB;
|
||||||
|
use sammo\DTO\BettingItem;
|
||||||
|
use sammo\Validator;
|
||||||
|
use sammo\GameConst;
|
||||||
|
use sammo\KVStorage;
|
||||||
|
use sammo\UserLogger;
|
||||||
|
use sammo\Util;
|
||||||
|
|
||||||
|
class OpenUniqueAuction extends \sammo\BaseAPI
|
||||||
|
{
|
||||||
|
public function validateArgs(): ?string
|
||||||
|
{
|
||||||
|
$availableItems = [];
|
||||||
|
foreach (GameConst::$allItems as $items) {
|
||||||
|
foreach ($items as $itemKey => $amount) {
|
||||||
|
if ($amount == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$availableItems[$itemKey] = $amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$v = new Validator($this->args);
|
||||||
|
$v->rule('required', [
|
||||||
|
'itemID',
|
||||||
|
'amount'
|
||||||
|
])
|
||||||
|
->rule('int', 'amount')
|
||||||
|
->rule('min', 'amount', GameConst::$inheritItemUniqueMinPoint)
|
||||||
|
->rule('keyExists', 'item', $availableItems);
|
||||||
|
|
||||||
|
|
||||||
|
if (!$v->validate()) {
|
||||||
|
return $v->errorStr();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRequiredSessionMode(): int
|
||||||
|
{
|
||||||
|
return static::REQ_GAME_LOGIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function launch(Session $session, ?DateTimeInterface $modifiedSince, ?string $reqEtag)
|
||||||
|
{
|
||||||
|
/** @var int */
|
||||||
|
$bettingID = $this->args['bettingID'];
|
||||||
|
/** @var int[] */
|
||||||
|
$bettingType = $this->args['bettingType'];
|
||||||
|
/** @var int */
|
||||||
|
$amount = $this->args['amount'];
|
||||||
|
|
||||||
|
$bettingHelper = new Betting($bettingID);
|
||||||
|
try {
|
||||||
|
$bettingHelper->bet($session->generalID, $session->userID, $bettingType, $amount);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return $e->getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'result' => true
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
+202
-5
@@ -3,7 +3,13 @@
|
|||||||
namespace sammo;
|
namespace sammo;
|
||||||
|
|
||||||
use Ds\Map;
|
use Ds\Map;
|
||||||
|
use sammo\DTO\AuctionBidItem;
|
||||||
|
use sammo\DTO\AuctionBidItemData;
|
||||||
use sammo\DTO\AuctionInfo;
|
use sammo\DTO\AuctionInfo;
|
||||||
|
use sammo\Enums\InheritanceKey;
|
||||||
|
use sammo\Enums\RankColumn;
|
||||||
|
use sammo\Enums\ResourceType;
|
||||||
|
use sammo\VO\InheritancePointType;
|
||||||
|
|
||||||
class Auction
|
class Auction
|
||||||
{
|
{
|
||||||
@@ -25,22 +31,213 @@ class Auction
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static public function openAuction(AuctionInfo $info)
|
static public function openAuction(AuctionInfo $info, General $general): ?string
|
||||||
{
|
{
|
||||||
$db = DB::db();
|
$db = DB::db();
|
||||||
$auctionID = $info->id;
|
$auctionID = $info->id;
|
||||||
$auctionStor = KVStorage::getStorage($db, 'auction');
|
$auctionStor = KVStorage::getStorage($db, 'auction');
|
||||||
|
|
||||||
|
$openedAuction = $general->getAuxVar('openedAuction') ?? [];
|
||||||
|
$prevAuctionID = $openedAuction[$info->reqResource->value] ?? null;
|
||||||
|
if ($prevAuctionID !== null) {
|
||||||
|
//XXX: 이 구조보다는 차라리 DB에 insert로 넣는 게 나을 수도.
|
||||||
|
$prevAuction = new Auction($prevAuctionID, $general);
|
||||||
|
if (!$prevAuction->info->finished) {
|
||||||
|
return '아직 경매가 끝나지 않았습니다.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$auctionStor->setValue("id_{$auctionID}", $info->toArray());
|
$auctionStor->setValue("id_{$auctionID}", $info->toArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function __construct(private readonly int $auctionID)
|
public function __construct(private readonly int $auctionID, private General $general)
|
||||||
{
|
{
|
||||||
$db = DB::db();
|
$db = DB::db();
|
||||||
$auctionStor = KVStorage::getStorage($db, 'auction');
|
$auctionStor = KVStorage::getStorage($db, 'auction');
|
||||||
$rawBettingInfo = $auctionStor->getValue("id_{$auctionID}");
|
$rawAuctionInfo = $auctionStor->getValue("id_{$auctionID}");
|
||||||
if ($rawBettingInfo === null) {
|
if ($rawAuctionInfo === null) {
|
||||||
throw new \RuntimeException("해당 경매가 없습니다: {$auctionID}");
|
throw new \RuntimeException("해당 경매가 없습니다: {$auctionID}");
|
||||||
}
|
}
|
||||||
$this->info = AuctionInfo::fromArray($rawBettingInfo);
|
$this->info = AuctionInfo::fromArray($rawAuctionInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function closeAuction(): void
|
||||||
|
{
|
||||||
|
$db = DB::db();
|
||||||
|
$auctionStor = KVStorage::getStorage($db, 'auction');
|
||||||
|
|
||||||
|
$this->info->finished = true;
|
||||||
|
|
||||||
|
$openedAuction = $this->general->getAuxVar('openedAuction') ?? [];
|
||||||
|
if (key_exists($this->info->reqResource->value, $openedAuction)) {
|
||||||
|
unset($openedAuction[$this->info->reqResource->value]);
|
||||||
|
$this->general->setAuxVar('openedAuction', $openedAuction);
|
||||||
|
}
|
||||||
|
$auctionID = $this->info->id;
|
||||||
|
$auctionStor->setValue("id_{$auctionID}", $this->info->toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function bidInheritPoint(int $amount, ?int $priority, string $now): void
|
||||||
|
{
|
||||||
|
$db = DB::db();
|
||||||
|
|
||||||
|
$auctionInfo = $this->info;
|
||||||
|
$general = $this->general;
|
||||||
|
|
||||||
|
$rawHighestBid = $db->queryFirstRow(
|
||||||
|
'SELECT * FROM ng_auction WHERE auction_id = %i ORDER BY `amount` DESC LIMIT 1',
|
||||||
|
$auctionInfo->id
|
||||||
|
);
|
||||||
|
if (!$rawHighestBid) {
|
||||||
|
throw new \RuntimeException('최고 입찰가가 없습니다.');
|
||||||
|
}
|
||||||
|
$highestBid = AuctionBidItem::fromArray($rawHighestBid);
|
||||||
|
if ($amount <= $highestBid->amount) {
|
||||||
|
throw new \RuntimeException('현재입찰가보다 높게 입찰해야 합니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$rawMyPrevBid = $db->queryFirstRow(
|
||||||
|
'SELECT * FROM ng_auction WHERE general_id = %i AND auction_id = %i ORDER BY `amount` DESC LIMIT 1',
|
||||||
|
$general->getID(),
|
||||||
|
$auctionInfo->id
|
||||||
|
);
|
||||||
|
if ($rawMyPrevBid === null) {
|
||||||
|
$myPrevBid = null;
|
||||||
|
} else {
|
||||||
|
$myPrevBid = AuctionBidItem::fromArray($rawMyPrevBid);
|
||||||
|
}
|
||||||
|
|
||||||
|
$morePoint = $amount - ($myPrevBid ? $myPrevBid->amount : 0);
|
||||||
|
$currPoint = $general->getInheritancePoint(InheritanceKey::previous);
|
||||||
|
if($currPoint === null || $currPoint < $morePoint){
|
||||||
|
throw new \RuntimeException('유산포인트가 부족합니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
//여기서부터 입찰 성공
|
||||||
|
|
||||||
|
$newBid = new AuctionBidItem(
|
||||||
|
null,
|
||||||
|
$auctionInfo->id,
|
||||||
|
$general->getVar('owner'),
|
||||||
|
$general->getID(),
|
||||||
|
$amount,
|
||||||
|
$now,
|
||||||
|
new AuctionBidItemData(
|
||||||
|
$general->getVar('owner_name'),
|
||||||
|
$general->getName(),
|
||||||
|
$priority
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$db->insert('ng_auction', $newBid->toArray());
|
||||||
|
|
||||||
|
$general->increaseInheritancePoint(InheritanceKey::previous, -$morePoint);
|
||||||
|
$general->increaseRankVar(RankColumn::inherit_point_spent_dynamic, $morePoint);
|
||||||
|
|
||||||
|
if ($myPrevBid === null || $myPrevBid->id !== $highestBid->id) {
|
||||||
|
$oldBidder = General::createGeneralObjFromDB($highestBid->general_id);
|
||||||
|
$oldBidder->increaseInheritancePoint(InheritanceKey::previous, $highestBid->amount);
|
||||||
|
//TODO: 전역 알림이 나타나야한다.
|
||||||
|
$oldBidder->getLogger()->pushGeneralActionLog(
|
||||||
|
"유산포인트 {$auctionInfo->id}번 경매에 상회입찰이 되었습니다.",
|
||||||
|
);
|
||||||
|
$oldBidder->applyDB($db);
|
||||||
|
}
|
||||||
|
$general->applyDB($db);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function bid(int $amount, ?int $priority): void
|
||||||
|
{
|
||||||
|
$auctionInfo = $this->info;
|
||||||
|
$general = $this->general;
|
||||||
|
|
||||||
|
if ($auctionInfo->finished) {
|
||||||
|
throw new \RuntimeException('경매가 이미 끝났습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = TimeUtil::now();
|
||||||
|
|
||||||
|
if ($auctionInfo->closeDate < $now) {
|
||||||
|
throw new \RuntimeException('경매가 이미 끝났습니다.');
|
||||||
|
}
|
||||||
|
if ($auctionInfo->closeDate > $now) {
|
||||||
|
throw new \RuntimeException('경매가 아직 시작되지 않았습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($auctionInfo->buyImmediatelyAmount !== null && $auctionInfo->buyImmediatelyAmount < $amount) {
|
||||||
|
throw new \RuntimeException('즉시판매가보다 높을 수 없습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($auctionInfo->reqResource === ResourceType::inheritancePoint) {
|
||||||
|
$this->bidInheritPoint($amount, $priority, $now);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//reqResource는 말 그대로 '구매자가 내야하는 자원'이다.
|
||||||
|
|
||||||
|
$db = DB::db();
|
||||||
|
|
||||||
|
$rawHighestBid = $db->queryFirstRow(
|
||||||
|
'SELECT * FROM ng_auction WHERE auction_id = %i ORDER BY `amount` DESC LIMIT 1',
|
||||||
|
$auctionInfo->id
|
||||||
|
);
|
||||||
|
if (!$rawHighestBid) {
|
||||||
|
throw new \RuntimeException('최고 입찰가가 없습니다.');
|
||||||
|
}
|
||||||
|
$highestBid = AuctionBidItem::fromArray($rawHighestBid);
|
||||||
|
if ($amount <= $highestBid->amount) {
|
||||||
|
throw new \RuntimeException('현재입찰가보다 높게 입찰해야 합니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$rawMyPrevBid = $db->queryFirstRow(
|
||||||
|
'SELECT * FROM ng_auction WHERE general_id = %i AND auction_id = %i ORDER BY `amount` DESC LIMIT 1',
|
||||||
|
$general->getID(),
|
||||||
|
$auctionInfo->id
|
||||||
|
);
|
||||||
|
if ($rawMyPrevBid === null) {
|
||||||
|
$myPrevBid = null;
|
||||||
|
} else {
|
||||||
|
$myPrevBid = AuctionBidItem::fromArray($rawMyPrevBid);
|
||||||
|
}
|
||||||
|
|
||||||
|
$morePoint = $amount - ($myPrevBid ? $myPrevBid->amount : 0);
|
||||||
|
$resType = $auctionInfo->reqResource;
|
||||||
|
$minReqRes = match ($resType) {
|
||||||
|
ResourceType::gold => GameConst::$defaultGold,
|
||||||
|
ResourceType::rice => GameConst::$defaultRice,
|
||||||
|
};
|
||||||
|
|
||||||
|
if ($general->getVar($resType->value) < $morePoint + $minReqRes) {
|
||||||
|
throw new \RuntimeException($resType->getName() . '이 부족합니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
//여기서부터 입찰 성공
|
||||||
|
|
||||||
|
$newBid = new AuctionBidItem(
|
||||||
|
null,
|
||||||
|
$auctionInfo->id,
|
||||||
|
$general->getVar('owner'),
|
||||||
|
$general->getID(),
|
||||||
|
$amount,
|
||||||
|
$now,
|
||||||
|
new AuctionBidItemData(
|
||||||
|
$general->getVar('owner_name'),
|
||||||
|
$general->getName(),
|
||||||
|
$priority
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$db->insert('ng_auction', $newBid->toArray());
|
||||||
|
|
||||||
|
$general->setVar($resType->value, $general->getVar($resType->value) - $morePoint);
|
||||||
|
|
||||||
|
if ($myPrevBid === null || $myPrevBid->id !== $highestBid->id) {
|
||||||
|
$oldBidder = General::createGeneralObjFromDB($highestBid->general_id);
|
||||||
|
$oldBidder->increaseVar($resType->value, $highestBid->amount);
|
||||||
|
//TODO: 전역 알림이 나타나야한다.
|
||||||
|
$oldBidder->getLogger()->pushGeneralActionLog(
|
||||||
|
"{$resType->getName()} {$auctionInfo->id}번 경매에 상회입찰이 되었습니다.",
|
||||||
|
);
|
||||||
|
$oldBidder->applyDB($db);
|
||||||
|
}
|
||||||
|
$general->applyDB($db);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,14 @@
|
|||||||
namespace sammo\DTO;
|
namespace sammo\DTO;
|
||||||
|
|
||||||
use sammo\DTO\Attr\JsonString;
|
use sammo\DTO\Attr\JsonString;
|
||||||
|
use sammo\DTO\Attr\NullIsUndefined;
|
||||||
use sammo\DTO\Attr\RawName;
|
use sammo\DTO\Attr\RawName;
|
||||||
|
|
||||||
class AuctionBidItem extends DTO
|
class AuctionBidItem extends DTO
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public int $id,
|
#[NullIsUndefined]
|
||||||
|
public ?int $id,
|
||||||
#[RawName('auction_id')]
|
#[RawName('auction_id')]
|
||||||
public int $auctionID,
|
public int $auctionID,
|
||||||
public ?int $owner,
|
public ?int $owner,
|
||||||
|
|||||||
@@ -2,11 +2,16 @@
|
|||||||
|
|
||||||
namespace sammo\DTO;
|
namespace sammo\DTO;
|
||||||
|
|
||||||
|
use sammo\DTO\Attr\NullIsUndefined;
|
||||||
|
|
||||||
class AuctionBidItemData extends DTO
|
class AuctionBidItemData extends DTO
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public string $ownerName,
|
#[NullIsUndefined]
|
||||||
|
public ?string $ownerName,
|
||||||
public string $generalName,
|
public string $generalName,
|
||||||
|
#[NullIsUndefined]
|
||||||
|
public ?int $priority,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ class AuctionInfo extends DTO
|
|||||||
public string $type,
|
public string $type,
|
||||||
public bool $finished,
|
public bool $finished,
|
||||||
public ResourceType $reqResource,
|
public ResourceType $reqResource,
|
||||||
|
public int $minAmount,
|
||||||
|
public ?int $buyImmediatelyAmount,
|
||||||
public string $openDate,
|
public string $openDate,
|
||||||
public string $closeDate,
|
public string $closeDate,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -6,4 +6,13 @@ enum ResourceType: string
|
|||||||
case gold = 'gold';
|
case gold = 'gold';
|
||||||
case rice = 'rice';
|
case rice = 'rice';
|
||||||
case inheritancePoint = 'inheritancePoint';
|
case inheritancePoint = 'inheritancePoint';
|
||||||
|
|
||||||
|
public function getName(): string
|
||||||
|
{
|
||||||
|
return match($this){
|
||||||
|
ResourceType::gold => '금',
|
||||||
|
ResourceType::rice => '쌀',
|
||||||
|
ResourceType::inheritancePoint => '유산 포인트',
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user