feat: Simplification of UI and icons creation
This commit is contained in:
+154
-12
@@ -128,16 +128,6 @@
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.btn--buy {
|
||||
flex-direction: column;
|
||||
min-width: 7.5rem;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.btn__label {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------- form fields */
|
||||
|
||||
.field {
|
||||
@@ -312,7 +302,7 @@ select,
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* Narrow phones: the buy button drops under the text. */
|
||||
/* Narrow phones: the price drops under the text. */
|
||||
@media (max-width: 479px) {
|
||||
.card {
|
||||
flex-wrap: wrap;
|
||||
@@ -322,10 +312,14 @@ select,
|
||||
flex-basis: calc(100% - var(--icon-size) - var(--gap));
|
||||
}
|
||||
|
||||
.btn--buy {
|
||||
.card__price {
|
||||
flex: 1 0 100%;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
padding-top: 0.35rem;
|
||||
border-top: 1px solid var(--border);
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -642,3 +636,151 @@ select,
|
||||
.tile .hint {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------ blocks you click to buy */
|
||||
|
||||
.card--buy {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
touch-action: manipulation;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
transition:
|
||||
box-shadow 0.12s ease,
|
||||
transform 0.08s ease,
|
||||
opacity 0.12s ease;
|
||||
}
|
||||
|
||||
/* A soft outline on hover (only where there is a real hover: not on touch screens). */
|
||||
@media (hover: hover) {
|
||||
.card--buy:hover:not(.is-disabled) {
|
||||
box-shadow: 0 0 0 2px var(--accent);
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 55%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.card--buy:active:not(.is-disabled) {
|
||||
transform: scale(0.985);
|
||||
}
|
||||
|
||||
.card--buy.is-disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* What it costs, on the right of the block; a fixed width keeps the figures aligned. */
|
||||
.card__price {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
align-self: center;
|
||||
gap: 0.1rem;
|
||||
min-width: 6.5rem;
|
||||
margin-left: auto;
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.card__price strong {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.card__price small {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.card__hint {
|
||||
align-self: center;
|
||||
margin-right: auto;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------- purchase animations */
|
||||
/* Quick on purpose (0.15 to 0.25 s): feedback, not a show. */
|
||||
|
||||
@keyframes fx-pop {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 0 0 0 var(--accent);
|
||||
}
|
||||
40% {
|
||||
transform: scale(1.04);
|
||||
box-shadow: 0 0 0 5px color-mix(in srgb, var(--accent) 45%, transparent);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 0 0 0 transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fx-bump {
|
||||
50% {
|
||||
transform: scale(1.35);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fx-spend {
|
||||
from {
|
||||
color: var(--bad);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes floater {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -100%);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, calc(-100% - 26px));
|
||||
}
|
||||
}
|
||||
|
||||
.fx-pop {
|
||||
animation: fx-pop 0.2s ease-out;
|
||||
}
|
||||
|
||||
.fx-bump {
|
||||
animation: fx-bump 0.15s ease-out;
|
||||
}
|
||||
|
||||
.fx-spend {
|
||||
animation: fx-spend 0.2s ease-out;
|
||||
}
|
||||
|
||||
.floater {
|
||||
position: fixed;
|
||||
z-index: 20;
|
||||
color: var(--accent);
|
||||
font-weight: 700;
|
||||
pointer-events: none;
|
||||
text-shadow: 0 1px 2px rgb(0 0 0 / 0.3);
|
||||
animation: floater 0.25s ease-out forwards;
|
||||
}
|
||||
|
||||
.floater--good {
|
||||
color: var(--good);
|
||||
}
|
||||
|
||||
.floater--bad {
|
||||
color: var(--bad);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.fx-pop,
|
||||
.fx-bump,
|
||||
.fx-spend {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.floater {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.card--buy {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
<label><input type="radio" name="buy-amount" value="max"><span data-i18n="ui.max"></span></label>
|
||||
</fieldset>
|
||||
</div>
|
||||
<p class="hint" data-i18n="ui.clickToBuy"></p>
|
||||
<ul class="cards" id="generators"></ul>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -29,14 +29,14 @@
|
||||
"marketingInfo": "Level {level} → demand ×{multiplier} · {cost}",
|
||||
"owned": "Owned: {count}",
|
||||
"rateEach": "{rate}/s each",
|
||||
"buyN": "Buy ×{count}",
|
||||
"upgradeCount": "{owned} / {total}",
|
||||
"hintDemand": "At this price customers want {demand}/s; you make {production}/s.",
|
||||
"hintDemandOnly": "At this price customers want {demand}/s.",
|
||||
"marketShort": "Demand outpaces your output: you are selling too cheap.",
|
||||
"marketLong": "You make more than customers want: stock piles up. Try a lower price.",
|
||||
"marketBalanced": "Supply and demand are about even.",
|
||||
"menu": "Menu"
|
||||
"menu": "Menu",
|
||||
"clickToBuy": "Click a block to buy it."
|
||||
},
|
||||
"effect": {
|
||||
"click_mult": "Manual work ×{value}",
|
||||
@@ -91,7 +91,6 @@
|
||||
"tabUnlocked": "New section unlocked: {name}",
|
||||
"contractDone": "Contract complete! {reward}",
|
||||
"contractFailed": "Contract failed: time ran out.",
|
||||
"eventOpportunity": "Opportunity: {title}! Seize it in the Contracts tab.",
|
||||
"eventIncident": "Incident: {title}",
|
||||
"achievement": "Achievement: {title}",
|
||||
"restarted": "A new run begins! +{gain} {@theme.res.legacy}"
|
||||
@@ -125,11 +124,11 @@
|
||||
"amountLabel": "Amount per trade (share of your funds when buying, of the holding when selling)",
|
||||
"amount": "Amount per trade",
|
||||
"nextUpdate": "Next price update in {seconds} s",
|
||||
"buy": "Buy",
|
||||
"sell": "Sell",
|
||||
"priceLine": "Price {price} ({gap} vs fair value)",
|
||||
"heldLine": "Held: {value} ({profit})",
|
||||
"heldNone": "Nothing held"
|
||||
"heldNone": "Nothing held",
|
||||
"clickToBuy": "Click to buy"
|
||||
},
|
||||
"gamble": {
|
||||
"intro": "{@theme.res.chips} are hard to buy and harder to keep. Stake some on a cycle: win and the stake doubles, hit a jackpot and it multiplies, lose and it is gone, crash and you lose even more. They also boost demand.",
|
||||
@@ -311,7 +310,6 @@
|
||||
"perLevel": "Each level: {effect}",
|
||||
"total": "Now: {effect}",
|
||||
"none": "No level yet",
|
||||
"buy": "Buy ×{count}",
|
||||
"maxed": "Maxed",
|
||||
"name": {
|
||||
"generator": "{target} overhaul",
|
||||
|
||||
@@ -29,14 +29,14 @@
|
||||
"marketingInfo": "Niveau {level} → demande ×{multiplier} · {cost}",
|
||||
"owned": "Possédé : {count}",
|
||||
"rateEach": "{rate}/s chacun",
|
||||
"buyN": "Acheter ×{count}",
|
||||
"upgradeCount": "{owned} / {total}",
|
||||
"hintDemand": "À ce prix, les clients veulent {demand}/s ; vous produisez {production}/s.",
|
||||
"hintDemandOnly": "À ce prix, les clients veulent {demand}/s.",
|
||||
"marketShort": "La demande dépasse votre production : vous vendez trop bon marché.",
|
||||
"marketLong": "Vous produisez plus que la demande : le stock s'accumule. Essayez un prix plus bas.",
|
||||
"marketBalanced": "L'offre et la demande sont à peu près égales.",
|
||||
"menu": "Menu"
|
||||
"menu": "Menu",
|
||||
"clickToBuy": "Cliquez sur un bloc pour l'acheter."
|
||||
},
|
||||
"effect": {
|
||||
"click_mult": "Travail manuel ×{value}",
|
||||
@@ -91,7 +91,6 @@
|
||||
"tabUnlocked": "Nouvelle section débloquée : {name}",
|
||||
"contractDone": "Contrat terminé ! {reward}",
|
||||
"contractFailed": "Contrat échoué : le temps est écoulé.",
|
||||
"eventOpportunity": "Opportunité : {title} ! Saisissez-la dans l'onglet Contrats.",
|
||||
"eventIncident": "Incident : {title}",
|
||||
"achievement": "Succès : {title}",
|
||||
"restarted": "Une nouvelle partie commence ! +{gain} ({@theme.res.legacy})"
|
||||
@@ -125,11 +124,11 @@
|
||||
"amountLabel": "Montant par transaction (part de vos fonds à l'achat, du titre détenu à la vente)",
|
||||
"amount": "Montant par transaction",
|
||||
"nextUpdate": "Prochaine mise à jour des prix dans {seconds} s",
|
||||
"buy": "Acheter",
|
||||
"sell": "Vendre",
|
||||
"priceLine": "Prix {price} ({gap} par rapport à la valeur juste)",
|
||||
"heldLine": "Détenu : {value} ({profit})",
|
||||
"heldNone": "Rien de détenu"
|
||||
"heldNone": "Rien de détenu",
|
||||
"clickToBuy": "Cliquer pour acheter"
|
||||
},
|
||||
"gamble": {
|
||||
"intro": "{@theme.res.chips} : difficiles à acheter, plus difficiles encore à garder. Misez-en sur un cycle : gagnez et la mise double, tirez le jackpot et elle se multiplie, perdez et elle disparaît, plantez-vous et vous perdez encore plus. Ils augmentent aussi la demande.",
|
||||
@@ -311,7 +310,6 @@
|
||||
"perLevel": "Chaque niveau : {effect}",
|
||||
"total": "Actuellement : {effect}",
|
||||
"none": "Aucun niveau pour l'instant",
|
||||
"buy": "Acheter ×{count}",
|
||||
"maxed": "Au maximum",
|
||||
"name": {
|
||||
"generator": "Refonte : {target}",
|
||||
|
||||
Reference in New Issue
Block a user