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

View File

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

View File

@ -87,7 +87,8 @@ window.addEventListener("load", () => {
emitEvent("setup-end");
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"));
});

View File

@ -2,7 +2,7 @@ import { configuration } from "./configuration.js";
import { objectKeepKeys } from "./js-utils.js";
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) {
case "legacy_block":
return a.name === b.name
@ -13,6 +13,10 @@ export function checkEquivalence(a, b) {
case "legacy_fluid":
return a.name === b.name;
case "legacy_item":
return a.name === b.name
&& a.metadata === b.metadata;
case "oredict":
return a.oredict === b.oredict;
@ -47,16 +51,30 @@ export function stripEntryToMinimums(entry) {
throw new Error(`unknown entry type ${entry.type}`);
}
export function findFullEntry(type, entry, { configurationHint } = {}) {
export function findFullEntry(type, entry, { configurationHint, all = false } = {}) {
const possibleEntries = [
...Object.entries(configuration.configuration),
...configuration.addons.flatMap(a => Object.entries(a.configuration)),
]
.filter(([k, v]) => (!configurationHint || k === configurationHint) && type in v)
.flatMap(([, v]) => v[type]);
if (all) {
return possibleEntries.filter(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 } = {}) {
const possibleEntries = [
...Object.entries(configuration.configuration),
@ -70,7 +88,12 @@ export function findFullEntryAnywhere(entry, { configurationHint } = {}) {
: []),
] : [[k, v]]))
.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 };
}

View File

@ -4,8 +4,14 @@ import { configuration } from "./configuration.js";
import { getPlannerDom, getPlannerMultiblockType, getSelectedCells } from "./planner.js";
import { DomList } from "./dom-list.js";
import { getAtlasIndexFromMetadata, getAtlasMetadata, makeAtlasImage } from "./atlas.js";
import { getSolverMetadata, setSolverMetadata } from "./solver.js";
import { findFullEntry, findFullEntryAnywhere, lookupFromMetadata } from "./ncpf-utils.js";
import { deleteSolverMetadata, getSolverMetadata, setSolverMetadata } from "./solver.js";
import {
checkEquivalence,
findFullEntry,
findFullEntryAnywhere,
findFullEntryFromAtlasMetadata,
lookupFromMetadata, stripEntryToMinimums
} from "./ncpf-utils.js";
import { createTooltip, setTooltip, tooltipDataToPlain } from "./tooltip.js";
import { Filter } from "./filter.js";
import { debounce } from "./dom-utils.js";
@ -28,8 +34,15 @@ registerLoadCallback(() => {
listenTo([ "planner:selection:removed", "planner:selection:added"], rescanSelectionRecipes);
});
function initializeRecipePicker(params) {
const { parent, title, entryListType, multiblockType, recipes, currentSelection, updateSelection } = params;
function initializeRecipePicker({
parent,
title,
entryListType,
multiblockType,
recipes,
currentSelection,
updateSelection,
}) {
parent.querySelectorAll('[data-text="recipe:type"]')
.forEach(e => e.innerText = title);
@ -37,7 +50,7 @@ function initializeRecipePicker(params) {
function updateSelectedRecipe(selectedRecipe) {
if (!selectedRecipe) {
parent.querySelectorAll('[data-text="recipe:selected"]')
.forEach(e => e.innerText = "None");
.forEach(e => e.innerText = "Unknown");
parent.querySelectorAll('[data-atlas="recipe:selected"]')
.forEach(e => e.replaceChildren(makeAtlasImage(-1)));
return;
@ -174,20 +187,55 @@ function rescanSelectionRecipes() {
const multiblockType = getPlannerMultiblockType();
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;
}
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();
initializeRecipePicker({
parent: designItem,
title: "Block Recipe",
entryType: "ncpf:block_recipes",
recipes: tile.modules["ncpf:block_recipes"].recipes,
recipes,
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);
@ -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) {
console.log("unknown recipe", recipe, metadata);
}

View File

@ -36,6 +36,12 @@ export function getSolverMetadata(id) {
return solverMetadata[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];
}

View File

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

View File

@ -179,14 +179,13 @@ input:not([type="checkbox"]):not([type="radio"]):not([type="file"]) {
user-select: none;
-webkit-user-drag: none;
background-size: calc(var(--scale-factor) * var(--atlas-resolution))
calc(var(--scale-factor) * var(--atlas-resolution));
background-size: calc(var(--atlas-resolution) * var(--atlas-scale, 1) * var(--scale-factor));
width: calc(var(--scale-factor) * var(--size-x));
height: calc(var(--scale-factor) * var(--size-y));
width: calc(var(--default-size) * var(--scale-factor));
height: calc(var(--default-size) * var(--scale-factor));
background-position-x: calc(var(--scale-factor) * var(--raw-x) * -1);
background-position-y: calc(var(--scale-factor) * var(--raw-y) * -1);
background-position-x: calc(var(--raw-x) * var(--atlas-scale, 1) * var(--scale-factor) * -1);
background-position-y: calc(var(--raw-y) * var(--atlas-scale, 1) * var(--scale-factor) * -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)));
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);
}
@ -257,6 +254,11 @@ input:not([type="checkbox"]):not([type="radio"]):not([type="file"]) {
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] {
outline: calc(var(--scale-factor) * 1px) solid var(--block-active-outline-color, #000);
}