Rebuild bet sizing as a bottom sheet with situation-aware presets
Bet/Raise now opens a sheet whose presets follow the spot: open sizes in BB (adjusted for limpers), 3bet/4bet multiples, postflop pot fractions and raise multiples. Adds -/+ steppers with hold-to-repeat, Min/All-in at the slider ends, and a square-curve slider so small sizes stay precise with deep stacks. The hand view now exposes the street's raise count so the client can tell a 3bet from a 4bet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ed5617a47a
commit
b6ce648b5a
4 files changed
+412
-77
No files matched your search
+184
-30
@@ -2342,52 +2342,74 @@ body.page-table {
|
||||
color: var(--label-3);
|
||||
}
|
||||
|
||||
/* レイズ額: レイズボタンから上に開き、閉じるときはボタンへ戻る */
|
||||
.raise-panel {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
bottom: calc(100% + 10px);
|
||||
z-index: 6;
|
||||
max-width: 496px;
|
||||
margin: 0 auto;
|
||||
padding: 14px 16px 14px;
|
||||
border-radius: 24px;
|
||||
/* ベット/レイズ額のシート: テーブル(ポット・相手のベット)が見えるよう背景は薄く暗くするだけ */
|
||||
.bet-backdrop::before {
|
||||
background: rgba(0, 0, 0, 0.16);
|
||||
}
|
||||
.bet-sheet {
|
||||
max-width: 520px;
|
||||
background: var(--material-thick);
|
||||
-webkit-backdrop-filter: blur(30px) saturate(180%);
|
||||
backdrop-filter: blur(30px) saturate(180%);
|
||||
box-shadow: var(--shadow-l), inset 0 0 0 0.5px var(--material-edge);
|
||||
transform-origin: 82% 100%;
|
||||
box-shadow: 0 -12px 40px rgba(0, 0, 0, 0.2), inset 0 0.5px 0 var(--material-edge);
|
||||
}
|
||||
.raise-head {
|
||||
.bet-sheet::after {
|
||||
background: var(--material-thick);
|
||||
}
|
||||
.bet-header {
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
.bet-heading {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
text-align: left;
|
||||
}
|
||||
.bet-context {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: var(--label-2);
|
||||
font-size: 0.8125rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.bet-body {
|
||||
padding: 4px 20px 2px;
|
||||
}
|
||||
.bet-amount-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
.raise-value {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
min-width: 0;
|
||||
font-family: var(--font-num);
|
||||
cursor: text;
|
||||
}
|
||||
.raise-value .cur {
|
||||
color: var(--label-2);
|
||||
font-size: 1.5rem;
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.raise-amount {
|
||||
width: 8ch;
|
||||
width: 7.5ch;
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
color: var(--label);
|
||||
font-family: var(--font-num);
|
||||
font-size: 2.125rem;
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.022em;
|
||||
line-height: 1.15;
|
||||
letter-spacing: -0.025em;
|
||||
font-variant-numeric: tabular-nums;
|
||||
-moz-appearance: textfield;
|
||||
appearance: textfield;
|
||||
@@ -2401,17 +2423,131 @@ body.page-table {
|
||||
box-shadow: inset 0 -2px 0 var(--orange);
|
||||
}
|
||||
.raise-meta {
|
||||
display: grid;
|
||||
flex: none;
|
||||
justify-items: end;
|
||||
gap: 1px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.raise-meta b {
|
||||
font-size: 1.0625rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.raise-meta span {
|
||||
color: var(--label-2);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.raise-meta span:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 場面ごとの候補(3bet の 3x・ベットの 50% など)。上に倍率、下にその額 */
|
||||
.bet-presets {
|
||||
display: grid;
|
||||
grid-auto-columns: 1fr;
|
||||
grid-auto-flow: column;
|
||||
gap: 6px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.chip-btn {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
gap: 1px;
|
||||
min-width: 0;
|
||||
min-height: 54px;
|
||||
padding: 6px 2px;
|
||||
border: 0;
|
||||
border-radius: 14px;
|
||||
background: var(--fill-2);
|
||||
color: var(--label);
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease, color 0.2s ease, transform 0.45s var(--ease-spring);
|
||||
}
|
||||
.chip-btn b {
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.chip-btn span {
|
||||
overflow: hidden;
|
||||
color: var(--label-2);
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
.chip-btn:active:not(:disabled) {
|
||||
transform: scale(0.93);
|
||||
transition: transform 0.1s ease-out;
|
||||
}
|
||||
.chip-btn.active {
|
||||
background: var(--orange);
|
||||
color: var(--on-orange);
|
||||
}
|
||||
.chip-btn.active span {
|
||||
color: var(--on-orange);
|
||||
opacity: 0.72;
|
||||
}
|
||||
.chip-btn:disabled {
|
||||
color: var(--label-3);
|
||||
cursor: default;
|
||||
}
|
||||
.chip-btn:disabled span {
|
||||
color: var(--label-3);
|
||||
}
|
||||
|
||||
/* −/+ とスライダー */
|
||||
.bet-adjust {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.step-btn {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex: none;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
background: var(--fill-2);
|
||||
color: var(--label);
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
transition: transform 0.45s var(--ease-spring), opacity 0.2s ease;
|
||||
}
|
||||
.step-btn .i {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
stroke-width: 2.6;
|
||||
}
|
||||
.step-btn:active:not(:disabled) {
|
||||
transform: scale(0.88);
|
||||
transition: transform 0.1s ease-out;
|
||||
}
|
||||
.step-btn:disabled {
|
||||
opacity: 0.35;
|
||||
cursor: default;
|
||||
}
|
||||
/* 自前のスライダー: つかんだ位置のまま指に付いてくる。端を越えるとゴムのように抵抗する */
|
||||
.bet-slider {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 44px;
|
||||
margin: 4px 0 8px;
|
||||
outline: none;
|
||||
touch-action: none;
|
||||
cursor: pointer;
|
||||
@@ -2460,33 +2596,51 @@ body.page-table {
|
||||
.bet-slider:focus-visible .bet-thumb::before {
|
||||
box-shadow: 0 0 0 3px var(--blue), 0 3px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.presets {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 6px;
|
||||
|
||||
/* スライダーの両端: 最小とオールイン */
|
||||
.bet-ends {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.chip-btn {
|
||||
.end-btn {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
min-height: 34px;
|
||||
padding: 4px 0;
|
||||
min-height: 30px;
|
||||
padding: 4px 12px;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: var(--fill-2);
|
||||
color: var(--label);
|
||||
background: transparent;
|
||||
color: var(--label-2);
|
||||
font: inherit;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease, color 0.2s ease, transform 0.45s var(--ease-spring);
|
||||
}
|
||||
.chip-btn:active {
|
||||
.end-btn span {
|
||||
color: var(--label);
|
||||
}
|
||||
.end-btn:active {
|
||||
transform: scale(0.93);
|
||||
transition: transform 0.1s ease-out;
|
||||
}
|
||||
.chip-btn.active {
|
||||
.end-btn.active {
|
||||
background: var(--fill-2);
|
||||
color: var(--label);
|
||||
}
|
||||
.end-btn.allin.active {
|
||||
background: var(--orange);
|
||||
color: var(--on-orange);
|
||||
}
|
||||
.end-btn.allin.active span {
|
||||
color: inherit;
|
||||
}
|
||||
.bet-sheet .sheet-footer .btn {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* ショーダウンの勝者選択(キャッシュオンリー) */
|
||||
.award-panel {
|
||||
|
||||
+184
-24
@@ -31,6 +31,15 @@
|
||||
|
||||
const reviewSheet = Fluid.Sheet($('review-modal'));
|
||||
const nameSheet = Fluid.Sheet($('name-modal'), { dismissible: false, focus: '#name-input' });
|
||||
// 払って閉じた・背景を押した・Esc のときも「戻る」と同じ
|
||||
const betSheet = Fluid.Sheet($('bet-modal'), {
|
||||
focus: '#raise-range',
|
||||
onClose: () => {
|
||||
if (!raising) return;
|
||||
raising = false;
|
||||
render();
|
||||
},
|
||||
});
|
||||
|
||||
// ---------- 名前 ----------
|
||||
function ensureName() {
|
||||
@@ -108,6 +117,8 @@
|
||||
$('main').hidden = true;
|
||||
$('action-bar').hidden = true;
|
||||
reviewSheet.close();
|
||||
raising = false;
|
||||
betSheet.close();
|
||||
$('not-found').hidden = false;
|
||||
// 自分から切断するので「接続が切れました」は出さない
|
||||
if (socket) {
|
||||
@@ -246,6 +257,7 @@
|
||||
|
||||
renderSeats(s);
|
||||
if (me) renderActionBar(s, me);
|
||||
else betSheet.close();
|
||||
renderReview(s);
|
||||
renderTimer();
|
||||
|
||||
@@ -473,10 +485,14 @@
|
||||
$('sit-in').hidden = !m.sittingOut;
|
||||
$('rebuy').hidden = !m.canRebuy;
|
||||
$('rebuy').textContent = `リバイ(${chips(s.startingStack)})`;
|
||||
$('actions').hidden = alt || raising || awarding || reviewWaiting;
|
||||
$('raise-actions').hidden = alt || !raising;
|
||||
// レイズ額のパネルはレイズボタンから出て、閉じるときはボタンへ戻る
|
||||
Fluid.presence($('raise-panel'), !alt && raising, { scale: 0.86, y: 28 });
|
||||
$('actions').hidden = alt || awarding || reviewWaiting;
|
||||
if (raising && o && !alt) {
|
||||
renderRaisePanel(o);
|
||||
betSheet.open();
|
||||
} else {
|
||||
raising = false;
|
||||
betSheet.close();
|
||||
}
|
||||
|
||||
const fold = $('act-fold');
|
||||
const call = $('act-call');
|
||||
@@ -494,8 +510,6 @@
|
||||
: o.minRaiseTo >= o.maxRaiseTo
|
||||
? `All-in ${chips(o.maxRaiseTo)}`
|
||||
: o.isBet ? 'Bet' : 'Raise';
|
||||
|
||||
if (raising) renderRaisePanel(o);
|
||||
}
|
||||
|
||||
// ---------- レビュー(レビュー付きCPU戦の作成者のみ) ----------
|
||||
@@ -665,18 +679,78 @@
|
||||
});
|
||||
|
||||
// ---------- レイズ ----------
|
||||
function presetValue(o, kind) {
|
||||
// 場面ごとの額の候補。プリフロップはオープン(BB の何倍)・3bet 以降(相手の額の何倍)、
|
||||
// フロップ以降はベット(ポットの何割)・レイズ(相手のベットの何倍)。
|
||||
// ポットの何割は「コールしたあとのポット」に対する額を上乗せする(ポットレイズと同じ考え方)。
|
||||
// def は開いたときに選んでおく候補
|
||||
function betContext(o) {
|
||||
const h = state.hand;
|
||||
const bb = state.bigBlind;
|
||||
const cur = h.currentBet;
|
||||
const potAfterCall = h.potTotal + o.toCall;
|
||||
const base = h.currentBet;
|
||||
const v = {
|
||||
min: o.minRaiseTo,
|
||||
half: base + Math.round(potAfterCall / 2),
|
||||
threeq: base + Math.round((potAfterCall * 3) / 4),
|
||||
pot: base + potAfterCall,
|
||||
max: o.maxRaiseTo,
|
||||
}[kind];
|
||||
return clampRaise(o, v);
|
||||
const pot = (f) => cur + potAfterCall * f;
|
||||
const times = (x) => ({ label: `${x}x`, to: cur * x });
|
||||
const potPreset = { label: 'Pot', to: pot(1) };
|
||||
|
||||
if (h.street === 'PREFLOP') {
|
||||
const r = h.raises || 0;
|
||||
if (r === 0) {
|
||||
// リンプした人がいれば 1人につき 1BB 足す
|
||||
const limpers = Object.entries(h.players).filter(
|
||||
([seat, p]) => Number(seat) !== h.bbSeat && !p.folded && p.bet >= cur
|
||||
).length;
|
||||
return {
|
||||
title: limpers ? 'アイソレート' : 'オープン',
|
||||
note: limpers ? `リンプ ${limpers}人(+${limpers}BB)` : '',
|
||||
presets: [2, 2.5, 3, 4].map((x) => ({ label: `${x + limpers}BB`, to: (x + limpers) * bb })).concat(potPreset),
|
||||
def: limpers ? 2 : 1,
|
||||
};
|
||||
}
|
||||
const xs = r === 1 ? [2.5, 3, 4] : r === 2 ? [2.2, 2.5, 3] : [2, 2.5];
|
||||
return {
|
||||
title: `${r + 2}bet`,
|
||||
note: `相手 ${chips(cur)}`,
|
||||
presets: xs.map(times).concat(potPreset),
|
||||
def: 1,
|
||||
};
|
||||
}
|
||||
if (o.isBet) {
|
||||
return {
|
||||
title: 'ベット',
|
||||
note: '',
|
||||
presets: [
|
||||
{ label: '33%', to: pot(1 / 3) },
|
||||
{ label: '50%', to: pot(1 / 2) },
|
||||
{ label: '75%', to: pot(3 / 4) },
|
||||
potPreset,
|
||||
{ label: '150%', to: pot(1.5) },
|
||||
],
|
||||
def: 1,
|
||||
};
|
||||
}
|
||||
return {
|
||||
title: (h.raises || 0) >= 2 ? 'リレイズ' : 'レイズ',
|
||||
note: `相手 ${chips(cur)}`,
|
||||
presets: [2.5, 3, 4].map(times).concat(potPreset),
|
||||
def: 1,
|
||||
};
|
||||
}
|
||||
|
||||
// 候補の額を出せる範囲に収める(SB 単位に丸めるとブラインドが小さいうちは 50% が 57% になったりするので 1 単位)。
|
||||
// 最小に届かない候補は押せない。オールインを超える候補は最初の 1つだけ All-in として残す。
|
||||
// 左から額の小さい順に並べる(Pot が 4BB より小さいこともある)。def は並べ替える前の番号
|
||||
function betPresets(o, ctx) {
|
||||
let allInShown = false;
|
||||
return ctx.presets
|
||||
.map((p, i) => ({ label: p.label, raw: Math.round(p.to), def: i === ctx.def }))
|
||||
.sort((a, b) => a.raw - b.raw)
|
||||
.map((p) => {
|
||||
const to = clampRaise(o, p.raw);
|
||||
const allIn = to >= o.maxRaiseTo;
|
||||
const disabled = p.raw < o.minRaiseTo || (allIn && allInShown);
|
||||
if (allIn) allInShown = true;
|
||||
return { label: p.label, to, allIn, disabled, def: p.def };
|
||||
});
|
||||
}
|
||||
|
||||
function clampRaise(o, v) {
|
||||
@@ -704,12 +778,15 @@
|
||||
knob.onUpdate = paintKnob;
|
||||
if ('ResizeObserver' in window) new ResizeObserver(() => paintKnob(knob.value)).observe(slider);
|
||||
|
||||
const fracOf = (o, v) => (o.maxRaiseTo > o.minRaiseTo ? (v - o.minRaiseTo) / (o.maxRaiseTo - o.minRaiseTo) : 1);
|
||||
// つまみの位置と額は 2乗の関係にする(スタックが深くても、よく使う小さい額を細かく選べるように)
|
||||
const CURVE = 2;
|
||||
const fracOf = (o, v) =>
|
||||
o.maxRaiseTo > o.minRaiseTo ? Fluid.clamp((v - o.minRaiseTo) / (o.maxRaiseTo - o.minRaiseTo), 0, 1) ** (1 / CURVE) : 1;
|
||||
// SB 単位に丸める(最大はオールイン額そのまま)
|
||||
function valueAt(o, f) {
|
||||
if (f >= 1) return o.maxRaiseTo;
|
||||
const unit = Math.max(1, state.smallBlind);
|
||||
const raw = o.minRaiseTo + f * (o.maxRaiseTo - o.minRaiseTo);
|
||||
const raw = o.minRaiseTo + f ** CURVE * (o.maxRaiseTo - o.minRaiseTo);
|
||||
return clampRaise(o, o.minRaiseTo + Math.round((raw - o.minRaiseTo) / unit) * unit);
|
||||
}
|
||||
const formatBB = (x) => (Math.round(x * 10) / 10).toLocaleString('en-US');
|
||||
@@ -730,10 +807,42 @@
|
||||
if (knob.target !== f) knob.to(f, { damping: 1, response: 0.3 });
|
||||
}
|
||||
paintKnob(knob.value);
|
||||
|
||||
const h = state.hand;
|
||||
const ctx = betContext(o);
|
||||
$('bet-title').textContent = ctx.title;
|
||||
$('bet-context').textContent = [`ポット ${chips(h.potTotal)}`, ctx.note].filter(Boolean).join(' · ');
|
||||
const presets = betPresets(o, ctx);
|
||||
patch(
|
||||
$('presets'),
|
||||
presets
|
||||
.map(
|
||||
(p) =>
|
||||
`<button type="button" class="chip-btn${p.allIn ? ' allin' : ''}" data-to="${p.to}"${p.disabled ? ' disabled' : ''}>` +
|
||||
`<b>${esc(p.label)}</b><span>${p.disabled ? '—' : p.allIn ? 'All-in' : chips(p.to)}</span></button>`
|
||||
)
|
||||
.join('')
|
||||
);
|
||||
// 同じ額の候補が 2つあっても光らせるのは 1つだけ
|
||||
let lit = false;
|
||||
for (const b of $('presets').children) {
|
||||
b.classList.toggle('active', presetValue(o, b.dataset.preset) === raiseValue);
|
||||
const on = !lit && !b.disabled && Number(b.dataset.to) === raiseValue;
|
||||
if (on) lit = true;
|
||||
b.classList.toggle('active', on);
|
||||
}
|
||||
$('raise-meta').textContent = state.bigBlind > 0 ? `${formatBB(raiseValue / state.bigBlind)} BB` : '';
|
||||
$('raise-min-amt').textContent = chips(o.minRaiseTo);
|
||||
$('raise-max-amt').textContent = chips(o.maxRaiseTo);
|
||||
$('raise-min').classList.toggle('active', raiseValue === o.minRaiseTo);
|
||||
$('raise-max').classList.toggle('active', raiseValue === o.maxRaiseTo);
|
||||
$('raise-minus').disabled = raiseValue <= o.minRaiseTo;
|
||||
$('raise-plus').disabled = raiseValue >= o.maxRaiseTo;
|
||||
|
||||
$('raise-bb').textContent = state.bigBlind > 0 ? `${formatBB(raiseValue / state.bigBlind)} BB` : '';
|
||||
// 相手の額の何倍か(オープン以外のプリフロップとレイズ)、ポットの何%か(ベット)
|
||||
let ratio = '';
|
||||
if (o.isBet) ratio = h.potTotal > 0 ? `ポットの ${Math.round((raiseValue / h.potTotal) * 100)}%` : '';
|
||||
else if (h.street !== 'PREFLOP' || h.raises > 0) ratio = `${formatBB(raiseValue / h.currentBet)}x`;
|
||||
$('raise-ratio').textContent = ratio;
|
||||
const allIn = raiseValue >= o.maxRaiseTo;
|
||||
$('raise-confirm').textContent = allIn
|
||||
? `All-in ${chips(raiseValue)}`
|
||||
@@ -846,9 +955,13 @@
|
||||
const o = options();
|
||||
if (!o || !o.canRaise) return;
|
||||
if (o.minRaiseTo >= o.maxRaiseTo) return sendAction('raise', o.maxRaiseTo); // オールインしかない
|
||||
// 場面のよくある額(オープン 2.5BB・3bet 3x・ベット 50% など)を選んだ状態で開く
|
||||
const ctx = betContext(o);
|
||||
const presets = betPresets(o, ctx);
|
||||
const def = presets.find((p) => p.def && !p.disabled) || presets.find((p) => !p.disabled);
|
||||
raising = true;
|
||||
raiseValue = o.minRaiseTo;
|
||||
knob.set(0);
|
||||
raiseValue = def ? def.to : o.minRaiseTo;
|
||||
knob.set(fracOf(o, raiseValue));
|
||||
render();
|
||||
});
|
||||
$('raise-cancel').addEventListener('click', () => {
|
||||
@@ -860,10 +973,57 @@
|
||||
if (o) sendAction('raise', clampRaise(o, raiseValue));
|
||||
});
|
||||
$('presets').addEventListener('click', (e) => {
|
||||
const b = e.target.closest('[data-preset]');
|
||||
const b = e.target.closest('[data-to]');
|
||||
const o = options();
|
||||
if (b && o) setRaise(o, presetValue(o, b.dataset.preset));
|
||||
if (!b || b.disabled || !o) return;
|
||||
Fluid.haptic(6);
|
||||
setRaise(o, Number(b.dataset.to));
|
||||
});
|
||||
$('raise-min').addEventListener('click', () => {
|
||||
const o = options();
|
||||
if (o) setRaise(o, o.minRaiseTo);
|
||||
});
|
||||
$('raise-max').addEventListener('click', () => {
|
||||
const o = options();
|
||||
if (o) setRaise(o, o.maxRaiseTo);
|
||||
});
|
||||
|
||||
// −/+: 1BB ずつ。押し続けると少し待ってから連続で、だんだん速く動く
|
||||
function stepper(btn, dir) {
|
||||
let timer = null;
|
||||
let count = 0;
|
||||
const step = () => {
|
||||
const o = options();
|
||||
if (!o) return stop();
|
||||
const before = raiseValue;
|
||||
setRaise(o, raiseValue + dir * Math.max(1, state.bigBlind));
|
||||
if (raiseValue === before) return stop();
|
||||
if (raiseValue === o.maxRaiseTo || raiseValue === o.minRaiseTo) Fluid.haptic(8);
|
||||
};
|
||||
const stop = () => {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
};
|
||||
const repeat = () => {
|
||||
step();
|
||||
count++;
|
||||
if (timer) timer = setTimeout(repeat, count < 6 ? 140 : count < 20 ? 70 : 35);
|
||||
};
|
||||
btn.addEventListener('pointerdown', (e) => {
|
||||
if (e.pointerType === 'mouse' && e.button !== 0) return;
|
||||
e.preventDefault(); // 長押しで文字選択・メニューが出ないように
|
||||
step();
|
||||
count = 0;
|
||||
timer = setTimeout(repeat, 380);
|
||||
});
|
||||
for (const type of ['pointerup', 'pointerleave', 'pointercancel']) btn.addEventListener(type, stop);
|
||||
// キーボード(Enter / Space)から押されたとき
|
||||
btn.addEventListener('click', (e) => {
|
||||
if (e.detail === 0) step();
|
||||
});
|
||||
}
|
||||
stepper($('raise-minus'), -1);
|
||||
stepper($('raise-plus'), 1);
|
||||
$('raise-amount').addEventListener('input', (e) => {
|
||||
const o = options();
|
||||
const v = Number(e.target.value);
|
||||
|
||||
Reference in new issue
Block a user