feat,wip: 금,쌀 거래장 관련 로직 조정
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace sammo\API\Auction;
|
||||
|
||||
use sammo\Session;
|
||||
use DateTimeInterface;
|
||||
use sammo\AuctionUniqueItem;
|
||||
use sammo\Validator;
|
||||
use sammo\GameConst;
|
||||
use sammo\General;
|
||||
|
||||
use function sammo\buildItemClass;
|
||||
|
||||
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', '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,
|
||||
];
|
||||
}
|
||||
}
|
||||
+121
-22
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
<?php
|
||||
|
||||
namespace sammo;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use sammo\DTO\AuctionBidItem;
|
||||
use sammo\DTO\AuctionInfo;
|
||||
use sammo\DTO\AuctionInfoDetail;
|
||||
use sammo\Enums\AuctionType;
|
||||
use sammo\Enums\ResourceType;
|
||||
|
||||
abstract class AuctionBasicResource extends Auction
|
||||
{
|
||||
const MIN_AUCTION_AMOUNT = 100;
|
||||
const MAX_AUCTION_AMOUNT = 10000;
|
||||
static AuctionType $auctionType;
|
||||
static ResourceType $hostRes;
|
||||
static ResourceType $bidderRes;
|
||||
|
||||
static public function openBuyRiceAuction(General $general, int $amount, int $closeTurnCnt, int $startBidAmount, int $finishBidAmount): self|string
|
||||
{
|
||||
if ($closeTurnCnt < 1 || $closeTurnCnt > 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}번 거래 <C>성사</>로 {$bidderResName} <C>{$bidAmount}</>{$josaUlBidder} 지불, {$hostResName} <C>{$auctionAmount}</>{$josaUlHost} 획득!",
|
||||
ActionLogger::EVENT_PLAIN
|
||||
);
|
||||
$bidder->getLogger()->pushGeneralActionLog(
|
||||
"{$auctionID}번 거래 <C>성사</>로 {$hostResName} <C>{$auctionAmount}</>{$josaUlHost} 판매, {$bidderResName} <C>{$bidAmount}</>{$josaUlBidder} 획득!",
|
||||
ActionLogger::EVENT_PLAIN
|
||||
);
|
||||
|
||||
$josaYiHost = JosaUtil::pick($auctionHost->getName(), '이');
|
||||
$josaYiBidder = JosaUtil::pick($bidder->getName(), '이');
|
||||
$josaRo = JosaUtil::pick($bidAmount, '로');
|
||||
|
||||
$auctionLog = [];
|
||||
$auctionLog[] = "{$auctionID}번 {$hostResName} 경매 <C>성사</> : <Y>{$auctionHost->getName()}</>{$josaYiHost} {$hostResName} <C>{$auctionAmount}</> 판매, <Y>{$bidder->getName()}</>{$josaYiBidder} <C>{$bidAmount}</> 구매";
|
||||
|
||||
|
||||
if ($highestBid->amount === $this->info->detail->finishBidAmount) {
|
||||
$auctionLog[0] .= ' <M>★ 즉시구매가 거래 ★</>';
|
||||
} else if ($highestBid->amount === $this->info->detail->startBidAmount) {
|
||||
$auctionLog[0] .= " <R>★ 최고가 거래 ★</>";
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace sammo;
|
||||
|
||||
use sammo\Enums\AuctionType;
|
||||
use sammo\Enums\ResourceType;
|
||||
|
||||
/** 경매에 쌀을 매물로 등록, 입찰자가 금으로 구매 */
|
||||
class AuctionBuyRice extends AuctionBasicResource
|
||||
{
|
||||
static AuctionType $auctionType = AuctionType::SellRice;
|
||||
static ResourceType $hostRes = ResourceType::rice;
|
||||
static ResourceType $bidderRes = ResourceType::gold;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace sammo;
|
||||
|
||||
use sammo\DTO\AuctionBidItem;
|
||||
use sammo\DTO\AuctionInfo;
|
||||
use sammo\DTO\AuctionInfoDetail;
|
||||
use sammo\Enums\AuctionType;
|
||||
use sammo\Enums\ResourceType;
|
||||
|
||||
/** 경매에 금을 매물로 등록, 입찰자가 쌀로 판매 */
|
||||
class AuctionSellRice extends AuctionBasicResource
|
||||
{
|
||||
static AuctionType $auctionType = AuctionType::SellRice;
|
||||
static ResourceType $hostRes = ResourceType::gold;
|
||||
static ResourceType $bidderRes = ResourceType::rice;
|
||||
}
|
||||
+113
-161
@@ -14,7 +14,6 @@ use sammo\RandUtil;
|
||||
|
||||
class AuctionUniqueItem extends Auction
|
||||
{
|
||||
|
||||
static public function genObfuscatedName(int $id): string
|
||||
{
|
||||
$db = DB::db();
|
||||
@@ -63,7 +62,7 @@ class AuctionUniqueItem extends Auction
|
||||
$itemKey = $item->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("<C>{$itemName}</>{$josaUl} 습득했습니다!");
|
||||
$logger->pushGeneralHistoryLog("<C>{$itemName}</>{$josaUl} 습득");
|
||||
$logger->pushGlobalActionLog("<Y>{$generalName}</>{$josaYi} <C>{$itemName}</>{$josaUl} 습득했습니다!");
|
||||
$logger->pushGlobalHistoryLog("<C><b>【보물발견】</b></><D><b>{$nationName}</b></>의 <Y>{$generalName}</>{$josaYi} <C>{$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("<C>{$itemName}</>{$josaUl} 습득했습니다!");
|
||||
$logger->pushGeneralHistoryLog("<C>{$itemName}</>{$josaUl} 습득");
|
||||
$logger->pushGlobalActionLog("<Y>{$generalName}</>{$josaYi} <C>{$itemName}</>{$josaUl} 습득했습니다!");
|
||||
$logger->pushGlobalHistoryLog("<C><b>【보물발견】</b></><D><b>{$nationName}</b></>의 <Y>{$generalName}</>{$josaYi} <C>{$itemName}</>{$josaUl} 습득했습니다!");
|
||||
|
||||
$general->applyDB($db);
|
||||
|
||||
$givenUnique = $gameStor->getValue('givenUnique') ?? [];
|
||||
$givenUnique[$itemKey] = ($givenUnique[$itemKey] ?? 0) + 1;
|
||||
$gameStor->setValue('givenUnique', $givenUnique);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
<?php
|
||||
namespace sammo\Enums;
|
||||
|
||||
/**
|
||||
* 입찰자 기준
|
||||
*/
|
||||
enum AuctionType: string{
|
||||
case Gold = 'gold';
|
||||
case Rice = 'rice';
|
||||
/** 쌀을 매물로 등록, 금으로 구매 */
|
||||
case BuyRice = 'buyRice';
|
||||
/** 금을 매물로 등록, 쌀로 판매 */
|
||||
case SellRice = 'sellRice';
|
||||
/** 유미크를 매물로 등록, 유산 포인트로 구매 */
|
||||
case UniqueItem = 'uniqueItem';
|
||||
}
|
||||
+1
-1
@@ -693,7 +693,7 @@ CREATE TABLE `vote_comment` (
|
||||
# 경매 객체는 KVStorage에 저장
|
||||
CREATE TABLE `ng_auction` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`type` ENUM('gold','rice','uniqueItem') NOT NULL COLLATE 'utf8mb4_bin',
|
||||
`type` ENUM('buyRice','sellRice','uniqueItem') NOT NULL COLLATE 'utf8mb4_bin',
|
||||
`finished` BIT(1) NOT NULL,
|
||||
`target` VARCHAR(50) NULL DEFAULT NULL COLLATE 'utf8mb4_bin',
|
||||
`opener_general_id` INT(11) NOT NULL,
|
||||
|
||||
Reference in New Issue
Block a user