allow selecting block recipes

This commit is contained in:
CodedSakura 2026-07-17 11:55:33 +03:00
parent deea8abdd4
commit 481e700087
10 changed files with 172 additions and 30 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,8 +1,15 @@
import { emitEvent } from "./events.js"; import { emitEvent } from "./events.js";
import { checkEquivalence } from "./ncpf-utils.js"; import { checkEquivalence } from "./ncpf-utils.js";
import { registerLoadCallback } from "./main.js";
const defaultSize = 16; // px
export let atlases = []; export let atlases = [];
registerLoadCallback(() => {
document.body.style.setProperty("--default-size", `${defaultSize}px`);
});
async function getTextureSize(data) { async function getTextureSize(data) {
const texture = data.texture; const texture = data.texture;
if (!texture) { if (!texture) {
@ -22,8 +29,8 @@ async function getTextureSize(data) {
function putTextureOnCanvas(ctx, atlasSize, data) { function putTextureOnCanvas(ctx, atlasSize, data) {
if (!data.duplicate) { if (!data.duplicate) {
const atlasIndexX = data.index % atlasSize; const atlasIndexX = data.textureIndex % atlasSize;
const atlasIndexY = (data.index - atlasIndexX) / atlasSize; const atlasIndexY = (data.textureIndex - atlasIndexX) / atlasSize;
const x = atlasIndexX * data.size[0]; const x = atlasIndexX * data.size[0];
const y = atlasIndexY * data.size[1]; const y = atlasIndexY * data.size[1];
ctx.drawImage(data.image, x, y); ctx.drawImage(data.image, x, y);
@ -94,13 +101,14 @@ export async function buildAtlas(data) {
} }
const dedupTextureMap = new Map(); const dedupTextureMap = new Map();
t.forEach(texture => { t.forEach((texture, i) => {
texture.index = i;
if (dedupTextureMap.has(texture.texture)) { if (dedupTextureMap.has(texture.texture)) {
texture.index = dedupTextureMap.get(texture.texture) texture.textureIndex = dedupTextureMap.get(texture.texture)
texture.duplicate = true; texture.duplicate = true;
} else { } else {
texture.index = dedupTextureMap.size; texture.textureIndex = dedupTextureMap.size;
dedupTextureMap.set(texture.texture, texture.index); dedupTextureMap.set(texture.texture, texture.textureIndex);
texture.duplicate = false; texture.duplicate = false;
} }
}); });
@ -143,9 +151,10 @@ export function makeAtlasImage(index) {
const originalIndex = index; const originalIndex = index;
for (const atlas of atlases) { for (const atlas of atlases) {
if (index < atlas.length) { if (index < atlas.length) {
const atlasIndexX = index % atlas.size; const meta = atlas.metadata[index];
const atlasIndexY = (index - atlasIndexX) / atlas.size; const atlasIndexX = meta.textureIndex % atlas.size;
const size = atlas.metadata[index].size; const atlasIndexY = (meta.textureIndex - atlasIndexX) / atlas.size;
const size = meta.size;
const x = atlasIndexX * size[0]; const x = atlasIndexX * size[0];
const y = atlasIndexY * size[1]; const y = atlasIndexY * size[1];
@ -155,6 +164,7 @@ export function makeAtlasImage(index) {
div.classList.add("atlas-texture"); div.classList.add("atlas-texture");
div.style.backgroundImage = `url(${atlas.url})`; div.style.backgroundImage = `url(${atlas.url})`;
div.style.setProperty("--atlas-resolution", `${atlas.resolution}px`); div.style.setProperty("--atlas-resolution", `${atlas.resolution}px`);
div.style.setProperty("--atlas-scale", `${defaultSize / size[0]}`);
div.style.setProperty("--raw-x", `${x}px`); div.style.setProperty("--raw-x", `${x}px`);
div.style.setProperty("--raw-y", `${y}px`); div.style.setProperty("--raw-y", `${y}px`);
div.style.setProperty("--size-x", `${size[0]}px`); div.style.setProperty("--size-x", `${size[0]}px`);
@ -183,13 +193,15 @@ export function getAtlasMetadata(index) {
} }
export function getAtlasIndexFromMetadata(metadata, configuration, configurationList = null) { export function getAtlasIndexFromMetadata(metadata, configuration, configurationList = null) {
let index = 0;
for (const atlas of atlases) { for (const atlas of atlases) {
for (const atlasItemMeta of atlas.metadata) { for (const atlasItemMeta of atlas.metadata) {
if (checkEquivalence(metadata, atlasItemMeta) if (checkEquivalence(metadata, atlasItemMeta)
&& atlasItemMeta.configuration === configuration && atlasItemMeta.configuration === configuration
&& (!configurationList || atlasItemMeta.configurationList === configurationList)) { && (!configurationList || atlasItemMeta.configurationList === configurationList)) {
return atlasItemMeta.index; return index;
} }
index++;
} }
} }
return -1; return -1;

View File

@ -126,6 +126,8 @@ export async function saveFile(name = undefined) {
const minimizedBlock = minimizedBlocks[designIndex]; const minimizedBlock = minimizedBlocks[designIndex];
let recipeIndex = -1; let recipeIndex = -1;
if (!solverMetadata) return -1;
if (multiblockType === "nuclearcraft:overhaul_sfr") { if (multiblockType === "nuclearcraft:overhaul_sfr") {
if (!minimizedBlock.modules) { if (!minimizedBlock.modules) {
minimizedBlock.modules = {}; minimizedBlock.modules = {};

View File

@ -87,7 +87,8 @@ window.addEventListener("load", () => {
emitEvent("setup-end"); emitEvent("setup-end");
void loadConfiguration("default"); void loadConfiguration("default");
// void loadConfiguration("fuel-test");
// awaitEvent("config:end").then(() => loadExampleFile("tiny reactor")); // awaitEvent("config:end").then(() => loadExampleFile("Pandora's Fission Reactor"));
// awaitEvent("config:end").then(() => loadExampleFile("multi-reactor")); // awaitEvent("config:end").then(() => loadExampleFile("multi-reactor"));
}); });

View File

@ -2,7 +2,7 @@ import { configuration } from "./configuration.js";
import { objectKeepKeys } from "./js-utils.js"; import { objectKeepKeys } from "./js-utils.js";
export function checkEquivalence(a, b) { export function checkEquivalence(a, b) {
if (!a.type || !b.type || a.type !== b.type) return false; if (!a || !b || !a.type || !b.type || a.type !== b.type) return false;
switch (a.type) { switch (a.type) {
case "legacy_block": case "legacy_block":
return a.name === b.name return a.name === b.name
@ -13,6 +13,10 @@ export function checkEquivalence(a, b) {
case "legacy_fluid": case "legacy_fluid":
return a.name === b.name; return a.name === b.name;
case "legacy_item":
return a.name === b.name
&& a.metadata === b.metadata;
case "oredict": case "oredict":
return a.oredict === b.oredict; return a.oredict === b.oredict;
@ -47,16 +51,30 @@ export function stripEntryToMinimums(entry) {
throw new Error(`unknown entry type ${entry.type}`); throw new Error(`unknown entry type ${entry.type}`);
} }
export function findFullEntry(type, entry, { configurationHint } = {}) { export function findFullEntry(type, entry, { configurationHint, all = false } = {}) {
const possibleEntries = [ const possibleEntries = [
...Object.entries(configuration.configuration), ...Object.entries(configuration.configuration),
...configuration.addons.flatMap(a => Object.entries(a.configuration)), ...configuration.addons.flatMap(a => Object.entries(a.configuration)),
] ]
.filter(([k, v]) => (!configurationHint || k === configurationHint) && type in v) .filter(([k, v]) => (!configurationHint || k === configurationHint) && type in v)
.flatMap(([, v]) => v[type]); .flatMap(([, v]) => v[type]);
if (all) {
return possibleEntries.filter(other => checkEquivalence(entry, other));
}
return possibleEntries.find(other => checkEquivalence(entry, other)); return possibleEntries.find(other => checkEquivalence(entry, other));
} }
export function findFullEntryFromAtlasMetadata(metadata, options = {}) {
return findFullEntry(
metadata.configurationList,
metadata,
{
configurationHint: metadata.configuration,
...options,
},
);
}
export function findFullEntryAnywhere(entry, { configurationHint } = {}) { export function findFullEntryAnywhere(entry, { configurationHint } = {}) {
const possibleEntries = [ const possibleEntries = [
...Object.entries(configuration.configuration), ...Object.entries(configuration.configuration),
@ -70,7 +88,12 @@ export function findFullEntryAnywhere(entry, { configurationHint } = {}) {
: []), : []),
] : [[k, v]])) ] : [[k, v]]))
.flatMap(([t, v]) => v.map(e => [t, e])); .flatMap(([t, v]) => v.map(e => [t, e]));
const [ type, foundEntry ] = possibleEntries.find(([, other]) => checkEquivalence(entry, other)); const findResult = possibleEntries.find(([, other]) => checkEquivalence(entry, other));
if (!findResult) {
console.log("did not find", entry, "in", possibleEntries);
return null;
}
const [ type, foundEntry ] = findResult;
return { type, entry: foundEntry }; return { type, entry: foundEntry };
} }

View File

@ -4,8 +4,14 @@ import { configuration } from "./configuration.js";
import { getPlannerDom, getPlannerMultiblockType, getSelectedCells } from "./planner.js"; import { getPlannerDom, getPlannerMultiblockType, getSelectedCells } from "./planner.js";
import { DomList } from "./dom-list.js"; import { DomList } from "./dom-list.js";
import { getAtlasIndexFromMetadata, getAtlasMetadata, makeAtlasImage } from "./atlas.js"; import { getAtlasIndexFromMetadata, getAtlasMetadata, makeAtlasImage } from "./atlas.js";
import { getSolverMetadata, setSolverMetadata } from "./solver.js"; import { deleteSolverMetadata, getSolverMetadata, setSolverMetadata } from "./solver.js";
import { findFullEntry, findFullEntryAnywhere, lookupFromMetadata } from "./ncpf-utils.js"; import {
checkEquivalence,
findFullEntry,
findFullEntryAnywhere,
findFullEntryFromAtlasMetadata,
lookupFromMetadata, stripEntryToMinimums
} from "./ncpf-utils.js";
import { createTooltip, setTooltip, tooltipDataToPlain } from "./tooltip.js"; import { createTooltip, setTooltip, tooltipDataToPlain } from "./tooltip.js";
import { Filter } from "./filter.js"; import { Filter } from "./filter.js";
import { debounce } from "./dom-utils.js"; import { debounce } from "./dom-utils.js";
@ -28,8 +34,15 @@ registerLoadCallback(() => {
listenTo([ "planner:selection:removed", "planner:selection:added"], rescanSelectionRecipes); listenTo([ "planner:selection:removed", "planner:selection:added"], rescanSelectionRecipes);
}); });
function initializeRecipePicker(params) { function initializeRecipePicker({
const { parent, title, entryListType, multiblockType, recipes, currentSelection, updateSelection } = params; parent,
title,
entryListType,
multiblockType,
recipes,
currentSelection,
updateSelection,
}) {
parent.querySelectorAll('[data-text="recipe:type"]') parent.querySelectorAll('[data-text="recipe:type"]')
.forEach(e => e.innerText = title); .forEach(e => e.innerText = title);
@ -37,7 +50,7 @@ function initializeRecipePicker(params) {
function updateSelectedRecipe(selectedRecipe) { function updateSelectedRecipe(selectedRecipe) {
if (!selectedRecipe) { if (!selectedRecipe) {
parent.querySelectorAll('[data-text="recipe:selected"]') parent.querySelectorAll('[data-text="recipe:selected"]')
.forEach(e => e.innerText = "None"); .forEach(e => e.innerText = "Unknown");
parent.querySelectorAll('[data-atlas="recipe:selected"]') parent.querySelectorAll('[data-atlas="recipe:selected"]')
.forEach(e => e.replaceChildren(makeAtlasImage(-1))); .forEach(e => e.replaceChildren(makeAtlasImage(-1)));
return; return;
@ -174,20 +187,55 @@ function rescanSelectionRecipes() {
const multiblockType = getPlannerMultiblockType(); const multiblockType = getPlannerMultiblockType();
const tileMetadata = getAtlasMetadata(selectedCells[0].dataset.tile); const tileMetadata = getAtlasMetadata(selectedCells[0].dataset.tile);
const tile = findFullEntry("blocks", tileMetadata, { configurationHint: multiblockType }); const tiles = findFullEntryFromAtlasMetadata(tileMetadata, { all: true });
if (!tile.modules || !tile.modules["ncpf:block_recipes"]) { console.log(tiles);
if (!tiles[0] || !tiles[0].modules || !tiles[0].modules["ncpf:block_recipes"]) {
return; return;
} }
const cellRecipes = selectedCells.map(v => getSolverMetadata(v)?.block_recipe);
const recipeSet = cellRecipes.some(v => v);
const recipeAllSame = cellRecipes.every(v => v === cellRecipes[0] || checkEquivalence(v, cellRecipes[0]));
console.log(">", cellRecipes, recipeSet, recipeAllSame);
const recipes = tiles.flatMap(t => t.modules["ncpf:block_recipes"].recipes);
recipes.unshift({
modules: {
["plannerator:display_name"]: {
display_name: "Unfiltered",
},
},
});
let currentRecipe = null
if (recipeAllSame) {
if (recipeSet) {
currentRecipe = recipes.find(r => checkEquivalence(cellRecipes[0], r));
} else {
currentRecipe = recipes[0];
}
}
const designItem = designRecipes.makeItem(); const designItem = designRecipes.makeItem();
initializeRecipePicker({ initializeRecipePicker({
parent: designItem, parent: designItem,
title: "Block Recipe", title: "Block Recipe",
entryType: "ncpf:block_recipes", entryType: "ncpf:block_recipes",
recipes: tile.modules["ncpf:block_recipes"].recipes, recipes,
multiblockType, multiblockType,
updateSelection: (e) => {
if (e === recipes[0]) {
selectedCells.forEach(v => deleteSolverMetadata(v));
return;
}
const stripped = stripEntryToMinimums(e);
selectedCells.forEach(v => setSolverMetadata(v, { block_recipe: stripped }));
},
currentSelection: currentRecipe,
}); });
designRecipes.addItem(designItem); designRecipes.addItem(designItem);
@ -215,6 +263,47 @@ function* getRecipeTooltip(recipe, metadata) {
}; };
} }
if ("nuclearcraft:overhaul_sfr:fuel_stats" in recipe.modules) {
hasAdditionalData = true;
const stats = recipe.modules["nuclearcraft:overhaul_sfr:fuel_stats"];
const { entry: output, type } = findFullEntryAnywhere(stats.output, { configurationHint: multiblockType });
const outputName = output.modules["plannerator:display_name"].display_name;
const atlasId = getAtlasIndexFromMetadata(output, multiblockType, type);
yield `Efficiency: ${stats.efficiency}`;
yield `Heat: ${stats.heat}`;
yield `Time: ${stats.time}`;
yield `Criticality: ${stats.criticality}`;
if (stats.self_priming) {
yield "Self-priming";
}
yield {
type: "item",
atlasIndex: atlasId,
name: outputName,
prefix: "Output: ",
};
}
if ("nuclearcraft:overhaul_sfr:irradiator_stats" in recipe.modules) {
hasAdditionalData = true;
const stats = recipe.modules["nuclearcraft:overhaul_sfr:irradiator_stats"];
const { entry: output, type } = findFullEntryAnywhere(stats.output, { configurationHint: multiblockType });
const outputName = output.modules["plannerator:display_name"].display_name;
const atlasId = getAtlasIndexFromMetadata(output, multiblockType, type);
yield `Efficiency: ${stats.efficiency}`;
yield `Heat: ${stats.heat}`;
yield {
type: "item",
atlasIndex: atlasId,
name: outputName,
prefix: "Output: ",
};
}
if (!hasAdditionalData) { if (!hasAdditionalData) {
console.log("unknown recipe", recipe, metadata); console.log("unknown recipe", recipe, metadata);
} }

View File

@ -36,6 +36,12 @@ export function getSolverMetadata(id) {
return solverMetadata[id]; return solverMetadata[id];
} }
export function deleteSolverMetadata(id) { export function deleteSolverMetadata(id) {
if (typeof id === "object") {
// assume DOM object
if (id.dataset && id.dataset.solverMetadata) {
return deleteSolverMetadata(id.dataset.solverMetadata);
}
}
delete solverMetadata[id]; delete solverMetadata[id];
} }

View File

@ -68,6 +68,8 @@ export function deleteTooltip(id) {
} }
export function setTooltip(element, data) { export function setTooltip(element, data) {
if (data.length === 0) return;
if (element.dataset.tooltip) { if (element.dataset.tooltip) {
tooltips[Number(element.dataset.tooltip)] = data; tooltips[Number(element.dataset.tooltip)] = data;
return; return;
@ -142,6 +144,9 @@ function createTooltipDom(id) {
break; break;
case "item": case "item":
if (row.prefix) {
rowElem.appendChild(document.createTextNode(row.prefix));
}
rowElem.appendChild(makeAtlasImage(row.atlasIndex)); rowElem.appendChild(makeAtlasImage(row.atlasIndex));
rowElem.appendChild(document.createTextNode(row.name)); rowElem.appendChild(document.createTextNode(row.name));
rowElem.classList.add("tooltip--item"); rowElem.classList.add("tooltip--item");

View File

@ -179,14 +179,13 @@ input:not([type="checkbox"]):not([type="radio"]):not([type="file"]) {
user-select: none; user-select: none;
-webkit-user-drag: none; -webkit-user-drag: none;
background-size: calc(var(--scale-factor) * var(--atlas-resolution)) background-size: calc(var(--atlas-resolution) * var(--atlas-scale, 1) * var(--scale-factor));
calc(var(--scale-factor) * var(--atlas-resolution));
width: calc(var(--scale-factor) * var(--size-x)); width: calc(var(--default-size) * var(--scale-factor));
height: calc(var(--scale-factor) * var(--size-y)); height: calc(var(--default-size) * var(--scale-factor));
background-position-x: calc(var(--scale-factor) * var(--raw-x) * -1); background-position-x: calc(var(--raw-x) * var(--atlas-scale, 1) * var(--scale-factor) * -1);
background-position-y: calc(var(--scale-factor) * var(--raw-y) * -1); background-position-y: calc(var(--raw-y) * var(--atlas-scale, 1) * var(--scale-factor) * -1);
opacity: var(--atlas-opacity, 1); opacity: var(--atlas-opacity, 1);
} }
@ -197,8 +196,6 @@ input:not([type="checkbox"]):not([type="radio"]):not([type="file"]) {
width: calc(var(--scale-factor) * var(--size-x, var(--default-size))); width: calc(var(--scale-factor) * var(--size-x, var(--default-size)));
height: calc(var(--scale-factor) * var(--size-y, var(--default-size))); height: calc(var(--scale-factor) * var(--size-y, var(--default-size)));
outline: calc(var(--scale-factor) * 0.5px) solid var(--cell-empty-outline-color, #ccc);
outline-offset: calc(var(--scale-factor) * -0.5px);
opacity: var(--atlas-opacity, 1); opacity: var(--atlas-opacity, 1);
} }
@ -257,6 +254,11 @@ input:not([type="checkbox"]):not([type="radio"]):not([type="file"]) {
outline: none; outline: none;
} }
#planner .atlas-air {
outline: calc(var(--scale-factor) * 0.5px) solid var(--cell-empty-outline-color, #ccc);
outline-offset: calc(var(--scale-factor) * -0.5px);
}
#block-picker-list [data-active] { #block-picker-list [data-active] {
outline: calc(var(--scale-factor) * 1px) solid var(--block-active-outline-color, #000); outline: calc(var(--scale-factor) * 1px) solid var(--block-active-outline-color, #000);
} }