diff --git a/hwe/sammo/API/Auction/OpenUniqueAuction.php b/hwe/sammo/API/Auction/OpenUniqueAuction.php new file mode 100644 index 00000000..21c6e2ee --- /dev/null +++ b/hwe/sammo/API/Auction/OpenUniqueAuction.php @@ -0,0 +1,71 @@ + $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 + ]; + } +} diff --git a/hwe/sammo/Auction.php b/hwe/sammo/Auction.php index a5282512..e4c127c9 100644 --- a/hwe/sammo/Auction.php +++ b/hwe/sammo/Auction.php @@ -3,7 +3,13 @@ namespace sammo; use Ds\Map; +use sammo\DTO\AuctionBidItem; +use sammo\DTO\AuctionBidItemData; use sammo\DTO\AuctionInfo; +use sammo\Enums\InheritanceKey; +use sammo\Enums\RankColumn; +use sammo\Enums\ResourceType; +use sammo\VO\InheritancePointType; 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(); $auctionID = $info->id; $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()); } - public function __construct(private readonly int $auctionID) + public function __construct(private readonly int $auctionID, private General $general) { $db = DB::db(); $auctionStor = KVStorage::getStorage($db, 'auction'); - $rawBettingInfo = $auctionStor->getValue("id_{$auctionID}"); - if ($rawBettingInfo === null) { + $rawAuctionInfo = $auctionStor->getValue("id_{$auctionID}"); + if ($rawAuctionInfo === null) { 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); } } diff --git a/hwe/sammo/DTO/AuctionBidItem.php b/hwe/sammo/DTO/AuctionBidItem.php index 296a2df7..d16beffc 100644 --- a/hwe/sammo/DTO/AuctionBidItem.php +++ b/hwe/sammo/DTO/AuctionBidItem.php @@ -3,12 +3,14 @@ namespace sammo\DTO; use sammo\DTO\Attr\JsonString; +use sammo\DTO\Attr\NullIsUndefined; use sammo\DTO\Attr\RawName; class AuctionBidItem extends DTO { public function __construct( - public int $id, + #[NullIsUndefined] + public ?int $id, #[RawName('auction_id')] public int $auctionID, public ?int $owner, diff --git a/hwe/sammo/DTO/AuctionBidItemData.php b/hwe/sammo/DTO/AuctionBidItemData.php index 86f7e9d3..76d51e9d 100644 --- a/hwe/sammo/DTO/AuctionBidItemData.php +++ b/hwe/sammo/DTO/AuctionBidItemData.php @@ -2,11 +2,16 @@ namespace sammo\DTO; +use sammo\DTO\Attr\NullIsUndefined; + class AuctionBidItemData extends DTO { public function __construct( - public string $ownerName, + #[NullIsUndefined] + public ?string $ownerName, public string $generalName, + #[NullIsUndefined] + public ?int $priority, ) { } } diff --git a/hwe/sammo/DTO/AuctionInfo.php b/hwe/sammo/DTO/AuctionInfo.php index d1b86113..7534081c 100644 --- a/hwe/sammo/DTO/AuctionInfo.php +++ b/hwe/sammo/DTO/AuctionInfo.php @@ -13,6 +13,8 @@ class AuctionInfo extends DTO public string $type, public bool $finished, public ResourceType $reqResource, + public int $minAmount, + public ?int $buyImmediatelyAmount, public string $openDate, public string $closeDate, ) { diff --git a/hwe/sammo/Enums/ResourceType.php b/hwe/sammo/Enums/ResourceType.php index d24481d0..ca0f0348 100644 --- a/hwe/sammo/Enums/ResourceType.php +++ b/hwe/sammo/Enums/ResourceType.php @@ -6,4 +6,13 @@ enum ResourceType: string case gold = 'gold'; case rice = 'rice'; case inheritancePoint = 'inheritancePoint'; + + public function getName(): string + { + return match($this){ + ResourceType::gold => '금', + ResourceType::rice => '쌀', + ResourceType::inheritancePoint => '유산 포인트', + }; + } }