From 6e87aef09fb92904243ebb4749a8ad3eb8757fad Mon Sep 17 00:00:00 2001 From: Hide_D Date: Mon, 30 May 2022 01:10:23 +0900 Subject: [PATCH] =?UTF-8?q?feat(wip):=20=EC=9C=A0=EB=8B=88=ED=81=AC=20?= =?UTF-8?q?=EA=B2=BD=EB=A7=A4=20=EC=84=B8=EB=B6=80=20=EC=A7=84=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hwe/sammo/Auction.php | 108 +++++++++++---- hwe/sammo/AuctionUniqueItem.php | 197 +++++++++++++++++++++++++-- hwe/sammo/DTO/AuctionBidItemData.php | 2 + hwe/sammo/DTO/AuctionInfo.php | 7 +- hwe/sammo/Enums/AuctionType.php | 8 ++ 5 files changed, 281 insertions(+), 41 deletions(-) create mode 100644 hwe/sammo/Enums/AuctionType.php diff --git a/hwe/sammo/Auction.php b/hwe/sammo/Auction.php index c4fc69d6..62ce874e 100644 --- a/hwe/sammo/Auction.php +++ b/hwe/sammo/Auction.php @@ -20,6 +20,16 @@ class Auction protected AuctionInfo $info; public const LAST_AUCTION_ID_KEY = 'last_auction_id'; + + public const COEFF_AUCTION_CLOSE_MINUTES = 24; + public const COEFF_EXTENSION_MINUTES_PER_BID = 1 / 6; + public const COEFF_EXTENSION_MINUTES_LIMIT_BY_BID = 1; + public const COEFF_EXTENSION_MINUTES_BY_EXTENSION_QUERY = 1; + public const MIN_AUCTION_CLOSE_MINUTES = 30; + public const MIN_EXTENSION_MINUTES_PER_BID = 1; + public const MIN_EXTENSION_MINUTES_LIMIT_BY_BID = 5; + public const MIN_EXTENSION_MINUTES_BY_EXTENSION_QUERY = 5; + static public function genNextAuctionID(): int { $db = DB::db(); @@ -41,14 +51,14 @@ class Auction $auctionStor = KVStorage::getStorage($db, 'auction'); $openedAuction = $general->getAuxVar('openedAuction') ?? []; - $prevAuctionID = $openedAuction[$info->reqResource->value] ?? null; + $prevAuctionID = $openedAuction[$info->type->value] ?? null; if ($prevAuctionID !== null) { //XXX: 이 구조보다는 차라리 DB에 insert로 넣는 게 나을 수도. $prevAuction = new Auction($prevAuctionID, $general); if (!$prevAuction->info->finished) { return '아직 경매가 끝나지 않았습니다.'; } - unset($openedAuction[$info->reqResource->value]); + unset($openedAuction[$info->type->value]); $general->setAuxVar('openedAuction', $openedAuction); } @@ -62,7 +72,7 @@ class Auction 'SELECT * FROM ng_auction WHERE auction_id = %i ORDER BY `amount` DESC LIMIT 1', $this->info->id ); - if(!$rawHighestBid){ + if (!$rawHighestBid) { return null; } @@ -99,31 +109,49 @@ class Auction return $this->info; } - public function extendCloseDate(DateTimeInterface $date): void + public function extendCloseDate(DateTimeInterface $date, bool $force = false): ?string { + if(!$force){ + if ($this->info->remainCloseExtensionCnt === null) { + return '연장할 수 없는 경매입니다.'; + } + if ($this->info->remainCloseExtensionCnt === 0) { + return '더 이상 연장할 수 없습니다'; + } + if ($this->info->remainCloseExtensionCnt > 0) { + $this->info->remainCloseExtensionCnt--; + } + } + $db = DB::db(); $auctionStor = KVStorage::getStorage($db, 'auction'); - $this->info->closeDate = DateTimeImmutable::createFromInterface($date); + $gameStor = KVStorage::getStorage($db, 'game_env'); + $turnTerm = $gameStor->getValue('turnterm'); + $closeDate = DateTimeImmutable::createFromInterface($date); + $this->info->closeDate = $closeDate; + $this->info->availableLatestBidCloseDate = $closeDate->add(TimeUtil::secondsToDateInterval( + max(static::MIN_EXTENSION_MINUTES_PER_BID, $turnTerm * static::COEFF_EXTENSION_MINUTES_PER_BID) * 60 + )); $auctionStor->setValue("id_{$this->info->id}", $this->info->toArray()); + return null; } public function refundBid(AuctionBidItem $bidItem, string $reason): void { - if($bidItem->auctionID !== $this->info->id){ + if ($bidItem->auctionID !== $this->info->id) { throw new \RuntimeException('잘못된 경매입니다.'); } $db = DB::db(); - if($bidItem->generalID === $this->general->generalID) { + if ($bidItem->generalID === $this->general->generalID) { $oldBidder = $this->general; } else { - $oldBidder = General::createGeneralObjFromDB($bidItem->general_id); + $oldBidder = General::createGeneralObjFromDB($bidItem->generalID); } - if($this->info->reqResource === ResourceType::inheritancePoint){ + if ($this->info->reqResource === ResourceType::inheritancePoint) { $oldBidder->increaseInheritancePoint(InheritanceKey::previous, $bidItem->amount); - } - else{ + } else { $oldBidder->increaseVar($this->info->reqResource->value, $bidItem->amount); } @@ -161,22 +189,21 @@ class Auction $this->info->finished = true; $openedAuction = $this->general->getAuxVar('openedAuction') ?? []; - if (key_exists($this->info->reqResource->value, $openedAuction)) { - unset($openedAuction[$this->info->reqResource->value]); + if (key_exists($this->info->type->value, $openedAuction)) { + unset($openedAuction[$this->info->type->value]); $this->general->setAuxVar('openedAuction', $openedAuction); } if ($isRollback) { $highestBid = $this->getHighestBid(); - if($highestBid !== null){ + if ($highestBid !== null) { $this->refundBid($highestBid, "{$this->info->title} 경매가 취소되었습니다."); } - } $auctionID = $this->info->id; $auctionStor->setValue("id_{$auctionID}", $this->info->toArray()); } - private function bidInheritPoint(int $amount, \DateTimeImmutable $now): void + private function bidInheritPoint(int $amount, \DateTimeImmutable $now, bool $tryExtendCloseDate): ?string { $db = DB::db(); @@ -185,7 +212,7 @@ class Auction $highestBid = $this->getHighestBid(); if ($highestBid !== null && $amount <= $highestBid->amount) { - throw new \RuntimeException('현재입찰가보다 높게 입찰해야 합니다.'); + return '현재입찰가보다 높게 입찰해야 합니다.'; } $rawMyPrevBid = $db->queryFirstRow( @@ -202,7 +229,7 @@ class Auction $morePoint = $amount - ($myPrevBid ? $myPrevBid->amount : 0); $currPoint = $general->getInheritancePoint(InheritanceKey::previous); if ($currPoint === null || $currPoint < $morePoint) { - throw new \RuntimeException('유산포인트가 부족합니다.'); + return '유산포인트가 부족합니다.'; } //여기서부터 입찰 성공 @@ -217,9 +244,26 @@ class Auction new AuctionBidItemData( $general->getVar('owner_name'), $general->getName(), + $tryExtendCloseDate, ) ); $db->insert('ng_auction', $newBid->toArray()); + if ($db->affectedRows() == 0) { + return '입찰에 실패했습니다: DB 오류'; + } + + $gameStor = KVStorage::getStorage($db, 'game_env'); + $turnTerm = $gameStor->getValue('turnterm'); + + if ($this->info->availableLatestBidCloseDate !== null) { + $extendedCloseDate = $this->info->closeDate->add(TimeUtil::secondsToDateInterval( + max(static::MIN_EXTENSION_MINUTES_PER_BID, $turnTerm * static::COEFF_EXTENSION_MINUTES_PER_BID) * 60 + )); + + if ($extendedCloseDate > $this->info->closeDate && $this->info->closeDate < $this->info->availableLatestBidCloseDate) { + $this->extendCloseDate(min($extendedCloseDate, $this->info->availableLatestBidCloseDate)); + } + } $general->increaseInheritancePoint(InheritanceKey::previous, -$morePoint); $general->increaseRankVar(RankColumn::inherit_point_spent_dynamic, $morePoint); @@ -228,33 +272,33 @@ class Auction $this->refundBid($highestBid, "{$auctionInfo->title} 경매에 상회입찰자가 나타났습니다."); } $general->applyDB($db); + return null; } - public function bid(int $amount): void + public function bid(int $amount, bool $tryExtendCloseDate = false): ?string { $auctionInfo = $this->info; $general = $this->general; if ($auctionInfo->finished) { - throw new \RuntimeException('경매가 이미 끝났습니다.'); + return '경매가 이미 끝났습니다.'; } $now = new \DateTimeImmutable(); if ($auctionInfo->closeDate < $now) { - throw new \RuntimeException('경매가 이미 끝났습니다.'); + return '경매가 이미 끝났습니다.'; } - if ($auctionInfo->closeDate > $now) { - throw new \RuntimeException('경매가 아직 시작되지 않았습니다.'); + if ($auctionInfo->openDate > $now) { + return '경매가 아직 시작되지 않았습니다.'; } if ($auctionInfo->buyImmediatelyAmount !== null && $auctionInfo->buyImmediatelyAmount < $amount) { - throw new \RuntimeException('즉시판매가보다 높을 수 없습니다.'); + return '즉시판매가보다 높을 수 없습니다.'; } if ($auctionInfo->reqResource === ResourceType::inheritancePoint) { - $this->bidInheritPoint($amount, $now); - return; + return $this->bidInheritPoint($amount, $now, $tryExtendCloseDate); } //reqResource는 말 그대로 '구매자가 내야하는 자원'이다. @@ -263,7 +307,7 @@ class Auction $highestBid = $this->getHighestBid(); if ($highestBid !== null && $amount <= $highestBid->amount) { - throw new \RuntimeException('현재입찰가보다 높게 입찰해야 합니다.'); + return '현재입찰가보다 높게 입찰해야 합니다.'; } $myPrevBid = $this->getMyPrevBid(); @@ -276,7 +320,7 @@ class Auction }; if ($general->getVar($resType->value) < $morePoint + $minReqRes) { - throw new \RuntimeException($resType->getName() . '이 부족합니다.'); + return $resType->getName() . '이 부족합니다.'; } //여기서부터 입찰 성공 @@ -291,9 +335,16 @@ class Auction new AuctionBidItemData( $general->getVar('owner_name'), $general->getName(), + $tryExtendCloseDate, ) ); + $db->insert('ng_auction', $newBid->toArray()); + if ($db->affectedRows() == 0) { + return '입찰에 실패했습니다: DB 오류'; + } + + $general->setVar($resType->value, $general->getVar($resType->value) - $morePoint); @@ -301,5 +352,6 @@ class Auction $this->refundBid($highestBid, "{$auctionInfo->title} 경매에 상회입찰자가 나타났습니다."); } $general->applyDB($db); + return null; } } diff --git a/hwe/sammo/AuctionUniqueItem.php b/hwe/sammo/AuctionUniqueItem.php index e52969d2..6a6e87df 100644 --- a/hwe/sammo/AuctionUniqueItem.php +++ b/hwe/sammo/AuctionUniqueItem.php @@ -3,7 +3,9 @@ namespace sammo; use DateTimeImmutable; +use sammo\DTO\AuctionBidItem; use sammo\DTO\AuctionInfo; +use sammo\Enums\AuctionType; use sammo\Enums\InheritanceKey; use sammo\Enums\ResourceType; use sammo\RandUtil; @@ -17,14 +19,14 @@ class AuctionUniqueItem extends Auction $gameStor = KVStorage::getStorage($db, 'game_env'); $namePool = $gameStor->getValue('obfuscatedNamePool'); - if($namePool === null){ + if ($namePool === null) { $rng = new RandUtil(new LiteHashDRBG(Util::simpleSerialize( UniqueConst::$hiddenSeed, ))); $namePool = []; - foreach(GameConst::$randGenFirstName as $ch0){ - foreach(GameConst::$randGenMiddleName as $ch1){ - foreach(GameConst::$randGenLastName as $ch2){ + foreach (GameConst::$randGenFirstName as $ch0) { + foreach (GameConst::$randGenMiddleName as $ch1) { + foreach (GameConst::$randGenLastName as $ch2) { $namePool[] = "{$ch0}{$ch1}{$ch2}"; } } @@ -48,42 +50,55 @@ class AuctionUniqueItem extends Auction return '최소 경매 금액은 ' . GameConst::$inheritItemUniqueMinPoint . '입니다.'; } - if($general->getInheritancePoint(InheritanceKey::previous) < $startAmount){ + if ($general->getInheritancePoint(InheritanceKey::previous) < $startAmount) { return '경매를 시작할 포인트가 부족합니다.'; } - $auctionID = static::genNextAuctionID(); + if ($item->isBuyable()) { + return '구매할 수 있는 아이템입니다.'; + } + + if (GameConst::$allItems) + + $auctionID = static::genNextAuctionID(); $now = new DateTimeImmutable(); $db = DB::db(); $gameStor = KVStorage::getStorage($db, 'game_env'); $turnTerm = $gameStor->getValue('turnterm'); - $closeDate = $now->add(TimeUtil::secondsToDateInterval(24 * 60 * $turnTerm * 60)); + $closeDate = $now->add(TimeUtil::secondsToDateInterval( + max(static::MIN_AUCTION_CLOSE_MINUTES, $turnTerm * static::COEFF_AUCTION_CLOSE_MINUTES) * 60 + )); + $availableLatestBidCloseDate = $closeDate->add(TimeUtil::secondsToDateInterval( + max(static::MIN_EXTENSION_MINUTES_LIMIT_BY_BID, $turnTerm * static::COEFF_EXTENSION_MINUTES_LIMIT_BY_BID) * 60 + )); $info = new AuctionInfo( $auctionID, static::genObfuscatedName($general->getID()), $general->getID(), + AuctionType::UniqueItem, $item->getRawClassName(), "{$item->getName()} 경매", false, ResourceType::inheritancePoint, $startAmount, null, + 1, $now, - $closeDate + $closeDate, + $availableLatestBidCloseDate, ); $result = static::openAuction($info, $general); - if($result !== null){ + if ($result !== null) { return $result; } $auction = new static($auctionID, $general); - try{ + try { $auction->bid($startAmount); - } - catch(\Exception $e){ + } catch (\Exception $e) { //실패해선 안된다. $msg = $e->getMessage(); $auction->closeAuction(); @@ -92,4 +107,162 @@ class AuctionUniqueItem extends Auction return null; } + + private function giveUniqueItem(AuctionBidItem $highestBid, General $bidder): ?string + { + + $itemKey = $this->info->target; + $itemObj = buildItemClass($itemKey); + $general = $bidder; + $availableItemTypes = []; + $reasons = []; + $db = DB::db(); + $gameStor = KVStorage::getStorage($db, 'game_env'); + [$startYear, $year] = $gameStor->getValuesAsArray(['startyear', 'year']); + $relYear = $year - $startYear; + $availableEquipUniqueCnt = 1; + foreach (GameConst::$maxUniqueItemLimit as $tmpVals) { + [$targetYear, $targetTrialCnt] = $tmpVals; + if ($relYear < $targetYear) { + break; + } + $availableEquipUniqueCnt = $targetTrialCnt; + } + + $availableEquipUniqueCnt = Util::valueFit($availableEquipUniqueCnt, null, count(GameConst::$allItems)); + + foreach ($general->getItems() as $item) { + if (!$item->isBuyable()) { + $availableEquipUniqueCnt -= 1; + } + } + + if($availableEquipUniqueCnt <= 0){ + return '유니크 아이템 소유 제한 상태입니다. 종료 시간이 연장됩니다.'; + } + + foreach (GameConst::$allItems as $itemType => $itemList) { + //아직은 그런 경우는 없지만 동일 유니크를 여러 부위에 장착할 수 있을지도 모름 + if (!key_exists($itemKey, $itemList)) { + continue; + } + + $ownItem = $general->getItem($itemType); + if ($ownItem->getRawClassName() == $itemKey) { + //FIXME: 이 경우에는 환불이 되던가 해야함. + $reasons[] = '이미 그 유니크를 가지고 있습니다.'; + continue; + } + + if (!$ownItem->isBuyable()) { + $reasons[] = '이미 다른 유니크를 가지고 있습니다.'; + continue; + } + + $availableCnt = $itemList[$itemKey]; + $occupiedCnt = $db->queryFirstField('SELECT count(*) FROM general WHERE %b = %s', $itemType, $itemKey); + if ($occupiedCnt >= $availableCnt) { + //FIXME: 이 경우에는 환불이 되던가 해야함. + $reasons[] = '그 유니크는 모두 점유되었습니다.'; + continue; + } + $availableItemTypes[] = $itemType; + } + + if (!$availableItemTypes) { + return join(' ', $reasons); + } + + $itemType = $availableItemTypes[0]; + + $general->setVar($itemType, $itemKey); + + $logger = $general->getLogger(); + $nationName = $general->getStaticNation()['name']; + $generalName = $general->getName(); + $josaYi = JosaUtil::pick($generalName, '이'); + $itemName = $itemObj->getName(); + $itemRawName = $itemObj->getRawName(); + $josaUl = JosaUtil::pick($itemRawName, '을'); + + $logger->pushGeneralActionLog("{$itemName}{$josaUl} 습득했습니다!"); + $logger->pushGeneralHistoryLog("{$itemName}{$josaUl} 습득"); + $logger->pushGlobalActionLog("{$generalName}{$josaYi} {$itemName}{$josaUl} 습득했습니다!"); + $logger->pushGlobalHistoryLog("【보물발견】{$nationName}{$generalName}{$josaYi} {$itemName}{$josaUl} 습득했습니다!"); + + $general->applyDB($db); + return null; + } + + public function checkCloseDate(): ?bool + { + $now = new DateTimeImmutable(); + if ($now < $this->info->closeDate) { + return null; + } + + //경매를 닫아야한다. + $highestBid = $this->getHighestBid(); + if ($highestBid === null) { + $this->closeAuction(); + return true; + } + + if ($highestBid->data->tryExtendCloseDate) { + //연장 요청이 있었다. + $extendedCloseDate = $this->info->closeDate->add(TimeUtil::secondsToDateInterval( + max(static::MIN_EXTENSION_MINUTES_BY_EXTENSION_QUERY, $this->info->turnTerm * static::COEFF_EXTENSION_MINUTES_BY_EXTENSION_QUERY) * 60 + )); + + if ($this->extendCloseDate($extendedCloseDate) === null) { + return false; + } + } + + $bidder = General::createGeneralObjFromDB($highestBid->generalID); + + $failReason = $this->giveUniqueItem($highestBid, $bidder); + if ($failReason === null) { + $this->closeAuction(); + return true; + } + + $staticNation = $bidder->getStaticNation(); + $src = new MessageTarget(0, '', 0, 'System', '#000000'); + $dest = new MessageTarget( + $bidder->getID(), + $bidder->getName(), + $bidder->getNationID(), + $staticNation['name'], + $staticNation['color'], + GetImageURL($bidder->getVar('imgsvr'), $bidder->getVar('picture')) + ); + + //TODO: 전역 알림이 나타나야한다. 일반 메시지보다는 중요하고, 메시지보단 약하게.. + //TODO: 바로가기를 제공하는 편이 좋을 것 같다. + $msg = new Message( + Message::MSGTYPE_PRIVATE, + $src, + $dest, + $failReason, + new \DateTime(), + new \DateTime('9999-12-31'), + [] + ); + $msg->send(true); + + //아이템 제한에 걸렸다면 자동 연장 + $extendedCloseDate = $this->info->closeDate->add(TimeUtil::secondsToDateInterval( + max(static::MIN_EXTENSION_MINUTES_BY_EXTENSION_QUERY, $this->info->turnTerm * static::COEFF_EXTENSION_MINUTES_BY_EXTENSION_QUERY) * 60 + )); + + $this->extendCloseDate($extendedCloseDate, true); + return false; + } + + public function bid(int $amount, bool $tryExtendCloseDate = false): ?string + { + //TODO: 입찰 가능 제한확인(다른 경매 진행 등) + return parent::bid($amount, $tryExtendCloseDate); + } } diff --git a/hwe/sammo/DTO/AuctionBidItemData.php b/hwe/sammo/DTO/AuctionBidItemData.php index 41478689..ddb23d4d 100644 --- a/hwe/sammo/DTO/AuctionBidItemData.php +++ b/hwe/sammo/DTO/AuctionBidItemData.php @@ -10,6 +10,8 @@ class AuctionBidItemData extends DTO #[NullIsUndefined] public ?string $ownerName, public string $generalName, + #[NullIsUndefined] + public ?bool $tryExtendCloseDate, ) { } } diff --git a/hwe/sammo/DTO/AuctionInfo.php b/hwe/sammo/DTO/AuctionInfo.php index c34dd50f..10087714 100644 --- a/hwe/sammo/DTO/AuctionInfo.php +++ b/hwe/sammo/DTO/AuctionInfo.php @@ -4,6 +4,7 @@ namespace sammo\DTO; use sammo\DTO\Attr\Convert; use sammo\DTO\Converter\DateTimeConverter; +use sammo\Enums\AuctionType; use sammo\Enums\ResourceType; class AuctionInfo extends DTO @@ -12,17 +13,21 @@ class AuctionInfo extends DTO public int $id, public string $openerName, public int $openerGeneralID, - public string $type, + public AuctionType $type, + public ?string $target, public string $title, public bool $finished, public ResourceType $reqResource, public int $minAmount, public ?int $buyImmediatelyAmount, + public ?int $remainCloseDateExtensionCnt, #[Convert(DateTimeConverter::class)] public \DateTimeImmutable $openDate, #[Convert(DateTimeConverter::class)] public \DateTimeImmutable $closeDate, + #[Convert(DateTimeConverter::class)] + public ?\DateTimeImmutable $availableLatestBidCloseDate, ) { } } diff --git a/hwe/sammo/Enums/AuctionType.php b/hwe/sammo/Enums/AuctionType.php new file mode 100644 index 00000000..ee40883e --- /dev/null +++ b/hwe/sammo/Enums/AuctionType.php @@ -0,0 +1,8 @@ +