Compare commits

...

5 Commits

Author SHA1 Message Date
CodedSakura 4fb19ada79 fix issue with stale metadata throwing errors 2026-08-11 15:38:36 +03:00
CodedSakura bf66f5ea31 fix moderator line validation 2026-08-11 15:27:18 +03:00
CodedSakura fdd4fde699 implement heat sink validation 2026-08-11 14:12:31 +03:00
CodedSakura 2b542e5f34 implement basic moderator flux math (unverified) 2026-08-10 17:12:01 +03:00
CodedSakura 617db13cb4 begin implementing planner 2026-07-31 19:27:30 +03:00
12 changed files with 412 additions and 36 deletions

View File

@ -1,5 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="GitSharedSettings">
<option name="FORCE_PUSH_PROHIBITED_PATTERNS">
<list />
</option>
<option name="synchronizeBranchProtectionRules" value="false" />
</component>
<component name="VcsDirectoryMappings"> <component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" /> <mapping directory="" vcs="Git" />
</component> </component>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"addons":[],"configuration":{"nuclearcraft:overhaul_sfr":{"blocks":[{"name":"nuclearcraft:solid_fission_controller","type":"legacy_block"},{"name":"nuclearcraft:fission_vent","type":"legacy_block","blockstate":{"active":false}},{"name":"nuclearcraft:fission_vent","type":"legacy_block","blockstate":{"active":true}},{"name":"nuclearcraft:fission_casing","type":"legacy_block"},{"name":"nuclearcraft:fission_glass","type":"legacy_block"},{"metadata":2,"name":"nuclearcraft:fission_source","type":"legacy_block","blockstate":{"type":"californium"}},{"name":"nuclearcraft:solid_fission_cell","type":"legacy_block","modules":{"ncpf:block_recipes":{"recipes":[{"oredict":"ingotHEU233ZA","type":"oredict"}]}}},{"name":"nuclearcraft:fission_cell_port","type":"legacy_block","blockstate":{"active":false},"modules":{"ncpf:block_recipes":{"recipes":[{"oredict":"ingotHEU233ZA","type":"oredict"}]}}},{"name":"nuclearcraft:fission_cell_port","type":"legacy_block","blockstate":{"active":true},"modules":{"ncpf:block_recipes":{"recipes":[{"oredict":"ingotHEU233ZA","type":"oredict"}]}}},{"name":"nuclearcraft:heavy_water_moderator","type":"legacy_block"},{"metadata":0,"name":"nuclearcraft:fission_shield","type":"legacy_block","blockstate":{"active":false,"type":"boron_silver"}},{"metadata":0,"name":"nuclearcraft:fission_shield","type":"legacy_block","blockstate":{"active":true,"type":"boron_silver"}}],"coolant_recipes":[{"elements":[{"name":"water","type":"legacy_fluid"},{"name":"condensate_water","type":"legacy_fluid"}],"type":"list"}]}},"designs":[{"design":[[[3,3,3,3,3,3,3,3,3],[3,0,1,2,7,8,4,4,3],[3,4,4,4,4,4,4,4,3],[3,4,4,4,4,4,4,4,3],[3,4,4,4,4,4,4,4,3],[3,4,4,4,4,4,4,4,3],[3,3,3,3,3,3,3,3,3]],[[3,5,4,4,4,4,4,4,3],[4,6,-1,-1,-1,6,-1,-1,4],[4,-1,-1,-1,-1,-1,-1,-1,4],[4,-1,-1,-1,-1,-1,-1,-1,4],[4,-1,-1,-1,-1,-1,-1,-1,4],[4,-1,-1,-1,-1,-1,-1,-1,4],[3,4,4,4,4,4,4,4,3]],[[3,4,4,4,4,4,4,4,3],[4,9,-1,-1,-1,9,-1,-1,4],[4,-1,-1,-1,-1,-1,-1,-1,4],[4,-1,-1,-1,-1,-1,-1,-1,4],[4,-1,-1,-1,-1,-1,-1,-1,4],[4,-1,-1,-1,-1,-1,-1,-1,4],[3,4,4,4,4,4,4,4,3]],[[3,4,4,4,4,4,4,4,3],[4,10,-1,-1,-1,10,-1,-1,4],[4,-1,-1,-1,-1,-1,-1,-1,4],[4,-1,-1,-1,-1,-1,-1,-1,4],[4,-1,-1,-1,-1,-1,-1,-1,4],[4,-1,-1,-1,-1,-1,-1,-1,4],[3,4,4,4,4,4,4,4,3]],[[3,4,4,4,4,4,4,4,3],[4,9,-1,-1,-1,9,-1,-1,4],[4,-1,-1,-1,-1,-1,-1,-1,4],[4,-1,-1,-1,-1,-1,-1,-1,4],[4,-1,-1,-1,-1,-1,-1,-1,4],[4,-1,-1,-1,-1,-1,-1,-1,4],[3,4,4,4,4,4,4,4,3]],[[3,4,4,4,4,4,4,4,3],[4,6,9,11,9,6,-1,-1,4],[4,-1,-1,-1,-1,-1,-1,-1,4],[4,-1,-1,-1,-1,-1,-1,-1,4],[4,-1,-1,-1,-1,-1,-1,-1,4],[4,-1,-1,-1,-1,-1,-1,-1,4],[3,4,4,4,4,4,4,4,3]],[[3,4,4,4,4,4,4,4,3],[4,-1,-1,-1,-1,-1,-1,-1,4],[4,-1,-1,-1,-1,-1,-1,-1,4],[4,-1,-1,-1,-1,-1,-1,-1,4],[4,-1,-1,-1,-1,-1,-1,-1,4],[4,-1,-1,-1,-1,-1,-1,-1,4],[3,4,4,4,4,4,4,4,3]],[[3,4,4,4,4,4,4,4,3],[4,-1,-1,-1,-1,-1,-1,-1,4],[4,-1,-1,-1,-1,-1,-1,-1,4],[4,-1,-1,-1,-1,-1,-1,-1,4],[4,-1,-1,-1,-1,-1,-1,-1,4],[4,-1,-1,-1,-1,-1,-1,-1,4],[3,4,4,4,4,4,4,4,3]],[[3,3,3,3,3,3,3,3,3],[3,4,4,4,4,4,4,4,3],[3,4,4,4,4,4,4,4,3],[3,4,4,4,4,4,4,4,3],[3,4,4,4,4,4,4,4,3],[3,4,4,4,4,4,4,4,3],[3,3,3,3,3,3,3,3,3]]],"block_recipes":[[[0,0]],[[0,0]],[[0,0]]],"type":"nuclearcraft:overhaul_sfr","coolant_recipe":0,"dimensions":[9,7,9]}],"version":1}

