188 lines
5.6 KiB
JavaScript
188 lines
5.6 KiB
JavaScript
// Gamble tab: chips are hard to buy and hard to keep. Stake them on a cycle.
|
||
|
||
import { chipCost, odds, reliabilityCost } from '../engine/gamble.js';
|
||
import { chipDemandBonus } from '../engine/formulas.js';
|
||
import { t } from '../services/i18n.js';
|
||
import { h, icon } from './dom.js';
|
||
import { bump, floater, pop, purchased } from './fx.js';
|
||
import { moneyFormatter, setText } from './text.js';
|
||
|
||
const STAKES = [
|
||
{ id: 'one', count: () => 1 },
|
||
{ id: 'tenth', fraction: 0.1 },
|
||
{ id: 'half', fraction: 0.5 },
|
||
{ id: 'all', fraction: 1 },
|
||
];
|
||
const LOG_SIZE = 6;
|
||
|
||
export function createGambleView(root, actions) {
|
||
let ctx;
|
||
let fmt;
|
||
let money;
|
||
let stake = STAKES[0];
|
||
let els;
|
||
const log = [];
|
||
|
||
const stakeCount = (chips) => (stake.count ? stake.count() : Math.max(1, Math.ceil(chips * stake.fraction)));
|
||
|
||
function stakeLabel(option) {
|
||
if (option.id === 'one') return t('gamble.stakeOne');
|
||
if (option.id === 'all') return t('gamble.stakeAll');
|
||
return t('gamble.stakePercent', { percent: option.fraction * 100 });
|
||
}
|
||
|
||
function renderLog() {
|
||
els.log.replaceChildren(
|
||
...log.map((entry) =>
|
||
h('li', { class: `log__entry log__entry--${entry.outcome}`, text: t(`gamble.result.${entry.outcome}`, { amount: fmt.number(Math.abs(entry.delta)) }) }),
|
||
),
|
||
);
|
||
els.logEmpty.hidden = log.length > 0;
|
||
}
|
||
|
||
function run(state) {
|
||
const result = actions.run(stakeCount(state.chips));
|
||
if (!result) return;
|
||
log.unshift(result);
|
||
pop(els.run);
|
||
const sign = result.delta > 0 ? '+' : '−';
|
||
floater(els.run, `${sign}${fmt.number(Math.abs(result.delta))}`, undefined, result.delta > 0 ? 'good' : 'bad');
|
||
bump(els.chips);
|
||
log.length = Math.min(log.length, LOG_SIZE);
|
||
renderLog();
|
||
}
|
||
|
||
let lastState;
|
||
|
||
function rebuild(newCtx, formatter) {
|
||
ctx = newCtx;
|
||
fmt = formatter;
|
||
money = moneyFormatter(fmt);
|
||
|
||
els = {
|
||
chips: h('strong'),
|
||
bonus: h('span', { class: 'hint' }),
|
||
buyChip: h('button', {
|
||
class: 'btn btn--wide',
|
||
type: 'button',
|
||
onclick: () => {
|
||
if (actions.buyChip()) {
|
||
purchased(els.buyChip, '+1');
|
||
bump(els.chips);
|
||
}
|
||
},
|
||
}),
|
||
reliability: h('button', {
|
||
class: 'btn btn--wide',
|
||
type: 'button',
|
||
onclick: () => {
|
||
if (actions.buyReliability()) purchased(els.reliability, '+1');
|
||
},
|
||
}),
|
||
odds: h('p', { class: 'hint' }),
|
||
run: h('button', { class: 'btn btn--primary btn--big', type: 'button', onclick: () => run(lastState) }),
|
||
log: h('ul', { class: 'log' }),
|
||
logEmpty: h('p', { class: 'hint', text: t('gamble.logEmpty') }),
|
||
};
|
||
|
||
root.replaceChildren(
|
||
h(
|
||
'section',
|
||
{ class: 'panel' },
|
||
h('h2', { class: 'panel__title', text: t('theme.tab.gamble') }),
|
||
h('p', { class: 'hint', text: t('gamble.intro') }),
|
||
h(
|
||
'div',
|
||
{ class: 'tile' },
|
||
icon('res.chips'),
|
||
h('div', {}, h('span', { class: 'tile__label', text: t('theme.res.chips') }), els.chips, els.bonus),
|
||
),
|
||
els.buyChip,
|
||
els.reliability,
|
||
),
|
||
h(
|
||
'section',
|
||
{ class: 'panel' },
|
||
h('h2', { class: 'panel__title', text: t('gamble.play') }),
|
||
h('p', { class: 'field__label', text: t('gamble.stake') }),
|
||
h(
|
||
'fieldset',
|
||
{ class: 'segmented' },
|
||
h('legend', { class: 'sr-only', text: t('gamble.stake') }),
|
||
...STAKES.map((option) =>
|
||
h(
|
||
'label',
|
||
{},
|
||
h('input', {
|
||
type: 'radio',
|
||
name: 'gamble-stake',
|
||
value: option.id,
|
||
checked: option === stake,
|
||
onchange: () => {
|
||
stake = option;
|
||
},
|
||
}),
|
||
h('span', { text: stakeLabel(option) }),
|
||
),
|
||
),
|
||
),
|
||
els.odds,
|
||
els.run,
|
||
h('h3', { class: 'dialog__subtitle', text: t('gamble.log') }),
|
||
els.logEmpty,
|
||
els.log,
|
||
),
|
||
);
|
||
renderLog();
|
||
}
|
||
|
||
function update(state, d) {
|
||
lastState = state;
|
||
const g = ctx.rules.gamble;
|
||
|
||
setText(els.chips, fmt.number(state.chips));
|
||
setText(els.bonus, t('gamble.bonus', { bonus: fmt.number(chipDemandBonus(state.chips, g)) }));
|
||
|
||
const cost = chipCost(state, ctx);
|
||
setText(els.buyChip, t('gamble.buyChip', { cost: money(cost) }));
|
||
els.buyChip.disabled = state.funds < cost || state.chips >= g.maxChips;
|
||
|
||
const maxed = state.reliabilityLevel >= g.reliability.maxLevel;
|
||
setText(
|
||
els.reliability,
|
||
maxed
|
||
? t('gamble.reliabilityMax', { level: state.reliabilityLevel })
|
||
: t('gamble.reliability', {
|
||
level: state.reliabilityLevel,
|
||
max: g.reliability.maxLevel,
|
||
cost: money(reliabilityCost(state, ctx)),
|
||
}),
|
||
);
|
||
els.reliability.disabled = maxed || state.funds < reliabilityCost(state, ctx);
|
||
|
||
const p = odds(state, ctx, d.mods);
|
||
const percent = (x) => fmt.number(Math.round(x * 1000) / 10);
|
||
setText(
|
||
els.odds,
|
||
t('gamble.odds', {
|
||
win: percent(p.win),
|
||
jackpot: percent(p.jackpot),
|
||
multiplier: g.jackpotMultiplier,
|
||
crash: percent(p.crash),
|
||
lose: percent(p.lose),
|
||
}),
|
||
);
|
||
|
||
const cooling = state.gambleCooldown > 0;
|
||
setText(
|
||
els.run,
|
||
cooling
|
||
? t('gamble.cooldown', { seconds: fmt.number(Math.ceil(state.gambleCooldown * 10) / 10) })
|
||
: `${t('theme.action.gamble')} (${fmt.number(Math.min(stakeCount(state.chips), state.chips))})`,
|
||
);
|
||
els.run.disabled = cooling || state.chips < 1;
|
||
}
|
||
|
||
return { rebuild, update };
|
||
}
|