From f607350442ceb9bc59f665974b95c70b1ca410a0 Mon Sep 17 00:00:00 2001 From: Hide_D Date: Mon, 6 Jun 2022 22:33:31 +0900 Subject: [PATCH] =?UTF-8?q?feat,wip:=20=EA=B8=88,=EC=8C=80=20=EA=B1=B0?= =?UTF-8?q?=EB=9E=98=EC=9E=A5=20=EA=B4=80=EB=A0=A8=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hwe/sammo/API/Auction/OpenBuyRiceAuction.php | 69 +++++ hwe/sammo/Auction.php | 143 ++++++++-- hwe/sammo/AuctionBasicResource.php | 223 +++++++++++++++ hwe/sammo/AuctionBuyRice.php | 14 + hwe/sammo/AuctionSellRice.php | 17 ++ hwe/sammo/AuctionUniqueItem.php | 274 ++++++++----------- hwe/sammo/DTO/AuctionInfoDetail.php | 10 +- hwe/sammo/Enums/AuctionType.php | 10 +- hwe/sql/schema.sql | 2 +- 9 files changed, 572 insertions(+), 190 deletions(-) create mode 100644 hwe/sammo/API/Auction/OpenBuyRiceAuction.php create mode 100644 hwe/sammo/AuctionBasicResource.php create mode 100644 hwe/sammo/AuctionBuyRice.php create mode 100644 hwe/sammo/AuctionSellRice.php diff --git a/hwe/sammo/API/Auction/OpenBuyRiceAuction.php b/hwe/sammo/API/Auction/OpenBuyRiceAuction.php new file mode 100644 index 00000000..ef3928ec --- /dev/null +++ b/hwe/sammo/API/Auction/OpenBuyRiceAuction.php @@ -0,0 +1,69 @@ + $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', 'itemID', $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) + { + $itemID = $this->args['itemID']; + $amount = $this->args['amount']; + $generalID = $session->generalID; + + $itemObj = buildItemClass($itemID); + $general = General::createGeneralObjFromDB($generalID); + + $auctionResult = AuctionUniqueItem::openItemAuction($itemObj, $general, $amount); + + if(is_string($auctionResult)) { + return $auctionResult; + } + + return [ + 'result' => true, + 'auctionID' => $auctionResult->id, + ]; + } +} diff --git a/hwe/sammo/Auction.php b/hwe/sammo/Auction.php index 1d609c80..e4b923f3 100644 --- a/hwe/sammo/Auction.php +++ b/hwe/sammo/Auction.php @@ -12,7 +12,7 @@ use sammo\Enums\InheritanceKey; use sammo\Enums\RankColumn; use sammo\Enums\ResourceType; -class Auction +abstract class Auction { protected AuctionInfo $info; @@ -26,22 +26,15 @@ class Auction public const MIN_EXTENSION_MINUTES_LIMIT_BY_BID = 5; public const MIN_EXTENSION_MINUTES_BY_EXTENSION_QUERY = 5; - static public function openAuction(AuctionInfo $info, General $general): int|string + protected AuctionBidItem|null|false $_highestBid = false; + + static protected function openAuction(AuctionInfo $info, General $general): int|string { $db = DB::db(); if ($info->id !== null) { return 'id가 지정되어 있습니다.'; } - $prevAuctionID = $db->queryFirstField( - 'SELECT id FROM ng_auction WHERE opener_general_id = %i AND finished = 0 AND `type` = %s', - $general->getID(), - $info->type - ); - if ($prevAuctionID !== null) { - return '아직 경매가 끝나지 않았습니다.'; - } - $db->insert('ng_auction', $info->toArray()); return $db->insertId(); } @@ -49,15 +42,31 @@ class Auction public function getHighestBid(): ?AuctionBidItem { $db = DB::db(); - $rawHighestBid = $db->queryFirstRow( - 'SELECT * FROM ng_auction_bid WHERE auction_id = %i ORDER BY `amount` DESC LIMIT 1', - $this->info->id - ); + + if($this->_highestBid !== false){ + return $this->_highestBid; + } + + if (!$this->info->detail->isReverse) { + $rawHighestBid = $db->queryFirstRow( + 'SELECT * FROM ng_auction_bid WHERE auction_id = %i ORDER BY `amount` DESC LIMIT 1', + $this->info->id + ); + } else { + $rawHighestBid = $db->queryFirstRow( + 'SELECT * FROM ng_auction_bid WHERE auction_id = %i ORDER BY `amount` ASC LIMIT 1', + $this->info->id + ); + } + if (!$rawHighestBid) { + $this->_highestBid = null; return null; } - return AuctionBidItem::fromArray($rawHighestBid); + $highestBid = AuctionBidItem::fromArray($rawHighestBid); + $this->_highestBid = $highestBid; + return $highestBid; } public function getMyPrevBid(): ?AuctionBidItem @@ -89,6 +98,18 @@ class Auction return $this->info; } + public function shrinkCloseDate(?DateTimeInterface $date): ?string{ + if($date === null){ + $date = new DateTimeImmutable(); + } + + $this->info->closeDate = $date; + $db = DB::db(); + $db->update('ng_auction', $this->info->toArrayExcept('id'), 'id = %i', $this->info->id); + + return null; + } + public function extendCloseDate(DateTimeInterface $date, bool $force = false): ?string { if (!$force) { @@ -171,6 +192,7 @@ class Auction if ($highestBid !== null) { $this->refundBid($highestBid, "{$this->info->title} 경매가 취소되었습니다."); } + $this->rollbackAuction(); } $db->update('ng_auction', $this->info->toArrayExcept('id'), 'id = %i', $this->info->id); @@ -248,7 +270,7 @@ class Auction return null; } - public function bid(int $amount, bool $tryExtendCloseDate = false): ?string + protected function _bid(int $amount, bool $tryExtendCloseDate = false): ?string { $auctionInfo = $this->info; $general = $this->general; @@ -266,10 +288,17 @@ class Auction return '경매가 아직 시작되지 않았습니다.'; } - if ($auctionInfo->buyImmediatelyAmount !== null && $auctionInfo->buyImmediatelyAmount < $amount) { - return '즉시판매가보다 높을 수 없습니다.'; + if (!$auctionInfo->detail->isReverse) { + if ($auctionInfo->buyImmediatelyAmount !== null && $auctionInfo->buyImmediatelyAmount < $amount) { + return '즉시판매가보다 높을 수 없습니다.'; + } + } else { + if ($auctionInfo->buyImmediatelyAmount !== null && $auctionInfo->buyImmediatelyAmount > $amount) { + return '즉시판매가보다 낮을 수 없습니다.'; + } } + if ($auctionInfo->reqResource === ResourceType::inheritancePoint) { return $this->bidInheritPoint($amount, $now, $tryExtendCloseDate); } @@ -279,10 +308,17 @@ class Auction $db = DB::db(); $highestBid = $this->getHighestBid(); - if ($highestBid !== null && $amount <= $highestBid->amount) { - return '현재입찰가보다 높게 입찰해야 합니다.'; + if (!$auctionInfo->detail->isReverse) { + if ($highestBid !== null && $amount <= $highestBid->amount) { + return '현재입찰가보다 높게 입찰해야 합니다.'; + } + } else { + if ($highestBid !== null && $amount >= $highestBid->amount) { + return '현재입찰가보다 낮게 입찰해야 합니다.'; + } } + $myPrevBid = $this->getMyPrevBid(); $morePoint = $amount - ($myPrevBid ? $myPrevBid->amount : 0); @@ -317,7 +353,7 @@ class Auction return '입찰에 실패했습니다: DB 오류'; } - $general->setVar($resType->value, $general->getVar($resType->value) - $morePoint); + $general->increaseVar($resType->value, -$morePoint); if ($myPrevBid === null || ($highestBid !== null && $myPrevBid->id !== $highestBid->id)) { $this->refundBid($highestBid, "{$auctionInfo->title} 경매에 상회입찰자가 나타났습니다."); @@ -325,4 +361,67 @@ class Auction $general->applyDB($db); return null; } + + public function tryFinish(): ?bool + { + $now = new DateTimeImmutable(); + if ($now < $this->info->closeDate) { + return null; + } + + //경매를 닫아야한다. + $highestBid = $this->getHighestBid(); + if ($highestBid === null) { + $this->closeAuction(true); + 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->finishAuction($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); + return false; + } + + abstract public function bid(int $amount, bool $tryExtendCloseDate = false): ?string; + + abstract protected function rollbackAuction(): void; + abstract protected function finishAuction(AuctionBidItem $highestBid, General $bidder): ?string; } diff --git a/hwe/sammo/AuctionBasicResource.php b/hwe/sammo/AuctionBasicResource.php new file mode 100644 index 00000000..9dd1d7c9 --- /dev/null +++ b/hwe/sammo/AuctionBasicResource.php @@ -0,0 +1,223 @@ + 24) { + return '종료기한은 1 ~ 24 턴 이어야 합니다.'; + } + if ($amount < self::MIN_AUCTION_AMOUNT || $amount > self::MAX_AUCTION_AMOUNT) { + return '거래량은 ' . self::MIN_AUCTION_AMOUNT . ' ~ ' . self::MAX_AUCTION_AMOUNT . ' 이어야 합니다.'; + } + if ($startBidAmount < $amount * 0.5 || $amount * 2 < $startBidAmount) { + return '시작거래가는 50% ~ 200% 이어야 합니다.'; + } + if ($finishBidAmount < $amount * 1.1 || $amount * 2 < $finishBidAmount) { + return '즉시거래가는 110% ~ 200% 이어야 합니다.'; + } + if ($finishBidAmount < $startBidAmount * 1.1) { + return '즉시거래가는 시작판매가의 110% 이상이어야 합니다.'; + } + + $hostRes = static::$hostRes; + $hostResName = $hostRes->getName(); + $bidderRes = static::$bidderRes; + $minimumRes = static::$hostRes === ResourceType::rice ? GameConst::$generalMinimumRice : GameConst::$generalMinimumGold; + if ($general->getVar($hostRes->value) < $amount + $minimumRes) { + return "기본 {$hostRes->getName()} {$minimumRes}은 거래할 수 없습니다."; + } + + $db = DB::db(); + $prevAuctionID = $db->queryFirstField( + 'SELECT id FROM ng_auction WHERE opener_general_id = %i AND finished = 0 AND `type` IN %ls', + $general->getID(), + [AuctionType::BuyRice->value, AuctionType::SellRice->value], + ); + if ($prevAuctionID !== null) { + return '아직 경매가 끝나지 않았습니다.'; + } + + $now = new \DateTimeImmutable(); + $gameStor = KVStorage::getStorage($db, 'game_env'); + $turnTerm = $gameStor->getValue('turnterm'); + $closeDate = $now->add(TimeUtil::secondsToDateInterval($closeTurnCnt * $turnTerm * 60)); + + $openResult = static::openAuction(new AuctionInfo( + null, + static::$auctionType, + false, + "$amount", + $general->getId(), + $bidderRes, + $now, + $closeDate, + new AuctionInfoDetail( + "{$hostResName} {$amount} 경매", + $general->getName(), + $amount, + false, + $startBidAmount, + $finishBidAmount, + null, + null + ) + ), $general); + + if (is_string($openResult)) { + return $openResult; + } + + $general->increaseVarWithLimit($hostRes->value, -$amount, 0); + $general->applyDB($db); + + return new self($openResult, $general); + } + + protected function rollbackAuction(): void + { + if ($this->general->getID() === $this->info->openerGeneralID) { + $auctionHost = $this->general; + } else { + $auctionHost = General::createGeneralObjFromDB($this->info->openerGeneralID); + } + + $hostRes = static::$hostRes; + $hostResName = $hostRes->getName(); + + $auctionHost->increaseVar($hostRes->value, $this->info->detail->amount); + $auctionHost->applyDB(DB::db()); + + $staticNation = $auctionHost->getStaticNation(); + $src = new MessageTarget(0, '', 0, 'System', '#000000'); + $dest = new MessageTarget( + $auctionHost->getID(), + $auctionHost->getName(), + $auctionHost->getNationID(), + $staticNation['name'], + $staticNation['color'], + GetImageURL($auctionHost->getVar('imgsvr'), $auctionHost->getVar('picture')) + ); + + //TODO: 전역 알림이 나타나야한다. 일반 메시지보다는 중요하고, 메시지보단 약하게.. + //TODO: 바로가기를 제공하는 편이 좋을 것 같다. + $msg = new Message( + Message::MSGTYPE_PRIVATE, + $src, + $dest, + "{$this->auctionID}번 {$hostResName} 경매에 입찰이 없어 취소되었습니다.", + new \DateTime(), + new \DateTime('9999-12-31'), + [] + ); + $msg->send(true); + } + + protected function finishAuction(AuctionBidItem $highestBid, General $bidder): ?string + { + if ($this->general->getID() === $this->info->openerGeneralID) { + $auctionHost = $this->general; + } else { + $auctionHost = General::createGeneralObjFromDB($this->info->openerGeneralID); + } + + $highestBid = $this->getHighestBid(); + if ($highestBid === null) { + throw new \Exception('입찰자가 없습니다.'); + } + + if ($this->general->getID() === $highestBid->generalID) { + $bidder = $this->general; + } else { + $bidder = General::createGeneralObjFromDB($highestBid->generalID); + } + + $hostRes = static::$hostRes; + $hostResName = $hostRes->getName(); + $bidderRes = static::$bidderRes; + $bidderResName = $bidderRes->getName(); + + $bidAmount = $highestBid->amount; + $auctionAmount = $this->info->detail->amount; + + //거래 종료이므로 서로 반대 + $josaUlBidder = JosaUtil::pick($bidAmount, '을'); + $josaUlHost = JosaUtil::pick($auctionAmount, '을'); + $auctionHost->increaseVar($bidderRes->value, $bidAmount); + $bidder->increaseVar($hostRes->value, $auctionAmount); + + $auctionID = $this->info->id; + + $auctionHost->getLogger()->pushGeneralActionLog( + "{$auctionID}번 거래 성사로 {$bidderResName} {$bidAmount}{$josaUlBidder} 지불, {$hostResName} {$auctionAmount}{$josaUlHost} 획득!", + ActionLogger::EVENT_PLAIN + ); + $bidder->getLogger()->pushGeneralActionLog( + "{$auctionID}번 거래 성사로 {$hostResName} {$auctionAmount}{$josaUlHost} 판매, {$bidderResName} {$bidAmount}{$josaUlBidder} 획득!", + ActionLogger::EVENT_PLAIN + ); + + $josaYiHost = JosaUtil::pick($auctionHost->getName(), '이'); + $josaYiBidder = JosaUtil::pick($bidder->getName(), '이'); + $josaRo = JosaUtil::pick($bidAmount, '로'); + + $auctionLog = []; + $auctionLog[] = "{$auctionID}번 {$hostResName} 경매 성사 : {$auctionHost->getName()}{$josaYiHost} {$hostResName} {$auctionAmount} 판매, {$bidder->getName()}{$josaYiBidder} {$bidAmount} 구매"; + + + if ($highestBid->amount === $this->info->detail->finishBidAmount) { + $auctionLog[0] .= ' ★ 즉시구매가 거래 ★'; + } else if ($highestBid->amount === $this->info->detail->startBidAmount) { + $auctionLog[0] .= " ★ 최고가 거래 ★"; + } + + pushAuctionLog(array_map( + fn ($log) => + $auctionHost->getLogger()->formatText($log, ActionLogger::EVENT_PLAIN), + $auctionLog + )); + + $db = DB::db(); + $bidder->applyDB($db); + $auctionHost->applyDB($db); + + return null; + } + + public function bid(int $amount, bool $_tryExtendCloseDate = true): ?string + { + if ($this->info->openerGeneralID === $this->general->getID()) { + return '자신이 연 경매에 입찰할 수 없습니다.'; + } + $result = $this->_bid($amount, true); + + if(is_string($result)){ + return $result; + } + + if($amount === $this->info->detail->finishBidAmount){ + //즉구, 1턴 후 지급 + $db = DB::db(); + $gameStor = KVStorage::getStorage($db, 'game_env'); + $turnTerm = $gameStor->getValue('turnterm'); + $date = (new DateTimeImmutable())->add(TimeUtil::secondsToDateInterval($turnTerm * 60)); + $this->shrinkCloseDate($date); + } + + } +} diff --git a/hwe/sammo/AuctionBuyRice.php b/hwe/sammo/AuctionBuyRice.php new file mode 100644 index 00000000..71bf451f --- /dev/null +++ b/hwe/sammo/AuctionBuyRice.php @@ -0,0 +1,14 @@ +getRawClassName(); $db = DB::db(); $auctionIDonProgress = $db->queryFirstField( - 'SELECT `id` FROM ng_auction WHERE `finished` = 0 AND `target` = %s AND `target` = %s', + 'SELECT `id` FROM ng_auction WHERE `finished` = 0 AND `type` = %s AND `target` = %s', AuctionType::UniqueItem->value, $itemKey ); @@ -71,6 +70,15 @@ class AuctionUniqueItem extends Auction return '이미 경매가 진행중입니다.'; } + $prevAuctionID = $db->queryFirstField( + 'SELECT id FROM ng_auction WHERE opener_general_id = %i AND finished = 0 AND `type` = %s', + $general->getID(), + AuctionType::UniqueItem->value, + ); + if ($prevAuctionID !== null) { + return '아직 경매가 끝나지 않았습니다.'; + } + $gameStor = KVStorage::getStorage($db, 'game_env'); $givenUnique = $gameStor->getValue('givenUnique') ?? []; $givenUniqueCnt = $givenUnique[$itemKey] ?? 0; @@ -108,7 +116,8 @@ class AuctionUniqueItem extends Auction new AuctionInfoDetail( "{$item->getName()} 경매", static::genObfuscatedName($general->getID()), - + 1, + false, $startAmount, null, 1, @@ -133,164 +142,9 @@ class AuctionUniqueItem extends Auction return $auction; } - private function giveUniqueItem(AuctionBidItem $highestBid, General $bidder): ?string + protected function rollbackAuction(): void { - - $itemKey = $this->info->target; - if ($itemKey === null) { - throw new \Exception('아이템 키가 없습니다.'); - } - $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); - - $givenUnique = $gameStor->getValue('givenUnique') ?? []; - $givenUnique[$itemKey] = ($givenUnique[$itemKey] ?? 0) + 1; - $gameStor->setValue('givenUnique', $givenUnique); - - 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 @@ -349,6 +203,104 @@ class AuctionUniqueItem extends Auction } } - return parent::bid($amount, $tryExtendCloseDate); + return $this->_bid($amount, $tryExtendCloseDate); + } + + protected function finishAuction(AuctionBidItem $highestBid, General $bidder): ?string + { + $itemKey = $this->info->target; + if ($itemKey === null) { + throw new \Exception('아이템 키가 없습니다.'); + } + $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) { + //제한에 걸렸다면 자동 연장 + $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 '유니크 아이템 소유 제한 상태입니다. 종료 시간이 연장됩니다.'; + } + + 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); + + $givenUnique = $gameStor->getValue('givenUnique') ?? []; + $givenUnique[$itemKey] = ($givenUnique[$itemKey] ?? 0) + 1; + $gameStor->setValue('givenUnique', $givenUnique); + return null; } } diff --git a/hwe/sammo/DTO/AuctionInfoDetail.php b/hwe/sammo/DTO/AuctionInfoDetail.php index b73dfc11..d394fd76 100644 --- a/hwe/sammo/DTO/AuctionInfoDetail.php +++ b/hwe/sammo/DTO/AuctionInfoDetail.php @@ -11,11 +11,13 @@ class AuctionInfoDetail extends DTO public function __construct( public string $title, public string $openerName, - - public int $minAmount, - + public int $amount, #[NullIsUndefined] - public ?int $buyImmediatelyAmount, + public ?bool $isReverse, + + public int $startBidAmount, + #[NullIsUndefined] + public ?int $finishBidAmount, #[NullIsUndefined] public ?int $remainCloseDateExtensionCnt, #[NullIsUndefined] diff --git a/hwe/sammo/Enums/AuctionType.php b/hwe/sammo/Enums/AuctionType.php index ee40883e..e0a7e39d 100644 --- a/hwe/sammo/Enums/AuctionType.php +++ b/hwe/sammo/Enums/AuctionType.php @@ -1,8 +1,14 @@