View File

@ -126,7 +126,7 @@
</section> </section>
</aside> </aside>
<footer> <footer>
<div>pre-alpha 1.6</div> <div>alpha 1.8</div>
<div> <div>
<a href="https://discord.gg/Eq72HwPqfg">Discord server</a> <a href="https://discord.gg/Eq72HwPqfg">Discord server</a>
</div> </div>
@ -146,7 +146,7 @@
Roadmap: Roadmap:
</p> </p>
<ul> <ul>
<li>Reactor verification and stats</li> <li>Reactor stats</li>
<li>Casing improvements</li> <li>Casing improvements</li>
<li>Keyboard shortcuts</li> <li>Keyboard shortcuts</li>
<li>MSR support</li> <li>MSR support</li>

View File

@ -39,7 +39,7 @@ registerLoadCallback(() => {
}); });
awaitEvent("config:end").then(() => { awaitEvent("config:end").then(() => {
void loadExampleFile("Pandora's Fission Reactor"); void loadExampleFile("shield test");
}); });
queueMicrotask(() => emitEvent("dev-mode:end")); queueMicrotask(() => emitEvent("dev-mode:end"));

View File

@ -132,7 +132,7 @@ export async function saveFile(name = undefined) {
if (!fullBlock || !fullBlock.modules || !("ncpf:block_recipes" in fullBlock.modules)) return; if (!fullBlock || !fullBlock.modules || !("ncpf:block_recipes" in fullBlock.modules)) return;
const designIndex = designBlocksAtlasMap[Number(designAsCellData[x][y][z].tile)].designIndex; const designIndex = designBlocksAtlasMap[Number(designAsCellData[x][y][z].tile)].designIndex;
const solverMetadata = getMetadata(designAsCellData[x][y][z].solverMetadata); const solverMetadata = getMetadata(designAsCellData[x][y][z].metadata);
const minimizedBlock = minimizedBlocks[designIndex]; const minimizedBlock = minimizedBlocks[designIndex];
let recipeIndex = -1; let recipeIndex = -1;
@ -156,6 +156,8 @@ export async function saveFile(name = undefined) {
} }
} }
console.log(recipeIndex)
return recipeIndex; return recipeIndex;
}) })
.filter(b => b != null) .filter(b => b != null)

View File

