feat: Simplification of UI and icons creation
This commit is contained in:
@@ -124,7 +124,6 @@ export function stepEvents(state, ctx, dt, d, rng, offline, totals) {
|
||||
totals.notices.push({ kind: 'event_incident', type: type.id });
|
||||
} else {
|
||||
ev.current = { type: type.id, remaining: cfg.claimSeconds };
|
||||
totals.notices.push({ kind: 'event_opportunity', type: type.id });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-4
@@ -166,9 +166,6 @@ function announce(notice) {
|
||||
case 'contract_failed':
|
||||
showToast(t('toast.contractFailed'), { error: true });
|
||||
break;
|
||||
case 'event_opportunity':
|
||||
showToast(t('toast.eventOpportunity', { title: t(`event.${notice.type}.title`) }));
|
||||
break;
|
||||
case 'event_incident':
|
||||
showToast(t('toast.eventIncident', { title: t(`event.${notice.type}.title`) }), { error: true });
|
||||
break;
|
||||
@@ -221,9 +218,11 @@ function render() {
|
||||
|
||||
// ------------------------------------------------------------------- actions
|
||||
|
||||
/** Wrap an engine action: run it on the current state, refresh the screen, report if it happened. */
|
||||
const act = (fn) => (...args) => {
|
||||
fn(state, ctx, ...args);
|
||||
const done = fn(state, ctx, ...args);
|
||||
render();
|
||||
return done;
|
||||
};
|
||||
|
||||
const view = createView({
|
||||
|
||||
@@ -23,7 +23,7 @@ export function createAchievementsView(root) {
|
||||
const li = h(
|
||||
'li',
|
||||
{ class: 'card' },
|
||||
icon(`ach.${a.id}`),
|
||||
icon('trophy'),
|
||||
h(
|
||||
'div',
|
||||
{ class: 'card__body' },
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
// and the board of offers.
|
||||
|
||||
import { t } from '../services/i18n.js';
|
||||
import { h } from './dom.js';
|
||||
import { h, makeBuyable, setBuyable } from './dom.js';
|
||||
import { purchased } from './fx.js';
|
||||
import { formatDuration, moneyFormatter, rewardText, setText } from './text.js';
|
||||
|
||||
const BUFF_LABEL = {
|
||||
@@ -133,24 +134,31 @@ export function createContractsView(root, actions) {
|
||||
if (signature !== offersSignature) {
|
||||
offersSignature = signature;
|
||||
els.offers.replaceChildren(
|
||||
...offers.map((offer, index) =>
|
||||
h(
|
||||
...offers.map((offer, index) => {
|
||||
const li = h(
|
||||
'li',
|
||||
{ class: 'card' },
|
||||
h(
|
||||
'div',
|
||||
{ class: 'card__body' },
|
||||
h('h3', { class: 'card__title', text: t(`contracts.template.${offer.template}`) }),
|
||||
h('p', { class: 'card__desc', text: t('contracts.goal', { target: fmt.number(offer.target), duration: formatDuration(offer.seconds) }) }),
|
||||
h('p', {
|
||||
class: 'card__desc',
|
||||
text: t('contracts.goal', { target: fmt.number(offer.target), duration: formatDuration(offer.seconds) }),
|
||||
}),
|
||||
h('p', { class: 'card__meta', text: rewardText(offer.reward, fmt, offer.seconds) }),
|
||||
),
|
||||
h('button', { class: 'btn btn--buy', type: 'button', text: t('contracts.accept'), 'data-index': index, onclick: () => actions.accept(index) }),
|
||||
),
|
||||
),
|
||||
h('div', { class: 'card__price' }, h('strong', { text: t('contracts.accept') })),
|
||||
);
|
||||
makeBuyable(li, () => {
|
||||
if (actions.accept(index)) purchased(li, '✓', null);
|
||||
});
|
||||
return li;
|
||||
}),
|
||||
);
|
||||
}
|
||||
els.offersNone.hidden = offers.length > 0;
|
||||
for (const button of els.offers.querySelectorAll('button')) button.disabled = Boolean(state.contracts.active);
|
||||
for (const li of els.offers.children) setBuyable(li, !state.contracts.active);
|
||||
}
|
||||
|
||||
function update(state) {
|
||||
|
||||
@@ -39,3 +39,31 @@ export function icon(key) {
|
||||
export function refreshStaticIcons(root = document) {
|
||||
for (const el of root.querySelectorAll('.resourcebar [data-icon]')) el.replaceWith(icon(el.dataset.icon));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ buyable blocks
|
||||
// A block you click to buy (instead of a button inside it): it is announced as
|
||||
// a button, reachable with the keyboard (Enter / Space), outlined when hovered
|
||||
// and greyed out when it cannot be bought.
|
||||
|
||||
/** Make `el` act as one big button. `activate(event)` runs on click, Enter or Space. */
|
||||
export function makeBuyable(el, activate) {
|
||||
el.classList.add('card--buy');
|
||||
el.setAttribute('role', 'button');
|
||||
el.tabIndex = 0;
|
||||
const allowed = () => el.getAttribute('aria-disabled') !== 'true';
|
||||
el.addEventListener('click', (event) => {
|
||||
if (allowed()) activate(event);
|
||||
});
|
||||
el.addEventListener('keydown', (event) => {
|
||||
if ((event.key === 'Enter' || event.key === ' ') && event.target === el) {
|
||||
event.preventDefault();
|
||||
if (allowed()) activate(event);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Show whether a buyable block can currently be bought. */
|
||||
export function setBuyable(el, canBuy) {
|
||||
el.classList.toggle('is-disabled', !canBuy);
|
||||
el.setAttribute('aria-disabled', String(!canBuy));
|
||||
}
|
||||
|
||||
+17
-14
@@ -2,7 +2,8 @@
|
||||
|
||||
import { endlessQuote, lineAvailable, lineLevel } from '../engine/endless.js';
|
||||
import { t } from '../services/i18n.js';
|
||||
import { h, icon } from './dom.js';
|
||||
import { h, icon, makeBuyable, setBuyable } from './dom.js';
|
||||
import { bump, purchased } from './fx.js';
|
||||
import { effectText, moneyFormatter, setText } from './text.js';
|
||||
|
||||
const AMOUNTS = [1, 10, 100, 'max'];
|
||||
@@ -22,14 +23,8 @@ export function createEndlessView(root, actions) {
|
||||
function rowFor(line) {
|
||||
const level = h('span', { class: 'badge' });
|
||||
const total = h('p', { class: 'card__meta' });
|
||||
const label = h('span', { class: 'btn__label' });
|
||||
const label = h('strong');
|
||||
const cost = h('small');
|
||||
const buy = h(
|
||||
'button',
|
||||
{ class: 'btn btn--buy', type: 'button', onclick: () => actions.buy(line.id, amount) },
|
||||
label,
|
||||
cost,
|
||||
);
|
||||
const li = h(
|
||||
'li',
|
||||
{ class: 'card', hidden: true },
|
||||
@@ -41,9 +36,15 @@ export function createEndlessView(root, actions) {
|
||||
h('p', { class: 'card__desc', text: t('endless.perLevel', { effect: effectText(line.effect, fmt) }) }),
|
||||
total,
|
||||
),
|
||||
buy,
|
||||
h('div', { class: 'card__price' }, label, cost),
|
||||
);
|
||||
return { li, level, total, label, cost, buy, line };
|
||||
const row = { li, level, total, label, cost, line, shown: 1 };
|
||||
makeBuyable(li, () => {
|
||||
if (!actions.buy(line.id, amount)) return;
|
||||
purchased(li, `+${fmt.number(row.shown)}`);
|
||||
bump(level);
|
||||
});
|
||||
return row;
|
||||
}
|
||||
|
||||
function rebuild(newCtx, formatter) {
|
||||
@@ -87,14 +88,15 @@ export function createEndlessView(root, actions) {
|
||||
h('h2', { class: 'panel__title', text: t('endless.title') }),
|
||||
amountGroup,
|
||||
),
|
||||
h('p', { class: 'hint', text: t('endless.intro') }),
|
||||
h('p', { class: 'hint', text: `${t('endless.intro')} ${t('ui.clickToBuy')}` }),
|
||||
list,
|
||||
);
|
||||
}
|
||||
|
||||
function update(state) {
|
||||
let any = false;
|
||||
for (const { li, level, total, label, cost, buy, line } of rows.values()) {
|
||||
for (const row of rows.values()) {
|
||||
const { li, level, total, label, cost, line } = row;
|
||||
const open = lineAvailable(state, line);
|
||||
li.hidden = !open;
|
||||
if (!open) continue;
|
||||
@@ -112,9 +114,10 @@ export function createEndlessView(root, actions) {
|
||||
? t('endless.total', { effect: effectText({ ...line.effect, value: line.effect.value ** lv }, fmt) })
|
||||
: t('endless.none'),
|
||||
);
|
||||
setText(label, maxed ? t('endless.maxed') : t('endless.buy', { count: fmt.number(quote.count) }));
|
||||
row.shown = quote.count;
|
||||
setText(label, maxed ? t('endless.maxed') : `×${fmt.number(quote.count)}`);
|
||||
setText(cost, maxed ? '' : money(quote.cost));
|
||||
buy.disabled = maxed || quote.count <= 0 || !(state.funds >= quote.cost);
|
||||
setBuyable(li, !maxed && quote.count > 0 && state.funds >= quote.cost);
|
||||
}
|
||||
root.hidden = !any;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// Quick feedback animations (0.15 to 0.25 s), so buying feels immediate.
|
||||
// All of them are plain CSS animations restarted from here; users who prefer
|
||||
// reduced motion get none (see components.css).
|
||||
|
||||
const FALLBACK_MS = 400; // clean-up if the browser never fires animationend
|
||||
|
||||
/** Restart a one-shot CSS animation class on an element. */
|
||||
function play(el, className) {
|
||||
if (!el) return;
|
||||
el.classList.remove(className);
|
||||
void el.offsetWidth; // let the browser notice the class was removed
|
||||
el.classList.add(className);
|
||||
const done = () => el.classList.remove(className);
|
||||
el.addEventListener('animationend', done, { once: true });
|
||||
setTimeout(done, FALLBACK_MS);
|
||||
}
|
||||
|
||||
/** A block that was just bought: a quick pop and an accent glow. */
|
||||
export const pop = (el) => play(el, 'fx-pop');
|
||||
|
||||
/** A number that just changed: a quick bump. */
|
||||
export const bump = (el) => play(el, 'fx-bump');
|
||||
|
||||
/** A resource that was just spent: a quick flash. */
|
||||
export const spend = (el) => play(el, 'fx-spend');
|
||||
|
||||
/**
|
||||
* A short "+3" that rises from the middle of `anchor` (or from a point) and fades.
|
||||
* `at` = { x, y } in viewport coordinates, used when given instead of an element.
|
||||
* `tone` ('good' | 'bad') colours it green or red; by default it takes the accent colour.
|
||||
*/
|
||||
export function floater(anchor, text, at, tone) {
|
||||
const rect = at ? { left: at.x, top: at.y, width: 0 } : anchor?.getBoundingClientRect();
|
||||
if (!rect) return;
|
||||
const el = document.createElement('span');
|
||||
el.className = tone ? `floater floater--${tone}` : 'floater';
|
||||
el.textContent = text;
|
||||
el.style.left = `${rect.left + rect.width / 2}px`;
|
||||
el.style.top = `${rect.top}px`;
|
||||
document.body.append(el);
|
||||
el.addEventListener('animationend', () => el.remove(), { once: true });
|
||||
setTimeout(() => el.remove(), FALLBACK_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pop a bought block, float a label above it and flash what was spent (the funds
|
||||
* in the top bar unless `spentId` says otherwise, or is null for no flash).
|
||||
*/
|
||||
export function purchased(el, label, spentId = 'v-funds') {
|
||||
pop(el);
|
||||
floater(el, label);
|
||||
if (spentId) spend(document.getElementById(spentId));
|
||||
return true;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ 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 = [
|
||||
@@ -43,6 +44,10 @@ export function createGambleView(root, actions) {
|
||||
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();
|
||||
}
|
||||
@@ -57,8 +62,23 @@ export function createGambleView(root, actions) {
|
||||
els = {
|
||||
chips: h('strong'),
|
||||
bonus: h('span', { class: 'hint' }),
|
||||
buyChip: h('button', { class: 'btn btn--wide', type: 'button', onclick: () => actions.buyChip() }),
|
||||
reliability: h('button', { class: 'btn btn--wide', type: 'button', onclick: () => actions.buyReliability() }),
|
||||
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' }),
|
||||
|
||||
+18
-11
@@ -2,7 +2,8 @@
|
||||
|
||||
import { portfolio } from '../engine/investments.js';
|
||||
import { t } from '../services/i18n.js';
|
||||
import { h, icon } from './dom.js';
|
||||
import { h, icon, makeBuyable, setBuyable } from './dom.js';
|
||||
import { bump, floater, pop, purchased } from './fx.js';
|
||||
import { moneyFormatter, setText, signedPercent } from './text.js';
|
||||
|
||||
const AMOUNTS = [0.01, 0.1, 0.5, 1];
|
||||
@@ -20,17 +21,18 @@ export function createInvestView(root, actions) {
|
||||
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),
|
||||
onclick: (event) => {
|
||||
event.stopPropagation(); // the block itself buys
|
||||
if (actions.sell(society.id, amount)) {
|
||||
pop(root);
|
||||
floater(sell, '↘');
|
||||
bump(held);
|
||||
}
|
||||
},
|
||||
});
|
||||
const root = h(
|
||||
'li',
|
||||
@@ -47,9 +49,14 @@ export function createInvestView(root, actions) {
|
||||
h('p', { class: 'card__meta' }, held),
|
||||
),
|
||||
),
|
||||
h('div', { class: 'card__actions' }, buy, sell),
|
||||
h('div', { class: 'card__actions' }, h('span', { class: 'card__hint', text: t('invest.clickToBuy') }), sell),
|
||||
);
|
||||
return { root, change, price, held, buy, sell };
|
||||
makeBuyable(root, () => {
|
||||
if (!actions.buy(society.id, amount)) return;
|
||||
purchased(root, '↗');
|
||||
bump(held);
|
||||
});
|
||||
return { root, change, price, held, sell };
|
||||
}
|
||||
|
||||
function tierPanel(tier) {
|
||||
@@ -172,7 +179,7 @@ export function createInvestView(root, actions) {
|
||||
? 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;
|
||||
setBuyable(row.root, state.funds >= 0.01);
|
||||
row.sell.disabled = shares <= 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
import { legacyGain, perkCost, perkLevel } from '../engine/legacy.js';
|
||||
import { t } from '../services/i18n.js';
|
||||
import { h, icon } from './dom.js';
|
||||
import { h, icon, makeBuyable, setBuyable } from './dom.js';
|
||||
import { bump, purchased } from './fx.js';
|
||||
import { effectText, setText } from './text.js';
|
||||
|
||||
export function createLegacyView(root, actions) {
|
||||
@@ -30,8 +31,7 @@ export function createLegacyView(root, actions) {
|
||||
const perks = h('ul', { class: 'cards' });
|
||||
for (const perk of ctx.legacy) {
|
||||
const level = h('span', { class: 'badge' });
|
||||
const cost = h('span', { class: 'btn__label' });
|
||||
const buy = h('button', { class: 'btn btn--buy', type: 'button', onclick: () => actions.buyPerk(perk.id) }, cost);
|
||||
const cost = h('strong');
|
||||
const li = h(
|
||||
'li',
|
||||
{ class: 'card' },
|
||||
@@ -42,10 +42,15 @@ export function createLegacyView(root, actions) {
|
||||
h('h3', { class: 'card__title' }, h('span', { text: t(`legacy.perk.${perk.id}`) }), level),
|
||||
h('p', { class: 'card__desc', text: effectText(perk.effect, fmt) }),
|
||||
),
|
||||
buy,
|
||||
h('div', { class: 'card__price' }, cost),
|
||||
);
|
||||
makeBuyable(li, () => {
|
||||
if (!actions.buyPerk(perk.id)) return;
|
||||
purchased(li, '+1', null);
|
||||
bump(level);
|
||||
});
|
||||
perks.append(li);
|
||||
perkRows.set(perk.id, { perk, level, cost, buy });
|
||||
perkRows.set(perk.id, { perk, li, level, cost });
|
||||
}
|
||||
|
||||
root.replaceChildren(
|
||||
@@ -68,7 +73,13 @@ export function createLegacyView(root, actions) {
|
||||
els.need,
|
||||
els.restart,
|
||||
),
|
||||
h('section', { class: 'panel' }, h('h2', { class: 'panel__title', text: t('legacy.perks') }), perks),
|
||||
h(
|
||||
'section',
|
||||
{ class: 'panel' },
|
||||
h('h2', { class: 'panel__title', text: t('legacy.perks') }),
|
||||
h('p', { class: 'hint', text: t('ui.clickToBuy') }),
|
||||
perks,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -92,13 +103,13 @@ export function createLegacyView(root, actions) {
|
||||
setText(els.restart, t('legacy.restart'));
|
||||
els.restart.disabled = gain < 1;
|
||||
|
||||
for (const { perk, level, cost, buy } of perkRows.values()) {
|
||||
for (const { perk, li, level, cost } of perkRows.values()) {
|
||||
const lv = perkLevel(state, perk);
|
||||
const maxed = lv >= perk.maxLevel;
|
||||
const price = perkCost(perk, lv);
|
||||
setText(level, t('legacy.level', { level: lv, max: perk.maxLevel }));
|
||||
setText(cost, maxed ? t('legacy.maxed') : t('legacy.perkCost', { cost: fmt.number(price) }));
|
||||
buy.disabled = maxed || state.legacy.points < price;
|
||||
setBuyable(li, !maxed && state.legacy.points >= price);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
import { labCost, researchStatus } from '../engine/research.js';
|
||||
import { t } from '../services/i18n.js';
|
||||
import { h, icon } from './dom.js';
|
||||
import { h, icon, makeBuyable, setBuyable } from './dom.js';
|
||||
import { purchased } from './fx.js';
|
||||
import { effectText, moneyFormatter, setText } from './text.js';
|
||||
|
||||
const BRANCHES = ['prod', 'mkt', 'log', 'fin', 'ins'];
|
||||
@@ -16,11 +17,8 @@ export function createResearchView(root, actions) {
|
||||
|
||||
function nodeRow(node) {
|
||||
const status = h('p', { class: 'card__meta' });
|
||||
const buy = h('button', {
|
||||
class: 'btn btn--buy',
|
||||
type: 'button',
|
||||
onclick: () => actions.buyNode(node.id),
|
||||
});
|
||||
const cost = h('strong');
|
||||
const price = h('div', { class: 'card__price' }, cost);
|
||||
const li = h(
|
||||
'li',
|
||||
{ class: 'card' },
|
||||
@@ -32,9 +30,12 @@ export function createResearchView(root, actions) {
|
||||
h('p', { class: 'card__desc', text: effectText(node.effect, fmt) }),
|
||||
status,
|
||||
),
|
||||
buy,
|
||||
price,
|
||||
);
|
||||
return { li, status, buy, node };
|
||||
makeBuyable(li, () => {
|
||||
if (actions.buyNode(node.id)) purchased(li, '✓', null);
|
||||
});
|
||||
return { li, status, price, cost, node };
|
||||
}
|
||||
|
||||
function rebuild(newCtx, formatter) {
|
||||
@@ -47,7 +48,13 @@ export function createResearchView(root, actions) {
|
||||
rp: h('strong'),
|
||||
rate: h('span', { class: 'hint' }),
|
||||
labs: h('span', { class: 'hint' }),
|
||||
buyLab: h('button', { class: 'btn btn--wide', type: 'button', onclick: () => actions.buyLab() }),
|
||||
buyLab: h('button', {
|
||||
class: 'btn btn--wide',
|
||||
type: 'button',
|
||||
onclick: () => {
|
||||
if (actions.buyLab()) purchased(els.buyLab, '+1');
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const branchPanels = BRANCHES.map((branch) => {
|
||||
@@ -70,7 +77,7 @@ export function createResearchView(root, actions) {
|
||||
'section',
|
||||
{ class: 'panel panel--wide' },
|
||||
h('h2', { class: 'panel__title', text: t('tab.research') }),
|
||||
h('p', { class: 'hint', text: t('research.intro') }),
|
||||
h('p', { class: 'hint', text: `${t('research.intro')} ${t('ui.clickToBuy')}` }),
|
||||
h(
|
||||
'div',
|
||||
{ class: 'tile' },
|
||||
@@ -95,13 +102,13 @@ export function createResearchView(root, actions) {
|
||||
setText(els.buyLab, maxed ? t('research.labMax') : t('research.buyLab', { cost: money(cost) }));
|
||||
els.buyLab.disabled = maxed || state.funds < cost;
|
||||
|
||||
for (const { li, status, buy, node } of nodeRows.values()) {
|
||||
for (const { li, status, price, cost, node } of nodeRows.values()) {
|
||||
const s = researchStatus(state, node);
|
||||
li.classList.toggle('card--done', s === 'owned');
|
||||
li.classList.toggle('card--locked', s === 'locked');
|
||||
buy.hidden = s !== 'available';
|
||||
buy.disabled = state.rp < node.cost;
|
||||
setText(buy, t('research.cost', { cost: fmt.number(node.cost) }));
|
||||
price.hidden = s !== 'available';
|
||||
setText(cost, t('research.cost', { cost: fmt.number(node.cost) }));
|
||||
setBuyable(li, s === 'available' && state.rp >= node.cost);
|
||||
if (s === 'owned') setText(status, t('research.owned'));
|
||||
else if (s === 'locked') {
|
||||
const names = node.requires.map((id) => t(`research.node.${id}`)).join(', ');
|
||||
|
||||
+47
-30
@@ -3,15 +3,13 @@
|
||||
|
||||
import { generatorQuote, isUnlocked, materialPurchase, upgradeVisible } from '../engine/engine.js';
|
||||
import { t } from '../services/i18n.js';
|
||||
import { $, h, icon, refreshStaticIcons } from './dom.js';
|
||||
import { $, h, icon, makeBuyable, refreshStaticIcons, setBuyable } from './dom.js';
|
||||
import { bump, floater, pop, purchased } from './fx.js';
|
||||
import { effectText, setText } from './text.js';
|
||||
|
||||
const SLIDER_STEPS = 1000;
|
||||
const MARKET_TOLERANCE = 1.15; // demand within ±15 % of output counts as balanced
|
||||
const TREND ={ '-1': '▼', 1: '▲' }; // last move of the material price
|
||||
|
||||
function setText(node, text) {
|
||||
if (node.textContent !== text) node.textContent = text;
|
||||
}
|
||||
const TREND = { '-1': '▼', 1: '▲' }; // last move of the material price
|
||||
|
||||
/**
|
||||
* @param actions callbacks into the game: make, buyMaterial, buyMarketing,
|
||||
@@ -44,6 +42,7 @@ export function createView(actions) {
|
||||
let ctx;
|
||||
let fmt;
|
||||
let buyAmount = 1;
|
||||
let current = null; // the state and derived values of the last refresh
|
||||
let generatorRows = new Map();
|
||||
let upgradeRows = new Map();
|
||||
let upgradeSignature = null;
|
||||
@@ -53,9 +52,22 @@ export function createView(actions) {
|
||||
|
||||
// ---------------------------------------------------------------- controls
|
||||
|
||||
el.make.addEventListener('click', () => actions.make());
|
||||
el.buyMaterial.addEventListener('click', () => actions.buyMaterial());
|
||||
el.buyMarketing.addEventListener('click', () => actions.buyMarketing());
|
||||
el.make.addEventListener('click', (event) => {
|
||||
if (!actions.make()) return;
|
||||
pop(el.make);
|
||||
// The "+N" rises from the pointer (or from the button when used with the keyboard).
|
||||
const at = event.clientX || event.clientY ? { x: event.clientX, y: event.clientY } : undefined;
|
||||
floater(el.make, `+${fmt.number(current.d.clickYield)}`, at);
|
||||
});
|
||||
el.buyMaterial.addEventListener('click', () => {
|
||||
if (actions.buyMaterial()) purchased(el.buyMaterial, '+');
|
||||
});
|
||||
el.buyMarketing.addEventListener('click', () => {
|
||||
if (actions.buyMarketing()) {
|
||||
purchased(el.buyMarketing, '+1');
|
||||
bump(el.buyMarketingInfo);
|
||||
}
|
||||
});
|
||||
|
||||
const priceFromSlider = () => {
|
||||
const { minPrice, maxPrice } = ctx.rules.market;
|
||||
@@ -78,24 +90,17 @@ export function createView(actions) {
|
||||
|
||||
// ------------------------------------------------------------------- lists
|
||||
|
||||
function effectText(effect) {
|
||||
const params = { value: fmt.number(effect.value) };
|
||||
if (effect.target) params.target = t(`theme.gen.${effect.target}.name`);
|
||||
if (effect.type === 'batch_discount') params.percent = fmt.number(Math.round((1 - effect.value) * 100));
|
||||
return t(`effect.${effect.type}`, params);
|
||||
/** What a click on a generator block buys right now (at least one). */
|
||||
function shownQuote(id) {
|
||||
const quote = generatorQuote(current.state, ctx, id, buyAmount);
|
||||
return quote.count > 0 ? quote : generatorQuote(current.state, ctx, id, 1);
|
||||
}
|
||||
|
||||
function generatorRow(g) {
|
||||
const owned = h('span', { class: 'badge' });
|
||||
const rate = h('span');
|
||||
const amount = h('span', { class: 'btn__label' });
|
||||
const amount = h('strong');
|
||||
const cost = h('small');
|
||||
const buy = h(
|
||||
'button',
|
||||
{ class: 'btn btn--buy', type: 'button', onclick: () => actions.buyGenerator(g.id, buyAmount) },
|
||||
amount,
|
||||
cost,
|
||||
);
|
||||
const root = h(
|
||||
'li',
|
||||
{ class: 'card', hidden: true },
|
||||
@@ -107,14 +112,20 @@ export function createView(actions) {
|
||||
h('p', { class: 'card__desc', text: t(`theme.gen.${g.id}.desc`) }),
|
||||
h('p', { class: 'card__meta' }, rate),
|
||||
),
|
||||
buy,
|
||||
h('div', { class: 'card__price' }, amount, cost),
|
||||
);
|
||||
makeBuyable(root, () => {
|
||||
const { count } = shownQuote(g.id);
|
||||
if (!actions.buyGenerator(g.id, buyAmount)) return;
|
||||
purchased(root, `+${fmt.number(count)}`);
|
||||
bump(owned);
|
||||
});
|
||||
el.generators.append(root);
|
||||
return { root, owned, rate, amount, cost, buy };
|
||||
return { root, owned, rate, amount, cost };
|
||||
}
|
||||
|
||||
function upgradeRow(u) {
|
||||
const cost = h('small');
|
||||
const cost = h('strong');
|
||||
const root = h(
|
||||
'li',
|
||||
{ class: 'card' },
|
||||
@@ -123,11 +134,16 @@ export function createView(actions) {
|
||||
'div',
|
||||
{ class: 'card__body' },
|
||||
h('h3', { class: 'card__title', text: t(`theme.upg.${u.id}`) }),
|
||||
h('p', { class: 'card__desc', text: effectText(u.effect) }),
|
||||
h('p', { class: 'card__desc', text: effectText(u.effect, fmt) }),
|
||||
),
|
||||
h('button', { class: 'btn btn--buy', type: 'button', onclick: () => actions.buyUpgrade(u.id) }, cost),
|
||||
h('div', { class: 'card__price' }, cost),
|
||||
);
|
||||
return { root, cost, buy: root.lastElementChild, upgrade: u };
|
||||
makeBuyable(root, () => {
|
||||
if (!actions.buyUpgrade(u.id)) return;
|
||||
purchased(root, '✓');
|
||||
bump(el.upgradeCount);
|
||||
});
|
||||
return { root, cost, upgrade: u };
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ public
|
||||
@@ -160,9 +176,9 @@ export function createView(actions) {
|
||||
|
||||
setText(row.owned, t('ui.owned', { count: fmt.number(owned) }));
|
||||
setText(row.rate, t('ui.rateEach', { rate: fmt.number(d.perGenerator[g.id]) }));
|
||||
setText(row.amount, t('ui.buyN', { count: fmt.number(quote.count) }));
|
||||
setText(row.amount, `×${fmt.number(quote.count)}`);
|
||||
setText(row.cost, money(quote.cost));
|
||||
row.buy.disabled = state.funds < quote.cost;
|
||||
setBuyable(row.root, state.funds >= quote.cost);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +193,7 @@ export function createView(actions) {
|
||||
}
|
||||
for (const row of upgradeRows.values()) {
|
||||
setText(row.cost, money(row.upgrade.cost));
|
||||
row.buy.disabled = state.funds < row.upgrade.cost;
|
||||
setBuyable(row.root, state.funds >= row.upgrade.cost);
|
||||
}
|
||||
setText(
|
||||
el.upgradeCount,
|
||||
@@ -222,6 +238,7 @@ export function createView(actions) {
|
||||
|
||||
/** @param flow { income } smoothed funds per second */
|
||||
function update(state, d, flow) {
|
||||
current = { state, d };
|
||||
setText(el.units, fmt.number(Math.floor(state.units)));
|
||||
setText(el.funds, money(state.funds));
|
||||
setText(el.material, fmt.number(Math.floor(state.material)));
|
||||
|
||||
Reference in New Issue
Block a user