feat: More bonus (exponential) and method to list icons needed
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
// Loads the game data from disk (the browser loads the same files over HTTP).
|
||||
// Shared by the tests and the scripts in this folder.
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { buildContext } from '../public/js/engine/context.js';
|
||||
import { DATA_FILES } from '../public/js/services/data.js';
|
||||
|
||||
export const PUBLIC_DIR = fileURLToPath(new URL('../public/', import.meta.url));
|
||||
|
||||
export const readJson = (relativePath) => JSON.parse(readFileSync(PUBLIC_DIR + relativePath, 'utf8'));
|
||||
|
||||
export function loadContext() {
|
||||
const entries = Object.entries(DATA_FILES).map(([key, file]) => [key, readJson(`data/${file}.json`)]);
|
||||
return buildContext(Object.fromEntries(entries));
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
// The complete list of icons a theme must provide, derived from the game data
|
||||
// (so it can never drift from the game). Every key is what the UI passes to
|
||||
// icon(key) in public/js/ui/dom.js; a theme maps it to a file in theme.json.
|
||||
|
||||
import { readdirSync } from 'node:fs';
|
||||
import { PUBLIC_DIR, readJson } from './context.js';
|
||||
|
||||
export const THEME_IDS = readdirSync(`${PUBLIC_DIR}themes`, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => entry.name)
|
||||
.sort((a, b) => readJson(`themes/${a}/theme.json`).order - readJson(`themes/${b}/theme.json`).order);
|
||||
|
||||
function flatten(object, prefix = '', out = {}) {
|
||||
for (const [key, value] of Object.entries(object)) {
|
||||
const path = prefix ? `${prefix}.${key}` : key;
|
||||
if (value && typeof value === 'object') flatten(value, path, out);
|
||||
else out[path] = String(value);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** English texts of a theme: the core locale plus the theme's own, merged like the game does. */
|
||||
function dictionaryFor(themeId) {
|
||||
return { ...flatten(readJson('locales/en.json')), ...flatten(readJson(`themes/${themeId}/locales/en.json`)) };
|
||||
}
|
||||
|
||||
/** Same interpolation as services/i18n.js: {param} and {@other.key}. */
|
||||
function render(dict, key, params = {}, depth = 0) {
|
||||
const text = dict[key];
|
||||
if (text === undefined) return key;
|
||||
return text.replace(/\{(@?)([\w.]+)\}/g, (match, embed, name) => {
|
||||
if (embed) return depth < 5 ? render(dict, name, params, depth + 1) : '';
|
||||
return name in params ? params[name] : match;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Groups of icons. `themed` groups have a different subject in every theme
|
||||
* (so the table has one column per theme); the others have the same subject
|
||||
* everywhere (but still need drawing in each theme's style).
|
||||
* Each icon's `label(dict)` gives what to draw, from that theme's texts.
|
||||
*/
|
||||
export function iconGroups(ctx) {
|
||||
const text = (key) => (dict) => render(dict, key);
|
||||
const resource = (name) => ({ key: `res.${name}`, label: text(`theme.res.${name}`) });
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'res',
|
||||
title: 'Resources',
|
||||
shownIn: 'top bar (units, funds, material), Gamble, Research and Legacy screens',
|
||||
themed: true,
|
||||
icons: ['units', 'funds', 'material', 'chips', 'research', 'legacy'].map(resource),
|
||||
},
|
||||
{
|
||||
id: 'gen',
|
||||
title: 'Automation tiers',
|
||||
shownIn: 'Production screen, Automation list',
|
||||
themed: true,
|
||||
icons: ctx.generators.map((g) => ({ key: `gen.${g.id}`, label: text(`theme.gen.${g.id}.name`) })),
|
||||
},
|
||||
{
|
||||
id: 'upg',
|
||||
title: 'Upgrades',
|
||||
shownIn: 'Production screen, Upgrades list',
|
||||
themed: true,
|
||||
icons: ctx.upgrades.map((u) => ({ key: `upg.${u.id}`, label: text(`theme.upg.${u.id}`) })),
|
||||
},
|
||||
{
|
||||
id: 'inv',
|
||||
title: 'Investment societies',
|
||||
shownIn: 'Investments screen',
|
||||
themed: true,
|
||||
icons: ctx.investments.societies.map((s) => ({ key: `inv.${s.id}`, label: text(`theme.inv.${s.id}.name`) })),
|
||||
},
|
||||
{
|
||||
id: 'endless',
|
||||
title: 'Endless upgrades',
|
||||
shownIn: 'Production screen, Endless upgrades panel',
|
||||
themed: true,
|
||||
icons: ctx.endless.map((line) => ({
|
||||
key: `endless.${line.id}`,
|
||||
label: (dict) =>
|
||||
line.effect.target
|
||||
? render(dict, 'endless.name.generator', { target: dict[`theme.gen.${line.effect.target}.name`] })
|
||||
: render(dict, `endless.name.${line.id}`),
|
||||
})),
|
||||
},
|
||||
{
|
||||
id: 'research',
|
||||
title: 'Research',
|
||||
shownIn: 'Research screen',
|
||||
themed: false,
|
||||
icons: ctx.research.map((r) => ({ key: `research.${r.id}`, label: text(`research.node.${r.id}`) })),
|
||||
},
|
||||
{
|
||||
id: 'legacy',
|
||||
title: 'Legacy perks',
|
||||
shownIn: 'Legacy screen',
|
||||
themed: false,
|
||||
icons: ctx.legacy.map((p) => ({ key: `legacy.${p.id}`, label: text(`legacy.perk.${p.id}`) })),
|
||||
},
|
||||
{
|
||||
id: 'ach',
|
||||
title: 'Achievements',
|
||||
shownIn: 'Achievements screen',
|
||||
themed: false,
|
||||
icons: ctx.achievements.map((a) => ({ key: `ach.${a.id}`, label: text(`ach.title.${a.id}`) })),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function allIconKeys(ctx) {
|
||||
return iconGroups(ctx).flatMap((group) => group.icons.map((icon) => icon.key));
|
||||
}
|
||||
|
||||
/** Markdown for the README: totals, then one table per group. */
|
||||
export function renderMarkdown(ctx) {
|
||||
const groups = iconGroups(ctx);
|
||||
const dicts = Object.fromEntries(THEME_IDS.map((id) => [id, dictionaryFor(id)]));
|
||||
const perTheme = groups.reduce((sum, g) => sum + g.icons.length, 0);
|
||||
const themeColumns = THEME_IDS.map((id) => `\`${id}\``);
|
||||
|
||||
const lines = [
|
||||
`**${perTheme} icons per theme, ${perTheme * THEME_IDS.length} in total** (${THEME_IDS.length} themes: ${themeColumns.join(', ')}).`,
|
||||
'',
|
||||
'| Group | Icons per theme | Shown in |',
|
||||
'|---|---:|---|',
|
||||
...groups.map((g) => `| ${g.title} | ${g.icons.length} | ${g.shownIn} |`),
|
||||
`| **Total** | **${perTheme}** | |`,
|
||||
'',
|
||||
];
|
||||
|
||||
for (const group of groups) {
|
||||
lines.push(`#### ${group.title} (${group.icons.length})`, '');
|
||||
if (group.themed) {
|
||||
lines.push(
|
||||
`| Key | ${themeColumns.join(' | ')} |`,
|
||||
`|---|${THEME_IDS.map(() => '---').join('|')}|`,
|
||||
...group.icons.map(
|
||||
(icon) => `| \`${icon.key}\` | ${THEME_IDS.map((id) => icon.label(dicts[id])).join(' | ')} |`,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
lines.push(
|
||||
"| Key | Subject (the same in every theme, drawn in each theme's style) |",
|
||||
'|---|---|',
|
||||
...group.icons.map((icon) => `| \`${icon.key}\` | ${icon.label(dicts[THEME_IDS[0]])} |`),
|
||||
);
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
return lines.join('\n').trimEnd();
|
||||
}
|
||||
|
||||
/** theme.json "assets" skeleton: every key mapped to its default file name. */
|
||||
export function assetsSkeleton(ctx, extension = 'svg') {
|
||||
return Object.fromEntries(allIconKeys(ctx).map((key) => [key, `img/${key.replaceAll('.', '-')}.${extension}`]));
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Icon list tooling.
|
||||
//
|
||||
// npm run icons print the exhaustive list as Markdown
|
||||
// npm run icons -- --write refresh the list inside README.md
|
||||
// npm run icons -- --manifest print a theme.json "assets" skeleton (every key -> img/<key>.svg)
|
||||
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { assetsSkeleton, renderMarkdown } from './icon-keys.js';
|
||||
import { loadContext } from './context.js';
|
||||
|
||||
export const README = fileURLToPath(new URL('../README.md', import.meta.url));
|
||||
export const START = '<!-- icons:start (generated by `npm run icons -- --write`, do not edit by hand) -->';
|
||||
export const END = '<!-- icons:end -->';
|
||||
|
||||
/** README text with the generated list refreshed between the markers. */
|
||||
export function withIconList(readme, ctx) {
|
||||
const start = readme.indexOf(START);
|
||||
const end = readme.indexOf(END);
|
||||
if (start < 0 || end < start) throw new Error('README.md has no icon list markers');
|
||||
return `${readme.slice(0, start + START.length)}\n\n${renderMarkdown(ctx)}\n\n${readme.slice(end)}`;
|
||||
}
|
||||
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
const ctx = loadContext();
|
||||
const args = process.argv.slice(2);
|
||||
if (args.includes('--write')) {
|
||||
writeFileSync(README, withIconList(readFileSync(README, 'utf8'), ctx));
|
||||
console.log('README.md updated');
|
||||
} else if (args.includes('--manifest')) {
|
||||
console.log(JSON.stringify({ assets: assetsSkeleton(ctx) }, null, 2));
|
||||
} else {
|
||||
console.log(renderMarkdown(ctx));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user