@ -9,17 +9,19 @@ function createMetadata(data) {
return id; return id;
} }
export function setDomMetadata(domNode, data) { export function setDomMetadata(domNode, data) {
if (domNode.dataset.solverMetadata) { if (domNode.dataset.metadata) {
metadata[Number(domNode.dataset.solverMetadata)] = data; metadata[Number(domNode.dataset.metadata)] = data;
return; return;
} }
domNode.dataset.solverMetadata = createMetadata(data).toString(10); domNode.dataset.metadata = createMetadata(data).toString(10);
} }
export function setCellMetadata(node, key, value) { export function setCellMetadata(node, key, value) {
if (!node.dataset.solverMetadata) { if (!node.dataset.metadata) {
node.dataset.solverMetadata = createMetadata({ [key]: value }).toString(10); node.dataset.metadata = createMetadata({ [key]: value }).toString(10);
} else if (!metadata[Number(node.dataset.metadata)]) {
metadata[Number(node.dataset.metadata)] = { [key]: value };
} else { } else {
metadata[Number(node.dataset.solverMetadata)][key] = value; metadata[Number(node.dataset.metadata)][key] = value;
} }
processCellOverlays(node, key, true); processCellOverlays(node, key, true);
} }
@ -27,8 +29,8 @@ export function setCellMetadata(node, key, value) {
export function getMetadata(id) { export function getMetadata(id) {
if (typeof id === "object") { if (typeof id === "object") {
// assume DOM element // assume DOM element
if (id.dataset && id.dataset.solverMetadata) { if (id.dataset && id.dataset.metadata) {
return getMetadata(id.dataset.solverMetadata); return getMetadata(id.dataset.metadata);
} }
return null; return null;
} }
@ -38,21 +40,23 @@ export function getMetadata(id) {
return metadata[id]; return metadata[id];
} }
export function getCellMetadata(node, key) { export function getCellMetadata(node, key) {
return metadata[Number(node.dataset.solverMetadata)][key]; if (!node.dataset.metadata) return null;
return metadata[Number(node.dataset.metadata)][key];
} }
export function deleteMetadata(id) { export function deleteMetadata(id) {
if (typeof id === "object") { if (typeof id === "object") {
// assume DOM object // assume DOM object
if (id.dataset && id.dataset.solverMetadata) { if (id.dataset && id.dataset.metadata) {
clearCellOverlays(id); clearCellOverlays(id);
return deleteMetadata(id.dataset.solverMetadata); return deleteMetadata(id.dataset.metadata);
} }
} }
delete metadata[id]; delete metadata[id];
} }
export function clearCellMetadata(node, key) { export function clearCellMetadata(node, key) {
if (!node.dataset.solverMetadata) return; if (!node.dataset.metadata) return;
delete metadata[Number(node.dataset.solverMetadata)][key]; if (!metadata[Number(node.dataset.metadata)]) return;
delete metadata[Number(node.dataset.metadata)][key];
processCellOverlays(node, key, false); processCellOverlays(node, key, false);
} }

View File

@ -3,6 +3,8 @@ import { getAtlasIndexFromMetadata, makeAtlasImage } from "./atlas.js";
export function processCellOverlays(node, key = null, added = null) { export function processCellOverlays(node, key = null, added = null) {
if (key) { if (key) {
node.replaceChildren(...node.querySelectorAll(`:scope > *:not([data-overlay="${key}"])`));
const metadata = getCellMetadata(node, key); const metadata = getCellMetadata(node, key);
const atlasIndex = getAtlasIndexFromMetadata(metadata); const atlasIndex = getAtlasIndexFromMetadata(metadata);
if (atlasIndex && atlasIndex !== -1) { if (atlasIndex && atlasIndex !== -1) {
@ -11,6 +13,14 @@ export function processCellOverlays(node, key = null, added = null) {
overlayNode.classList.add("planner--overlay"); overlayNode.classList.add("planner--overlay");
node.appendChild(overlayNode); node.appendChild(overlayNode);
} }
if (key === "active" && added) {
const overlayNode = makeAtlasImage(-1);
overlayNode.dataset.overlay = key;
overlayNode.dataset.active = metadata ? "true" : "false";
overlayNode.classList.add("planner--overlay");
node.appendChild(overlayNode);
}
// FIXME: handle remove // FIXME: handle remove
} }
// TODO: handle generic update // TODO: handle generic update

View File

@ -6,7 +6,7 @@ import { initialize3dArray } from "./js-utils.js";
import { recall, save } from "./storage.js"; import { recall, save } from "./storage.js";
import { registerLoadCallback } from "./main.js"; import { registerLoadCallback } from "./main.js";
import { getBlockPickerActiveItem, getBlockPickerRecipe } from "./block-picker.js"; import { getBlockPickerActiveItem, getBlockPickerRecipe } from "./block-picker.js";
import { clearCellMetadata, getMetadata, setCellMetadata } from "./metadata.js"; import { clearCellMetadata, deleteMetadata, getMetadata, setCellMetadata } from "./metadata.js";
import { retainTooltipHover } from "./tooltip.js"; import { retainTooltipHover } from "./tooltip.js";
import { checkEquivalence, lookupFromMetadata, parseRuleAsString } from "./ncpf-utils.js"; import { checkEquivalence, lookupFromMetadata, parseRuleAsString } from "./ncpf-utils.js";
@ -250,13 +250,14 @@ function cellClickEvent(e) {
if (e.target.tile === "-1") return; if (e.target.tile === "-1") return;
e.target.dataset.previewRemove = "yes"; e.target.dataset.previewRemove = "yes";
clearCellMetadata(e.target, "block_recipe"); deleteMetadata(e.target);
} else if (e.buttons === 1) { } else if (e.buttons === 1) {
// add // add
if (e.target.dataset.preview === activeItem.toString(10)) return; if (e.target.dataset.preview === activeItem.toString(10)) return;
e.target.dataset.preview = activeItem.toString(10); e.target.dataset.preview = activeItem.toString(10);
e.target.replaceChildren(makeAtlasImage(activeItem)); e.target.replaceChildren(makeAtlasImage(activeItem));
deleteMetadata(e.target);
const pickerRecipe = getBlockPickerRecipe(); const pickerRecipe = getBlockPickerRecipe();
if (pickerRecipe) { if (pickerRecipe) {
setCellMetadata(e.target, "block_recipe", pickerRecipe); setCellMetadata(e.target, "block_recipe", pickerRecipe);
@ -324,7 +325,7 @@ export function getPlannerDom() {
return document.getElementById("planner"); return document.getElementById("planner");
} }
export function getPlannerCellsDom() { export function getPlannerCellsDom() {
return document.querySelectorAll("#planner [data-tile]"); return [...document.querySelectorAll("#planner [data-tile]")];
} }
export function getPlannerCellDom(x, y, z) { export function getPlannerCellDom(x, y, z) {
return document.querySelector(`#planner [data-x="${x}"][data-y="${y}"][data-z="${z}"]`); return document.querySelector(`#planner [data-x="${x}"][data-y="${y}"][data-z="${z}"]`);
@ -355,8 +356,14 @@ export function* getCellTooltip(cell) {
if ("nuclearcraft:overhaul_sfr:fuel_stats" in recipe.modules) { if ("nuclearcraft:overhaul_sfr:fuel_stats" in recipe.modules) {
const stats = recipe.modules["nuclearcraft:overhaul_sfr:fuel_stats"]; const stats = recipe.modules["nuclearcraft:overhaul_sfr:fuel_stats"];
if (stats.self_priming) { if (solverMetadata.primer) {
yield "Self-priming"; if (solverMetadata.primer.dom === cell) {
yield "Primed (self-priming)";
} else {
const direction = ["x", "y", "z"].flatMap((d) => solverMetadata.primer[d] !== cell.dataset[d] ? d : [])
.map(d => (Number(cell.dataset[d]) < Number(solverMetadata.primer[d]) ? "+" : "-") + d.toUpperCase())[0];
yield `Primed (${solverMetadata.primer.tile.modules["plannerator:display_name"].display_name}, ${direction})`;
}
} else { } else {
yield "Not primed"; yield "Not primed";
} }
@ -366,7 +373,7 @@ export function* getCellTooltip(cell) {
`Criticality: `, `Criticality: `,
{ {
type: "bold", type: "bold",
text: `0/${stats.criticality}`, text: `${solverMetadata.flux ?? 0}/${stats.criticality}`,
}, },
], ],
}; };
@ -378,6 +385,22 @@ export function* getCellTooltip(cell) {
const moderator = data.modules["nuclearcraft:overhaul_sfr:moderator"]; const moderator = data.modules["nuclearcraft:overhaul_sfr:moderator"];
yield `Flux factor: ${moderator.flux}`; yield `Flux factor: ${moderator.flux}`;
yield `Efficiency: ${moderator.efficiency}`; yield `Efficiency: ${moderator.efficiency}`;
yield {
type: "compound",
items: [
"Moderator ",
{
type: "bold",
text: solverMetadata?.active ? "active" : "inactive",
},
" for heat sinks",
],
};
if (solverMetadata?.flux) {
for (const [axis, flux] of Object.entries(solverMetadata.flux)) {
yield `Flux on ${axis}: ${flux}`;
}
}
} }
if ("nuclearcraft:overhaul_sfr:reflector" in data.modules) { if ("nuclearcraft:overhaul_sfr:reflector" in data.modules) {
@ -388,13 +411,19 @@ export function* getCellTooltip(cell) {
if ("nuclearcraft:overhaul_sfr:heat_sink" in data.modules) { if ("nuclearcraft:overhaul_sfr:heat_sink" in data.modules) {
const heatSink = data.modules["nuclearcraft:overhaul_sfr:heat_sink"]; const heatSink = data.modules["nuclearcraft:overhaul_sfr:heat_sink"];
if (heatSink.rules) { if (heatSink.rules && !solverMetadata.active) {
for (const rule of heatSink.rules) { for (const rule of heatSink.rules) {
yield "Rule not met: " + parseRuleAsString(rule, { configurationHint: metadata.configuration }); yield "Rule not met: " + parseRuleAsString(rule, { configurationHint: metadata.configuration });
} }
} }
} }
if ("nuclearcraft:overhaul_sfr:irradiator" in data.modules) {
if (solverMetadata?.flux) {
yield `Flux: ${solverMetadata.flux}`;
}
}
yield { yield {
type: "italics", type: "italics",
text: metadata.configurationAddon.name, text: metadata.configurationAddon.name,

View File

@ -88,7 +88,6 @@ function initializeRecipePicker({
const filter = Filter.fromInput(filterElem); const filter = Filter.fromInput(filterElem);
const recipeList = new DomList(parent.querySelector(":scope > .select")); const recipeList = new DomList(parent.querySelector(":scope > .select"));
recipeList.clear(); recipeList.clear();
console.log(recipes);
for (let i = 0; i < recipes.length; i++) { for (let i = 0; i < recipes.length; i++) {
const recipe = recipes[i]; const recipe = recipes[i];
const recipeItem = recipeList.makeItem(); const recipeItem = recipeList.makeItem();
@ -131,9 +130,6 @@ function initializeRecipePicker({
} }
function rescanDesignRecipes() { function rescanDesignRecipes() {
console.log("rescan recipes");
console.log(configuration);
const multiblockType = getPlannerMultiblockType(); const multiblockType = getPlannerMultiblockType();
const configs = [ const configs = [
@ -198,8 +194,6 @@ function rescanSelectionRecipes() {
const tileMetadata = getAtlasMetadata(selectedCells.length > 0 ? selectedCells[0].dataset.tile : pickerTile); const tileMetadata = getAtlasMetadata(selectedCells.length > 0 ? selectedCells[0].dataset.tile : pickerTile);
const tiles = findFullEntryFromAtlasMetadata(tileMetadata, { all: true }); const tiles = findFullEntryFromAtlasMetadata(tileMetadata, { all: true });
console.log(tiles);
if (!tiles[0] || !tiles[0].modules || !tiles[0].modules["ncpf:block_recipes"]) { if (!tiles[0] || !tiles[0].modules || !tiles[0].modules["ncpf:block_recipes"]) {
return; return;
} }
@ -207,7 +201,6 @@ function rescanSelectionRecipes() {
const cellRecipes = selectedCells.map(v => getMetadata(v)?.block_recipe); const cellRecipes = selectedCells.map(v => getMetadata(v)?.block_recipe);
const recipeSet = cellRecipes.some(v => v); const recipeSet = cellRecipes.some(v => v);
const recipeAllSame = cellRecipes.every(v => v === cellRecipes[0] || checkEquivalence(v, cellRecipes[0])); 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); const recipes = tiles.flatMap(t => t.modules["ncpf:block_recipes"].recipes);

View File

@ -1,7 +1,24 @@
import { emitEvent } from "./events.js"; import { awaitEvent, emitEvent, listenTo } from "./events.js";
import { getPlannerCellDom, getPlannerDom } from "./planner.js"; import {
import { findFullEntry } from "./ncpf-utils.js"; getPlannerCellDom,
import { setCellMetadata, setDomMetadata } from "./metadata.js"; getPlannerCellsDom,
getPlannerDom,
getPlannerMultiblockSize,
getPlannerMultiblockType
} from "./planner.js";
import { checkEquivalence, findFullEntry, findFullEntryFromAtlasMetadata } from "./ncpf-utils.js";
import { clearCellMetadata, deleteMetadata, getMetadata, setCellMetadata, setDomMetadata } from "./metadata.js";
import { registerLoadCallback } from "./main.js";
import { getAtlasMetadata } from "./atlas.js";
import { initialize3dArray } from "./js-utils.js";
const SFR_MODERATOR_MAX_LEN = 3;
const SFR_MODERATOR_REFLECTOR_MAX_LEN = 2;
registerLoadCallback(() => {
listenTo("fileLoad:end", runSolver);
listenTo("planner:cell:change", runSolver);
});
export function resetSolver(design = undefined, designConfiguration) { export function resetSolver(design = undefined, designConfiguration) {
const planner = getPlannerDom(); const planner = getPlannerDom();
@ -22,10 +39,12 @@ export function resetSolver(design = undefined, designConfiguration) {
for (let y = 0; y < design.dimensions[1]; y++) { for (let y = 0; y < design.dimensions[1]; y++) {
for (let z = 0; z < design.dimensions[2]; z++) { for (let z = 0; z < design.dimensions[2]; z++) {
const block = designConfiguration.blocks[design.design[x][y][z]]; const block = designConfiguration.blocks[design.design[x][y][z]];
const cellDom = getPlannerCellDom(x, y, z);
deleteMetadata(cellDom);
if (!block || !block.modules || !block.modules["ncpf:block_recipes"]) continue; if (!block || !block.modules || !block.modules["ncpf:block_recipes"]) continue;
const blockRecipeIndex = designBlockRecipes.shift(); const blockRecipeIndex = designBlockRecipes.shift();
const blockRecipe = block.modules["ncpf:block_recipes"].recipes[blockRecipeIndex]; const blockRecipe = block.modules["ncpf:block_recipes"].recipes[blockRecipeIndex];
setCellMetadata(getPlannerCellDom(x, y, z), "block_recipe", blockRecipe); setCellMetadata(cellDom, "block_recipe", blockRecipe);
} }
} }
} }
@ -34,3 +53,303 @@ export function resetSolver(design = undefined, designConfiguration) {
emitEvent("solver:reset:done"); emitEvent("solver:reset:done");
} }
const sixAxes = {
"-X": { x: -1, y: 0, z: 0 },
"-Y": { x: 0, y: -1, z: 0 },
"-Z": { x: 0, y: 0, z: -1 },
"+X": { x: 1, y: 0, z: 0 },
"+Y": { x: 0, y: 1, z: 0 },
"+Z": { x: 0, y: 0, z: 1 },
};
function *travelUntil(grid, { x, y, z }, {x: dirX = 0, y: dirY = 0, z: dirZ = 0}, shouldStop = (pos) => false) {
x = Number(x);
y = Number(y);
z = Number(z);
while (!shouldStop({x, y, z})
&& x > 0 && y > 0 && z > 0
&& x < grid.length - 1 && y < grid[0].length - 1 && z < grid[0][0].length - 1) {
x += dirX;
y += dirY;
z += dirZ;
yield grid[x][y][z];
}
}
function neighbor(grid, { x, y, z }, {x: dirX = 0, y: dirY = 0, z: dirZ = 0}) {
x = Number(x);
y = Number(y);
z = Number(z);
if (x > 0 && y > 0 && z > 0
&& x < grid.length - 1 && y < grid[0].length - 1 && z < grid[0][0].length - 1) {
x += dirX;
y += dirY;
z += dirZ;
return grid[x][y][z];
}
}
async function debugBlockSolver(data) {
const planner = getPlannerDom();
if (planner.dataset.debugSolver) {
emitEvent("solver:pause", data);
await awaitEvent("solver:continue");
}
}
export async function runSolver() {
const startTime = performance.now()
emitEvent("solver:begin");
const multiblockType = getPlannerMultiblockType();
const plannerBlocks = getPlannerCellsDom()
.map(b => ({ dom: b, ...b.dataset }))
.filter(b => b.tile !== "-1")
.map(b => (b.metadata ? { ...b, metadata: getMetadata(b.metadata) } : b));
const tileMap = Object.fromEntries([...new Set(plannerBlocks.map(b => b.tile))]
.map(idx => [idx, getAtlasMetadata(idx)])
.map(([idx, md]) => [idx, findFullEntry("blocks", md, { configurationHint: multiblockType })]));
plannerBlocks.forEach(v => v.tile = tileMap[v.tile]);
plannerBlocks.forEach(v => clearCellMetadata(v.dom, "active"));
const plannerBlocksGrid = initialize3dArray(...getPlannerMultiblockSize());
plannerBlocks.forEach(b => {
plannerBlocksGrid[b.x][b.y][b.z] = b;
});
if (multiblockType === "nuclearcraft:overhaul_sfr") {
const fuelCells = plannerBlocks.filter(b => b.tile.modules && b.tile.modules["nuclearcraft:overhaul_sfr:fuel_cell"]);
// const neutronSources = plannerBlocks.filter(b => b.tile.modules && b.tile.modules["nuclearcraft:overhaul_sfr:neutron_source"]);
// const reflectors = plannerBlocks.filter(b => b.tile.modules && b.tile.modules["nuclearcraft:overhaul_sfr:reflector"]);
// const shields = plannerBlocks.filter(b => b.tile.modules && b.tile.modules["nuclearcraft:overhaul_sfr:neutron_shield"]);
// const moderators = plannerBlocks.filter(b => b.tile.modules && b.tile.modules["nuclearcraft:overhaul_sfr:moderator"]);
const heatSinks = plannerBlocks.filter(b => b.tile.modules && b.tile.modules["nuclearcraft:overhaul_sfr:heat_sink"]);
// const irradiators = plannerBlocks.filter(b => b.tile.modules && b.tile.modules["nuclearcraft:overhaul_sfr:irradiator"]);
// const conductors = plannerBlocks.filter(b => b.tile.modules && b.tile.modules["nuclearcraft:overhaul_sfr:conductor"]);
fuelCells.forEach(cell => {
const recipeList = cell.tile.modules["ncpf:block_recipes"].recipes;
cell.recipe = recipeList.find(r => checkEquivalence(r, cell.metadata?.block_recipe));
// TODO: handle blank recipe
if (!cell.recipe) return; // FIXME
cell.axes = Object.values(sixAxes).map(dir => [...travelUntil(
plannerBlocksGrid,
cell,
dir,
({x, y, z}) => {
const block = plannerBlocksGrid[x][y][z];
if (block === cell) {
return false;
}
if (!block) {
return false;
}
if (block.tile.modules) {
if (block.tile.modules["nuclearcraft:overhaul_sfr:fuel_cell"]) return true;
if (block.tile.modules["nuclearcraft:overhaul_sfr:reflector"]) return true;
if (block.tile.modules["nuclearcraft:overhaul_sfr:irradiator"]) return true;
}
return false;
}
)]);
if (cell.recipe.modules["nuclearcraft:overhaul_sfr:fuel_stats"].self_priming) {
cell.primer = cell;
cell.active = true;
} else if (cell.axes.some(a => (a[a.length - 1]?.tile?.modules ?? {})["nuclearcraft:overhaul_sfr:neutron_source"])) {
const primerAxis = cell.axes.find(a => (a[a.length - 1]?.tile?.modules ?? {})["nuclearcraft:overhaul_sfr:neutron_source"]);
cell.primer = primerAxis[primerAxis.length - 1];
cell.active = true;
}
setCellMetadata(cell.dom, "primer", cell.primer);
});
console.log(fuelCells)
await debugBlockSolver("primed");
const seenModerators = new Set();
const activeCells = fuelCells.filter(c => c.active);
let activeCellIndex = 0;
while (activeCellIndex < activeCells.length) {
const cell = activeCells[activeCellIndex];
if (!cell.axes) return; // ignore bad cells
Object.keys(sixAxes).map((directionName, i) => {
let valid = false;
let moderatorPath = [];
let length = 0;
let reflectMultiplier = 1;
let reflect = false;
let fluxDestination;
for (const block of cell.axes[i]) {
if (!block) break;
if (block.tile.modules["nuclearcraft:overhaul_sfr:moderator"]) {
moderatorPath.push(block);
seenModerators.add(block);
length += 1;
continue;
}
if (block.tile.modules["nuclearcraft:overhaul_sfr:neutron_shield"]) {
length += 1;
continue;
}
if (block.tile.modules["nuclearcraft:overhaul_sfr:fuel_cell"] || block.tile.modules["nuclearcraft:overhaul_sfr:irradiator"]) {
valid = length <= SFR_MODERATOR_MAX_LEN;
fluxDestination = block;
}
if (block.tile.modules["nuclearcraft:overhaul_sfr:reflector"]) {
valid = length <= SFR_MODERATOR_REFLECTOR_MAX_LEN;
reflect = true;
reflectMultiplier = block.tile.modules["nuclearcraft:overhaul_sfr:reflector"].reflectivity;
console.log(reflectMultiplier)
fluxDestination = cell;
}
break;
}
if (moderatorPath.length < 1) {
valid = false;
}
if (!valid) return;
moderatorPath[0].active = true;
let totalFlux = 0;
for (const mod of moderatorPath) {
totalFlux += mod.tile.modules["nuclearcraft:overhaul_sfr:moderator"].flux;
if (!mod.flux) {
mod.flux = {};
}
mod.flux[directionName] = totalFlux;
}
if (reflect) {
totalFlux *= reflectMultiplier;
const backAxis = directionName.replace(/^[+-]/, s => s === "+" ? "-" : "+");
for (const mod of moderatorPath.toReversed()) {
totalFlux += mod.tile.modules["nuclearcraft:overhaul_sfr:moderator"].flux * reflectMultiplier;
mod.flux[backAxis] = totalFlux;
}
}
fluxDestination.flux = (fluxDestination.flux ?? 0) + totalFlux;
if (fluxDestination.tile.modules["nuclearcraft:overhaul_sfr:fuel_cell"] && !fluxDestination.active) {
if (!fluxDestination.recipe) return; // FIXME
const fuelStats = fluxDestination.recipe.modules["nuclearcraft:overhaul_sfr:fuel_stats"];
if (fuelStats.criticality <= fluxDestination.flux) {
fluxDestination.active = true;
activeCells.push(fluxDestination);
}
}
});
activeCellIndex++;
}
seenModerators.forEach(moderator => {
setCellMetadata(moderator.dom, "flux", moderator.flux);
if (moderator.active) {
setCellMetadata(moderator.dom, "active", moderator.active);
}
});
fuelCells.forEach(cell => {
if (cell.flux) {
setCellMetadata(cell.dom, "flux", cell.flux);
}
setCellMetadata(cell.dom, "active", cell.active ?? false);
});
await debugBlockSolver("moderators");
heatSinks.forEach(hs => {
hs.neighbors = {};
Object.entries(sixAxes).forEach(([name, dir]) => {
hs.neighbors[name] = neighbor(plannerBlocksGrid, hs, dir);
});
hs.rules = hs.tile.modules["nuclearcraft:overhaul_sfr:heat_sink"].rules;
});
let heatSinkProcessed = false;
const unprocessedHeatSinks = new Set(heatSinks);
do {
heatSinkProcessed = false;
unprocessedHeatSinks.forEach(hs => {
if (hs.active) {
console.log("active hs in queue", hs);
return;
}
const activeNeighbors = Object.entries(hs.neighbors).filter(([_, n]) => {
if (!n) return false;
if (n.active) return true;
if (n.tile.modules) {
return n.tile.modules["nuclearcraft:overhaul_sfr:reflector"]
|| n.tile.modules["nuclearcraft:overhaul_sfr:irradiator"]
|| n.tile.modules["nuclearcraft:overhaul_sfr:casing"]
|| n.tile.modules["nuclearcraft:overhaul_sfr:neutron_shield"];
}
return false;
});
if (activeNeighbors.length === 0) return;
let valid = false;
for (const rule of hs.rules) {
if (checkHeatSinkRule(hs, rule, activeNeighbors)) {
valid = true;
break;
}
}
if (valid) {
heatSinkProcessed = true;
unprocessedHeatSinks.delete(hs);
hs.active = true;
setCellMetadata(hs.dom, "active", true);
}
});
} while (heatSinkProcessed);
unprocessedHeatSinks.forEach(hs => {
setCellMetadata(hs.dom, "active", false);
});
console.log(unprocessedHeatSinks);
await debugBlockSolver("heatsinks");
}
const endTime = performance.now()
console.log(`Solver took ${endTime - startTime}ms`);
}
function checkHeatSinkRule(hs, rule, activeNeighbors) {
switch (rule.type) {
case "and":
return rule.rules.every(r => checkHeatSinkRule(hs, r, activeNeighbors));
case "or":
return rule.rules.some(r => checkHeatSinkRule(hs, r, activeNeighbors));
case "between": {
const blocks = activeNeighbors
.map(([,b]) => b.tile)
.filter(b => {
if (rule.block.type === "module") {
return b.modules && rule.block.name in b.modules;
}
return checkEquivalence(rule.block, b);
});
return rule.min <= blocks.length && blocks.length <= rule.max;
}
case "axial": {
const axialPairs = activeNeighbors
.filter(([,b]) => {
if (rule.block.type === "module") {
return b.tile.modules && rule.block.name in b.tile.modules;
}
return checkEquivalence(rule.block, b.tile);
})
.reduce((acc, [dir, block]) => {
acc[dir[1]] = acc[dir[1]] ?? [];
acc[dir[1]].push(block);
return acc;
}, {});
const count = Object.values(axialPairs).filter(p => p.length === 2).length;
return rule.min <= count && count <= rule.max;
}
default:
console.warn("unknown rule: %o", rule);
return false;
}
}

View File

@ -268,6 +268,17 @@ input:not([type="checkbox"]):not([type="radio"]):not([type="file"]) {
inset: 0; inset: 0;
opacity: var(--overlay-opacity, 0.8); opacity: var(--overlay-opacity, 0.8);
} }
.planner--cell > .planner--overlay[data-overlay="block_recipe"] {
transform: scale(var(--recipe-scale-factor, 0.8));
}
#planner .planner--cell > .planner--overlay[data-overlay="active"][data-active="true"] {
outline-offset: calc(var(--scale-factor) * -1px);
outline: calc(var(--scale-factor) * 1px) solid var(--cell-active-outline, #0f08);
}
#planner .planner--cell > .planner--overlay[data-overlay="active"][data-active="false"] {
outline-offset: calc(var(--scale-factor) * -1px);
outline: calc(var(--scale-factor) * 1px) solid var(--cell-inactive-outline, #f008);
}
.planner--resize { .planner--resize {
--resize-width: var(--width); --resize-width: var(--width);