Skip to content

Commit e983cf9

Browse files
TMHSDigitalclaude
andcommitted
Engine filter: hide other-engine-only entries, list engine picks first
An Engine select (Any, Godot, Unity, Unreal Engine) on the catalog hides entries whose engine_lock names another engine and sorts entries tagged for the chosen engine (godot and godot-4, unity, unreal-engine and unreal) first within each category. It is carried in the URL as engine=, shown as an active-filter chip, and unknown values are ignored. On a UE-only entry page the engine lock is now the first row of the facts panel. Checked in Chromium: ?engine=godot shows 412 of 417 entries, with the UE-only ones hidden and Godot tools first. Closes #160 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 864aaea commit e983cf9

5 files changed

Lines changed: 56 additions & 6 deletions

File tree

‎site/lib/entry-page.mjs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,14 @@ export function entryPageHtml({ entry, leadHtml, restHtml, deprecatedReasonHtml,
8181
const licence =
8282
entry.license_spdx && entry.license_spdx !== entry.license ? `${entry.license} (${entry.license_spdx})` : entry.license;
8383
const rows = [
84+
// Usable in one engine only: the first thing a reader must know.
85+
entry.engine_lock ? ["Engine lock", esc(ENGINE_LOCK_NOTES[entry.engine_lock] || entry.engine_lock)] : null,
8486
["Licence", esc(licence)],
8587
["Commercial use", esc(commercialLabel(entry.commercial))],
8688
["Credit", esc(c.value)],
8789
["Status", `${esc(entry.status)}: ${esc(STATUS_NOTES[entry.status] || "")}`],
8890
["Verified", `<span class="verified is-${age.bucket}">${esc(ageText)}</span>`],
8991
entry.maintenance ? ["Maintenance", `${esc(entry.maintenance)}: ${esc(MAINTENANCE_NOTES[entry.maintenance] || "")}`] : null,
90-
entry.engine_lock ? ["Engine lock", esc(ENGINE_LOCK_NOTES[entry.engine_lock] || entry.engine_lock)] : null,
9192
entry.ue_versions ? ["Unreal Engine", `${esc(entry.ue_versions)}, as stated by the source`] : null,
9293
entry.publisher ? ["Publisher", esc(entry.publisher)] : null,
9394
["Formats", esc((entry.formats || []).join(", ") || "none listed")],

‎site/lib/lib.test.mjs‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -763,7 +763,7 @@ eq("every word must match", kScore("ui zzz"), 0);
763763
eq("name hit outranks summary hit", kScore("kit") > kScore("panels"), true);
764764
eq("regex characters are literal", kScore("c++"), 0);
765765

766-
const URL_DEF = { category: "all", q: "", active: true, review: true, deprecated: false, commercialOnly: false, noAttr: false, perspective: "any", license: "any", format: "any", sort: "name" };
766+
const URL_DEF = { category: "all", q: "", active: true, review: true, deprecated: false, commercialOnly: false, noAttr: false, perspective: "any", license: "any", format: "any", engine: "any", sort: "name" };
767767
const URL_ALLOWED = { category: ["all", "2d", "3d"], sort: ["name", "license", "verified"], perspective: ["any", "top_down"], license: ["any", "cc0"], format: ["any", "image"] };
768768
const urlSt = readState("?cat=2d&q=pixel&sort=bogus&view=top_down&deprecated=1&review=maybe&licence=cc0", URL_DEF, URL_ALLOWED);
769769
eq("url: known category kept", urlSt.category, "2d");
@@ -781,6 +781,16 @@ eq("url: blank q not written", stateQuery({ ...URL_DEF, q: " " }, URL_DEF), ""
781781
const fmtGroups = { image: { formats: ["PNG", "SVG"] } };
782782
eq("filter: needs-review hidden when review off", passesFilters({ status: "needs-review" }, { ...URL_DEF, review: false }), false);
783783
eq("filter: deprecated hidden by default", passesFilters({ status: "deprecated" }, URL_DEF), false);
784+
{
785+
const { ENGINES: EN, engineRank: er } = await import("./search.mjs");
786+
eq("engine: godot hides UE-only", passesFilters({ status: "active", engine_lock: "unreal" }, { ...URL_DEF, engine: "godot" }), false);
787+
eq("engine: unreal keeps UE-only", passesFilters({ status: "active", engine_lock: "unreal" }, { ...URL_DEF, engine: "unreal" }), true);
788+
eq("engine: agnostic entries stay", passesFilters({ status: "active" }, { ...URL_DEF, engine: "godot" }), true);
789+
eq("engine: godot-tagged ranks first", er({ tags: ["godot-4"] }, "godot") - er({ tags: ["pixel"] }, "godot"), -1);
790+
eq("engine: url round-trips", readState("?engine=unity", URL_DEF, { engine: ["any", ...Object.keys(EN)] }).engine, "unity");
791+
eq("engine: written to the url", stateQuery({ ...URL_DEF, engine: "godot" }, URL_DEF), "engine=godot");
792+
eq("engine: unknown value ignored", readState("?engine=cryengine", URL_DEF, { engine: ["any", ...Object.keys(EN)] }).engine, "any");
793+
}
784794
eq("filter: commercial-only keeps varies", passesFilters({ status: "active", commercial: "varies" }, { ...URL_DEF, commercialOnly: true }), true);
785795
eq("filter: commercial-only drops unknown", passesFilters({ status: "active", commercial: "unknown" }, { ...URL_DEF, commercialOnly: true }), false);
786796
eq("filter: no-attribution needs false, not unknown", passesFilters({ status: "active", attribution_required: "unknown" }, { ...URL_DEF, noAttr: true }), false);

‎site/lib/search.mjs‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export function passesFilters(entry, state, formatGroups = {}) {
7878
if (state.noAttr && entry.attribution_required !== false) return false;
7979
if (state.perspective !== "any" && entry.camera_perspective !== state.perspective) return false;
8080
if (state.license !== "any" && entry.licenseFamily !== state.license) return false;
81+
if (state.engine && state.engine !== "any" && entry.engine_lock && entry.engine_lock !== state.engine) return false;
8182
if (state.format !== "any") {
8283
const wanted = formatGroups[state.format]?.formats || [];
8384
if (!(entry.formats || []).some((f) => wanted.includes(f))) return false;
@@ -86,12 +87,32 @@ export function passesFilters(entry, state, formatGroups = {}) {
8687
}
8788

8889
/** Choice params: URL key -> state field. `allowed` supplies each field's legal values. */
90+
/**
91+
* The engine filter. An entry locked to another engine (engine_lock) is
92+
* hidden; entries tagged for the chosen engine sort first. Everything else
93+
* is engine-agnostic and stays listed.
94+
*/
95+
export const ENGINES = {
96+
godot: { label: "Godot", tags: ["godot", "godot-4"] },
97+
unity: { label: "Unity", tags: ["unity"] },
98+
unreal: { label: "Unreal Engine", tags: ["unreal-engine", "unreal"] },
99+
};
100+
101+
/** 0 when an entry is made for `engine` (tagged for it or locked to it), else 1. */
102+
export function engineRank(entry, engine) {
103+
const e = ENGINES[engine];
104+
if (!e) return 0;
105+
if (entry.engine_lock === engine) return 0;
106+
return (entry.tags || []).some((t) => e.tags.includes(t)) ? 0 : 1;
107+
}
108+
89109
const CHOICES = [
90110
["cat", "category"],
91111
["sort", "sort"],
92112
["view", "perspective"],
93113
["licence", "license"],
94114
["format", "format"],
115+
["engine", "engine"],
95116
];
96117
/** On/off params: URL key -> state field, written as 1 or 0. */
97118
const SWITCHES = [
@@ -132,7 +153,7 @@ export function stateQuery(state, defaults) {
132153
const field = Object.fromEntries(CHOICES);
133154
if (state.category !== defaults.category) p.set("cat", state.category);
134155
if (state.q.trim()) p.set("q", state.q.trim());
135-
for (const key of ["sort", "view", "licence", "format"]) {
156+
for (const key of ["sort", "view", "licence", "format", "engine"]) {
136157
if (state[field[key]] !== defaults[field[key]]) p.set(key, state[field[key]]);
137158
}
138159
for (const [key, f] of SWITCHES) {

‎site/public/app.js‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { closestEntries, passesFilters as passes, queryTests as compileQuery, readState, scoreFields, searchFields, slugOf, stateQuery } from "./search.js";
1+
import { closestEntries, passesFilters as passes, queryTests as compileQuery, readState, scoreFields, searchFields, slugOf, stateQuery, ENGINES, engineRank } from "./search.js";
22
import { CARD_LIMIT, LIST_LIMIT, commercialLabel, entryCardHtml, escapeHtml, groupHeadingHtml, groupMoreHtml, PERSPECTIVE_LABELS } from "./cards.mjs";
33
import { installViewToggles } from "./views.js";
44

@@ -29,6 +29,7 @@ import { installViewToggles } from "./views.js";
2929
perspective: "any",
3030
license: "any",
3131
format: "any",
32+
engine: "any",
3233
sort: "name",
3334
};
3435
const state = { ...DEFAULTS };
@@ -78,6 +79,7 @@ import { installViewToggles } from "./views.js";
7879
sort: Object.keys(SORTS),
7980
perspective: ["any", ...Object.keys(PERSPECTIVE_LABELS)],
8081
license: ["any", ...Object.keys(LICENSE_FAMILIES)],
82+
engine: ["any", ...Object.keys(ENGINES)],
8183
format: ["any", ...Object.keys(FORMAT_GROUPS)],
8284
})
8385
);
@@ -143,7 +145,7 @@ import { installViewToggles } from "./views.js";
143145
// Searching under the default sort: best match first, then by name.
144146
list.sort((a, b) => score.get(b.id) - score.get(a.id) || SORTS.name(a, b));
145147
} else {
146-
list.sort(SORTS[state.sort] || SORTS.name);
148+
list.sort(sorter());
147149
}
148150
return { list, counts, total: pool.length, ranked };
149151
}
@@ -179,6 +181,13 @@ import { installViewToggles } from "./views.js";
179181
a.name.localeCompare(b.name),
180182
};
181183

184+
/** The chosen sort, with entries made for the chosen engine first. */
185+
function sorter() {
186+
const by = SORTS[state.sort] || SORTS.name;
187+
if (state.engine === "any") return by;
188+
return (a, b) => engineRank(a, state.engine) - engineRank(b, state.engine) || by(a, b);
189+
}
190+
182191
/* ---------------------------------------------------------- rendering */
183192

184193
/** A category's view on this page: the config default (cards or list). */
@@ -278,6 +287,7 @@ import { installViewToggles } from "./views.js";
278287
});
279288
if (state.license !== DEFAULTS.license)
280289
chips.push({ key: "license", label: `licence: ${LICENSE_FAMILIES[state.license] || state.license}` });
290+
if (state.engine !== DEFAULTS.engine) chips.push({ key: "engine", label: `engine: ${ENGINES[state.engine]?.label || state.engine}` });
281291
if (state.format !== DEFAULTS.format)
282292
chips.push({ key: "format", label: `format: ${FORMAT_GROUPS[state.format]?.label || state.format}` });
283293
if (!state.active) chips.push({ key: "active", label: "hiding active" });
@@ -443,7 +453,7 @@ import { installViewToggles } from "./views.js";
443453
placedKey = key;
444454
const shown = new Set(list.map((e) => e.id));
445455
const rest = data.entries.filter((e) => !shown.has(e.id));
446-
const all = ranked ? [...list, ...rest] : data.entries.slice().sort(SORTS[state.sort] || SORTS.name);
456+
const all = ranked ? [...list, ...rest] : data.entries.slice().sort(sorter());
447457
// Entries not rendered yet (the preview page) are simply not placed.
448458
const node = (e) => cardNodes.get(e.id);
449459
const order = [flatHeading];
@@ -670,6 +680,7 @@ import { installViewToggles } from "./views.js";
670680

671681
const selects = [
672682
["#license-family", "license", LICENSE_FAMILIES],
683+
["#engine", "engine", Object.fromEntries(Object.entries(ENGINES).map(([k, e]) => [k, e.label]))],
673684
["#format-group", "format", Object.fromEntries(Object.entries(FORMAT_GROUPS).map(([k, g]) => [k, g.label]))],
674685
];
675686
for (const [sel, field, choices] of selects) {
@@ -870,6 +881,7 @@ import { installViewToggles } from "./views.js";
870881
$("#sort").value = state.sort;
871882
$("#perspective").value = state.perspective;
872883
$("#license-family").value = state.license;
884+
$("#engine").value = state.engine;
873885
$("#format-group").value = state.format;
874886
}
875887

‎site/public/index.html‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,12 @@ <h2 id="catalog-title">Catalog</h2>
140140
<option value="any">Any</option>
141141
</select>
142142
</label>
143+
<label class="check sort-field" title="Hides entries licensed for another engine only, and lists ones made for this engine first">
144+
<span>Engine</span>
145+
<select id="engine">
146+
<option value="any">Any</option>
147+
</select>
148+
</label>
143149
<label class="check sort-field" title="Entries offering at least one of these formats">
144150
<span>Format</span>
145151
<select id="format-group">

0 commit comments

Comments
 (0)