feat: New features (research, invest, gambling, new game+, trophies)
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
// Investments tab: nine societies in three risk tiers, all visible at once.
|
||||
|
||||
import { portfolio } from '../engine/investments.js';
|
||||
import { t } from '../services/i18n.js';
|
||||
import { h, icon } from './dom.js';
|
||||
import { moneyFormatter, setText, signedPercent } from './text.js';
|
||||
|
||||
const AMOUNTS = [0.01, 0.1, 0.5, 1];
|
||||
const TIERS = ['low', 'mid', 'high'];
|
||||
|
||||
export function createInvestView(root, actions) {
|
||||
let ctx;
|
||||
let fmt;
|
||||
let money;
|
||||
let amount = 0.1;
|
||||
let summary;
|
||||
const rows = new Map();
|
||||
|
||||
function societyRow(society) {
|
||||
const change = h('span', { class: 'badge' });
|
||||
const price = h('span');
|
||||
const held = h('span');
|
||||
const buy = h('button', {
|
||||
class: 'btn',
|
||||
type: 'button',
|
||||
text: t('invest.buy'),
|
||||
onclick: () => actions.buy(society.id, amount),
|
||||
});
|
||||
const sell = h('button', {
|
||||
class: 'btn',
|
||||
type: 'button',
|
||||
text: t('invest.sell'),
|
||||
onclick: () => actions.sell(society.id, amount),
|
||||
});
|
||||
const root = h(
|
||||
'li',
|
||||
{ class: 'card card--stack' },
|
||||
h(
|
||||
'div',
|
||||
{ class: 'card__row' },
|
||||
icon(`inv.${society.id}`),
|
||||
h(
|
||||
'div',
|
||||
{ class: 'card__body' },
|
||||
h('h3', { class: 'card__title' }, h('span', { text: t(`theme.inv.${society.id}.name`) }), change),
|
||||
h('p', { class: 'card__meta' }, price),
|
||||
h('p', { class: 'card__meta' }, held),
|
||||
),
|
||||
),
|
||||
h('div', { class: 'card__actions' }, buy, sell),
|
||||
);
|
||||
return { root, change, price, held, buy, sell };
|
||||
}
|
||||
|
||||
function tierPanel(tier) {
|
||||
const cfg = ctx.investments.tiers[tier];
|
||||
const list = h('ul', { class: 'cards' });
|
||||
for (const society of ctx.investments.societies.filter((s) => s.tier === tier)) {
|
||||
const row = societyRow(society);
|
||||
rows.set(society.id, row);
|
||||
list.append(row.root);
|
||||
}
|
||||
return h(
|
||||
'section',
|
||||
{ class: `panel panel--${tier}` },
|
||||
h('h2', { class: 'panel__title', text: t(`invest.tier.${tier}`) }),
|
||||
h('p', {
|
||||
class: 'hint',
|
||||
text: t('invest.tierInfo', {
|
||||
swing: fmt.number(cfg.swing * 100),
|
||||
dividend: fmt.number(cfg.dividend * 100),
|
||||
}),
|
||||
}),
|
||||
list,
|
||||
);
|
||||
}
|
||||
|
||||
function rebuild(newCtx, formatter) {
|
||||
ctx = newCtx;
|
||||
fmt = formatter;
|
||||
money = moneyFormatter(fmt);
|
||||
rows.clear();
|
||||
|
||||
summary = {
|
||||
value: h('strong'),
|
||||
basis: h('strong'),
|
||||
profit: h('strong'),
|
||||
next: h('p', { class: 'hint' }),
|
||||
};
|
||||
const amountGroup = h(
|
||||
'fieldset',
|
||||
{ class: 'segmented' },
|
||||
h('legend', { class: 'sr-only', text: t('invest.amount') }),
|
||||
...AMOUNTS.map((a) =>
|
||||
h(
|
||||
'label',
|
||||
{},
|
||||
h('input', {
|
||||
type: 'radio',
|
||||
name: 'invest-amount',
|
||||
value: String(a),
|
||||
checked: a === amount,
|
||||
onchange: () => {
|
||||
amount = a;
|
||||
},
|
||||
}),
|
||||
h('span', { text: `${a * 100}%` }),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
root.replaceChildren(
|
||||
h(
|
||||
'section',
|
||||
{ class: 'panel panel--wide' },
|
||||
h('h2', { class: 'panel__title', text: t('tab.invest') }),
|
||||
h('p', {
|
||||
class: 'hint',
|
||||
text: t('invest.intro', {
|
||||
seconds: ctx.investments.intervalSeconds,
|
||||
fee: fmt.number(ctx.investments.fee * 100),
|
||||
}),
|
||||
}),
|
||||
h(
|
||||
'dl',
|
||||
{ class: 'stats' },
|
||||
h('div', {}, h('dt', { text: t('invest.portfolio') }), h('dd', {}, summary.value)),
|
||||
h('div', {}, h('dt', { text: t('invest.invested') }), h('dd', {}, summary.basis)),
|
||||
h('div', {}, h('dt', { text: t('invest.profit') }), h('dd', {}, summary.profit)),
|
||||
),
|
||||
h('p', { class: 'field__label', text: t('invest.amountLabel') }),
|
||||
amountGroup,
|
||||
summary.next,
|
||||
),
|
||||
...TIERS.map(tierPanel),
|
||||
);
|
||||
}
|
||||
|
||||
function update(state) {
|
||||
const p = portfolio(state, ctx);
|
||||
setText(summary.value, money(p.value));
|
||||
setText(summary.basis, money(p.basis));
|
||||
setText(summary.profit, money(p.profit));
|
||||
summary.profit.classList.toggle('pos', p.profit > 0);
|
||||
summary.profit.classList.toggle('neg', p.profit < 0);
|
||||
const remaining = Math.ceil(ctx.investments.intervalSeconds - state.investTimer);
|
||||
setText(summary.next, t('invest.nextUpdate', { seconds: remaining }));
|
||||
|
||||
for (const society of ctx.investments.societies) {
|
||||
const row = rows.get(society.id);
|
||||
const price = state.invest.prices[society.id];
|
||||
const shares = state.invest.holdings[society.id];
|
||||
const change = state.invest.change[society.id];
|
||||
const basis = state.invest.basis[society.id];
|
||||
|
||||
const arrow = change > 0 ? '▲' : change < 0 ? '▼' : '■';
|
||||
setText(row.change, `${arrow} ${signedPercent(change, fmt)}`);
|
||||
row.change.classList.toggle('pos', change > 0);
|
||||
row.change.classList.toggle('neg', change < 0);
|
||||
|
||||
setText(
|
||||
row.price,
|
||||
t('invest.priceLine', {
|
||||
price: fmt.price(price),
|
||||
gap: signedPercent(price / society.basePrice - 1, fmt),
|
||||
}),
|
||||
);
|
||||
const value = shares * price;
|
||||
setText(
|
||||
row.held,
|
||||
shares > 0
|
||||
? t('invest.heldLine', { value: money(value), profit: signedPercent(basis > 0 ? value / basis - 1 : 0, fmt) })
|
||||
: t('invest.heldNone'),
|
||||
);
|
||||
row.buy.disabled = state.funds < 0.01;
|
||||
row.sell.disabled = shares <= 0;
|
||||
}
|
||||
}
|
||||
|
||||
return { rebuild, update };
|
||||
}
|
||||
Reference in New Issue
Block a user