feat: Simplification of UI and icons creation

This commit is contained in:
gauvainboiche
2026-10-08 16:52:21 +02:00
parent a88b8f5f14
commit 2f7c307f0e
22 changed files with 1099 additions and 408 deletions
+44 -57
View File
@@ -35,10 +35,8 @@ function render(dict, key, params = {}, depth = 0) {
}
/**
* 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.
* Groups of icons. Each icon's `label(dict)` says what to draw, from that
* theme's texts. Every group has to be drawn in every theme's style.
*/
export function iconGroups(ctx) {
const text = (key) => (dict) => render(dict, key);
@@ -46,38 +44,23 @@ export function iconGroups(ctx) {
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) =>
@@ -87,25 +70,24 @@ export function iconGroups(ctx) {
})),
},
{
id: 'research',
title: 'Investment societies',
shownIn: 'Investments screen',
icons: ctx.investments.societies.map((s) => ({ key: `inv.${s.id}`, label: text(`theme.inv.${s.id}.name`) })),
},
{
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}`) })),
title: 'Trophy',
shownIn: 'Achievements screen (one shared icon for all achievements)',
icons: [{ key: 'trophy', label: () => 'A trophy' }],
},
];
}
@@ -114,46 +96,51 @@ 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) {
/** Default file name of an icon: gen.gen1 -> img/gen-gen1.svg */
export function fileFor(key, extension = 'svg') {
return `img/${key.replaceAll('.', '-')}.${extension}`;
}
/** The plain-text work list written to ICONS.txt: one checklist per theme. */
export function renderText(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 total = groups.reduce((sum, g) => sum + g.icons.length, 0);
const all = groups.flatMap((g) => g.icons);
const keyWidth = Math.max(...all.map((icon) => icon.key.length));
const fileWidth = Math.max(...all.map((icon) => fileFor(icon.key).length));
const lines = [
`**${perTheme} icons per theme, ${perTheme * THEME_IDS.length} in total** (${THEME_IDS.length} themes: ${themeColumns.join(', ')}).`,
'ICONS TO PRODUCE',
'================',
'',
'| Group | Icons per theme | Shown in |',
'|---|---:|---|',
...groups.map((g) => `| ${g.title} | ${g.icons.length} | ${g.shownIn} |`),
`| **Total** | **${perTheme}** | |`,
'Generated by "npm run icons -- --write" from the game data: do not edit by hand.',
`${total} icons per theme, ${total * THEME_IDS.length} in total (${THEME_IDS.length} themes: ${THEME_IDS.join(', ')}).`,
'',
'Format One square image per icon: SVG preferred (or PNG, 128x128 px or more),',
' transparent background, readable at 28 px (top bar) and 48 px (lists).',
"Files public/themes/<theme>/img/<file>, then listed in that theme's theme.json:",
' { "assets": { "<key>": "img/<file>" } } (an icon that is not listed keeps its grey square)',
'',
'SUMMARY (icons per theme)',
...groups.map((g) => ` ${String(g.icons.length).padStart(3)} ${g.title} [${g.shownIn}]`),
` ${String(total).padStart(3)} TOTAL`,
];
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]])} |`),
);
for (const themeId of THEME_IDS) {
const dict = dictionaryFor(themeId);
lines.push('', '', `THEME: ${themeId}`, '-'.repeat(7 + themeId.length));
for (const group of groups) {
lines.push('', `${group.title} (${group.icons.length})`);
for (const icon of group.icons) {
const file = fileFor(icon.key).padEnd(fileWidth);
lines.push(` [ ] ${file} ${icon.key.padEnd(keyWidth)} ${icon.label(dict)}`);
}
}
lines.push('');
}
return lines.join('\n').trimEnd();
return `${lines.join('\n')}\n`;
}
/** 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}`]));
return Object.fromEntries(allIconKeys(ctx).map((key) => [key, fileFor(key, extension)]));
}
+8 -18
View File
@@ -1,35 +1,25 @@
// 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 print the work list
// npm run icons -- --write write it to ICONS.txt
// npm run icons -- --manifest print a theme.json "assets" skeleton (every key -> img/<key>.svg)
import { readFileSync, writeFileSync } from 'node:fs';
import { writeFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { assetsSkeleton, renderMarkdown } from './icon-keys.js';
import { loadContext } from './context.js';
import { assetsSkeleton, renderText } from './icon-keys.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)}`;
}
export const ICONS_FILE = fileURLToPath(new URL('../ICONS.txt', import.meta.url));
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');
writeFileSync(ICONS_FILE, renderText(ctx));
console.log('ICONS.txt updated');
} else if (args.includes('--manifest')) {
console.log(JSON.stringify({ assets: assetsSkeleton(ctx) }, null, 2));
} else {
console.log(renderMarkdown(ctx));
process.stdout.write(renderText(ctx));
}
}