feat,refac: 경매장 재설계, 유니크 경매장 구현 (#221)

- 경매장을 모든 입찰 기록이 남는 새로운 경매장 로직으로 변경
  - ng_auction, ng_auction_bid
    - DTO 사용
  - 상회입찰 시 개인메시지로 알림
- 기존의 '배경에서 조용히 이루어지는' 유니크 입찰을 공개된 유니크 경매장으로 변경
  - 종료 기간 명시
    - 종료 기간에 가까워질때 입찰하면 자동 연장
    - 최대 연장기간 있음
  - 유니크 제한인 경우 24턴 연장
  - 중원정세에서 '보물수배'로 알림

Reviewed-on: https://storage.hided.net/gitea/devsam/core/pulls/221
This commit was merged in pull request #221.
This commit is contained in:
2022-06-09 01:21:55 +09:00
parent 7964aa5f6b
commit dd38344319
64 changed files with 3053 additions and 1530 deletions
-2
View File
@@ -45,7 +45,6 @@ return [
'hwe/api.php', 'hwe/api.php',
'hwe/a_traffic.php', 'hwe/a_traffic.php',
'hwe/battle_simulator.php', 'hwe/battle_simulator.php',
'hwe/b_auction.php',
'hwe/b_battleCenter.php', 'hwe/b_battleCenter.php',
'hwe/b_betting.php', 'hwe/b_betting.php',
'hwe/v_chiefCenter.php', 'hwe/v_chiefCenter.php',
@@ -60,7 +59,6 @@ return [
'hwe/v_processing.php', 'hwe/v_processing.php',
'hwe/b_tournament.php', 'hwe/b_tournament.php',
'hwe/b_troop.php', 'hwe/b_troop.php',
'hwe/c_auction.php',
'hwe/c_tournament.php', 'hwe/c_tournament.php',
'hwe/func_auction.php', 'hwe/func_auction.php',
'hwe/func_command.php', 'hwe/func_command.php',
+6 -6
View File
@@ -50,9 +50,9 @@ switch ($btn) {
$db->update('general', [ $db->update('general', [
'turntime' => $db->sqleval('DATE_SUB(turntime, INTERVAL %i MINUTE)', $minute) 'turntime' => $db->sqleval('DATE_SUB(turntime, INTERVAL %i MINUTE)', $minute)
], true); ], true);
$db->update('auction', [ $db->update('ng_auction', [
'expire' => $db->sqleval('DATE_SUB(expire, INTERVAL %i MINUTE)', $minute) 'close_date' => $db->sqleval('DATE_SUB(close_date, INTERVAL %i MINUTE)', $minute)
], true); ], 'finished = 0');
if ($locked) { if ($locked) {
unlock(); unlock();
} }
@@ -78,9 +78,9 @@ switch ($btn) {
$db->update('general', [ $db->update('general', [
'turntime' => $db->sqleval('DATE_ADD(turntime, INTERVAL %i MINUTE)', $minute) 'turntime' => $db->sqleval('DATE_ADD(turntime, INTERVAL %i MINUTE)', $minute)
], true); ], true);
$db->update('auction', [ $db->update('ng_auction', [
'expire' => $db->sqleval('DATE_ADD(expire, INTERVAL %i MINUTE)', $minute) 'close_date' => $db->sqleval('DATE_ADD(close_date, INTERVAL %i MINUTE)', $minute)
], true); ], 'finished = 0');
if ($locked) { if ($locked) {
unlock(); unlock();
} }
+14 -43
View File
@@ -48,54 +48,25 @@ $db = DB::db();
<font color=red>접속제한</font><br><b style=background-color:red;>블럭회원</b> <font color=red>접속제한</font><br><b style=background-color:red;>블럭회원</b>
</td> </td>
<td width=105 rowspan=12> <td width=105 rowspan=12>
<?php <select name=genlist[] size=20 multiple style='color:white;background-color:black;font-size:14px'>
<?php
$generalList = $db->query('SELECT `no`, `name`, npc, `block` FROM general ORDER BY npc, binary(`name`)');
echo " foreach ($generalList as $general) {
<select name=genlist[] size=20 multiple style='color:white;background-color:black;font-size:14px'>"; $style = "style=;";
$generalList = $db->query('SELECT `no`, `name`, npc, `block` FROM general ORDER BY npc, binary(`name`)'); if ($general['block'] > 0) {
$style .= "background-color:red;";
foreach ($generalList as $general) {
$style = "style=;";
if ($general['block'] > 0) {
$style .= "background-color:red;";
}
$npcColor = getNPCColor($general['npc']);
if($npcColor !== null){
$style .= "color:{$npcColor};";
}
echo "
<option value={$general['no']} $style>{$general['name']}</option>";
}
echo "
</select>
</td>
<td width=100 align=center>아이템 지급</td>
<td width=504>
<select name=weapon size=1 style='color:white;background-color:black;font-size:14px'>";
foreach (GameConst::$allItems as $itemCategories) {
foreach ($itemCategories as $item => $cnt) {
if ($cnt == 0) {
continue;
} }
$itemObj = buildItemClass($item);
if ($itemObj->isBuyable()) { $npcColor = getNPCColor($general['npc']);
continue; if ($npcColor !== null) {
$style .= "color:{$npcColor};";
} }
echo "<option value={$general['no']} $style>{$general['name']}</option>";
} }
} ?>
for ($i = 0; $i < 27; $i++) {
echo "
<option value={$i}>{$i}</option>";
}
?>
</select> </select>
<input type=submit name=btn value='무기지급'>
<input type=submit name=btn value='책지급'>
<input type=submit name=btn value='말지급'>
<input type=submit name=btn value='도구지급'>
</td> </td>
</tr> </tr>
<tr> <tr>
-89
View File
@@ -200,95 +200,6 @@ switch ($btn) {
$msg->send(true); $msg->send(true);
} }
break; break;
case "무기지급":
if ($item == 'None') {
$text = "무기 회수!";
} else {
$text = getItemName($item) . " 지급!";
}
foreach ($genlist as $generalID) {
$msg = new Message(Message::MSGTYPE_PRIVATE, $src, MessageTarget::buildQuick($generalID), $text, new \DateTime(), new \DateTime('9999-12-31'), []);
$msg->send(true);
}
if ($item === 'None') {
$db->update('general', [
'weapon' => 'None'
], '`no` IN %li', $genlist);
} else {
$db->update('general', [
'weapon' => $item
], '`no` IN %li', $genlist, $item);
}
break;
case "책지급":
if ($item == 'None') {
$text = "책 회수!";
} else {
$text = getItemName($item) . " 지급!";
}
foreach ($genlist as $generalID) {
$msg = new Message(Message::MSGTYPE_PRIVATE, $src, MessageTarget::buildQuick($generalID), $text, new \DateTime(), new \DateTime('9999-12-31'), []);
$msg->send(true);
}
if ($item == 'None') {
$db->update('general', [
'book' => 'None'
], '`no` IN %li', $genlist);
} else {
$db->update('general', [
'book' => $item
], '`no` IN %li', $genlist);
}
break;
case "말지급":
if ($item == 'None') {
$text = "말 회수!";
} else {
$text = getItemName($item) . " 지급!";
}
foreach ($genlist as $generalID) {
$msg = new Message(Message::MSGTYPE_PRIVATE, $src, MessageTarget::buildQuick($generalID), $text, new \DateTime(), new \DateTime('9999-12-31'), []);
$msg->send(true);
}
if ($item == 'None') {
$db->update('general', [
'horse' => 'None'
], '`no` IN %li', $genlist);
} else {
$db->update('general', [
'horse' => $item
], '`no` IN %li', $genlist);
}
break;
case "도구지급":
if ($item == 'None') {
$text = "특수도구 회수!";
} else {
$text = getItemName($item) . " 지급!";
}
foreach ($genlist as $generalID) {
$msg = new Message(Message::MSGTYPE_PRIVATE, $src, MessageTarget::buildQuick($generalID), $text, new \DateTime(), new \DateTime('9999-12-31'), []);
$msg->send(true);
}
if ($item == 'None') {
$db->update('general', [
'item' => 'None'
], '`no` IN %li', $genlist);
} else {
$db->update('general', [
'item' => $item
], '`no` IN %li AND item < %i', $genlist, $item);
}
break;
case "하야입력": case "하야입력":
$db->update('general_turn', [ $db->update('general_turn', [
'action' => 'che_하야', 'action' => 'che_하야',
-306
View File
@@ -1,306 +0,0 @@
<?php
namespace sammo;
include "lib.php";
include "func.php";
$msg = Util::getReq('msg');
$msg2 = Util::getReq('msg2');
//로그인 검사
$session = Session::requireGameLogin()->setReadOnly();
$userID = Session::getUserID();
$db = DB::db();
$gameStor = KVStorage::getStorage($db, 'game_env');
increaseRefresh("거래장", 2);
$me = $db->queryFirstRow('SELECT no,special,con,turntime from general where owner=%i', $userID);
$con = checkLimit($me['con']);
if ($con >= 2) {
printLimitMsg($me['turntime']);
exit();
}
$tradeCount = $db->queryFirstField('SELECT count(no) FROM auction WHERE no1=%i', $me['no']);
$bidCount = $db->queryFirstField('SELECT count(no) FROM auction where no2=%i', $me['no']);
$btCount = $tradeCount + $bidCount;
if ($session->userGrade >= 5 || $btCount < 1) {
$btn = "submit";
} else {
$btn = "hidden";
}
if ($msg == "") {
$msg = "-";
}
if ($msg2 == "") {
$msg2 = "-";
}
?>
<!DOCTYPE html>
<html>
<head>
<title><?= UniqueConst::$serverName ?>: 거래장</title>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=1024" />
<?= WebUtil::printJS('../d_shared/common_path.js') ?>
<?= WebUtil::printCSS('../d_shared/common.css') ?>
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css" />
<?= WebUtil::printDist('ts', ['common']) ?>
</head>
<body>
<table align=center width=1000 class='tb_layout bg0'>
<tr>
<td>거 래 장<br><?= closeButton() ?></td>
</tr>
<tr>
<td align=center class='bg2'>
<font color=orange size=6><b>거 래 장</b></font><input type=button value='갱신' onclick="location.replace('b_auction.php')">
</td>
</tr>
</table>
<table align=center width=1000 class='tb_layout bg0'>
<form method=post action=c_auction.php>
<tr>
<td colspan=11 align=center bgcolor=orange>
<font size=5>팝 니 다</font>
</td>
</tr>
<tr align=center class='bg1'>
<td width=68>거래번호</td>
<td width=48>선택</td>
<td width=98>판매자</td>
<td width=118>물품</td>
<td width=88>수량</td>
<td width=88>시작판매가</td>
<td width=88>현재판매가</td>
<td width=88>즉시판매가</td>
<td width=48>단가</td>
<td width=98>구매 예정자</td>
<td width=148>거래종료</td>
</tr>
<?php
$chk = 0;
foreach ($db->query('SELECT * from auction where type=0 order by expire') as $auction) {
$radio = "";
$alert = "";
$alert2 = "";
if ($auction['no1'] == $me['no']) {
$radio = " disabled";
} elseif ($auction['no2'] > 0 && $auction['amount'] * 2 <= $auction['value']) {
$radio = " disabled";
$alert = "<font color=red>";
$alert2 = "</font>";
} elseif ($auction['no2'] > 0 && $auction['topv'] <= $auction['value']) {
$radio = " disabled";
$alert = "<font color=red>";
$alert2 = "</font>";
} elseif ($chk == 0) {
$radio = " checked";
$chk = 1;
}
$pv = round($auction['value'] * 100 / $auction['amount']) / 100 + 0.001;
$pv = substr((string)$pv, 0, 4);
echo "
<tr align=center>
<td>{$auction['no']}</td>
<td><input type=radio name=sel value={$auction['no']}{$radio}></td>
<td>{$auction['name1']}</td>
<td>쌀</td>
<td>{$auction['amount']}</td>
<td>금 {$auction['cost']}</td>
<td>{$alert}금 {$auction['value']}{$alert2}</td>
<td>{$alert}금 {$auction['topv']}{$alert2}</td>
<td>{$alert}{$pv}{$alert2}</td>
<td>{$alert}{$auction['name2']}{$alert2}</td>
<td>{$auction['expire']}</td>
</tr>
";
}
?>
<tr height=25>
<td align=center class='bg1'>등록결과</td>
<td colspan=10><?= ConvertLog($msg) ?></td>
</tr>
<tr>
<td align=center class='bg1'>입찰등록</td>
<td colspan=10>
 지불할 금액: <input type=text style=color:white;background-color:black; size=6 maxlength=6 name=value>
<input type=<?= $btn ?> name=btn value='구매시도' onclick='return confirm("정말 입찰하시겠습니까?");'>
</td>
</tr>
<tr>
<td align=center class='bg1'>거래등록</td>
<td colspan=10>
 종료: <input type=text style=color:white;background-color:black; size=2 maxlength=2 name=term value=12>턴 후
 물품: 쌀
 판매량: <input type=text style=color:white;background-color:black; size=5 maxlength=5 name=amount value=1000>
 시작가: <input type=text style=color:white;background-color:black; size=5 maxlength=5 name=cost value=500>
 즉구가: <input type=text style=color:white;background-color:black; size=5 maxlength=5 name=topv value=2000>
<input type=<?= $btn ?> name=btn value='판매' onclick='return confirm("정말 판매하시겠습니까?");'>
</td>
</tr>
<tr>
<td colspan=11>
ㆍ<font color=cyan>Hint</font>) 거래자가 판매(물품 판매, 금 수령), 입찰자가 구매(물품 구입, 금 지불).<br>
ㆍ<font color=cyan>Hint</font>) 단가가 1.00보다 높을수록 판매자 유리.<br>
ㆍ<font color=cyan>Hint</font>) 단가가 1.00보다 낮을수록 입찰자 유리.<br>
</td>
</tr>
</form>
</table>
<br>
<table align=center width=1000 class='tb_layout bg0'>
<form method=post action=c_auction.php>
<tr>
<td colspan=11 align=center bgcolor=skyblue>
<font size=5>삽 니 다</font>
</td>
</tr>
<tr align=center class='bg1'>
<td width=68>거래번호</td>
<td width=48>선택</td>
<td width=98>구매자</td>
<td width=118>물품</td>
<td width=88>수량</td>
<td width=88>시작구매가</td>
<td width=88>현재구매가</td>
<td width=88>즉시구매가</td>
<td width=48>단가</td>
<td width=98>판매 예정자</td>
<td width=148>거래종료</td>
</tr>
<?php
$chk = 0;
foreach ($db->query('SELECT * from auction where type=1 order by expire') as $auction) {
$radio = "";
$alert = "";
$alert2 = "";
if ($auction['no1'] == $me['no']) {
$radio = " disabled";
} elseif ($auction['no2'] > 0 && $auction['amount'] >= $auction['value'] * 2) {
$radio = " disabled";
$alert = "<font color=red>";
$alert2 = "</font>";
} elseif ($auction['no2'] > 0 && $auction['topv'] >= $auction['value']) {
$radio = " disabled";
$alert = "<font color=red>";
$alert2 = "</font>";
} elseif ($chk == 0) {
$radio = " checked";
$chk = 1;
}
$pv = round($auction['value'] * 100 / $auction['amount']) / 100 + 0.001;
$pv = substr((string)$pv, 0, 4);
echo "
<tr align=center>
<td>{$auction['no']}</td>
<td><input type=radio name=sel value={$auction['no']}{$radio}></td>
<td>{$auction['name1']}</td>
<td>쌀</td>
<td>{$auction['amount']}</td>
<td>금 {$auction['cost']}</td>
<td>{$alert}금 {$auction['value']}{$alert2}</td>
<td>{$alert}금 {$auction['topv']}{$alert2}</td>
<td>{$alert}{$pv}{$alert2}</td>
<td>{$alert}{$auction['name2']}{$alert2}</td>
<td>{$auction['expire']}</td>
</tr>
";
}
?>
<tr height=25>
<td align=center class='bg1'>등록결과</td>
<td colspan=10><?= ConvertLog($msg2) ?></td>
</tr>
<tr>
<td align=center class='bg1'>입찰등록</td>
<td colspan=10>
 수령할 금액: <input type=text style=color:white;background-color:black; size=6 maxlength=6 name=value>
<input type=<?= $btn ?> name=btn value='판매시도' onclick='return confirm("정말 입찰하시겠습니까?");'>
</td>
</tr>
<tr>
<td align=center class='bg1'>거래등록</td>
<td colspan=10>
 종료: <input type=text style=color:white;background-color:black; size=2 maxlength=2 name=term value=12>턴 후
 물품: 쌀
 구입량: <input type=text style=color:white;background-color:black; size=5 maxlength=5 name=amount value=1000>
 시작가: <input type=text style=color:white;background-color:black; size=5 maxlength=5 name=cost value=2000>
 즉구가: <input type=text style=color:white;background-color:black; size=5 maxlength=5 name=topv value=500>
<input type=<?= $btn ?> name=btn value='구매' onclick='return confirm("정말 구매하시겠습니까?");'>
</td>
</tr>
<tr>
<td colspan=11>
ㆍ<font color=cyan>Hint</font>) 거래자가 구매(물품 구매, 금 지불), 입찰자가 판매(물품 판매, 금 수령).<br>
ㆍ<font color=cyan>Hint</font>) 단가가 1.00보다 낮을수록 구매자 유리.<br>
ㆍ<font color=cyan>Hint</font>) 단가가 1.00보다 높을수록 입찰자 유리.<br>
</td>
</tr>
</form>
</table>
<br>
<table align=center width=1000 class='tb_layout bg0'>
<tr>
<td align=center class='bg2'>
<font size=5>최 근 기 록</font>
</td>
</tr>
<tr>
<td>
<?= getAuctionLogRecent(20) ?>
</td>
</tr>
<tr>
<td align=center class='bg2'>
<font size=5>도 움 말</font>
</td>
</tr>
<tr>
<td>
<font color=white size=2>
ㆍ판매거래는 거래자가 판매할 물품을 거래하면, 구입을 희망하는 사람이 현재가보다 높게 입찰하여 구입하는 방식입니다.<br>
ㆍ<font color=cyan>Hint</font>) 쌀이 귀한 경우는 입찰자가 많아서 자연스레 단가가 오르게 됩니다. (해당 물품을 사려는 가격이 오름)<br>
ㆍ<font color=cyan>Hint</font>) 쌀이 흔한 경우는 초기 가격을 낮게 책정해야 판매가 가능할 겁니다.<br>
ㆍ구매거래는 거래자가 구입할 물품을 거래하면, 판매를 희망하는 사람이 현재가보다 낮게 입찰하여 판매하는 방식입니다.<br>
ㆍ<font color=cyan>Hint</font>) 쌀이 흔한 경우는 입찰자가 많아서 자연스레 단가가 내리게 됩니다. (해당 물품을 팔려는 가격이 내림)<br>
ㆍ<font color=cyan>Hint</font>) 쌀이 귀한 경우는 초기 가격을 높게 책정해야 구입이 가능할 겁니다.<br>
ㆍ마감임박때 입찰하는 경우 입찰후 1턴 후로 종료시간이 연장됩니다.<br>
ㆍ즉시구매가로 입찰하는 경우 입찰후 1턴 후로 종료시간이 결정됩니다.<br>
ㆍ악용 방지를 위해 50% ~ 200%의 가격에서 거래시작이 가능합니다.<br>
ㆍ악용 방지를 위해 즉시판매가는 110% 이상, 즉시구매가는 90% 이하의 시세로 가능합니다.<br>
ㆍ악용 방지를 위해 즉시판매가는 시작판매가의 110% 이상, 즉시구매가는 시작구매가의 90% 이하로 가능합니다.<br>
ㆍ1인당 도합 1건의 거래와 입찰이 가능합니다.<br>
ㆍ기본금쌀 1000은 거래에 사용되지 못합니다.<br>
ㆍ유찰될 때는 거래 과실자에게 거래금의 1%가 벌금으로 부과됩니다.<br>
ㆍ<font color=magenta>10단위로 거래가 가능합니다. 1자리는 반올림 처리 됩니다.</font><br>
ㆍ<font color=red>★ 최고가 거래 ★</font> 혹은 <font color=red>★ 최저가 거래 ★</font> 는 암거래 및 악용의 가능성이니 감시 부탁드립니다.<br>
ㆍ거래와 입찰은 취소가 불가능하니 주의하세요!<br>
ㆍ<font color=cyan>Hint</font>) 단가는 금/쌀로 쌀1을 거래하기 위한 금의 양입니다.<br>
ㆍ<font color=cyan>Hint</font>) 단가가 높으면(>1.00) 쌀이 비싸므로 판매가 이득입니다.<br>
ㆍ<font color=cyan>Hint</font>) 단가가 낮으면(<1.00) 금이 비싸므로 구매가 이득입니다.<br>
ㆍ즐거운 거래!
</font>
</td>
</tr>
<tr>
<td><?= closeButton() ?></td>
</tr>
<tr>
<td><?= banner() ?> </td>
</tr>
</table>
</body>
</html>
-247
View File
@@ -1,247 +0,0 @@
<?php
namespace sammo;
include "lib.php";
include "func.php";
$v = new Validator($_POST + $_GET);
$v->rule('integer', [
'amount',
'cost',
'topv',
'value',
'term',
'sel'
]);
$btn = Util::getPost('btn');
$amount = Util::getPost('amount', 'int');
$cost = Util::getPost('cost', 'int');
$topv = Util::getPost('topv', 'int');
$value = Util::getPost('value', 'int');
$term = Util::getPost('term', 'int');
$sel = Util::getPost('sel', 'int');
$msg = '';
$msg2 = '';
//로그인 검사
$session = Session::requireGameLogin()->setReadOnly();
$userID = Session::getUserID();
$db = DB::db();
$gameStor = KVStorage::getStorage($db, 'game_env');
increaseRefresh("입찰", 1);
$turnterm = $gameStor->turnterm;
$me = $db->queryFirstRow('SELECT no,name,gold,rice,special from general where owner=%i', $userID);
$tradeCount = $db->queryFirstField('SELECT count(no) FROM auction WHERE no1=%i', $me['no']);
$bidCount = $db->queryFirstField('SELECT count(no) FROM auction WHERE no2=%i', $me['no']);
$btCount = $tradeCount + $bidCount;
$unit = $turnterm * 60;
$amount = Util::round($amount, -1);
$cost = Util::round($cost, -1);
$topv = Util::round($topv, -1);
$value = Util::round($value, -1);
if ($term > 24) {
$term = 24;
}
$valid = 1;
if ($session->userGrade >= 5 || $btCount < 1) {
} else {
$msg = "ㆍ<span class='ev_warning'>더이상 등록할 수 없습니다.</span>";
$msg2 = "ㆍ<span class='ev_warning'>더이상 등록할 수 없습니다.</span>";
$valid = 0;
$btn = "hidden";
}
if ($btn == "판매") {
if ($term < 0 || $term > 24) {
$msg = "ㆍ<span class='ev_warning'>종료기한은 1 ~ 24 턴 이어야 합니다.</span>";
$valid = 0;
}
if ($amount < 100 || $amount > 10000) {
$msg = "ㆍ<span class='ev_warning'>거래량은 100 ~ 10000 이어야 합니다.</span>";
$valid = 0;
}
if ($cost > $amount * 2 || $cost * 2 < $amount) {
$msg = "ㆍ<span class='ev_warning'>시작판매가는 50% ~ 200% 이어야 합니다.</span>";
$valid = 0;
}
if ($topv * 10 < $amount * 11 || $topv > $amount * 2) {
$msg = "ㆍ<span class='ev_warning'>즉시판매가는 110% ~ 200% 이어야 합니다.</span>";
$valid = 0;
}
if ($topv * 10 < $cost * 11) {
$msg = "ㆍ<span class='ev_warning'>즉시판매가는 시작판매가의 110% 이상이어야 합니다.</span>";
$valid = 0;
}
if ($amount > $me['rice'] - GameConst::$defaultRice) {
$msg = "ㆍ<span class='ev_warning'>기본 군량 ".GameConst::$defaultRice."은 거래할 수 없습니다.</span>";
$valid = 0;
}
if ($valid == 1) {
$msg = "ㆍ<span class='ev_success'>등록 성공.</span>";
$date = date("Y-m-d H:i:s", strtotime(date("Y-m-d H:i:s")) + $unit * $term);
$db->insert('auction', [
'type'=>0,
'no1'=>$me['no'],
'name1'=>$me['name'],
'amount'=>$amount,
'cost'=>$cost,
'value'=>$cost,
'topv'=>$topv,
'expire'=>$date
]);
}
} elseif ($btn == "구매시도") {
$auction = $db->queryFirstRow('SELECT no2,value,topv,expire,amount FROM auction WHERE no=%i LIMIT 1', $sel);
if ($value == $auction['topv']) {
$valid = 2;
}
if (!$auction) {
$msg = "ㆍ<span class='ev_warning'>종료된 거래입니다.</span>";
$valid = 0;
}
if ($auction['no2'] > 0 && $value <= $auction['value']) {
$msg = "ㆍ<span class='ev_warning'>현재판매가보다 높게 입찰해야 합니다.</span>";
$valid = 0;
}
if ($value < $auction['value']) {
$msg = "ㆍ<span class='ev_warning'>현재판매가보다 높게 입찰해야 합니다.</span>";
$valid = 0;
}
if ($value > $auction['topv']) {
$msg = "ㆍ<span class='ev_warning'>즉시판매가보다 높을 수 없습니다.</span>";
$valid = 0;
}
if ($value > $me['gold'] - GameConst::$defaultGold) {
$msg = "ㆍ<span class='ev_warning'>기본 자금 ".GameConst::$defaultGold."은 거래할 수 없습니다.</span>";
$valid = 0;
}
if ($valid == 1) {
$msg = "ㆍ<span class='ev_success'>입찰 성공.</span> 거래완료는 빨라도 현재로부터 1턴 뒤입니다.";
$date = date("Y-m-d H:i:s", strtotime(date("Y-m-d H:i:s")) + $unit);
if ($auction['expire'] > $date) {
$date = $auction['expire'];
}
$db->update('auction', [
'value'=>$value,
'no2'=>$me['no'],
'name2'=>$me['name'],
'expire'=>$date,
], 'no=%i', $sel);
} elseif ($valid == 2) {
$msg = "ㆍ<span class='ev_success'>즉시판매 성공.</span> 거래완료는 빨라도 현재로부터 1턴 뒤입니다.";
$date = date("Y-m-d H:i:s", strtotime(date("Y-m-d H:i:s")) + $unit);
$db->update('auction', [
'value'=>$value,
'no2'=>$me['no'],
'name2'=>$me['name'],
'expire'=>$date,
], 'no=%i', $sel);
}
} elseif ($btn == "구매") {
if ($term < 0 || $term > 24) {
$msg2 = "ㆍ<span class='ev_warning'>종료기한은 1 ~ 24 턴 이어야 합니다.</span>";
$valid = 0;
}
if ($amount < 100 || $amount > 10000) {
$msg2 = "ㆍ<span class='ev_warning'>거래량은 100 ~ 10000 이어야 합니다.</span>";
$valid = 0;
}
if ($cost > $amount * 2 || $cost * 2 < $amount) {
$msg2 = "ㆍ<span class='ev_warning'>시작구매가는 50% ~ 200% 이어야 합니다.</span>";
$valid = 0;
}
if ($topv < $amount * 0.5 || $topv > $amount * 0.9) {
$msg2 = "ㆍ<span class='ev_warning'>즉시구매가는 50% ~ 90% 이어야 합니다.</span>";
$valid = 0;
}
if ($topv > $cost * 0.9) {
$msg2 = "ㆍ<span class='ev_warning'>즉시구매가는 시작구매가의 90% 이하이어야 합니다.</span>";
$valid = 0;
}
if ($cost > $me['gold'] - GameConst::$defaultGold) {
$msg2 = "ㆍ<span class='ev_warning'>기본 자금 ".GameConst::$defaultGold."은 거래할 수 없습니다.</span>";
$valid = 0;
}
if ($valid == 1) {
$msg2 = "ㆍ<span class='ev_success'>등록 성공.</span>";
$date = date("Y-m-d H:i:s", strtotime(date("Y-m-d H:i:s")) + $unit * $term);
$db->insert('auction', [
'type'=>1,
'no1'=>$me['no'],
'name1'=>$me['name'],
'amount'=>$amount,
'cost'=>$cost,
'value'=>$cost,
'topv'=>$topv,
'expire'=>$date
]);
}
} elseif ($btn == "판매시도") {
$auction = $db->queryFirstRow('SELECT no2,value,topv,expire,amount FROM auction WHERE no=%i LIMIT 1', $sel);
if ($value == $auction['topv']) {
$valid = 2;
}
if (!$auction) {
$msg2 = "ㆍ<span class='ev_warning'>종료된 거래입니다.</span>";
$valid = 0;
}
if ($auction['no2'] > 0 && $value >= $auction['value']) {
$msg2 = "ㆍ<span class='ev_warning'>현재구매가보다 낮게 입찰해야 합니다.</span>";
$valid = 0;
}
if ($value > $auction['value']) {
$msg2 = "ㆍ<span class='ev_warning'>현재구매가보다 낮게 입찰해야 합니다.</span>";
$valid = 0;
}
if ($value < $auction['topv']) {
$msg2 = "ㆍ<span class='ev_warning'>즉시구매가보다 낮을 수 없습니다.</span>";
$valid = 0;
}
if ($value > $me['rice'] - GameConst::$defaultRice) {
$msg2 = "ㆍ<span class='ev_warning'>기본 군량 ".GameConst::$defaultRice."은 거래할 수 없습니다.</span>";
$valid = 0;
}
if ($valid == 1) {
$msg2 = "ㆍ<span class='ev_success'>입찰 성공.</span> 거래완료는 빨라도 현재로부터 1턴 뒤입니다.";
$date = date("Y-m-d H:i:s", strtotime(date("Y-m-d H:i:s")) + $unit);
if ($auction['expire'] > $date) {
$date = $auction['expire'];
}
$db->update('auction', [
'value'=>$value,
'no2'=>$me['no'],
'name2'=>$me['name'],
'expire'=>$date,
], 'no=%i', $sel);
} elseif ($valid == 2) {
$msg2 = "ㆍ<span class='ev_success'>즉시구매 성공.</span> 거래완료는 빨라도 현재로부터 1턴 뒤입니다.";
$date = date("Y-m-d H:i:s", strtotime(date("Y-m-d H:i:s")) + $unit);
$db->update('auction', [
'value'=>$value,
'no2'=>$me['no'],
'name2'=>$me['name'],
'expire'=>$date,
], 'no=%i', $sel);
}
}
Submit("b_auction.php", $msg, $msg2);
+18 -219
View File
@@ -3,6 +3,8 @@
namespace sammo; namespace sammo;
use DateTime; use DateTime;
use Ds\Set;
use sammo\Enums\AuctionType;
use sammo\Enums\InheritanceKey; use sammo\Enums\InheritanceKey;
use sammo\Enums\RankColumn; use sammo\Enums\RankColumn;
use sammo\Event\Action; use sammo\Event\Action;
@@ -1204,9 +1206,9 @@ function checkDelay()
$db->update('general', [ $db->update('general', [
'turntime' => $db->sqleval('DATE_ADD(turntime, INTERVAL %i MINUTE)', $minute) 'turntime' => $db->sqleval('DATE_ADD(turntime, INTERVAL %i MINUTE)', $minute)
], 'turntime<=DATE_ADD(turntime, INTERVAL %i MINUTE)', $term); ], 'turntime<=DATE_ADD(turntime, INTERVAL %i MINUTE)', $term);
$db->update('auction', [ $db->update('ng_auction', [
'expire' => $db->sqleval('DATE_ADD(expire, INTERVAL %i MINUTE)', $minute) 'close_date' => $db->sqleval('DATE_ADD(close_date, INTERVAL %i MINUTE)', $minute)
], true); ], 'finished = 0');
} }
} }
@@ -1557,6 +1559,7 @@ function CheckHall($no)
function giveRandomUniqueItem(RandUtil $rng, General $general, string $acquireType): bool function giveRandomUniqueItem(RandUtil $rng, General $general, string $acquireType): bool
{ {
$db = DB::db(); $db = DB::db();
$gameStor = KVStorage::getStorage($db, 'game_env');
//아이템 습득 상황 //아이템 습득 상황
$availableUnique = []; $availableUnique = [];
@@ -1588,6 +1591,18 @@ function giveRandomUniqueItem(RandUtil $rng, General $general, string $acquireTy
} }
} }
$auctionItems = $db->queryFirstColumn(
'SELECT `target` FROM `ng_auction` WHERE `type` = %s AND `finished` = 0',
AuctionType::UniqueItem->value
);
foreach ($auctionItems as $itemCode) {
if (key_exists($itemCode, $occupiedUnique)) {
$occupiedUnique[$itemCode]++;
} else {
$occupiedUnique[$itemCode] = 1;
}
}
foreach ($db->queryAllLists('SELECT namespace, count(*) as cnt FROM `storage` WHERE namespace LIKE "ut_%" GROUP BY namespace') as [$uniqueNS, $cnt]) { foreach ($db->queryAllLists('SELECT namespace, count(*) as cnt FROM `storage` WHERE namespace LIKE "ut_%" GROUP BY namespace') as [$uniqueNS, $cnt]) {
$itemCode = substr($uniqueNS, 3); $itemCode = substr($uniqueNS, 3);
$itemClass = buildItemClass($itemCode); $itemClass = buildItemClass($itemCode);
@@ -1631,10 +1646,7 @@ function giveRandomUniqueItem(RandUtil $rng, General $general, string $acquireTy
return false; return false;
} }
if ($general->getAuxVar('inheritRandomUnique')) { if ($general->getAuxVar('inheritRandomUnique')) {
$gameStor = KVStorage::getStorage($db, 'game_env');
[$year, $month, $initYear, $initMonth] = $gameStor->getValuesAsArray(['year', 'month', 'init_year', 'init_month']); [$year, $month, $initYear, $initMonth] = $gameStor->getValuesAsArray(['year', 'month', 'init_year', 'init_month']);
$relMonthByInit = Util::joinYearMonth($year, $month) - Util::joinYearMonth($initYear, $initMonth); $relMonthByInit = Util::joinYearMonth($year, $month) - Util::joinYearMonth($initYear, $initMonth);
@@ -1668,210 +1680,6 @@ function giveRandomUniqueItem(RandUtil $rng, General $general, string $acquireTy
return true; return true;
} }
function rollbackInheritUniqueTrial(General $general, string $itemKey, string $reason)
{
$ownerID = $general->getVar('owner');
$db = DB::db();
$itemTrials = $general->getAuxVar('inheritUniqueTrial');
LogText("선택유니크 롤백:{$ownerID}", [$itemKey, $itemTrials]);
unset($itemTrials[$itemKey]);
if (count($itemTrials) == 0) {
$itemTrials = null;
}
$general->setAuxVar('inheritUniqueTrial', $itemTrials);
$trialStor = KVStorage::getStorage($db, "ut_{$itemKey}");
$ownTrial = $trialStor->getValue("u{$ownerID}");
$itemObj = buildItemClass($itemKey);
$itemName = $itemObj->getName();
if ($ownTrial) {
//두 값이 general, KVStorage 둘다 있고, 이중에선 KVStorage 값을 기준으로 하자 따르자
[,, $amount] = $ownTrial;
$trialStor->deleteValue("u{$ownerID}");
$general->increaseInheritancePoint(InheritanceKey::previous, $amount);
$general->increaseRankVar(RankColumn::inherit_point_spent_dynamic, -$amount);
LogText("선택유니크 롤백포인트:{$ownerID}", $amount);
$userLogger = new UserLogger($ownerID);
$userLogger->push("{$itemName} 입찰에 사용한 {$amount} 포인트 반환", "inheritPoint");
}
//메시지
$staticNation = $general->getStaticNation();
$unlimited = new \DateTime('9999-12-31');
$src = new MessageTarget(0, '', 0, 'System', '#000000');
$dest = new MessageTarget($general->getID(), $general->getName(), $general->getNationID(), $staticNation['name'], $staticNation['color'], GetImageURL($general->getVar('imgsvr'), $general->getVar('picture')));
$josaUl = JosaUtil::pick($itemName, '을');
$msg = new Message(
Message::MSGTYPE_PRIVATE,
$src,
$dest,
"{$itemName}{$josaUl} 얻지 못했습니다. {$reason}",
new DateTime(),
$unlimited,
[]
);
$general->applyDB($db);
$msg->send(true);
}
function tryRollbackInheritUniqueItem(RandUtil $rng, General $general): void
{
tryInheritUniqueItem($rng, $general, 'Rollback', true);
}
function tryInheritUniqueItem(RandUtil $rng, General $general, string $acquireType = '아이템', bool $justRollback = false): bool
{
$ownerID = $general->getVar('owner');
if (!$ownerID) {
LogText("선택유니크 실패???: {$ownerID}", $general->getName());
return false;
}
$itemTrials = $general->getAuxVar('inheritUniqueTrial') ?? [];
arsort($itemTrials);
LogText("선택유니크항목: {$ownerID}", $itemTrials);
$db = DB::db();
$ownTarget = null;
$ownType = null;
foreach ($itemTrials as $itemKey => $amount) {
$availableItemTypes = [];
$reasons = [];
foreach (GameConst::$allItems as $itemType => $itemList) {
//아직은 그런 경우는 없지만 동일 유니크를 여러 부위에 장착할 수 있을지도 모름
if (!key_exists($itemKey, $itemList)) {
continue;
}
$ownItem = $general->getItem($itemType);
if ($ownItem->getRawClassName() == $itemKey) {
$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) {
$reasons[] = '그 유니크는 모두 점유되었습니다.';
continue;
}
$availableItemTypes[] = $itemType;
}
if (!$availableItemTypes) {
rollbackInheritUniqueTrial($general, $itemKey, join(' ', $reasons));
continue;
}
$reasons = [];
$itemType = $rng->choice($availableItemTypes);
$trialStor = KVStorage::getStorage($db, "ut_{$itemKey}"); //혹시 itemKey의 크기가 37이 넘을 수 있을까?
$anyTrials = $trialStor->getAll();
if (!$anyTrials) {
//순서가 꼬였던 모양, 실제 값은 storage를 우선시하자
rollbackInheritUniqueTrial($general, $itemKey, '절차상의 오류입니다.');
continue;
}
//XXX: 정렬할 필요 없지 않나?
usort($anyTrials, function ($lhsTrial, $rhsTrial) {
[,, $lhsAmount] = $lhsTrial;
[,, $rhsAmount] = $rhsTrial;
return $rhsAmount <=> $lhsAmount; //큰 값이 앞에 오도록
});
LogText("선택유니크상태 {$ownerID} {$itemKey}", $anyTrials);
//공동 1등인데 본인이 있을 수도 있다.
[,, $topAmount] = $anyTrials[0];
if ($amount < $topAmount) {
$compAmount = $topAmount / $amount;
if ($compAmount > 2.0) {
$compText = '엄청난 차이로 ';
} else if ($compAmount > 1.2) {
$compText = '큰 차이로 ';
} else if ($compAmount > 1.05) {
$compText = '';
} else {
$compText = '아슬아슬한 차이로 ';
}
rollbackInheritUniqueTrial($general, $itemKey, "{$compText}상위 입찰한 장수가 있습니다.");
continue;
}
//내가 1위다
if ($ownTarget !== null) {
//이미 다른 아이템을 얻기로 되어있다.
continue;
}
$ownTarget = $itemKey;
$ownType = $itemType;
}
unset($itemKey);
unset($itemType);
if ($ownTarget === null) {
return false;
}
if ($justRollback) {
return false;
}
LogText("선택유니크획득{$ownerID}", $ownTarget);
$trialStor = KVStorage::getStorage($db, "ut_{$ownTarget}");
$trialStor->deleteValue("u{$ownerID}");
//rollbackInheritUniqueTrial 과정 때문에 새로 받아와야함
$itemTrials = $general->getAuxVar('inheritUniqueTrial');
unset($itemTrials[$ownTarget]);
$general->setAuxVar('inheritUniqueTrial', $itemTrials);
$nationName = $general->getStaticNation()['name'];
$generalName = $general->getName();
$josaYi = JosaUtil::pick($generalName, '이');
$itemObj = buildItemClass($ownTarget);
$itemName = $itemObj->getName();
$itemRawName = $itemObj->getRawName();
$josaUl = JosaUtil::pick($itemRawName, '을');
$general->setVar($ownType, $ownTarget);
$logger = $general->getLogger();
$logger->pushGeneralActionLog("<C>{$itemName}</>{$josaUl} 습득했습니다!");
$logger->pushGeneralHistoryLog("<C>{$itemName}</>{$josaUl} 습득");
$logger->pushGlobalActionLog("<Y>{$generalName}</>{$josaYi} <C>{$itemName}</>{$josaUl} 습득했습니다!");
$logger->pushGlobalHistoryLog("<C><b>【{$acquireType}】</b></><D><b>{$nationName}</b></>의 <Y>{$generalName}</>{$josaYi} <C>{$itemName}</>{$josaUl} 습득했습니다!");
$general->applyDB($db);
//같은 종류의 유니크를 입찰했을 수 있으니 한번 더 검사한다.
tryRollbackInheritUniqueItem($rng, $general);
return true;
}
function tryUniqueItemLottery(RandUtil $rng, General $general, string $acquireType = '아이템'): bool function tryUniqueItemLottery(RandUtil $rng, General $general, string $acquireType = '아이템'): bool
{ {
$db = DB::db(); $db = DB::db();
@@ -1917,18 +1725,9 @@ function tryUniqueItemLottery(RandUtil $rng, General $general, string $acquireTy
$userLogger = new UserLogger($general->getVar('owner')); $userLogger = new UserLogger($general->getVar('owner'));
$userLogger->push(sprintf("유니크를 얻을 공간이 없어 %d 포인트 반환", GameConst::$inheritItemRandomPoint), "inheritPoint"); $userLogger->push(sprintf("유니크를 얻을 공간이 없어 %d 포인트 반환", GameConst::$inheritItemRandomPoint), "inheritPoint");
} }
tryRollbackInheritUniqueItem($rng, $general);
return false; return false;
} }
$inheritUnique = $general->getAuxVar('inheritUniqueTrial');
if ($acquireType != '설문조사' && $inheritUnique && count($inheritUnique) && $availableBuyUnique) {
$trialResult = tryInheritUniqueItem($rng, $general, $acquireType);
if ($trialResult) {
return true;
}
}
$scenario = $gameStor->scenario; $scenario = $gameStor->scenario;
$genCount = $db->queryFirstField('SELECT count(*) FROM general WHERE npc<2'); $genCount = $db->queryFirstField('SELECT count(*) FROM general WHERE npc<2');
+55 -212
View File
@@ -1,247 +1,90 @@
<?php <?php
namespace sammo; namespace sammo;
function registerAuction() { use sammo\Enums\AuctionType;
function registerAuction(RandUtil $rng)
{
$db = DB::db(); $db = DB::db();
$gameStor = KVStorage::getStorage($db, 'game_env');
$admin = $gameStor->getValues(['startyear', 'year', 'month', 'turnterm']);
$unit = 60 * $admin['turnterm'];
// 장수들 평금,평쌀 // 장수들 평금,평쌀
$general = $db->queryFirstRow('SELECT avg(gold) as gold, avg(rice) as rice,max(gold) as maxgold from general where npc<2'); [$avgGold, $avgRice] = $db->queryFirstList('SELECT avg(gold), avg(rice) from general where npc<2');
$avgGold = Util::valueFit($avgGold, 1000, 20000);
$avgRice = Util::valueFit($avgRice, 1000, 20000);
if($general['gold'] < 1000) { $general['gold'] = 1000; } $neutralAuctionCnt = Util::convertPairArrayToDict($db->queryAllLists(
if($general['gold'] > 20000) { $general['gold'] = 20000; } 'SELECT `type`, count(*) FROM ng_auction WHERE `type` IN %ls AND `host_general_id`=0 GROUP BY `type`',
if($general['rice'] < 1000) { $general['rice'] = 1000; } [AuctionType::BuyRice->value, AuctionType::SellRice->value],
if($general['rice'] > 20000) { $general['rice'] = 20000; } ));
$neutralbuyRiceCnt = $neutralAuctionCnt[AuctionType::BuyRice->value];
$count = $db->queryFirstField('SELECT count(*) FROM auction WHERE type=0 AND no1=0');
$count += 5;
// 판매건 등록 // 판매건 등록
if(Util::randBool(1/$count)) { if ($rng->nextBool(1 / ($neutralbuyRiceCnt + 5))) {
//평균 쌀의 5% ~ 25% //평균 쌀의 5% ~ 25%
$mul = rand() % 5 + 1; $mul = $rng->nextRangeInt(1, 5);
$amount = $general['rice'] / 20 * $mul; $amount = $avgRice / 20 * $mul;
$cost = $general['gold'] / 20 * 0.9 * $mul; $cost = $avgGold / 20 * 0.9 * $mul;
$topv = $amount * 2; $topv = $amount * 2;
if($cost <= $amount*0.8) { $cost = $amount*0.8; } $cost = Util::valueFit($cost, $amount * 0.8, $amount * 1.2);
if($cost >= $amount*1.2) { $cost = $amount*1.2; }
$amount = Util::round($amount, -1); $amount = Util::round($amount, -1);
$cost = Util::round($cost, -1); $cost = Util::round($cost, -1);
$topv = Util::round($topv, -1); $topv = Util::round($topv, -1);
$term = 3 + rand() % 10; $term = $rng->nextRangeInt(3, 12);
$date = date("Y-m-d H:i:s", strtotime(date("Y-m-d H:i:s")) + $unit * $term); $dummyGeneral = AuctionBasicResource::genDummy();
$db->insert('auction', [ AuctionBuyRice::openResourceAuction($dummyGeneral, $amount, $term, $cost, $topv);
'type'=>0,
'no1'=>0,
'name1'=>'ⓝ상인',
'amount'=>$amount,
'cost'=>$cost,
'value'=>$cost,
'topv'=>$topv,
'expire'=>$date
]);
} }
$count = $db->queryFirstField('SELECT count(*) FROM auction WHERE type=1 AND no1=0'); $neutralSellRiceCnt = $neutralAuctionCnt[AuctionType::SellRice->value];
$count += 5;
// 구매건 등록 // 구매건 등록
if(Util::randBool(1/$count)) { if ($rng->nextBool(1 / ($neutralSellRiceCnt + 5))) {
//평균 쌀의 5% ~ 25% //평균 쌀의 5% ~ 25%
$mul = Util::randRangeInt(1, 5); $mul = $rng->nextRangeInt(1, 5);
$amount = $general['rice'] / 20 * $mul; $amount = $avgGold / 20 * $mul;
$cost = $general['gold'] / 20 * 1.1 * $mul; $cost = $avgRice / 20 * 1.1 * $mul;
$topv = $amount * 0.5; $topv = $amount * 2;
if($cost <= $amount*0.8) { $cost = $amount*0.8; } $cost = Util::valueFit($cost, $amount * 0.8, $amount * 1.2);
if($cost >= $amount*1.2) { $cost = $amount*1.2; }
$amount = Util::round($amount, -1); $amount = Util::round($amount, -1);
$cost = Util::round($cost, -1); $cost = Util::round($cost, -1);
$topv = Util::round($topv, -1); $topv = Util::round($topv, -1);
$term = Util::randRangeInt(3, 12); $term = $rng->nextRangeInt(3, 12);
$date = date("Y-m-d H:i:s", strtotime(date("Y-m-d H:i:s")) + $unit * $term); $dummyGeneral = AuctionBasicResource::genDummy();
$db->insert('auction', [ AuctionSellRice::openResourceAuction($dummyGeneral, $amount, $term, $cost, $topv);
'type'=>1,
'no1'=>0,
'name1'=>'ⓝ상인',
'amount'=>$amount,
'cost'=>$cost,
'value'=>$cost,
'topv'=>$topv,
'expire'=>$date
]);
} }
} }
function processAuction() { function processAuction()
{
$db = DB::db(); $db = DB::db();
$gameStor = KVStorage::getStorage($db, 'game_env');
$date = TimeUtil::now(); $now = TimeUtil::now();
[$year, $month] = $gameStor->getValuesAsArray(['year', 'month']);
$admin = $gameStor->getValues(['year', 'month']); $auctionList = $db->queryAllLists(
'SELECT id, `type` FROM ng_auction WHERE `close_date` <= %s AND finished = 0',
$now
);
foreach($db->query('SELECT * from auction where expire<=%s', $date) as $auction){ if (!$auctionList) {
$josaYi1 = JosaUtil::pick($auction['name1'], '이'); return;
$josaYi2 = JosaUtil::pick($auction['name2'], '이');
// 쌀 처리
if($auction['no2'] == 0) {
// 상인건수가 아닌것만 출력
if($auction['no1'] != 0) {
$traderID = $db->queryFirstField('SELECT no FROM general WHERE no=%i', $auction['no1']);
$logger = new ActionLogger($traderID, 0, $year, $month);
$logger->pushGeneralActionLog("입찰자 부재로 {$auction['no']}번 거래 <M>유찰</>!", ActionLogger::EVENT_PLAIN);
$logger->flush();
$auctionLog = [];
if($auction['type'] == 0) {
$josaUlRice = JosaUtil::pick($auction['amount'], '을');
$auctionLog[] = "<S>◆</>{$admin['year']}{$admin['month']}월, {$auction['no']}번 <span class='sell'>판매</span> <M>유찰</> : <Y>{$auction['name1']}</>{$josaYi1} 쌀 <C>{$auction['amount']}</>{$josaUlRice} 판매, 그러나 입찰자 부재";
} else {
$josaUlRice = JosaUtil::pick($auction['amount'], '을');
$auctionLog[] = "<S>◆</>{$admin['year']}{$admin['month']}월, {$auction['no']}번 <S>구매</> <M>유찰</> : <Y>{$auction['name1']}</>{$josaYi1} 쌀 <C>{$auction['amount']}</>{$josaUlRice} 구매, 그러나 입찰자 부재";
}
pushAuctionLog($auctionLog);
}
continue;
}
if($auction['no1'] == 0) {
$trader = [
'no'=>0,
'name'=>'ⓝ상인',
'gold'=>99999,
'rice'=>99999
];
} else {
$trader = $db->queryFirstRow('SELECT no,name,gold,rice from general where no=%i', $auction['no1']);
}
$bidder = $db->queryFirstRow('SELECT no,name,gold,rice from general where no=%i', $auction['no2']);
$traderLogger = new ActionLogger($trader['no'], 0, $year, $month, false);
$bidderLogger = new ActionLogger($bidder['no'], 0, $year, $month, false);
$auctionLog = [];
//판매거래
if($auction['type'] == 0) {
if($auction['amount'] > $trader['rice'] - 1000) {
$gold = Util::round($auction['value'] * 0.01);
$db->update('general', [
'gold'=>Util::valueFit($trader['gold'] - $gold, 0)
], 'no=%i', $trader['no']);
$josaUlRice = JosaUtil::pick($auction['amount'], '을');
$josaRo = JosaUtil::pick($auction['value'], '로');
$traderLogger->pushGeneralActionLog("판매자의 군량 부족으로 {$auction['no']}번 거래 <M>유찰</>! 벌금 <C>{$gold}</>", ActionLogger::EVENT_PLAIN);
$bidderLogger->pushGeneralActionLog("판매자의 군량 부족으로 {$auction['no']}번 거래 <M>유찰</>!", ActionLogger::EVENT_PLAIN);
$auctionLog[0] = "<S>◆</>{$admin['year']}{$admin['month']}월, {$auction['no']}번 <span class='sell'>판매</span> <M>유찰</> : <Y>{$auction['name1']}</>{$josaYi1} 쌀 <C>{$auction['amount']}</>{$josaUlRice} 판매, <Y>{$auction['name2']}</>{$josaYi2} 금 <C>{$auction['value']}</>{$josaRo} 입찰, 그러나 판매자 군량부족, 벌금 <C>{$gold}</>";
} elseif($auction['value'] > $bidder['gold'] - 1000) {
$gold = Util::round($auction['value'] * 0.01);
$db->update('general', [
'gold'=>Util::valueFit($bidder['gold'] - $gold, 0)
], 'no=%i', $bidder['no']);
$josaUlRice = JosaUtil::pick($auction['amount'], '을');
$josaRo = JosaUtil::pick($auction['value'], '로');
$traderLogger->pushGeneralActionLog("입찰자의 자금 부족으로 {$auction['no']}번 거래 <M>유찰</>!", ActionLogger::EVENT_PLAIN);
$bidderLogger->pushGeneralActionLog("입찰자의 자금 부족으로 {$auction['no']}번 거래 <M>유찰</>! 벌금 <C>{$gold}</>", ActionLogger::EVENT_PLAIN);
$auctionLog[0] = "<S>◆</>{$admin['year']}{$admin['month']}월, {$auction['no']}번 <span class='sell'>판매</span> <M>유찰</> : <Y>{$auction['name1']}</>{$josaYi1} 쌀 <C>{$auction['amount']}</>{$josaUlRice} 판매, <Y>{$auction['name2']}</>{$josaYi2} 금 <C>{$auction['value']}</>{$josaRo} 입찰, 그러나 입찰자 자금부족, 벌금 <C>{$gold}</>";
} else {
$josaUlGold = JosaUtil::pick($auction['value'], '을');
$josaUlRice = JosaUtil::pick($auction['amount'], '을');
$josaRo = JosaUtil::pick($auction['value'], '로');
$traderLogger->pushGeneralActionLog("{$auction['no']}번 거래 <C>성사</>로 쌀 <C>{$auction['amount']}</>{$josaUlRice} 판매, 금 <C>{$auction['value']}</>{$josaUlGold} 획득!", ActionLogger::EVENT_PLAIN);
$bidderLogger->pushGeneralActionLog("{$auction['no']}번 거래 <C>성사</>로 금 <C>{$auction['value']}</>{$josaUlGold} 지불, 쌀 <C>{$auction['amount']}</>{$josaUlRice} 구입!", ActionLogger::EVENT_PLAIN);
$auctionLog[0] = "<S>◆</>{$admin['year']}{$admin['month']}월, {$auction['no']}번 <span class='sell'>판매</span> <C>성사</> : <Y>{$auction['name1']}</>{$josaYi1} 쌀 <C>{$auction['amount']}</>{$josaUlRice} 판매, <Y>{$auction['name2']}</>{$josaYi2} 금 <C>{$auction['value']}</>{$josaRo} 구매";
if($auction['value'] >= $auction['amount'] * 2) {
$auctionLog[0] .= " <R>★ 최고가 거래 ★</>";
} elseif($auction['value'] >= $auction['topv']) {
$auctionLog[0] .= " <M>★ 즉시구매가 거래 ★</>";
} elseif($auction['value'] * 2 <= $auction['amount']) {
$auctionLog[0] .= " <R>★ 최저가 거래 ★</>";
}
$db->update('general', [
'gold'=>$db->sqleval('gold + %i', $auction['value']),
'rice'=>$db->sqleval('rice - %i', $auction['amount']),
], 'no=%i', $auction['no1']);
$db->update('general', [
'gold'=>$db->sqleval('gold - %i', $auction['value']),
'rice'=>$db->sqleval('rice + %i', $auction['amount']),
], 'no=%i', $auction['no2']);
}
pushAuctionLog($auctionLog);
//구매거래
} else {
if($auction['amount'] > $bidder['rice'] - 1000) {
$gold = Util::round($auction['value'] * 0.01);
$db->update('general', [
'gold'=>Util::valueFit($bidder['gold'] - $gold, 0)
], 'no=%i', $bidder['no']);
$josaUlRice = JosaUtil::pick($auction['amount'], '을');
$josaRo = JosaUtil::pick($auction['value'], '로');
$traderLogger->pushGeneralActionLog("입찰자의 군량 부족으로 {$auction['no']}번 거래 <M>유찰</>!", ActionLogger::EVENT_PLAIN);
$bidderLogger->pushGeneralActionLog("입찰자의 군량 부족으로 {$auction['no']}번 거래 <M>유찰</>! 벌금 <C>{$gold}</>", ActionLogger::EVENT_PLAIN);
$auctionLog[0] = "<S>◆</>{$admin['year']}{$admin['month']}월, {$auction['no']}번 <S>구매</> <M>유찰</> : <Y>{$auction['name1']}</>{$josaYi1} 쌀 <C>{$auction['amount']}</>{$josaUlRice} 구매, <Y>{$auction['name2']}</>{$josaYi2} 금 <C>{$auction['value']}</>{$josaRo} 입찰, 그러나 입찰자 군량부족, 벌금 <C>{$gold}</>";
} elseif($auction['value'] > $trader['gold'] - 1000) {
$gold = Util::round($auction['value'] * 0.01);
$db->update('general', [
'gold'=>Util::valueFit($trader['gold'] - $gold, 0)
], 'no=%i', $trader['no']);
$josaUlRice = JosaUtil::pick($auction['amount'], '을');
$josaRo = JosaUtil::pick($auction['value'], '로');
$traderLogger->pushGeneralActionLog("구매자의 자금 부족으로 {$auction['no']}번 거래 <M>유찰</>! 벌금 <C>{$gold}</>", ActionLogger::EVENT_PLAIN);
$bidderLogger->pushGeneralActionLog("구매자의 자금 부족으로 {$auction['no']}번 거래 <M>유찰</>!", ActionLogger::EVENT_PLAIN);
$auctionLog[0] = "<S>◆</>{$admin['year']}{$admin['month']}월, {$auction['no']}번 <S>구매</> <M>유찰</> : <Y>{$auction['name1']}</>{$josaYi1} 쌀 <C>{$auction['amount']}</>{$josaUlRice} 구매, <Y>{$auction['name2']}</>{$josaYi2} 금 <C>{$auction['value']}</>{$josaRo} 입찰, 그러나 구매자 자금부족, 벌금 <C>{$gold}</>";
} else {
$josaUlGold = JosaUtil::pick($auction['value'], '을');
$josaUlRice = JosaUtil::pick($auction['amount'], '을');
$josaRo = JosaUtil::pick($auction['value'], '로');
$traderLogger->pushGeneralActionLog(
"{$auction['no']}번 거래 <C>성사</>로 금 <C>{$auction['value']}</>{$josaUlGold} 지불, 쌀 <C>{$auction['amount']}</>{$josaUlRice} 구입!", ActionLogger::EVENT_PLAIN
);
$bidderLogger->pushGeneralActionLog("{$auction['no']}번 거래 <C>성사</>로 쌀 <C>{$auction['amount']}</>{$josaUlRice} 판매, 금 <C>{$auction['value']}</>{$josaUlGold} 획득!", ActionLogger::EVENT_PLAIN);
$auctionLog[0] = "<S>◆</>{$admin['year']}{$admin['month']}월, {$auction['no']}번 <S>구매</> <C>성사</> : <Y>{$auction['name1']}</>{$josaYi1} 쌀 <C>{$auction['amount']}</>{$josaUlRice} 구매, <Y>{$auction['name2']}</>{$josaYi2} 금 <C>{$auction['value']}</>{$josaRo} 판매";
if($auction['value'] >= $auction['amount'] * 2) {
$auctionLog[0] .= " <R>★ 최고가 거래 ★</>";
} elseif($auction['value'] * 2 <= $auction['amount']) {
$auctionLog[0] .= " <R>★ 최저가 거래 ★</>";
} elseif($auction['value'] <= $auction['topv']) {
$auctionLog[0] .= " <M>★ 즉시구매가 거래 ★</>";
}
$db->update('general', [
'gold'=>$db->sqleval('gold - %i', $auction['value']),
'rice'=>$db->sqleval('rice + %i', $auction['amount']),
], 'no=%i', $auction['no1']);
$db->update('general', [
'gold'=>$db->sqleval('gold + %i', $auction['value']),
'rice'=>$db->sqleval('rice - %i', $auction['amount']),
], 'no=%i', $auction['no2']);
}
$traderLogger->flush();
$bidderLogger->flush();
pushAuctionLog($auctionLog);
}
$traderLogger->flush();
$bidderLogger->flush();
} }
$db->delete('auction', 'expire <= %s', $date); $dummyGeneral = AuctionBasicResource::genDummy();
foreach ($auctionList as [$auctionID, $rawAuctionType]) {
$auctionType = AuctionType::from($rawAuctionType);
if ($auctionType === AuctionType::BuyRice) {
$auction = new AuctionBuyRice($auctionID, $dummyGeneral);
} else if ($auctionType === AuctionType::SellRice) {
$auction = new AuctionSellRice($auctionID, $dummyGeneral);
} else if ($auctionType === AuctionType::UniqueItem) {
$auction = new AuctionUniqueItem($auctionID, $dummyGeneral);
} else {
throw new \Exception('Unknown auction type');
}
$auction->tryFinish();
}
} }
+1 -1
View File
@@ -510,7 +510,7 @@ function postUpdateMonthly(RandUtil $rng)
//토너먼트 개시 //토너먼트 개시
triggerTournament($rng); triggerTournament($rng);
// 시스템 거래건 등록 // 시스템 거래건 등록
registerAuction(); registerAuction($rng);
//전방설정 //전방설정
foreach (getAllNationStaticInfo() as $nation) { foreach (getAllNationStaticInfo() as $nation) {
if ($nation['level'] <= 0) { if ($nation['level'] <= 0) {
+2 -2
View File
@@ -90,8 +90,8 @@ function pushAuctionLog($log) {
pushRawFileLog(__DIR__."/logs/".UniqueConst::$serverID."/_auctionlog.txt", $log); pushRawFileLog(__DIR__."/logs/".UniqueConst::$serverID."/_auctionlog.txt", $log);
} }
function getAuctionLogRecent(int $count) { function getAuctionLogRecent(int $count): array {
return join('<br>', array_reverse(getFormattedFileLogRecent(__DIR__."/logs/".UniqueConst::$serverID."/_auctionlog.txt", $count, 300))); return array_reverse(getRawFileLogRecent(__DIR__."/logs/".UniqueConst::$serverID."/_auctionlog.txt", $count, 300));
} }
//DB-based //DB-based
+1 -1
View File
@@ -88,7 +88,7 @@ $color = "cyan";
$serverName = UniqueConst::$serverName; $serverName = UniqueConst::$serverName;
$serverCnt = $gameStor->server_cnt; $serverCnt = $gameStor->server_cnt;
$auctionCount = $db->queryFirstField('SELECT count(`no`) FROM auction'); $auctionCount = $db->queryFirstField('SELECT count(*) FROM ng_auction WHERE finished = 0');
$isTournamentActive = $gameStor->tournament > 0; $isTournamentActive = $gameStor->tournament > 0;
$isTournamentApplicationOpen = $gameStor->tournament == 1; $isTournamentApplicationOpen = $gameStor->tournament == 1;
$isBettingActive = $gameStor->tournament == 6; $isBettingActive = $gameStor->tournament == 6;
-2
View File
@@ -4,8 +4,6 @@ namespace sammo;
include "lib.php"; include "lib.php";
include "func.php"; include "func.php";
WebUtil::requireAJAX();
$session = Session::requireLogin([])->setReadOnly(); $session = Session::requireLogin([])->setReadOnly();
if(!class_exists('\\sammo\\DB')){ if(!class_exists('\\sammo\\DB')){
@@ -0,0 +1,51 @@
<?php
namespace sammo\API\Auction;
use sammo\Session;
use DateTimeInterface;
use sammo\AuctionBuyRice;
use sammo\AuctionUniqueItem;
use sammo\Validator;
use sammo\General;
class BidBuyRiceAuction extends \sammo\BaseAPI
{
public function validateArgs(): ?string
{
$v = new Validator($this->args);
$v->rule('required', [
'auctionID',
'amount',
])
->rule('int', 'amount')
->rule('int', 'auctionID');
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)
{
$auctionID = $this->args['auctionID'];
$amount = $this->args['amount'];
$generalID = $session->generalID;
$general = General::createGeneralObjFromDB($generalID);
$auction = new AuctionBuyRice($auctionID, $general);
$result = $auction->bid($amount, true);
if (is_string($result)) {
return $result;
}
return null;
}
}
@@ -0,0 +1,50 @@
<?php
namespace sammo\API\Auction;
use sammo\Session;
use DateTimeInterface;
use sammo\AuctionSellRice;
use sammo\Validator;
use sammo\General;
class BidSellRiceAuction extends \sammo\BaseAPI
{
public function validateArgs(): ?string
{
$v = new Validator($this->args);
$v->rule('required', [
'auctionID',
'amount',
])
->rule('int', 'amount')
->rule('int', 'auctionID');
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)
{
$auctionID = $this->args['auctionID'];
$amount = $this->args['amount'];
$generalID = $session->generalID;
$general = General::createGeneralObjFromDB($generalID);
$auction = new AuctionSellRice($auctionID, $general);
$result = $auction->bid($amount, true);
if (is_string($result)) {
return $result;
}
return null;
}
}
@@ -0,0 +1,53 @@
<?php
namespace sammo\API\Auction;
use sammo\Session;
use DateTimeInterface;
use sammo\AuctionUniqueItem;
use sammo\Validator;
use sammo\GameConst;
use sammo\General;
class BidUniqueAuction extends \sammo\BaseAPI
{
public function validateArgs(): ?string
{
$v = new Validator($this->args);
$v->rule('required', [
'auctionID',
'amount',
])
->rule('int', 'amount')
->rule('int', 'auctionID')
->rule('boolean', 'extendCloseDate');
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)
{
$auctionID = $this->args['auctionID'];
$amount = $this->args['amount'];
$tryExtendCloseDate = $this->arg['extendCloseDate'] ?? false;
$generalID = $session->generalID;
$general = General::createGeneralObjFromDB($generalID);
$auction = new AuctionUniqueItem($auctionID, $general);
$result = $auction->bid($amount, $tryExtendCloseDate);
if (is_string($result)) {
return $result;
}
return null;
}
}
@@ -0,0 +1,123 @@
<?php
namespace sammo\API\Auction;
use sammo\Session;
use DateTimeInterface;
use sammo\AuctionSellRice;
use sammo\DB;
use sammo\DTO\AuctionBidItem;
use sammo\DTO\AuctionInfo;
use sammo\Enums\AuctionType;
use sammo\Validator;
use sammo\General;
use sammo\Json;
use sammo\TimeUtil;
use sammo\Util;
use function sammo\getAuctionLogRecent;
class GetActiveResourceAuctionList extends \sammo\BaseAPI
{
public function validateArgs(): ?string
{
return null;
}
public function getRequiredSessionMode(): int
{
return static::REQ_GAME_LOGIN;
}
public function launch(Session $session, ?DateTimeInterface $modifiedSince, ?string $reqEtag)
{
$db = DB::db();
$buyRiceList = [];
$sellRiceList = [];
/** @var AuctionInfo[] */
$auctions = array_map(fn ($raw) => AuctionInfo::fromArray($raw), $db->query(
'SELECT * FROM `ng_auction` WHERE `type` IN %ls AND `finished` = 0 ORDER BY `close_date` ASC',
[
AuctionType::BuyRice->value,
AuctionType::SellRice->value,
]
));
$recentLogs = getAuctionLogRecent(20);
if (!$auctions) {
return [
'result' => true,
'buyRice' => $buyRiceList,
'sellRice' => $sellRiceList,
'recentLogs' => $recentLogs,
'generalID' => $session->generalID,
];
}
$auctionIDList = [];
foreach ($auctions as $auction) {
$auctionIDList[] = $auction->id;
}
$rawHighestBids = Util::convertArrayToDict($db->query(
'SELECT bid.* FROM `ng_auction_bid` bid INNER JOIN (
SELECT `auction_id`, MAX(`amount`) as `max_amount`
FROM `ng_auction_bid`
WHERE `auction_id` IN %li
GROUP BY `auction_id`
ORDER BY `amount`
) AS max_bid
ON bid.`auction_id` = max_bid.`auction_id` AND bid.`amount` = max_bid.`max_amount`',
$auctionIDList,
) ?? [], 'auction_id');
/** @var array<int,AuctionBidItem> */
$highestBids = Util::mapWithKey(
fn ($auctionID, $bid) => AuctionBidItem::fromArray($bid),
$rawHighestBids
);
foreach ($auctions as $auction) {
$rawAuction = [
'id' => $auction->id,
'type' => $auction->type->value,
'hostGeneralID' => $auction->hostGeneralID,
'hostName' => $auction->detail->hostName,
'openDate' => TimeUtil::format($auction->openDate, false),
'closeDate' => TimeUtil::format($auction->closeDate, false),
'amount' => $auction->detail->amount,
'startBidAmount' => $auction->detail->startBidAmount,
'finishBidAmount' => $auction->detail->finishBidAmount,
];
$highestBid = $highestBids[$rawAuction['id']] ?? null;
if ($highestBid === null) {
$rawAuction['highestBid'] = null;
} else {
$rawAuction['highestBid'] = [
'amount' => $highestBid->amount,
'date' => TimeUtil::format($highestBid->date, false),
'generalID' => $highestBid->generalID,
'generalName' => $highestBid->aux->generalName,
];
}
if ($rawAuction['type'] == AuctionType::BuyRice->value) {
$buyRiceList[] = $rawAuction;
} else {
$sellRiceList[] = $rawAuction;
}
}
return [
'result' => true,
'buyRice' => $buyRiceList,
'sellRice' => $sellRiceList,
'recentLogs' => $recentLogs,
'generalID' => $session->generalID,
];
}
}
@@ -0,0 +1,91 @@
<?php
namespace sammo\API\Auction;
use sammo\Session;
use DateTimeInterface;
use sammo\AuctionUniqueItem;
use sammo\DB;
use sammo\DTO\AuctionBidItem;
use sammo\DTO\AuctionInfo;
use sammo\Enums\AuctionType;
use sammo\TimeUtil;
use sammo\Validator;
class GetUniqueItemAuctionDetail extends \sammo\BaseAPI
{
public function validateArgs(): ?string
{
$v = new Validator($this->args);
$v->rule('required', [
'auctionID',
])
->rule('integer', 'auctionID');
if (!$v->validate()) {
return $v->errorStr();
}
$this->args['auctionID'] = (int)$this->args['auctionID'];
return null;
}
public function getRequiredSessionMode(): int
{
return static::REQ_GAME_LOGIN;
}
public function launch(Session $session, ?DateTimeInterface $modifiedSince, ?string $reqEtag)
{
$db = DB::db();
$generalID = $session->generalID;
$auctionID = $this->args['auctionID'];
$rawAuction = $db->queryFirstRow(
'SELECT * FROM `ng_auction` WHERE `type` = %s AND `id` = %i',
AuctionType::UniqueItem->value,
$auctionID
);
if (!$rawAuction) {
return '선택한 경매가 없습니다.';
}
$auction = AuctionInfo::fromArray($rawAuction);
/** @var AuctionBidItem[] */
$bidList = array_map(fn ($raw) => AuctionBidItem::fromArray($raw), $db->query(
'SELECT * FROM `ng_auction_bid` WHERE `auction_id` = %s ORDER BY `amount` DESC',
$auctionID
) ?? []);
$responseBid = [];
foreach ($bidList as $bid) {
$responseBid[] = [
'generalName' => $bid->aux->generalName,
'amount' => $bid->amount,
'isCallerHighestBidder' => $bid->generalID === $generalID,
'date' => TimeUtil::format($bid->date, false),
];
}
$obfuscatedName = AuctionUniqueItem::genObfuscatedName($generalID);
return [
'result' => true,
'auction' => [
'id' => $auction->id,
'finished' => $auction->finished,
'title' => $auction->detail->title,
'target' => $auction->target,
'isCallerHost' => $auction->hostGeneralID === $generalID,
'hostName' => $auction->detail->hostName,
'closeDate' => TimeUtil::format($auction->closeDate, false),
'remainCloseDateExtensionCnt' => $auction->detail->remainCloseDateExtensionCnt,
'availableLatestBidCloseDate' => TimeUtil::format($auction->detail->availableLatestBidCloseDate, false),
],
'bidList' => $responseBid,
'obfuscatedName' => $obfuscatedName,
];
}
}
@@ -0,0 +1,105 @@
<?php
namespace sammo\API\Auction;
use sammo\Session;
use DateTimeInterface;
use sammo\AuctionUniqueItem;
use sammo\DB;
use sammo\DTO\AuctionBidItem;
use sammo\DTO\AuctionInfo;
use sammo\Enums\AuctionType;
use sammo\TimeUtil;
use sammo\Util;
class GetUniqueItemAuctionList extends \sammo\BaseAPI
{
public function validateArgs(): ?string
{
return null;
}
public function getRequiredSessionMode(): int
{
return static::REQ_GAME_LOGIN;
}
public function launch(Session $session, ?DateTimeInterface $modifiedSince, ?string $reqEtag)
{
$db = DB::db();
$generalID = $session->generalID;
/** @var AuctionInfo[] */
$auctions = array_map(fn($raw)=>AuctionInfo::fromArray($raw), $db->query(
'SELECT * FROM `ng_auction` WHERE `type` = %s ORDER BY `close_date` ASC',
AuctionType::UniqueItem->value
) ?? []);
$obfuscatedName = AuctionUniqueItem::genObfuscatedName($generalID);
if(!$auctions){
return [
'result' => true,
'list' => [],
'obfuscatedName' => $obfuscatedName,
];
}
$auctionIDList = [];
foreach ($auctions as $auction) {
$auctionIDList[] = $auction->id;
}
$rawHighestBids = Util::convertArrayToDict($db->query(
'SELECT bid.* FROM `ng_auction_bid` bid INNER JOIN (
SELECT `auction_id`, MAX(`amount`) as `max_amount`
FROM `ng_auction_bid`
WHERE `auction_id` IN %li
GROUP BY `auction_id`
ORDER BY `amount`
) AS max_bid
ON bid.`auction_id` = max_bid.`auction_id` AND bid.`amount` = max_bid.`max_amount`',
$auctionIDList,
) ?? [], 'auction_id');
/** @var array<int,AuctionBidItem> */
$highestBids = Util::mapWithKey(
fn ($auctionID, $bid) => AuctionBidItem::fromArray($bid),
$rawHighestBids
);
$response = [];
foreach ($auctions as $auction) {
$auctionID = $auction->id;
$highestBid = $highestBids[$auctionID] ?? null;
if($highestBid === null){
continue;
}
$response[] = [
'id' => $auctionID,
'finished' => $auction->finished,
'title' => $auction->detail->title,
'target' => $auction->target,
'isCallerHost' => $auction->hostGeneralID === $generalID,
'hostName' => $auction->detail->hostName,
'closeDate' => TimeUtil::format($auction->closeDate, false),
'remainCloseDateExtensionCnt' => $auction->detail->remainCloseDateExtensionCnt,
'availableLatestBidCloseDate' => TimeUtil::format($auction->detail->availableLatestBidCloseDate, false),
'highestBid' => [
'generalName' => $highestBid->aux->generalName,
'amount' => $highestBid->amount,
'isCallerHighestBidder' => $highestBid->generalID === $generalID,
'date' => $highestBid->date,
],
];
}
return [
'result' => true,
'list' => $response,
'obfuscatedName' => $obfuscatedName,
];
}
}
@@ -0,0 +1,97 @@
<?php
namespace sammo\API\Auction;
use sammo\Session;
use DateTimeInterface;
use sammo\AuctionBuyRice;
use sammo\DB;
use sammo\Validator;
use sammo\GameConst;
use sammo\General;
use sammo\KVStorage;
use sammo\Util;
class OpenBuyRiceAuction 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', [
'amount',
'closeTurnCnt',
'startBidAmount',
'finishBidAmount',
])
->rule('int', 'amount')
->rule('int', 'closeTurnCnt')
->rule('min', 'amount', 100)
->rule('max', 'amount', 10000)
->rule('int', 'startBidAmount')
->rule('int', 'finishBidAmount');
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 */
$amount = $this->args['amount'];
/** @var int */
$closeTurnCnt = $this->args['closeTurnCnt'];
/** @var int */
$startBidAmount = $this->args['startBidAmount'];
/** @var int */
$finishBidAmount = $this->args['finishBidAmount'];
$generalID = $session->generalID;
$general = General::createGeneralObjFromDB($generalID);
$db = DB::db();
$gameStor = KVStorage::getStorage($db, 'game_env');
[$initYear, $initMonth, $year, $month] = $gameStor->getValuesAsArray(['init_year', 'init_month', 'year', 'month']);
$initYearMonth = Util::joinYearMonth($initYear, $initMonth);
$yearMonth = Util::joinYearMonth($year, $month);
if($yearMonth <= $initYearMonth + 3){
return '시작 후 3개월이 지나야 경매를 열 수 있습니다.';
}
$auctionResult = AuctionBuyRice::openResourceAuction(
$general,
$amount,
$closeTurnCnt,
$startBidAmount,
$finishBidAmount
);
if (is_string($auctionResult)) {
return $auctionResult;
}
return [
'result' => true,
'auctionID' => $auctionResult->getInfo()->id,
];
}
}
@@ -0,0 +1,97 @@
<?php
namespace sammo\API\Auction;
use sammo\Session;
use DateTimeInterface;
use sammo\AuctionSellRice;
use sammo\DB;
use sammo\Validator;
use sammo\GameConst;
use sammo\General;
use sammo\KVStorage;
use sammo\Util;
class OpenSellRiceAuction 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', [
'amount',
'closeTurnCnt',
'startBidAmount',
'finishBidAmount',
])
->rule('int', 'amount')
->rule('int', 'closeTurnCnt')
->rule('min', 'amount', 100)
->rule('max', 'amount', 10000)
->rule('int', 'startBidAmount')
->rule('int', 'finishBidAmount');
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 */
$amount = $this->args['amount'];
/** @var int */
$closeTurnCnt = $this->args['closeTurnCnt'];
/** @var int */
$startBidAmount = $this->args['startBidAmount'];
/** @var int */
$finishBidAmount = $this->args['finishBidAmount'];
$generalID = $session->generalID;
$general = General::createGeneralObjFromDB($generalID);
$db = DB::db();
$gameStor = KVStorage::getStorage($db, 'game_env');
[$initYear, $initMonth, $year, $month] = $gameStor->getValuesAsArray(['init_year', 'init_month', 'year', 'month']);
$initYearMonth = Util::joinYearMonth($initYear, $initMonth);
$yearMonth = Util::joinYearMonth($year, $month);
if($yearMonth <= $initYearMonth + 3){
return '시작 후 3개월이 지나야 경매를 열 수 있습니다.';
}
$auctionResult = AuctionSellRice::openResourceAuction(
$general,
$amount,
$closeTurnCnt,
$startBidAmount,
$finishBidAmount
);
if (is_string($auctionResult)) {
return $auctionResult;
}
return [
'result' => true,
'auctionID' => $auctionResult->getInfo()->id,
];
}
}
@@ -0,0 +1,82 @@
<?php
namespace sammo\API\Auction;
use sammo\Session;
use DateTimeInterface;
use sammo\AuctionUniqueItem;
use sammo\DB;
use sammo\Validator;
use sammo\GameConst;
use sammo\General;
use sammo\KVStorage;
use sammo\Util;
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);
$db = DB::db();
$gameStor = KVStorage::getStorage($db, 'game_env');
[$initYear, $initMonth, $year, $month] = $gameStor->getValuesAsArray(['init_year', 'init_month', 'year', 'month']);
$initYearMonth = Util::joinYearMonth($initYear, $initMonth);
$yearMonth = Util::joinYearMonth($year, $month);
if($yearMonth <= $initYearMonth + 3){
return '시작 후 3개월이 지나야 경매를 열 수 있습니다.';
}
$auctionResult = AuctionUniqueItem::openItemAuction($itemObj, $general, $amount);
if(is_string($auctionResult)) {
return $auctionResult;
}
return [
'result' => true,
'auctionID' => $auctionResult->getInfo()->id,
];
}
}
+4 -1
View File
@@ -2,6 +2,7 @@
namespace sammo\API\General; namespace sammo\API\General;
use Ds\Set;
use sammo\DB; use sammo\DB;
use sammo\Validator; use sammo\Validator;
@@ -9,6 +10,7 @@ use sammo\Session;
use sammo\GameConst; use sammo\GameConst;
use sammo\General; use sammo\General;
use sammo\JosaUtil; use sammo\JosaUtil;
use sammo\KVStorage;
class DropItem extends \sammo\BaseAPI class DropItem extends \sammo\BaseAPI
{ {
@@ -56,12 +58,13 @@ class DropItem extends \sammo\BaseAPI
$logger->pushGeneralActionLog("<C>{$itemName}</>{$josaUl} 버렸습니다."); $logger->pushGeneralActionLog("<C>{$itemName}</>{$josaUl} 버렸습니다.");
$nationName = $me->getStaticNation()['name']; $nationName = $me->getStaticNation()['name'];
$db = DB::db();
if (!$item->isBuyable()) { if (!$item->isBuyable()) {
$logger->pushGlobalActionLog("<Y>{$generalName}</>{$josaYi} <C>{$itemName}</>{$josaUl} 잃었습니다!"); $logger->pushGlobalActionLog("<Y>{$generalName}</>{$josaYi} <C>{$itemName}</>{$josaUl} 잃었습니다!");
$logger->pushGlobalHistoryLog("<R><b>【망실】</b></><D><b>{$nationName}</b></>의 <Y>{$generalName}</>{$josaYi} <C>{$itemName}</>{$josaUl} 잃었습니다!"); $logger->pushGlobalHistoryLog("<R><b>【망실】</b></><D><b>{$nationName}</b></>의 <Y>{$generalName}</>{$josaYi} <C>{$itemName}</>{$josaUl} 잃었습니다!");
} }
$me->applyDB(DB::db()); $me->applyDB($db);
return null; return null;
} }
@@ -1,104 +0,0 @@
<?php
namespace sammo\API\InheritAction;
use sammo\Session;
use DateTimeInterface;
use sammo\DB;
use sammo\Enums\RankColumn;
use sammo\GameConst;
use sammo\General;
use sammo\KVStorage;
use sammo\UserLogger;
use sammo\Validator;
use function sammo\buildItemClass;
class BuySpecificUnique 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', [
'item',
'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
{
//KVStrorage, General.aux 모두 쓰므로 lock;
return static::REQ_GAME_LOGIN;
}
public function launch(Session $session, ?DateTimeInterface $modifiedSince, ?string $reqEtag)
{
$itemKey = $this->args['item'];
$amount = $this->args['amount'];
$userID = $session->userID;
$generalID = $session->generalID;
$general = General::createGeneralObjFromDB($generalID);
if ($userID != $general->getVar('owner')) {
return '로그인 상태가 이상합니다. 다시 로그인해 주세요.';
}
$itemTrials = $general->getAuxVar('inheritUniqueTrial') ?? [];
if (key_exists($itemKey, $itemTrials)) {
return '이미 입찰한 아이템입니다. 다음 턴에 시도해 주세요.';
}
foreach(GameConst::$allItems as $itemType => $items){
if(!key_exists($itemKey, $items)){
continue;
}
$prevItem = $general->getItem($itemType);
if(!$prevItem->isBuyable()){
return '이미 같은 자리에 유니크를 보유하고 있습니다.';
}
break;
}
$db = DB::db();
$inheritStor = KVStorage::getStorage($db, "inheritance_{$userID}");
$trialStor = KVStorage::getStorage($db, "ut_{$itemKey}");
$previousPoint = ($inheritStor->getValue('previous') ?? [0, 0])[0];
if ($previousPoint < $amount) {
return '충분한 유산 포인트를 가지고 있지 않습니다.';
}
$itemObj = buildItemClass($itemKey);
$userLogger = new UserLogger($userID);
$userLogger->push("{$amount} 포인트로 유니크 {$itemObj->getName()} 구입 시도", "inheritPoint");
$userLogger->flush();
$itemTrials[$itemKey] = $amount;
$general->setAuxVar('inheritUniqueTrial', $itemTrials);
$inheritStor->setValue('previous', [$previousPoint - $amount, null]);
$general->increaseRankVar(RankColumn::inherit_point_spent_dynamic, $amount);
$trialStor->setValue("u{$userID}", [$userID, $generalID, $amount]);
$general->applyDB($db);
return null;
}
}
+524
View File
@@ -0,0 +1,524 @@
<?php
namespace sammo;
use DateTime;
use DateTimeImmutable;
use DateTimeInterface;
use sammo\DTO\AuctionBidItem;
use sammo\DTO\AuctionBidItemData;
use sammo\DTO\AuctionInfo;
use sammo\Enums\AuctionType;
use sammo\Enums\InheritanceKey;
use sammo\Enums\RankColumn;
use sammo\Enums\ResourceType;
abstract class Auction
{
protected AuctionInfo $info;
static AuctionType $auctionType;
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;
protected AuctionBidItem|null|false $_highestBid = false;
static public function genObfuscatedName(int $id): string
{
$db = DB::db();
$gameStor = KVStorage::getStorage($db, 'game_env');
$namePool = $gameStor->getValue('obfuscatedNamePool');
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) {
$namePool[] = "{$ch0}{$ch1}{$ch2}";
}
}
}
$namePool = $rng->shuffle($namePool);
$gameStor->setValue('obfuscatedNamePool', $namePool);
}
$dupIdx = intdiv($id, count($namePool));
$subIdx = $id % count($namePool);
if ($dupIdx == 0) {
return $namePool[$subIdx];
}
return "{$namePool[$subIdx]}{$dupIdx}";
}
static protected function openAuction(AuctionInfo $info, General $general): int|string
{
$db = DB::db();
if ($info->id !== null) {
return 'id가 지정되어 있습니다.';
}
$db->insert('ng_auction', $info->toArray());
return $db->insertId();
}
public function getHighestBid(): ?AuctionBidItem
{
$db = DB::db();
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;
}
$highestBid = AuctionBidItem::fromArray($rawHighestBid);
$this->_highestBid = $highestBid;
return $highestBid;
}
public function getMyPrevBid(): ?AuctionBidItem
{
$db = DB::db();
if (!$this->info->detail->isReverse) {
$rawMyPrevBid = $db->queryFirstRow(
'SELECT * FROM ng_auction_bid WHERE general_id = %i AND auction_id = %i ORDER BY `amount` DESC LIMIT 1',
$this->general->getID(),
$this->info->id
);
} else {
$rawMyPrevBid = $db->queryFirstRow(
'SELECT * FROM ng_auction_bid WHERE general_id = %i AND auction_id = %i ORDER BY `amount` ASC LIMIT 1',
$this->general->getID(),
$this->info->id
);
}
if (!$rawMyPrevBid) {
return null;
}
return AuctionBidItem::fromArray($rawMyPrevBid);
}
public function __construct(protected readonly int $auctionID, protected General $general)
{
$db = DB::db();
$rawAuctionInfo = $db->queryFirstRow('SELECT * FROM `ng_auction` WHERE id = %i', $auctionID);
if (!$rawAuctionInfo) {
throw new \RuntimeException("해당 경매가 없습니다: {$auctionID}");
}
$this->info = AuctionInfo::fromArray($rawAuctionInfo);
$thisAuctionType = static::$auctionType;
if ($this->info->type !== $thisAuctionType) {
throw new \RuntimeException("잘못된 경매 타입입니다: {$this->info->type->value} != {$thisAuctionType->value}");
}
}
public function getInfo(): AuctionInfo
{
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->toArray('id'), 'id = %i', $this->info->id);
return null;
}
public function extendLatestBidCloseDate(?DateTimeInterface $date): ?string
{
if ($date === null) {
$db = DB::db();
$gameStor = KVStorage::getStorage($db, 'game_env');
$turnTerm = $gameStor->getValue('turnterm');
$date = $this->info->closeDate->add(TimeUtil::secondsToDateInterval(
max(static::MIN_EXTENSION_MINUTES_PER_BID, $turnTerm * static::COEFF_EXTENSION_MINUTES_PER_BID) * 60
));
}
else{
$date = DateTimeImmutable::createFromInterface($date);
}
if ($this->info->detail->availableLatestBidCloseDate !== null && $date < $this->info->detail->availableLatestBidCloseDate) {
return '기간보다 짧습니다.';
}
$this->info->detail->availableLatestBidCloseDate = $date;
return null;
}
public function extendCloseDate(DateTimeInterface $date, bool $force = false): ?string
{
if (!$force) {
if ($this->info->detail->remainCloseDateExtensionCnt === null) {
return '연장할 수 없는 경매입니다.';
}
if ($this->info->detail->remainCloseDateExtensionCnt === 0) {
return '더 이상 연장할 수 없습니다';
}
if ($this->info->detail->remainCloseDateExtensionCnt > 0) {
$this->info->detail->remainCloseDateExtensionCnt--;
}
}
if ($date < $this->info->closeDate) {
return '종료 기간보다 짧습니다.';
}
$closeDate = DateTimeImmutable::createFromInterface($date);
$this->info->closeDate = $closeDate;
return null;
}
public function applyDB(): void
{
$db = DB::db();
$db->update('ng_auction', $this->info->toArray('id'), 'id = %i', $this->info->id);
}
public function refundBid(AuctionBidItem $bidItem, string $reason): void
{
if ($bidItem->auctionID !== $this->info->id) {
throw new \RuntimeException('잘못된 경매입니다.');
}
$db = DB::db();
if ($bidItem->generalID === $this->general->getID()) {
$oldBidder = $this->general;
} else {
$oldBidder = General::createGeneralObjFromDB($bidItem->generalID);
}
if ($this->info->reqResource === ResourceType::inheritancePoint) {
$oldBidder->increaseInheritancePoint(InheritanceKey::previous, $bidItem->amount);
$oldBidder->increaseRankVar(RankColumn::inherit_point_spent_dynamic, -$bidItem->amount);
} else {
$oldBidder->increaseVar($this->info->reqResource->value, $bidItem->amount);
}
if ($oldBidder instanceof DummyGeneral) {
return;
}
$staticNation = $oldBidder->getStaticNation();
$src = new MessageTarget(0, '', 0, 'System', '#000000');
$dest = new MessageTarget(
$oldBidder->getID(),
$oldBidder->getName(),
$oldBidder->getNationID(),
$staticNation['name'],
$staticNation['color'],
GetImageURL($oldBidder->getVar('imgsvr'), $oldBidder->getVar('picture'))
);
//TODO: 전역 알림이 나타나야한다. 일반 메시지보다는 중요하고, 메시지보단 약하게..
//TODO: 바로가기를 제공하는 편이 좋을 것 같다.
$msg = new Message(
Message::MSGTYPE_PRIVATE,
$src,
$dest,
$reason,
new DateTime(),
new DateTime('9999-12-31'),
[]
);
$oldBidder->applyDB($db);
$msg->send(true);
}
public function closeAuction(bool $isRollback = false): void
{
$db = DB::db();
$this->info->finished = true;
if ($isRollback) {
$highestBid = $this->getHighestBid();
if ($highestBid !== null) {
$this->refundBid($highestBid, "{$this->info->id}{$this->info->detail->title} 경매가 취소되었습니다.");
}
$this->rollbackAuction();
}
$db->update('ng_auction', $this->info->toArray('id'), 'id = %i', $this->info->id);
}
private function bidInheritPoint(int $amount, \DateTimeImmutable $now, bool $tryExtendCloseDate): ?string
{
$db = DB::db();
$auctionInfo = $this->info;
$general = $this->general;
$highestBid = $this->getHighestBid();
if ($highestBid !== null && $amount <= $highestBid->amount) {
return '현재입찰가보다 높게 입찰해야 합니다.';
}
$myPrevBid = $this->getMyPrevBid();
if ($myPrevBid !== null && $highestBid->no !== $myPrevBid->no) {
//이미 환불 받았으니 무효.
$myPrevBid = null;
}
$morePoint = $amount - ($myPrevBid ? $myPrevBid->amount : 0);
$currPoint = $general->getInheritancePoint(InheritanceKey::previous);
if ($currPoint === null || $currPoint < $morePoint) {
return '유산포인트가 부족합니다.';
}
$obfuscatedName = static::genObfuscatedName($general->getID());
//여기서부터 입찰 성공
$newBid = new AuctionBidItem(
null,
$auctionInfo->id,
$general->getVar('owner'),
$general->getID(),
$amount,
$now,
new AuctionBidItemData(
$general->getVar('owner_name'),
$obfuscatedName,
$tryExtendCloseDate,
)
);
$db->insert('ng_auction_bid', $newBid->toArray());
if ($db->affectedRows() == 0) {
return '입찰에 실패했습니다: DB 오류';
}
$gameStor = KVStorage::getStorage($db, 'game_env');
$turnTerm = $gameStor->getValue('turnterm');
if ($this->info->detail->availableLatestBidCloseDate !== null) {
$extendedCloseDate = $now->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->detail->availableLatestBidCloseDate) {
$this->extendCloseDate(min($extendedCloseDate, $this->info->detail->availableLatestBidCloseDate), true);
$this->applyDB();
}
}
$general->increaseInheritancePoint(InheritanceKey::previous, -$morePoint);
$general->increaseRankVar(RankColumn::inherit_point_spent_dynamic, $morePoint);
if ($highestBid !== null && $myPrevBid === null) {
$this->refundBid($highestBid, "{$auctionInfo->id}{$auctionInfo->detail->title}에 상회입찰자가 나타났습니다.");
}
$general->applyDB($db);
return null;
}
protected function _bid(int $amount, bool $tryExtendCloseDate = false): ?string
{
$auctionInfo = $this->info;
$general = $this->general;
if ($auctionInfo->finished) {
return '경매가 이미 끝났습니다.';
}
$now = new \DateTimeImmutable();
if ($auctionInfo->closeDate < $now) {
return '경매가 이미 끝났습니다.';
}
if ($auctionInfo->openDate > $now) {
return '경매가 아직 시작되지 않았습니다.';
}
if (!$auctionInfo->detail->isReverse) {
if ($auctionInfo->detail->finishBidAmount !== null && $auctionInfo->detail->finishBidAmount < $amount) {
return '즉시판매가보다 높을 수 없습니다.';
}
} else {
if ($auctionInfo->detail->finishBidAmount !== null && $auctionInfo->detail->finishBidAmount > $amount) {
return '즉시판매가보다 낮을 수 없습니다.';
}
}
if ($auctionInfo->reqResource === ResourceType::inheritancePoint) {
return $this->bidInheritPoint($amount, $now, $tryExtendCloseDate);
}
//reqResource는 말 그대로 '구매자가 내야하는 자원'이다.
$db = DB::db();
$highestBid = $this->getHighestBid();
if (!$auctionInfo->detail->isReverse) {
if ($highestBid !== null && $amount <= $highestBid->amount) {
return '현재입찰가보다 높게 입찰해야 합니다.';
}
} else {
if ($highestBid !== null && $amount >= $highestBid->amount) {
return '현재입찰가보다 낮게 입찰해야 합니다.';
}
}
$myPrevBid = $this->getMyPrevBid();
if ($myPrevBid !== null && $highestBid->no !== $myPrevBid->no) {
//이미 환불 받았으니 무효.
$myPrevBid = null;
}
$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) {
return $resType->getName() . '이 부족합니다.';
}
//여기서부터 입찰 성공
$newBid = new AuctionBidItem(
null,
$auctionInfo->id,
$general->getVar('owner'),
$general->getID(),
$amount,
$now,
new AuctionBidItemData(
$general->getVar('owner_name'),
$general->getName(),
$tryExtendCloseDate,
)
);
$db->insert('ng_auction_bid', $newBid->toArray());
if ($db->affectedRows() == 0) {
return '입찰에 실패했습니다: DB 오류';
}
$general->increaseVar($resType->value, -$morePoint);
$gameStor = KVStorage::getStorage($db, 'game_env');
$turnTerm = $gameStor->getValue('turnterm');
$extendedCloseDate = $now->add(TimeUtil::secondsToDateInterval(
max(static::MIN_EXTENSION_MINUTES_PER_BID, $turnTerm * static::COEFF_EXTENSION_MINUTES_PER_BID) * 60
));
if ($extendedCloseDate > $this->info->closeDate) {
$this->extendCloseDate($extendedCloseDate, true);
$this->applyDB();
}
if ($highestBid !== null && $myPrevBid === null) {
$this->refundBid($highestBid, "{$auctionInfo->id}{$auctionInfo->detail->title}에 상회입찰자가 나타났습니다.");
}
$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->aux->tryExtendCloseDate) {
$db = DB::db();
$gameStor = KVStorage::getStorage($db, 'game_env');
$turnTerm = $gameStor->getValue('turnterm');
//연장 요청이 있었다.
$extendedCloseDate = $this->info->closeDate->add(TimeUtil::secondsToDateInterval(
max(static::MIN_EXTENSION_MINUTES_BY_EXTENSION_QUERY, $turnTerm * static::COEFF_EXTENSION_MINUTES_BY_EXTENSION_QUERY) * 60
));
if ($this->extendCloseDate($extendedCloseDate) === null) {
$this->extendLatestBidCloseDate(null);
$this->applyDB();
return false;
}
}
$bidder = General::createGeneralObjFromDB($highestBid->generalID);
$failReason = $this->finishAuction($highestBid, $bidder);
if ($failReason === null) {
$this->closeAuction();
return true;
}
if ($bidder instanceof DummyGeneral) {
return false;
}
$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): ?string;
abstract protected function rollbackAuction(): void;
abstract protected function finishAuction(AuctionBidItem $highestBid, General $bidder): ?string;
}
+246
View File
@@ -0,0 +1,246 @@
<?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 ResourceType $hostRes;
static ResourceType $bidderRes;
static public function openResourceAuction(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();
if (!($general instanceof DummyGeneral)) {
$prevAuctionID = $db->queryFirstField(
'SELECT id FROM ng_auction WHERE host_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 static($openResult, $general);
}
static public function genDummy(bool $initFullLogger = true): DummyGeneral
{
$dummyGeneral = new DummyGeneral(false);
$dummyGeneral->setVar('name', '상인');
$dummyGeneral->setVar('gold', static::MAX_AUCTION_AMOUNT * 10);
$dummyGeneral->setVar('rice', static::MAX_AUCTION_AMOUNT * 10);
if($initFullLogger){
$db = DB::db();
$gameStor = KVStorage::getStorage($db, 'game_env');
[$year, $month] = $gameStor->getValuesAsArray(['year', 'month']);
$dummyGeneral->initLogger($year, $month);
}
return $dummyGeneral;
}
protected function rollbackAuction(): void
{
if ($this->general->getID() === $this->info->hostGeneralID) {
$auctionHost = $this->general;
} else if ($this->info->hostGeneralID == 0) {
$auctionHost = $this->genDummy();
} else {
$auctionHost = General::createGeneralObjFromDB($this->info->hostGeneralID);
}
$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->hostGeneralID) {
$auctionHost = $this->general;
} else if ($this->info->hostGeneralID == 0) {
$auctionHost = $this->genDummy();
} else {
$auctionHost = General::createGeneralObjFromDB($this->info->hostGeneralID);
}
$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(), '이');
$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) =>
$bidder->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 = false): ?string
{
if ($this->info->hostGeneralID === $this->general->getID()) {
return '자신이 연 경매에 입찰할 수 없습니다.';
}
$result = $this->_bid($amount, $tryExtendCloseDate);
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);
}
return null;
}
}
+14
View File
@@ -0,0 +1,14 @@
<?php
namespace sammo;
use sammo\Enums\AuctionType;
use sammo\Enums\ResourceType;
/** 경매에 쌀을 매물로 등록, 입찰자가 금으로 구매 */
class AuctionBuyRice extends AuctionBasicResource
{
static AuctionType $auctionType = AuctionType::BuyRice;
static ResourceType $hostRes = ResourceType::rice;
static ResourceType $bidderRes = ResourceType::gold;
}
+14
View File
@@ -0,0 +1,14 @@
<?php
namespace sammo;
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;
}
+293
View File
@@ -0,0 +1,293 @@
<?php
namespace sammo;
use DateTimeImmutable;
use Ds\Set;
use sammo\DTO\AuctionBidItem;
use sammo\DTO\AuctionInfo;
use sammo\DTO\AuctionInfoDetail;
use sammo\Enums\AuctionType;
use sammo\Enums\InheritanceKey;
use sammo\Enums\ResourceType;
use sammo\RandUtil;
class AuctionUniqueItem extends Auction
{
const COEFF_EXTENSION_MINUTES_LIMIT_UNIQUE_CNT = 24;
static AuctionType $auctionType = AuctionType::UniqueItem;
static public function openItemAuction(BaseItem $item, General $general, int $startAmount): self|string
{
if ($startAmount < GameConst::$inheritItemUniqueMinPoint) {
return '최소 경매 금액은 ' . GameConst::$inheritItemUniqueMinPoint . '입니다.';
}
if ($general->getInheritancePoint(InheritanceKey::previous) < $startAmount) {
return '경매를 시작할 포인트가 부족합니다.';
}
if ($item->isBuyable()) {
return '구매할 수 있는 아이템입니다.';
}
$itemKey = $item->getRawClassName();
$db = DB::db();
$auctionIDonProgress = $db->queryFirstField(
'SELECT `id` FROM ng_auction WHERE `finished` = 0 AND `type` = %s AND `target` = %s',
AuctionType::UniqueItem->value,
$itemKey
);
if ($auctionIDonProgress !== null) {
return '이미 경매가 진행중입니다.';
}
$prevAuctionID = $db->queryFirstField(
'SELECT id FROM ng_auction WHERE host_general_id = %i AND finished = 0 AND `type` = %s',
$general->getID(),
AuctionType::UniqueItem->value,
);
if ($prevAuctionID !== null) {
return '아직 경매가 끝나지 않았습니다.';
}
$gameStor = KVStorage::getStorage($db, 'game_env');
$now = new DateTimeImmutable();
[$turnTerm, $year, $month] = $gameStor->getValuesAsArray(['turnterm', 'year', 'month']);
$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(
null,
AuctionType::UniqueItem,
false,
$itemKey,
$general->getID(),
ResourceType::inheritancePoint,
$now,
$closeDate,
new AuctionInfoDetail(
"{$item->getName()} 경매",
static::genObfuscatedName($general->getID()),
1,
false,
$startAmount,
null,
1,
$availableLatestBidCloseDate,
)
);
$auctionID = static::openAuction($info, $general);
if (!is_int($auctionID)) {
return $auctionID;
}
$auction = new static($auctionID, $general);
try {
$auction->bid($startAmount, false);
} catch (\Exception $e) {
//실패해선 안된다.
$msg = $e->getMessage();
$auction->closeAuction();
return "경매를 시작했지만, 첫 입찰에 실패했습니다: {$msg}";
}
$itemName = $item->getName();
$josaRa = JosaUtil::pick($item->getRawName(), '라');
$logger = new ActionLogger(0, 0, $year, $month);
$logger->pushGlobalHistoryLog("<C><b>【보물수배】</b></>누군가가 <C>{$itemName}</>{$josaRa}는 보물을 구한다는 소문이 들려옵니다.");
$logger->flush();
return $auction;
}
protected function rollbackAuction(): void
{
// 유니크 옥션의 개최자는 운영자이므로 할 일이 없다.
}
public function bid(int $amount, bool $tryExtendCloseDate): ?string
{
$db = DB::db();
/** @var AuctionInfo[] */
$openUniqueAuctions = array_map(fn ($raw) => AuctionInfo::fromArray($raw), $db->query(
'SELECT * FROM `ng_auction` WHERE `finished` = 0 AND `type`= %s',
AuctionType::UniqueItem->value
) ?? []);
$auctionIDList = [];
foreach($openUniqueAuctions as $auction){
$auctionIDList[] = $auction->id;
}
$db = DB::db();
$rawHighestBids = Util::convertArrayToDict($db->query(
'SELECT bid.* FROM `ng_auction_bid` bid INNER JOIN (
SELECT `auction_id`, MAX(`amount`) as `max_amount`
FROM `ng_auction_bid`
WHERE `auction_id` IN %li
GROUP BY `auction_id`
ORDER BY `amount`
) AS max_bid
ON bid.`auction_id` = max_bid.`auction_id` AND bid.`amount` = max_bid.`max_amount`',
$auctionIDList,
) ?? [], 'auction_id');
/** @var array<int,AuctionBidItem> */
$highestBids = Util::mapWithKey(
fn ($auctionID, $bid) => AuctionBidItem::fromArray($bid),
$rawHighestBids
);
$itemCode = $this->info->target;
if($itemCode === null){
throw new \Exception('아이템 코드가 없습니다.');
}
$bidItemTypes = new Set();
foreach (GameConst::$allItems as $itemType => $itemList) {
if (key_exists($itemCode, $itemList) && $itemList[$itemCode] <= 0) {
continue;
}
$bidItemTypes->add($itemType);
}
foreach ($openUniqueAuctions as $auction) {
$auctionID = $auction->id;
if (!isset($highestBids[$auctionID])) {
continue;
}
if ($auctionID === $this->auctionID) {
continue;
}
$bid = $highestBids[$auctionID];
if ($bid->generalID !== $this->general->getID()) {
continue;
}
$itemCodeComp = $auction->target;
foreach (GameConst::$allItems as $itemType => $itemList) {
if (($itemList[$itemCodeComp] ?? 0) <= 0) {
continue;
}
if ($bidItemTypes->contains($itemType)) {
return '1순위 입찰자인 경매중에 같은 부위가 있습니다.';
}
}
}
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) {
$turnTerm = $gameStor->getValue('turnterm');
//제한에 걸렸다면 자동 연장
$extendedCloseDate = $this->info->closeDate->add(TimeUtil::secondsToDateInterval(
max(static::MIN_EXTENSION_MINUTES_BY_EXTENSION_QUERY, $turnTerm * static::COEFF_EXTENSION_MINUTES_LIMIT_UNIQUE_CNT) * 60
));
$this->extendCloseDate($extendedCloseDate, true);
$this->extendLatestBidCloseDate(null);
$this->applyDB();
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} 습득했습니다!");
$userLogger = new UserLogger($general->getVar('owner'));
$userLogger->push(sprintf("유니크 %s 경매로 %d 포인트 사용", $itemName, $highestBid->amount), "inheritPoint");
$general->applyDB($db);
return null;
}
}
@@ -13,8 +13,6 @@ use \sammo\{
use \sammo\Constraint\Constraint; use \sammo\Constraint\Constraint;
use \sammo\Constraint\ConstraintHelper; use \sammo\Constraint\ConstraintHelper;
use function sammo\tryRollbackInheritUniqueItem;
class che_NPC능동 extends Command\GeneralCommand{ class che_NPC능동 extends Command\GeneralCommand{
static protected $actionName = 'NPC능동'; static protected $actionName = 'NPC능동';
@@ -98,7 +96,6 @@ class che_NPC능동 extends Command\GeneralCommand{
$this->setResultTurn(new LastTurn(static::getName(), $this->arg)); $this->setResultTurn(new LastTurn(static::getName(), $this->arg));
} }
tryRollbackInheritUniqueItem($rng, $general);
$general->applyDB($db); $general->applyDB($db);
return true; return true;
@@ -15,8 +15,6 @@ use \sammo\Constraint\Constraint;
use \sammo\Constraint\ConstraintHelper; use \sammo\Constraint\ConstraintHelper;
use sammo\Enums\InheritanceKey; use sammo\Enums\InheritanceKey;
use function sammo\tryRollbackInheritUniqueItem;
class che_모반시도 extends Command\GeneralCommand{ class che_모반시도 extends Command\GeneralCommand{
static protected $actionName = '모반시도'; static protected $actionName = '모반시도';
@@ -99,7 +97,6 @@ class che_모반시도 extends Command\GeneralCommand{
$this->setResultTurn(new LastTurn(static::getName(), $this->arg)); $this->setResultTurn(new LastTurn(static::getName(), $this->arg));
$general->increaseInheritancePoint(InheritanceKey::active_action, 1); $general->increaseInheritancePoint(InheritanceKey::active_action, 1);
$general->checkStatChange(); $general->checkStatChange();
tryRollbackInheritUniqueItem($rng, $general);
$general->applyDB($db); $general->applyDB($db);
$lordGeneral->applyDB($db); $lordGeneral->applyDB($db);
-2
View File
@@ -17,7 +17,6 @@ use sammo\Enums\InheritanceKey;
use function sammo\DeleteConflict; use function sammo\DeleteConflict;
use function sammo\refreshNationStaticInfo; use function sammo\refreshNationStaticInfo;
use function sammo\tryRollbackInheritUniqueItem;
class che_방랑 extends Command\GeneralCommand{ class che_방랑 extends Command\GeneralCommand{
static protected $actionName = '방랑'; static protected $actionName = '방랑';
@@ -125,7 +124,6 @@ class che_방랑 extends Command\GeneralCommand{
refreshNationStaticInfo(); refreshNationStaticInfo();
$general->increaseInheritancePoint(InheritanceKey::active_action, 1); $general->increaseInheritancePoint(InheritanceKey::active_action, 1);
$this->setResultTurn(new LastTurn(static::getName(), $this->arg)); $this->setResultTurn(new LastTurn(static::getName(), $this->arg));
tryRollbackInheritUniqueItem($rng, $general);
$general->applyDB($db); $general->applyDB($db);
return true; return true;
-3
View File
@@ -21,8 +21,6 @@ use \sammo\Constraint\Constraint;
use \sammo\Constraint\ConstraintHelper; use \sammo\Constraint\ConstraintHelper;
use sammo\Enums\InheritanceKey; use sammo\Enums\InheritanceKey;
use function sammo\tryRollbackInheritUniqueItem;
class che_선양 extends Command\GeneralCommand class che_선양 extends Command\GeneralCommand
{ {
static protected $actionName = '선양'; static protected $actionName = '선양';
@@ -139,7 +137,6 @@ class che_선양 extends Command\GeneralCommand
$general->increaseInheritancePoint(InheritanceKey::active_action, 1); $general->increaseInheritancePoint(InheritanceKey::active_action, 1);
$this->setResultTurn(new LastTurn(static::getName(), $this->arg)); $this->setResultTurn(new LastTurn(static::getName(), $this->arg));
$general->checkStatChange(); $general->checkStatChange();
tryRollbackInheritUniqueItem($rng, $general);
$general->applyDB($db); $general->applyDB($db);
$destGeneral->applyDB($db); $destGeneral->applyDB($db);
@@ -13,8 +13,6 @@ use \sammo\{
use \sammo\Constraint\Constraint; use \sammo\Constraint\Constraint;
use \sammo\Constraint\ConstraintHelper; use \sammo\Constraint\ConstraintHelper;
use function sammo\tryRollbackInheritUniqueItem;
class che_소집해제 extends Command\GeneralCommand{ class che_소집해제 extends Command\GeneralCommand{
static protected $actionName = '소집해제'; static protected $actionName = '소집해제';
@@ -82,7 +80,6 @@ class che_소집해제 extends Command\GeneralCommand{
$general->addDedication($ded); $general->addDedication($ded);
$this->setResultTurn(new LastTurn(static::getName(), $this->arg)); $this->setResultTurn(new LastTurn(static::getName(), $this->arg));
$general->checkStatChange(); $general->checkStatChange();
tryRollbackInheritUniqueItem($rng, $general);
$general->applyDB($db); $general->applyDB($db);
return true; return true;
-3
View File
@@ -13,8 +13,6 @@ use \sammo\{
use \sammo\Constraint\Constraint; use \sammo\Constraint\Constraint;
use \sammo\Constraint\ConstraintHelper; use \sammo\Constraint\ConstraintHelper;
use function sammo\tryRollbackInheritUniqueItem;
class che_요양 extends Command\GeneralCommand{ class che_요양 extends Command\GeneralCommand{
static protected $actionName = '요양'; static protected $actionName = '요양';
@@ -71,7 +69,6 @@ class che_요양 extends Command\GeneralCommand{
$general->addDedication($ded); $general->addDedication($ded);
$this->setResultTurn(new LastTurn(static::getName(), $this->arg)); $this->setResultTurn(new LastTurn(static::getName(), $this->arg));
$general->checkStatChange(); $general->checkStatChange();
tryRollbackInheritUniqueItem($rng, $general);
$general->applyDB($db); $general->applyDB($db);
return true; return true;
@@ -13,7 +13,8 @@ use \sammo\{
GameConst, GameConst,
GameUnitConst, GameUnitConst,
LastTurn, LastTurn,
Command Command,
KVStorage
}; };
use function \sammo\buildItemClass; use function \sammo\buildItemClass;
@@ -181,7 +182,7 @@ class che_장비매매 extends Command\GeneralCommand
$general->onArbitraryAction($general, $rng, '장비매매', '판매', ['itemCode' => $itemCode]); $general->onArbitraryAction($general, $rng, '장비매매', '판매', ['itemCode' => $itemCode]);
$general->setItem($itemType, null); $general->setItem($itemType, null);
if(!$itemObj->isBuyable()){ if (!$itemObj->isBuyable()) {
$generalName = $general->getName(); $generalName = $general->getName();
$josaYi = JosaUtil::pick($generalName, '이'); $josaYi = JosaUtil::pick($generalName, '이');
$nationName = $general->getStaticNation()['name']; $nationName = $general->getStaticNation()['name'];
-2
View File
@@ -12,7 +12,6 @@ use \sammo\Command;
use \sammo\Json; use \sammo\Json;
use function \sammo\searchDistance; use function \sammo\searchDistance;
use function sammo\tryRollbackInheritUniqueItem;
use \sammo\Constraint\ConstraintHelper; use \sammo\Constraint\ConstraintHelper;
use sammo\CityConst; use sammo\CityConst;
@@ -214,7 +213,6 @@ class che_첩보 extends Command\GeneralCommand
$general->increaseVar('leadership_exp', 1); $general->increaseVar('leadership_exp', 1);
$this->setResultTurn(new LastTurn(static::getName(), $this->arg)); $this->setResultTurn(new LastTurn(static::getName(), $this->arg));
$general->checkStatChange(); $general->checkStatChange();
tryRollbackInheritUniqueItem($rng, $general);
$general->applyDB($db); $general->applyDB($db);
return true; return true;
-3
View File
@@ -15,8 +15,6 @@ use \sammo\Constraint\ConstraintHelper;
use sammo\CityConst; use sammo\CityConst;
use sammo\Enums\InheritanceKey; use sammo\Enums\InheritanceKey;
use function sammo\tryRollbackInheritUniqueItem;
class che_하야 extends Command\GeneralCommand{ class che_하야 extends Command\GeneralCommand{
static protected $actionName = '하야'; static protected $actionName = '하야';
@@ -118,7 +116,6 @@ class che_하야 extends Command\GeneralCommand{
$this->setResultTurn(new LastTurn(static::getName(), $this->arg)); $this->setResultTurn(new LastTurn(static::getName(), $this->arg));
$general->checkStatChange(); $general->checkStatChange();
tryRollbackInheritUniqueItem($rng, $general);
$general->applyDB($db); $general->applyDB($db);
return true; return true;
-2
View File
@@ -20,7 +20,6 @@ use sammo\Event\EventHandler;
use function sammo\refreshNationStaticInfo; use function sammo\refreshNationStaticInfo;
use function sammo\deleteNation; use function sammo\deleteNation;
use function sammo\tryRollbackInheritUniqueItem;
class che_해산 extends Command\GeneralCommand{ class che_해산 extends Command\GeneralCommand{
static protected $actionName = '해산'; static protected $actionName = '해산';
@@ -99,7 +98,6 @@ class che_해산 extends Command\GeneralCommand{
$oldGeneral->setVar('makelimit', 12); $oldGeneral->setVar('makelimit', 12);
$oldGeneral->applyDB($db); $oldGeneral->applyDB($db);
} }
tryRollbackInheritUniqueItem($rng, $general);
$general->applyDB($db); $general->applyDB($db);
// 이벤트 핸들러 동작 // 이벤트 핸들러 동작
-3
View File
@@ -12,7 +12,6 @@ use \sammo\LastTurn;
use \sammo\Command; use \sammo\Command;
use function \sammo\searchDistance; use function \sammo\searchDistance;
use function sammo\tryRollbackInheritUniqueItem;
use \sammo\Constraint\ConstraintHelper; use \sammo\Constraint\ConstraintHelper;
use sammo\CityConst; use sammo\CityConst;
@@ -296,7 +295,6 @@ class che_화계 extends Command\GeneralCommand
$this->setResultTurn(new LastTurn(static::getName(), $this->arg)); $this->setResultTurn(new LastTurn(static::getName(), $this->arg));
$general->checkStatChange(); $general->checkStatChange();
tryRollbackInheritUniqueItem($rng, $general);
$general->applyDB($db); $general->applyDB($db);
return false; return false;
} }
@@ -330,7 +328,6 @@ class che_화계 extends Command\GeneralCommand
$general->increaseRankVar(RankColumn::firenum, 1); $general->increaseRankVar(RankColumn::firenum, 1);
$this->setResultTurn(new LastTurn(static::getName(), $this->arg)); $this->setResultTurn(new LastTurn(static::getName(), $this->arg));
$general->checkStatChange(); $general->checkStatChange();
tryRollbackInheritUniqueItem($rng, $general);
$general->applyDB($db); $general->applyDB($db);
return true; return true;
-3
View File
@@ -7,8 +7,6 @@ use \sammo\JosaUtil;
use \sammo\LastTurn; use \sammo\LastTurn;
use \sammo\DB; use \sammo\DB;
use function sammo\tryRollbackInheritUniqueItem;
class 휴식 extends Command\GeneralCommand{ class 휴식 extends Command\GeneralCommand{
static protected $actionName = '휴식'; static protected $actionName = '휴식';
@@ -41,7 +39,6 @@ class 휴식 extends Command\GeneralCommand{
$logger->pushGeneralActionLog("아무것도 실행하지 않았습니다. <1>$date</>"); $logger->pushGeneralActionLog("아무것도 실행하지 않았습니다. <1>$date</>");
$this->setResultTurn(new LastTurn()); $this->setResultTurn(new LastTurn());
tryRollbackInheritUniqueItem($rng, $general);
$general->applyDB(DB::db()); $general->applyDB(DB::db());
return true; return true;
} }
+31
View File
@@ -0,0 +1,31 @@
<?php
namespace sammo\DTO;
use sammo\DTO\Attr\Convert;
use sammo\DTO\Attr\JsonString;
use sammo\DTO\Attr\NullIsUndefined;
use sammo\DTO\Attr\RawName;
use sammo\DTO\Converter\DateTimeConverter;
class AuctionBidItem extends DTO
{
public function __construct(
#[NullIsUndefined]
public ?int $no,
#[RawName('auction_id')]
public int $auctionID,
public ?int $owner,
#[RawName('general_id')]
public int $generalID,
public int $amount,
#[Convert(DateTimeConverter::class)]
public \DateTimeImmutable $date,
#[JsonString]
public AuctionBidItemData $aux,
) {
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace sammo\DTO;
use sammo\DTO\Attr\NullIsUndefined;
class AuctionBidItemData extends DTO
{
public function __construct(
#[NullIsUndefined]
public ?string $ownerName,
public string $generalName,
#[NullIsUndefined]
public ?bool $tryExtendCloseDate,
) {
}
}
+37
View File
@@ -0,0 +1,37 @@
<?php
namespace sammo\DTO;
use sammo\DTO\Attr\Convert;
use sammo\DTO\Attr\JsonString;
use sammo\DTO\Attr\NullIsUndefined;
use sammo\DTO\Attr\RawName;
use sammo\DTO\Converter\DateTimeConverter;
use sammo\Enums\AuctionType;
use sammo\Enums\ResourceType;
class AuctionInfo extends DTO
{
public function __construct(
#[NullIsUndefined]
public ?int $id,
public AuctionType $type,
public bool $finished,
public ?string $target,
#[RawName('host_general_id')]
public int $hostGeneralID,
#[RawName('req_resource')]
public ResourceType $reqResource,
#[RawName('open_date')]
#[Convert(DateTimeConverter::class)]
public \DateTimeImmutable $openDate,
#[RawName('close_date')]
#[Convert(DateTimeConverter::class)]
public \DateTimeImmutable $closeDate,
#[JsonString]
public AuctionInfoDetail $detail,
) {
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace sammo\DTO;
use sammo\DTO\Attr\Convert;
use sammo\DTO\Attr\NullIsUndefined;
use sammo\DTO\Converter\DateTimeConverter;
class AuctionInfoDetail extends DTO
{
public function __construct(
public string $title,
public string $hostName,
public int $amount,
#[NullIsUndefined]
public ?bool $isReverse,
public int $startBidAmount,
#[NullIsUndefined]
public ?int $finishBidAmount,
#[NullIsUndefined]
public ?int $remainCloseDateExtensionCnt,
#[NullIsUndefined]
#[Convert(DateTimeConverter::class)]
public ?\DateTimeImmutable $availableLatestBidCloseDate,
) {
}
}
+40 -28
View File
@@ -4,67 +4,79 @@ namespace sammo;
use sammo\Enums\InheritanceKey; use sammo\Enums\InheritanceKey;
class DummyGeneral extends General{ class DummyGeneral extends General
public function __construct(bool $initLogger=true){ {
public function __construct(bool $initLogger = true)
{
$raw = [ $raw = [
'no'=>0, 'no' => 0,
'name'=>'Dummy', 'name' => 'Dummy',
'npc'=>3, 'npc' => 3,
'city'=>0, 'city' => 0,
'nation'=>0, 'nation' => 0,
'officer_level'=>0, 'officer_level' => 0,
'crewtype'=>-1, 'crewtype' => -1,
'turntime'=>'2012-03-04 05:06:07.000000', 'turntime' => '2012-03-04 05:06:07.000000',
'experience'=>0, 'experience' => 0,
'dedication'=>0, 'dedication' => 0,
'gold'=>0, 'gold' => 0,
'rice'=>0, 'rice' => 0,
'leadership'=>10, 'leadership' => 10,
'strength'=>10, 'strength' => 10,
'intel'=>10, 'intel' => 10,
'imgsvr' => 0,
'picture' => 'default.jpg',
]; ];
$this->raw = $raw; $this->raw = $raw;
$this->resultTurn = new LastTurn(); $this->resultTurn = new LastTurn();
if($initLogger){ if ($initLogger) {
$this->initLogger(1, 1); $this->initLogger(1, 1);
} }
} }
public function getBattleInitSkillTriggerList(WarUnit $unit):?WarUnitTriggerCaller{ public function getBattleInitSkillTriggerList(WarUnit $unit): ?WarUnitTriggerCaller
{
return new WarUnitTriggerCaller(); return new WarUnitTriggerCaller();
} }
public function getBattlePhaseSkillTriggerList(WarUnit $unit):?WarUnitTriggerCaller{ public function getBattlePhaseSkillTriggerList(WarUnit $unit): ?WarUnitTriggerCaller
{
return new WarUnitTriggerCaller(); return new WarUnitTriggerCaller();
} }
public function onCalcStat(General $general, string $statName, $value, $aux=null){ public function onCalcStat(General $general, string $statName, $value, $aux = null)
{
return $value; return $value;
} }
public function onCalcOpposeStat(General $general, string $statName, $value, $aux=null){ public function onCalcOpposeStat(General $general, string $statName, $value, $aux = null)
{
return $value; return $value;
} }
public function getInheritancePoint(InheritanceKey $key, &$aux = null, bool $forceCalc = false): int|float{ public function getInheritancePoint(InheritanceKey $key, &$aux = null, bool $forceCalc = false): int|float
{
return 0; return 0;
} }
public function setInheritancePoint(InheritanceKey $key, $value, $aux = null){ public function setInheritancePoint(InheritanceKey $key, $value, $aux = null)
{
return; return;
} }
public function increaseInheritancePoint(InheritanceKey $key, $value, $aux = null){ public function increaseInheritancePoint(InheritanceKey $key, $value, $aux = null)
{
return; return;
} }
function applyDB($db):bool{ function applyDB($db): bool
if($this->logger){ {
if ($this->logger) {
$this->initLogger($this->logger->getYear(), $this->logger->getMonth()); $this->initLogger($this->logger->getYear(), $this->logger->getMonth());
} }
return true; return true;
} }
} }
+14
View File
@@ -0,0 +1,14 @@
<?php
namespace sammo\Enums;
/**
* 입찰자 기준
*/
enum AuctionType: string{
/** 쌀을 매물로 등록, 금으로 구매 */
case BuyRice = 'buyRice';
/** 금을 매물로 등록, 쌀로 판매 */
case SellRice = 'sellRice';
/** 유미크를 매물로 등록, 유산 포인트로 구매 */
case UniqueItem = 'uniqueItem';
}
+18
View File
@@ -0,0 +1,18 @@
<?php
namespace sammo\Enums;
enum ResourceType: string
{
case gold = 'gold';
case rice = 'rice';
case inheritancePoint = 'inheritPoint';
public function getName(): string
{
return match($this){
ResourceType::gold => '금',
ResourceType::rice => '쌀',
ResourceType::inheritancePoint => '유산 포인트',
};
}
}
+1 -17
View File
@@ -610,23 +610,7 @@ class General implements iAction
$refundPoint += GameConst::$inheritItemRandomPoint; $refundPoint += GameConst::$inheritItemRandomPoint;
} }
$itemTrials = $this->getAuxVar('inheritUniqueTrial') ?? []; //TODO: 경매 최우선 입찰자인경우 반환
foreach (array_keys($itemTrials) as $itemKey) {
$trialStor = KVStorage::getStorage($db, "ut_{$itemKey}");
$ownTrial = $trialStor->getValue("u{$userID}");
$itemObj = buildItemClass($itemKey);
$itemName = $itemObj->getName();
if (!$ownTrial) {
continue;
}
[,, $amount] = $ownTrial;
$trialStor->deleteValue("u{$userID}");
$userLogger->push("사망으로 {$itemName} 입찰에 사용한 {$amount} 포인트 반환", "inheritPoint");
$refundPoint += $amount;
}
if ($this->getAuxVar('inheritSpecificSpecialWar')) { if ($this->getAuxVar('inheritSpecificSpecialWar')) {
$this->setAuxVar('inheritSpecificSpecialWar', null); $this->setAuxVar('inheritSpecificSpecialWar', null);
View File
+4 -4
View File
@@ -32,9 +32,6 @@ DROP TABLE IF EXISTS ng_diplomacy;
# #
DROP TABLE IF EXISTS tournament; DROP TABLE IF EXISTS tournament;
#
DROP TABLE IF EXISTS auction;
# #
DROP TABLE IF EXISTS statistic; DROP TABLE IF EXISTS statistic;
@@ -66,4 +63,7 @@ ENGINE=Aria;
DROP TABLE IF EXISTS ng_betting; DROP TABLE IF EXISTS ng_betting;
DROP TABLE IF EXISTS vote; DROP TABLE IF EXISTS vote;
DROP TABLE IF EXISTS vote_comment; DROP TABLE IF EXISTS vote_comment;
DROP TABLE IF EXISTS `ng_auction`;
DROP TABLE IF EXISTS `ng_auction_bid`;
+42 -18
View File
@@ -441,23 +441,6 @@ CREATE TABLE `tournament` (
INDEX `grp` (`grp`, `grp_no`) INDEX `grp` (`grp`, `grp_no`)
) COLLATE = 'utf8mb4_general_ci' ENGINE = Aria; ) COLLATE = 'utf8mb4_general_ci' ENGINE = Aria;
############################## ##############################
##
##############################
create table `auction` (
`no` int(6) not null auto_increment,
`type` int(6) default 0,
`no1` int(6) default 0,
`name1` varchar(64) default '-',
`amount` int(6) default 0,
`cost` int(6) default 0,
`value` int(6) default 0,
`topv` int(6) default 0,
`no2` int(6) default 0,
`name2` varchar(64) default '-',
`expire` datetime,
PRIMARY KEY (`no`)
) COLLATE = 'utf8mb4_general_ci' ENGINE = Aria;
##############################
## ##
############################## ##############################
CREATE TABLE `statistic` ( CREATE TABLE `statistic` (
@@ -684,4 +667,45 @@ CREATE TABLE `vote_comment` (
`date` DATETIME NULL DEFAULT NULL, `date` DATETIME NULL DEFAULT NULL,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
INDEX `by_vote` (`vote_id`) INDEX `by_vote` (`vote_id`)
) COLLATE = 'utf8mb4_general_ci' ENGINE = Aria; ) COLLATE = 'utf8mb4_general_ci' ENGINE = Aria;
##############################
## /
##############################
# KVStorage에
CREATE TABLE `ng_auction` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`type` ENUM('buyRice','sellRice','uniqueItem') NOT NULL COLLATE 'utf8mb4_bin',
`finished` BIT(1) NOT NULL,
`target` VARCHAR(50) NULL DEFAULT NULL COLLATE 'utf8mb4_bin',
`host_general_id` INT(11) NOT NULL,
`req_resource` ENUM('gold','rice','inheritPoint') NOT NULL COLLATE 'utf8mb4_bin',
`open_date` DATETIME NOT NULL,
`close_date` DATETIME NOT NULL,
`detail` LONGTEXT NOT NULL COLLATE 'utf8mb4_bin',
PRIMARY KEY (`id`) USING BTREE,
INDEX `by_close` (`finished`, `type`, `close_date`) USING BTREE,
INDEX `by_general_id` (`host_general_id`, `type`, `finished`) USING BTREE,
CONSTRAINT `detail` CHECK (json_valid(`detail`))
)
COLLATE='utf8mb4_general_ci'
ENGINE=Aria
;
CREATE TABLE `ng_auction_bid` (
`no` INT(11) NOT NULL AUTO_INCREMENT,
`auction_id` INT(11) NOT NULL,
`owner` INT(11) NULL DEFAULT NULL,
`general_id` INT(11) NOT NULL,
`amount` INT(11) NOT NULL,
`date` DATETIME NOT NULL,
`aux` LONGTEXT NOT NULL COLLATE 'utf8mb4_bin',
PRIMARY KEY (`no`),
UNIQUE INDEX `by_general` (`general_id`, `auction_id`, `amount`),
UNIQUE INDEX `by_owner` (`owner`, `auction_id`, `amount`),
UNIQUE INDEX `by_amount` (`auction_id`, `amount`),
CONSTRAINT `aux` CHECK (json_valid(`aux`))
)
COLLATE='utf8mb4_general_ci'
ENGINE = Aria;
+1 -1
View File
@@ -16,5 +16,5 @@
<?=$btnBegin??''?><a href='b_battleCenter.php' target='_blank' class='open-window commandButton <?=$btnClass??""?> <?= $showSecret ? '' : 'disabled' ?>'>감 찰 부</a><?=$btnEnd??''?> <?=$btnBegin??''?><a href='b_battleCenter.php' target='_blank' class='open-window commandButton <?=$btnClass??""?> <?= $showSecret ? '' : 'disabled' ?>'>감 찰 부</a><?=$btnEnd??''?>
<?=$btnBegin??''?><a href='v_inheritPoint.php' class='commandButton <?=$btnClass??""?>'>유산 관리</a><?=$btnEnd??''?> <?=$btnBegin??''?><a href='v_inheritPoint.php' class='commandButton <?=$btnClass??""?>'>유산 관리</a><?=$btnEnd??''?>
<?=$btnBegin??''?><a href='b_myPage.php' class='commandButton <?=$btnClass??""?>'>내 정보&amp;설정</a><?=$btnEnd??''?> <?=$btnBegin??''?><a href='b_myPage.php' class='commandButton <?=$btnClass??""?>'>내 정보&amp;설정</a><?=$btnEnd??''?>
<?=$btnBegin??''?><a href='b_auction.php' target='_blank' class='open-window commandButton <?=$btnClass??""?>'>거 래 장</a><?=$btnEnd??''?> <?=$btnBegin??''?><a href='v_auction.php' target='_blank' class='open-window commandButton <?=$btnClass??""?>'>거 래 장</a><?=$btnEnd??''?>
<?=$btnBegin??''?><a href='b_betting.php' target='_blank' class='open-window <?=$btnClassForBetting??""?>'>베 팅 장</a><?=$btnEnd??''?> <?=$btnBegin??''?><a href='b_betting.php' target='_blank' class='open-window <?=$btnClassForBetting??""?>'>베 팅 장</a><?=$btnEnd??''?>
+32
View File
@@ -0,0 +1,32 @@
<template>
<BContainer id="container" :toast="{ root: true }" class="bg0">
<TopBackBar type="close" :title="isResAuction ? '경매장' : '유니크 경매장'" :reloadable="true" @reload="tryReload">
<BButton @click="isResAuction = true">/</BButton>
<BButton @click="isResAuction = false">유니크</BButton>
</TopBackBar>
<AuctionResource v-if="isResAuction"></AuctionResource>
<AuctionUniqueItem v-else></AuctionUniqueItem>
<BottomBar type="close"></BottomBar>
</BContainer>
</template>
<script lang="ts">
/*declare const staticValues: {
serverID: string,
turnterm: number,
serverNick: string,
};*/
</script>
<script lang="ts" setup>
import { BButton, BContainer } from "bootstrap-vue-3";
import TopBackBar from "@/components/TopBackBar.vue";
import BottomBar from "./components/BottomBar.vue";
import AuctionResource from "@/components/AuctionResource.vue";
import AuctionUniqueItem from "@/components/AuctionUniqueItem.vue";
import { ref } from "vue";
const isResAuction = ref(true);
function tryReload() {
console.log('갱신');
}
</script>
+16 -14
View File
@@ -62,12 +62,12 @@
> >
</div> </div>
<div class="row px-4"> <div class="row px-4">
<b-button class="col-6 offset-6" variant="primary" @click="setNextSpecialWar"> 구입 </b-button> <BButton class="col-6 offset-6" variant="primary" @click="setNextSpecialWar"> 구입 </BButton>
</div> </div>
</div> </div>
<div class="col col-md-4 col-sm-6 col-12 py-2"> <div class="col col-md-4 col-sm-6 col-12 py-2">
<div class="row px-4"> <div class="row px-4">
<div class="a-right col-6 align-self-center">유니크 입찰</div> <div class="a-right col-6 align-self-center">유니크 경매</div>
<div class="col-6"> <div class="col-6">
<select v-model="specificUnique" class="form-select col-6"> <select v-model="specificUnique" class="form-select col-6">
<option disabled selected :value="null"> 유니크 선택 </option> <option disabled selected :value="null"> 유니크 선택 </option>
@@ -89,14 +89,14 @@
</div> </div>
<div class="a-right"> <div class="a-right">
<small class="form-text text-muted" <small class="form-text text-muted"
>얻고자 하는 유니크 아이템 포인트 걸어 입찰합니다. 최고 포인트인 경우 다음 유니크를 얻습니다.<br /> >얻고자 하는 유니크 아이템으로 경매 시작합니다. 24 동안 진행됩니다.<br />
<!-- eslint-disable-next-line vue/no-v-html --> <!-- eslint-disable-next-line vue/no-v-html -->
<span style="color: white" v-html="specificUnique == null?'':availableUnique[specificUnique].info" /> <span style="color: white" v-html="specificUnique == null?'':availableUnique[specificUnique].info" />
</small> </small>
</div> </div>
<div class="row px-4"> <div class="row px-4">
<b-button class="col-6 offset-6" variant="primary" @click="buySpecificUnique"> 구입 </b-button> <BButton class="col-6 offset-6" variant="primary" @click="openUniqueItemAuction"> 경매 시작 </BButton>
</div> </div>
</div> </div>
</div> </div>
@@ -107,7 +107,7 @@
<div class="col col-md-4 col-sm-6 col-12 py-2"> <div class="col col-md-4 col-sm-6 col-12 py-2">
<div class="row px-4"> <div class="row px-4">
<div class="a-right col-6 align-self-center">랜덤 초기화</div> <div class="a-right col-6 align-self-center">랜덤 초기화</div>
<b-button class="col-6" variant="primary" @click="buySimple('ResetTurnTime')"> 구입 </b-button> <BButton class="col-6" variant="primary" @click="buySimple('ResetTurnTime')"> 구입 </BButton>
</div> </div>
<div class="a-right"> <div class="a-right">
<small class="form-text text-muted" <small class="form-text text-muted"
@@ -120,7 +120,7 @@
<div class="col col-md-4 col-sm-6 col-12 py-2"> <div class="col col-md-4 col-sm-6 col-12 py-2">
<div class="row px-4"> <div class="row px-4">
<div class="a-right col-6 align-self-center">랜덤 유니크 획득</div> <div class="a-right col-6 align-self-center">랜덤 유니크 획득</div>
<b-button class="col-6" variant="primary" @click="buySimple('BuyRandomUnique')"> 구입 </b-button> <BButton class="col-6" variant="primary" @click="buySimple('BuyRandomUnique')"> 구입 </BButton>
</div> </div>
<div class="a-right"> <div class="a-right">
<small class="form-text text-muted" <small class="form-text text-muted"
@@ -133,7 +133,7 @@
<div class="col col-md-4 col-sm-6 col-12 py-2"> <div class="col col-md-4 col-sm-6 col-12 py-2">
<div class="row px-4"> <div class="row px-4">
<div class="a-right col-6 align-self-center">즉시 전투 특기 초기화</div> <div class="a-right col-6 align-self-center">즉시 전투 특기 초기화</div>
<b-button class="col-6" variant="primary" @click="buySimple('ResetSpecialWar')"> 구입 </b-button> <BButton class="col-6" variant="primary" @click="buySimple('ResetSpecialWar')"> 구입 </BButton>
</div> </div>
<div class="a-right"> <div class="a-right">
<small class="form-text text-muted" <small class="form-text text-muted"
@@ -173,13 +173,13 @@
> >
</div> </div>
<div class="row px-4" style="margin-bottom: 1em"> <div class="row px-4" style="margin-bottom: 1em">
<b-button <BButton
variant="secondary" variant="secondary"
class="col col-md-6 col-4 offset-md-0 offset-4" class="col col-md-6 col-4 offset-md-0 offset-4"
@click="inheritBuff[buffKey] = prevInheritBuff[buffKey] ?? 0" @click="inheritBuff[buffKey] = prevInheritBuff[buffKey] ?? 0"
> >
리셋 </b-button 리셋 </BButton
><b-button variant="primary" class="col col-md-6 col-4" @click="buyInheritBuff(buffKey)"> 구입 </b-button> ><BButton variant="primary" class="col col-md-6 col-4" @click="buyInheritBuff(buffKey)"> 구입 </BButton>
</div> </div>
</div> </div>
</div> </div>
@@ -209,6 +209,7 @@ import NumberInputWithInfo from "@/components/NumberInputWithInfo.vue";
import { SammoAPI } from "./SammoAPI"; import { SammoAPI } from "./SammoAPI";
import type { inheritBuffType } from "./defs/API/InheritAction"; import type { inheritBuffType } from "./defs/API/InheritAction";
import * as JosaUtil from '@/util/JosaUtil'; import * as JosaUtil from '@/util/JosaUtil';
import { BButton } from "bootstrap-vue-3";
type InheritanceType = type InheritanceType =
| "previous" | "previous"
@@ -376,6 +377,7 @@ export default defineComponent({
components: { components: {
TopBackBar, TopBackBar,
NumberInputWithInfo, NumberInputWithInfo,
BButton,
}, },
data() { data() {
const inheritBuff = {} as Record<inheritBuffType, number>; const inheritBuff = {} as Record<inheritBuffType, number>;
@@ -519,7 +521,7 @@ export default defineComponent({
//TODO: 페이지 새로고침 필요없이 하도록 //TODO: 페이지 새로고침 필요없이 하도록
location.reload(); location.reload();
}, },
async buySpecificUnique() { async openUniqueItemAuction() {
if(this.specificUnique === null){ if(this.specificUnique === null){
alert("유니크를 선택해주세요."); alert("유니크를 선택해주세요.");
return; return;
@@ -544,8 +546,8 @@ export default defineComponent({
} }
try { try {
await SammoAPI.InheritAction.BuySpecificUnique({ await SammoAPI.Auction.OpenUniqueAuction({
item: this.specificUnique, itemID: this.specificUnique,
amount, amount,
}); });
} catch (e) { } catch (e) {
@@ -554,7 +556,7 @@ export default defineComponent({
return; return;
} }
alert("성공했습니다."); alert("성공했습니다. 경매장을 확인해주세요.");
//TODO: 페이지 새로고침 필요없이 하도록 //TODO: 페이지 새로고침 필요없이 하도록
location.reload(); location.reload();
}, },
+244 -160
View File
@@ -1,7 +1,16 @@
import type { Args } from "./processing/args"; import type { Args } from "./processing/args";
import { import {
callSammoAPI, extractHttpMethod, GET, PATCH, POST, PUT, callSammoAPI,
type APITail, type APICallT, type RawArgType, type ValidResponse, type InvalidResponse extractHttpMethod,
GET,
PATCH,
POST,
PUT,
type APITail,
type APICallT,
type RawArgType,
type ValidResponse,
type InvalidResponse,
} from "./util/callSammoAPI"; } from "./util/callSammoAPI";
export type { ValidResponse, InvalidResponse }; export type { ValidResponse, InvalidResponse };
import { APIPathGen, NumVar, StrVar } from "./util/APIPathGen.js"; import { APIPathGen, NumVar, StrVar } from "./util/APIPathGen.js";
@@ -12,168 +21,243 @@ import type { inheritBuffType } from "./defs/API/InheritAction";
import type { SetBlockWarResponse, GeneralListResponse as NationGeneralListResponse } from "./defs/API/Nation"; import type { SetBlockWarResponse, GeneralListResponse as NationGeneralListResponse } from "./defs/API/Nation";
import type { UploadImageResponse } from "./defs/API/Misc"; import type { UploadImageResponse } from "./defs/API/Misc";
import type { GeneralLogType, GetGeneralLogResponse, JoinArgs } from "./defs/API/General"; import type { GeneralLogType, GetGeneralLogResponse, JoinArgs } from "./defs/API/General";
import type { GetConstResponse, GetCurrentHistoryResponse, GetDiplomacyResponse, GetHistoryResponse } from "./defs/API/Global"; import type {
GetConstResponse,
GetCurrentHistoryResponse,
GetDiplomacyResponse,
GetHistoryResponse,
} from "./defs/API/Global";
import type { CachedMapResult, GeneralListResponse, ItemTypeKey, MapResult } from "./defs"; import type { CachedMapResult, GeneralListResponse, ItemTypeKey, MapResult } from "./defs";
import type { VoteDetailResult, VoteListResult } from "./defs/API/Vote"; import type { VoteDetailResult, VoteListResult } from "./defs/API/Vote";
import type { ActiveResourceAuctionList, OpenAuctionResponse, UniqueItemAuctionDetail, UniqueItemAuctionList } from "./defs/API/Auction";
const apiRealPath = { const apiRealPath = {
Betting: { Auction: {
Bet: PUT as APICallT<{ BidBuyRiceAuction: PUT as APICallT<{
bettingID: number, auctionID: number;
bettingType: number[], amount: number;
amount: number, }>,
}>, BidSellRiceAuction: PUT as APICallT<{
GetBettingDetail: NumVar('betting_id', auctionID: number;
GET as APICallT<undefined, BettingDetailResponse> amount: number;
), }>,
GetBettingList: GET as APICallT<{ GetActiveResourceAuctionList: GET as APICallT<undefined, ActiveResourceAuctionList>,
req?: 'bettingNation' | 'tournament' OpenBuyRiceAuction: POST as APICallT<
}, BettingListResponse>, {
}, amount: number;
Command: { closeTurnCnt: number;
GetReservedCommand: GET as APICallT<undefined, ReservedCommandResponse>, startBidAmount: number;
PushCommand: PUT as APICallT<{ finishBidAmount: number;
amount: number },
}>, OpenAuctionResponse
RepeatCommand: PUT as APICallT<{ >,
amount: number OpenSellRiceAuction: POST as APICallT<
}>, {
ReserveCommand: PUT as APICallT<{ amount: number;
turnList: number[], closeTurnCnt: number;
action: string, startBidAmount: number;
arg?: Args finishBidAmount: number;
}, ReserveCommandResponse>, },
ReserveBulkCommand: PUT as APICallT<{ OpenAuctionResponse
turnList: number[], >,
action: string,
arg?: Args BidUniqueAuction: PUT as APICallT<{
}[], ReserveBulkCommandResponse>, auctionID: number;
}, amount: number;
General: { }>,
Join: POST as APICallT<JoinArgs>, GetUniqueItemAuctionDetail: GET as APICallT<{
GetGeneralLog: GET as APICallT<{ auctionID: number;
reqType: GeneralLogType, }, UniqueItemAuctionDetail>,
reqTo?: number GetUniqueItemAuctionList: GET as APICallT<undefined, UniqueItemAuctionList>,
}, GetGeneralLogResponse>, OpenUniqueAuction: POST as APICallT<{
DropItem: PUT as APICallT<{ itemID: string,
itemType: ItemTypeKey amount: number,
}> }, OpenAuctionResponse>,
}, },
Global: { Betting: {
GeneralList: GET as APICallT<undefined, GeneralListResponse>, Bet: PUT as APICallT<{
GeneralListWithToken: GET as APICallT<undefined, GeneralListResponse>, bettingID: number;
GetConst: GET as APICallT<undefined, GetConstResponse>, bettingType: number[];
GetHistory: StrVar('serverID')( amount: number;
NumVar('year', }>,
NumVar('month', GET as APICallT<undefined, GetHistoryResponse> GetBettingDetail: NumVar("betting_id", GET as APICallT<undefined, BettingDetailResponse>),
))), GetBettingList: GET as APICallT<
GetCurrentHistory: GET as APICallT<undefined, GetCurrentHistoryResponse>, {
GetMap: GET as APICallT<{ req?: "bettingNation" | "tournament";
neutralView?: 0 | 1, },
showMe?: 0 | 1, BettingListResponse
}, MapResult>, >,
GetCachedMap: GET as APICallT<undefined, CachedMapResult>, },
GetDiplomacy: GET as APICallT<undefined, GetDiplomacyResponse>, Command: {
ExecuteEngine: POST as APICallT<undefined, ValidResponse & { updated: boolean }>, GetReservedCommand: GET as APICallT<undefined, ReservedCommandResponse>,
}, PushCommand: PUT as APICallT<{
InheritAction: { amount: number;
BuyHiddenBuff: PUT as APICallT<{ }>,
type: inheritBuffType, RepeatCommand: PUT as APICallT<{
level: number amount: number;
}>, }>,
BuyRandomUnique: PUT as APICallT<undefined>, ReserveCommand: PUT as APICallT<
BuySpecificUnique: PUT as APICallT<{ {
item: string, turnList: number[];
amount: number, action: string;
}>, arg?: Args;
ResetSpecialWar: PUT as APICallT<undefined>, },
ResetTurnTime: PUT as APICallT<undefined>, ReserveCommandResponse
SetNextSpecialWar: PUT as APICallT<{ >,
type: string, ReserveBulkCommand: PUT as APICallT<
}>, {
}, turnList: number[];
Misc: { action: string;
UploadImage: POST as APICallT<{ arg?: Args;
imageData: string, }[],
}, UploadImageResponse> ReserveBulkCommandResponse
}, >,
NationCommand: { },
GetReservedCommand: GET as APICallT<undefined, ChiefResponse>, General: {
PushCommand: PUT as APICallT<{ Join: POST as APICallT<JoinArgs>,
amount: number GetGeneralLog: GET as APICallT<
}>, {
RepeatCommand: PUT as APICallT<{ reqType: GeneralLogType;
amount: number reqTo?: number;
}>, },
ReserveCommand: PUT as APICallT<{ GetGeneralLogResponse
turnList: number[], >,
action: string, DropItem: PUT as APICallT<{
arg?: Args itemType: ItemTypeKey;
}, ReserveCommandResponse>, }>,
ReserveBulkCommand: PUT as APICallT<{ },
turnList: number[], Global: {
action: string, GeneralList: GET as APICallT<undefined, GeneralListResponse>,
arg?: Args GeneralListWithToken: GET as APICallT<undefined, GeneralListResponse>,
}[], ReserveBulkCommandResponse>, GetConst: GET as APICallT<undefined, GetConstResponse>,
}, GetHistory: StrVar("serverID")(NumVar("year", NumVar("month", GET as APICallT<undefined, GetHistoryResponse>))),
Nation: { GetCurrentHistory: GET as APICallT<undefined, GetCurrentHistoryResponse>,
GeneralList: GET as APICallT<undefined, NationGeneralListResponse>, GetMap: GET as APICallT<
SetNotice: PUT as APICallT<{ {
msg: string, neutralView?: 0 | 1;
}>, showMe?: 0 | 1;
SetScoutMsg: PUT as APICallT<{ },
msg: string, MapResult
}>, >,
SetBill: PATCH as APICallT<{ GetCachedMap: GET as APICallT<undefined, CachedMapResult>,
amount: number, GetDiplomacy: GET as APICallT<undefined, GetDiplomacyResponse>,
}>, ExecuteEngine: POST as APICallT<undefined, ValidResponse & { updated: boolean }>,
SetRate: PATCH as APICallT<{ },
amount: number, InheritAction: {
}>, BuyHiddenBuff: PUT as APICallT<{
SetSecretLimit: PATCH as APICallT<{ type: inheritBuffType;
amount: number, level: number;
}>, }>,
SetBlockWar: PATCH as APICallT<{ BuyRandomUnique: PUT as APICallT<undefined>,
value: boolean, ResetSpecialWar: PUT as APICallT<undefined>,
}, SetBlockWarResponse>, ResetTurnTime: PUT as APICallT<undefined>,
SetBlockScout: PATCH as APICallT<{ SetNextSpecialWar: PUT as APICallT<{
value: boolean, type: string;
}>, }>,
GetGeneralLog: GET as APICallT<{ },
generalID: number, Misc: {
reqType: GeneralLogType, UploadImage: POST as APICallT<
reqTo?: number {
}, GetGeneralLogResponse> imageData: string;
}, },
Vote: { UploadImageResponse
AddComment: POST as APICallT<{ >,
voteID: number, },
text: string, NationCommand: {
}>, GetReservedCommand: GET as APICallT<undefined, ChiefResponse>,
GetVoteList: GET as APICallT<undefined, VoteListResult>, PushCommand: PUT as APICallT<{
GetVoteDetail: GET as APICallT<{ amount: number;
voteID: number }>,
}, VoteDetailResult>, RepeatCommand: PUT as APICallT<{
NewVote: POST as APICallT<{ amount: number;
title: string, }>,
multipleOptions?: number, ReserveCommand: PUT as APICallT<
endDate?: string, {
options: string[], turnList: number[];
keepOldVote?: boolean, action: string;
}>, arg?: Args;
Vote: POST as APICallT<{ },
voteID: number, ReserveCommandResponse
selection: number[], >,
}, ValidResponse & {wonLottery: boolean}>, ReserveBulkCommand: PUT as APICallT<
} {
turnList: number[];
action: string;
arg?: Args;
}[],
ReserveBulkCommandResponse
>,
},
Nation: {
GeneralList: GET as APICallT<undefined, NationGeneralListResponse>,
SetNotice: PUT as APICallT<{
msg: string;
}>,
SetScoutMsg: PUT as APICallT<{
msg: string;
}>,
SetBill: PATCH as APICallT<{
amount: number;
}>,
SetRate: PATCH as APICallT<{
amount: number;
}>,
SetSecretLimit: PATCH as APICallT<{
amount: number;
}>,
SetBlockWar: PATCH as APICallT<
{
value: boolean;
},
SetBlockWarResponse
>,
SetBlockScout: PATCH as APICallT<{
value: boolean;
}>,
GetGeneralLog: GET as APICallT<
{
generalID: number;
reqType: GeneralLogType;
reqTo?: number;
},
GetGeneralLogResponse
>,
},
Vote: {
AddComment: POST as APICallT<{
voteID: number;
text: string;
}>,
GetVoteList: GET as APICallT<undefined, VoteListResult>,
GetVoteDetail: GET as APICallT<
{
voteID: number;
},
VoteDetailResult
>,
NewVote: POST as APICallT<{
title: string;
multipleOptions?: number;
endDate?: string;
options: string[];
keepOldVote?: boolean;
}>,
Vote: POST as APICallT<
{
voteID: number;
selection: number[];
},
ValidResponse & { wonLottery: boolean }
>,
},
} as const; } as const;
export const SammoAPI = APIPathGen(apiRealPath, (path: string[], tail: APITail, pathParam) => { export const SammoAPI = APIPathGen(apiRealPath, (path: string[], tail: APITail, pathParam) => {
const method = extractHttpMethod(tail); const method = extractHttpMethod(tail);
return (args?: RawArgType, returnError?: boolean) => { return (args?: RawArgType, returnError?: boolean) => {
if (returnError) { if (returnError) {
return callSammoAPI(method, path.join('/'), args, pathParam, true); return callSammoAPI(method, path.join("/"), args, pathParam, true);
} }
return callSammoAPI(method, path.join('/'), args, pathParam); return callSammoAPI(method, path.join("/"), args, pathParam);
}; };
}); });
+1
View File
@@ -22,6 +22,7 @@
"common": "common_deprecated.ts" "common": "common_deprecated.ts"
}, },
"ingame_vue": { "ingame_vue": {
"v_auction": "v_auction.ts",
"v_inheritPoint": "v_inheritPoint.ts", "v_inheritPoint": "v_inheritPoint.ts",
"v_board": "v_board.ts", "v_board": "v_board.ts",
"v_cachedMap": "v_cachedMap", "v_cachedMap": "v_cachedMap",
+289
View File
@@ -0,0 +1,289 @@
<template>
<div class="bg0">
<div class="bg2">거래장</div>
<div style="background-color: orange"> 구매</div>
<div class="row gx-0">
<div class="col">번호</div>
<div class="col">판매자</div>
<div class="col">물품</div>
<div class="col">수량</div>
<div class="col">시작 구매가</div>
<div class="col">현재 구매가</div>
<div class="col">즉시 구매가</div>
<div class="col">단가</div>
<div class="col">구매 예정자</div>
<div class="col">거래 종료</div>
</div>
<div v-for="auction of buyRice" :key="auction.id" class="row gx-0" @click="selectedBuyRiceAuction = auction">
<div class="col">{{ auction.id }}</div>
<div class="col">{{ auction.hostName }}</div>
<div class="col"></div>
<div class="col">{{ auction.amount }}</div>
<div class="col">{{ auction.startBidAmount }}</div>
<div class="col">{{ auction.highestBid ? auction.highestBid.amount : "-" }}</div>
<div class="col">{{ auction.finishBidAmount }}</div>
<div class="col">{{ auction.highestBid ? (auction.highestBid.amount / auction.amount).toFixed(2) : "-" }}</div>
<div class="col">{{ auction.highestBid ? auction.highestBid.generalName : "-" }}</div>
<div class="col">{{ auction.closeDate }}</div>
</div>
<div v-if="selectedBuyRiceAuction !== undefined" class="row">
<div class="col">{{ selectedBuyRiceAuction.id }} {{ selectedBuyRiceAuction.amount }} 경매에</div>
<div class="col">
<NumberInputWithInfo
v-model="bidAmountBuyRiceAuction"
:int="true"
:min="selectedBuyRiceAuction.startBidAmount"
:max="selectedBuyRiceAuction.finishBidAmount"
title="금"
:step="10"
></NumberInputWithInfo>
</div>
<div class="col"><BButton @click="bidBuyRiceAuction">입찰</BButton></div>
</div>
<div style="background-color: skyblue"> 판매</div>
<div class="row gx-0">
<div class="col">번호</div>
<div class="col">판매자</div>
<div class="col">물품</div>
<div class="col">수량</div>
<div class="col">시작 구매가</div>
<div class="col">현재 구매가</div>
<div class="col">즉시 구매가</div>
<div class="col">단가</div>
<div class="col">구매 예정자</div>
<div class="col">거래 종료</div>
</div>
<div v-for="auction of sellRice" :key="auction.id" class="row gx-0" @click="selectedSellRiceAuction = auction">
<div class="col">{{ auction.id }}</div>
<div class="col">{{ auction.hostName }}</div>
<div class="col"></div>
<div class="col">{{ auction.amount }}</div>
<div class="col">{{ auction.startBidAmount }}</div>
<div class="col">{{ auction.highestBid ? auction.highestBid.amount : "-" }}</div>
<div class="col">{{ auction.finishBidAmount }}</div>
<div class="col">{{ auction.highestBid ? (auction.highestBid.amount / auction.amount).toFixed(2) : "-" }}</div>
<div class="col">{{ auction.highestBid ? auction.highestBid.generalName : "-" }}</div>
<div class="col">{{ auction.closeDate }}</div>
</div>
<div v-if="selectedSellRiceAuction !== undefined" class="row">
<div class="col">{{ selectedSellRiceAuction.id }} {{ selectedSellRiceAuction.amount }} 경매에</div>
<div class="col">
<NumberInputWithInfo
v-model="bidAmountSellRiceAuction"
:int="true"
:min="selectedSellRiceAuction.startBidAmount"
:max="selectedSellRiceAuction.finishBidAmount"
title="쌀"
:step="10"
></NumberInputWithInfo>
</div>
<div class="col"><BButton @click="bidSellRiceAuction">입찰</BButton></div>
</div>
<div>경매 등록</div>
<div class="row">
<div class="col">
매물
<BButtonGroup>
<BButton :pressed="openAuctionInfo.type == 'buyRice'" @click="openAuctionInfo.type = 'buyRice'"> </BButton>
<BButton :pressed="openAuctionInfo.type == 'sellRice'" @click="openAuctionInfo.type = 'sellRice'">
</BButton>
</BButtonGroup>
</div>
<div class="col">
<NumberInputWithInfo
v-model="openAuctionInfo.closeTurnCnt"
:int="true"
:min="3"
:max="24"
title="기간(턴)"
:step="1"
></NumberInputWithInfo>
</div>
<div class="col">
<NumberInputWithInfo
v-model="openAuctionInfo.amount"
:int="true"
:min="100"
:max="10000"
title="수량"
:step="10"
></NumberInputWithInfo>
</div>
<div class="col">
<NumberInputWithInfo
v-model="openAuctionInfo.startBidAmount"
:int="true"
:min="100"
:max="10000"
title="시작구매가"
:step="10"
></NumberInputWithInfo>
</div>
<div class="col">
<NumberInputWithInfo
v-model="openAuctionInfo.finishBidAmount"
:int="true"
:min="100"
:max="10000"
title="즉시구매가"
:step="10"
></NumberInputWithInfo>
</div>
<div class="col">
<BButton @click="openAuction">등록</BButton>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import type { BasicResourceAuctionInfo } from "@/defs/API/Auction";
import { SammoAPI } from "@/SammoAPI";
import { unwrap } from "@/util/unwrap";
import { useToast, BButtonGroup, BButton } from "bootstrap-vue-3";
import { isString } from "lodash";
import { onMounted, reactive, ref, watch } from "vue";
import NumberInputWithInfo from "@/components/NumberInputWithInfo.vue";
const toasts = unwrap(useToast());
const buyRice = ref<BasicResourceAuctionInfo[]>([]);
const sellRice = ref<BasicResourceAuctionInfo[]>([]);
const selectedBuyRiceAuction = ref<BasicResourceAuctionInfo | undefined>(undefined);
const bidAmountBuyRiceAuction = ref<number>(0);
watch(selectedBuyRiceAuction, (auction) => {
if (!auction) {
return;
}
bidAmountBuyRiceAuction.value = auction.highestBid ? auction.highestBid.amount : auction.startBidAmount;
});
async function bidBuyRiceAuction() {
if (selectedBuyRiceAuction.value === undefined) {
return;
}
try {
await SammoAPI.Auction.BidBuyRiceAuction({
auctionID: selectedBuyRiceAuction.value.id,
amount: bidAmountBuyRiceAuction.value,
});
toasts.success({
title: "입찰 완료",
body: `입찰했습니다.`,
});
await refresh();
} catch (e) {
console.error(e);
if (isString(e)) {
toasts.danger({
title: "에러",
body: e,
});
}
}
}
const selectedSellRiceAuction = ref<BasicResourceAuctionInfo | undefined>(undefined);
const bidAmountSellRiceAuction = ref<number>(0);
watch(selectedSellRiceAuction, (auction) => {
if (!auction) {
return;
}
bidAmountSellRiceAuction.value = auction.highestBid ? auction.highestBid.amount : auction.startBidAmount;
});
async function bidSellRiceAuction() {
if (selectedSellRiceAuction.value === undefined) {
return;
}
try {
await SammoAPI.Auction.BidSellRiceAuction({
auctionID: selectedSellRiceAuction.value.id,
amount: bidAmountSellRiceAuction.value,
});
toasts.success({
title: "입찰 완료",
body: `입찰했습니다.`,
});
await refresh();
} catch (e) {
console.error(e);
if (isString(e)) {
toasts.danger({
title: "에러",
body: e,
});
}
}
}
type openAuctionT = {
type: "buyRice" | "sellRice";
amount: number;
startBidAmount: number;
finishBidAmount: number;
closeTurnCnt: number;
};
const openAuctionInfo = reactive<openAuctionT>({
type: "buyRice",
amount: 1000,
startBidAmount: 500,
finishBidAmount: 2000,
closeTurnCnt: 24,
});
async function refresh() {
try {
const result = await SammoAPI.Auction.GetActiveResourceAuctionList();
buyRice.value = result.buyRice;
sellRice.value = result.sellRice;
} catch (e) {
console.error(e);
if (isString(e)) {
toasts.danger({
title: "에러",
body: e,
});
}
}
}
async function openAuction() {
const { type, amount, startBidAmount, finishBidAmount, closeTurnCnt } = openAuctionInfo;
try {
const apiCall = type === "buyRice" ? SammoAPI.Auction.OpenBuyRiceAuction : SammoAPI.Auction.OpenSellRiceAuction;
const result = await apiCall({
amount,
startBidAmount,
finishBidAmount,
closeTurnCnt,
});
toasts.success({
title: "성공",
body: `${result.auctionID}번 경매로 등록되었습니다.`,
});
await refresh();
} catch (e) {
console.error(e);
if (isString(e)) {
toasts.danger({
title: "에러",
body: e,
});
}
}
}
onMounted(async () => {
void refresh();
console.log("mounted");
});
</script>
+155
View File
@@ -0,0 +1,155 @@
<template>
<div class="bg0">
<div class="bg2">유니크 경매장</div>
<div> 가명: {{ obfuscatedName }}</div>
<template v-if="currentAuction !== undefined">
<div class="bg1">상세</div>
<div class="row gx-0">
<div class="col-4 col-md-2 bg2">경매 번호</div>
<div class="col-4 col-md-2">{{ currentAuction.auction.id }}</div>
<div class="col-4 col-md-2 bg2">경매명</div>
<div class="col-4 col-md-2">{{ currentAuction.auction.title }}</div>
<div class="col-4 col-md-2 bg2">주최자(익명)</div>
<div class="col-4 col-md-2">{{ currentAuction.auction.hostName }}</div>
<div class="col-4 col-md-2 bg2">종료시점</div>
<div class="col-4 col-md-2">{{ currentAuction.auction.closeDate }}</div>
<div class="col-4 col-md-2 bg2">최대지연</div>
<div class="col-4 col-md-2">{{ currentAuction.auction.availableLatestBidCloseDate }}</div>
</div>
<div class="bg1">입찰자 목록</div>
<div class="row gx-0 bg2">
<div class="col-4">입찰자</div>
<div class="col-4">입찰포인트</div>
<div class="col-4">시각</div>
</div>
<div v-for="bidder of currentAuction.bidList" :key="bidder.amount" class="row gx-0">
<div class="col-4">{{ bidder.generalName }}</div>
<div class="col-4">{{ bidder.amount }}</div>
<div class="col-4">{{ bidder.date }}</div>
</div>
<div class="bg1">입찰하기</div>
<div class="row">
<div class="col">
<NumberInputWithInfo
v-model="bidAmount"
:int="true"
:min="currentAuction.bidList[0].amount"
title="유산포인트"
:step="1"
></NumberInputWithInfo>
</div>
<div class="col"><BButton @click="bidAuction">입찰</BButton></div>
</div>
</template>
<div class="bg1">목록</div>
<div v-for="[auctionID, auction] of auctionList" :key="auctionID" class="row" @click="currentAuctionID = auctionID">
<div class="col">{{ auction.id }}</div>
<div class="col">{{ auction.finished ? "종료됨" : "진행중" }}</div>
<div class="col">{{ auction.title }}</div>
<div class="col">{{ auction.hostName }}</div>
<div class="col">{{ auction.closeDate }}</div>
<div class="col">{{ auction.remainCloseDateExtensionCnt > 0 ? "남음" : "소진" }}</div>
<div class="col">{{ auction.highestBid.generalName }}</div>
<div class="col">{{ auction.highestBid.amount }}</div>
</div>
</div>
</template>
<script lang="ts" setup>
import type { UniqueItemAuctionDetail, UniqueItemAuctionList } from "@/defs/API/Auction";
import { SammoAPI } from "@/SammoAPI";
import { unwrap } from "@/util/unwrap";
import { useToast, BButton } from "bootstrap-vue-3";
import { isString } from "lodash";
import { onMounted, ref, watch } from "vue";
import NumberInputWithInfo from "@/components/NumberInputWithInfo.vue";
type AuctionItemInfo = UniqueItemAuctionList["list"][0];
const currentAuctionID = ref<number>();
const currentAuction = ref<UniqueItemAuctionDetail | undefined>(undefined);
const bidAmount = ref<number>(5000);
async function refreshDetail() {
if (currentAuctionID.value === undefined) {
return;
}
const auctionID = currentAuctionID.value;
try {
currentAuction.value = await SammoAPI.Auction.GetUniqueItemAuctionDetail({ auctionID });
} catch (e) {
console.error(e);
if (isString(e)) {
unwrap(useToast()).danger({
title: "에러",
body: e,
});
}
}
}
watch(currentAuctionID, () => {
void refreshDetail();
});
const auctionList = ref(new Map<number, AuctionItemInfo>());
const obfuscatedName = ref("");
const toasts = unwrap(useToast());
async function refresh() {
try {
const result = await SammoAPI.Auction.GetUniqueItemAuctionList();
obfuscatedName.value = result.obfuscatedName;
auctionList.value = new Map(result.list.map((auction) => [auction.id, auction]));
if (currentAuctionID.value === undefined && result.list.length > 0) {
currentAuctionID.value = result.list[0].id;
bidAmount.value = result.list[0].highestBid.amount;
}
} catch (e) {
console.error(e);
if (isString(e)) {
toasts.danger({
title: "에러",
body: e,
});
}
}
}
async function bidAuction() {
if (currentAuction.value === undefined) {
return;
}
const amount = bidAmount.value;
const auctionInfo = currentAuction.value.auction;
if (confirm(`${auctionInfo.title}${amount}유산포인트를 입찰하시겠습니까?`)) {
try {
await SammoAPI.Auction.BidUniqueAuction({ auctionID: auctionInfo.id, amount });
toasts.success({
title: "성공",
body: "입찰이 완료되었습니다.",
});
void refreshDetail();
void refresh();
} catch (e) {
console.error(e);
if (isString(e)) {
toasts.danger({
title: "에러",
body: e,
});
}
}
}
}
onMounted(() => {
void refresh();
});
</script>
+64
View File
@@ -0,0 +1,64 @@
import type { ValidResponse } from "@/util/callSammoAPI";
export type BasicResourceAuctionBidder = {
amount: number;
date: string;
generalID: number;
generalName: string;
};
export type BasicResourceAuctionInfo = {
id: number;
type: "buyRice" | "sellRice";
hostGeneralID: number;
hostName: string;
openDate: string;
closeDate: string;
amount: number;
startBidAmount: number;
finishBidAmount: number;
highestBid: BasicResourceAuctionBidder;
};
export type UniqueItemAuctionBidder = {
generalName: string;
amount: number;
isCallerHighestBidder: boolean;
date: string;
};
export type UniqueItemAuctionInfo = {
id: number;
finished: boolean;
title: string;
target: string;
isCallerHost: boolean;
hostName: string;
closeDate: string;
remainCloseDateExtensionCnt: number;
availableLatestBidCloseDate: string;
};
export type ActiveResourceAuctionList = ValidResponse & {
buyRice: BasicResourceAuctionInfo[];
sellRice: BasicResourceAuctionInfo[];
recentLogs: string[];
generalID: number;
};
export type UniqueItemAuctionList = ValidResponse & {
list: (UniqueItemAuctionInfo & {
highestBid: UniqueItemAuctionBidder;
})[];
obfuscatedName: string;
};
export type UniqueItemAuctionDetail = ValidResponse & {
auction: UniqueItemAuctionInfo;
bidList: UniqueItemAuctionBidder[];
obfuscatedName: string;
};
export type OpenAuctionResponse = ValidResponse & {
auctionID: number;
};
+14
View File
@@ -0,0 +1,14 @@
import "@scss/auction.scss";
import { createApp } from 'vue'
import PageAuction from '@/PageAuction.vue';
import BootstrapVue3, { BToastPlugin } from 'bootstrap-vue-3'
import { auto500px } from "./util/auto500px";
import { insertCustomCSS } from "./util/customCSS";
import { htmlReady } from "./util/htmlReady";
auto500px();
htmlReady(() => {
insertCustomCSS();
});
createApp(PageAuction).use(BootstrapVue3).use(BToastPlugin).mount('#app')
+39
View File
@@ -0,0 +1,39 @@
<?php
namespace sammo;
include "lib.php";
include "func.php";
//로그인 검사
$session = Session::requireGameLogin()->setReadOnly();
$userID = Session::getUserID();
$db = DB::db();
$gameStor = KVStorage::getStorage($db, 'game_env');
$me = $db->queryFirstRow('SELECT no, nation, officer_level, permission, con, turntime, belong, penalty FROM general WHERE owner=%i', $userID);
?>
<!DOCTYPE html>
<html>
<head>
<title><?= UniqueConst::$serverName ?>: 경매장</title>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=1024" />
<?= WebUtil::printJS('../d_shared/common_path.js', true) ?>
<?= WebUtil::printStaticValues([
'staticValues' => [
'serverID' => UniqueConst::$serverID,
'serverNick' => DB::prefix(),
'turnterm' => $gameStor->turnterm,
]
]) ?>
<?= WebUtil::printDist('vue', 'v_auction', true) ?>
</head>
<body>
<div id="app"></div>
</body>
</html>
+2 -2
View File
@@ -98,10 +98,10 @@ class APIHelper
} }
Json::die($result, $setCache ? 0 : Json::NO_CACHE); Json::die($result, $setCache ? 0 : Json::NO_CACHE);
} catch (\Exception $e) { } catch (\Exception $e) {
Json::dieWithReason($e->getMessage()); Json::dieWithReason($e->getMessage()."\n".$e->getTraceAsString());
} catch (\Throwable $e) { } catch (\Throwable $e) {
logExceptionByCustomHandler($e, false); logExceptionByCustomHandler($e, false);
Json::dieWithReason($e->getMessage()); Json::dieWithReason($e->getMessage()."\n".$e->getTraceAsString());
} catch (mixed $e) { } catch (mixed $e) {
Json::dieWithReason(strval($e)); Json::dieWithReason(strval($e));
} }
+11 -4
View File
@@ -78,11 +78,18 @@ class DefaultConverter implements Converter
return $raw; return $raw;
} }
if($type === 'bool' && is_bool($raw)){ if($type === 'bool'){
$success = true; if(is_bool($raw)){
return $raw; $success = true;
return $raw;
}
if($raw === 0 || $raw === 1){
$success = true;
return $raw !== 0;
}
} }
return null; return null;
} }
@@ -100,7 +107,7 @@ class DefaultConverter implements Converter
} }
} }
throw new \Exception('DefaultConverter can not convert'); throw new \Exception('DefaultConverter can not convert '.gettype($raw));
} }
public function convertTo(mixed $data): string|array|int|float|bool|null public function convertTo(mixed $data): string|array|int|float|bool|null
+9 -15
View File
@@ -2,6 +2,7 @@
namespace sammo\DTO; namespace sammo\DTO;
use Ds\Set;
use sammo\DTO\Attr\Convert; use sammo\DTO\Attr\Convert;
use sammo\DTO\Converter\DefaultConverter; use sammo\DTO\Converter\DefaultConverter;
@@ -110,10 +111,13 @@ abstract class DTO
return $object; return $object;
} }
public function toArray(): array public function toArray(string ...$exceptKeys): array
{ {
$reflection = new \ReflectionClass($this::class); $reflection = new \ReflectionClass($this::class);
$result = []; $result = [];
$exceptKeySet = new Set($exceptKeys);
foreach ($reflection->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) { foreach ($reflection->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) {
$value = $property->getValue($this); $value = $property->getValue($this);
$attrs = Util\DTOUtil::getAttrs($property); $attrs = Util\DTOUtil::getAttrs($property);
@@ -123,6 +127,10 @@ abstract class DTO
continue; continue;
} }
if($exceptKeySet->contains($name)){
continue;
}
if (key_exists(Attr\RawName::class, $attrs)) { if (key_exists(Attr\RawName::class, $attrs)) {
$rawAttr = $attrs[Attr\RawName::class]; $rawAttr = $attrs[Attr\RawName::class];
$attr = new Attr\RawName(...$rawAttr->getArguments()); $attr = new Attr\RawName(...$rawAttr->getArguments());
@@ -153,18 +161,4 @@ abstract class DTO
} }
return $result; return $result;
} }
public function toArrayExcept(string ...$keys): array{
$reflection = new \ReflectionClass($this::class);
$values = $this->toArray();
foreach($keys as $key){
if(!$reflection->hasProperty($key)){
throw new \Exception("Key {$key} does not exist");
}
if(key_exists($key, $values)){
unset($values[$key]);
}
}
return $values;
}
} }