470 lines
15 KiB
JavaScript
470 lines
15 KiB
JavaScript
import { atlases, makeAtlasImage } from "./atlas.js";
|
|
import { DomList } from "./dom-list.js";
|
|
import { getElements } from "./dom-utils.js";
|
|
import { emitEvent, listenTo } from "./events.js";
|
|
import { Filter } from "./filter.js";
|
|
import { initialize3dArray } from "./js-utils.js";
|
|
import { recall, save } from "./storage.js";
|
|
import { createTooltip, tooltipDataToPlain } from "./tooltip.js";
|
|
import { lookupFromMetadata, resolveBlockName } from "./ncpf-utils.js";
|
|
import { registerLoadCallback } from "./main.js";
|
|
|
|
const zoomOptions = [ 0.5, 1, 2, 3, 4, 5 ];
|
|
const zoomDefault = 2;
|
|
const zoomStorageKey = "zoom-size";
|
|
|
|
let suppressHover = false;
|
|
let multiblockSize = [9, 7, 9]; // width (screen x), height (xy grids), depth (screen y)
|
|
let multiblockType = "nuclearcraft:overhaul_sfr";
|
|
let activeItem = -1;
|
|
|
|
registerLoadCallback(() => {
|
|
listenTo("config:end", buildBlockPicker);
|
|
listenTo("config:end", buildPlanner);
|
|
|
|
listenTo("block-picker:filter:input", buildBlockPicker);
|
|
|
|
listenTo("planner:toggle-casing", e => {
|
|
const planner = document.getElementById("planner");
|
|
planner.dataset.casing = e.detail.state ? "show" : "hide";
|
|
});
|
|
|
|
const zoomLevel = recall(zoomStorageKey, zoomDefault);
|
|
document.getElementById("planner").style
|
|
.setProperty("--scale-factor", zoomLevel);
|
|
document.querySelectorAll('select[data-input="planner:zoom"]').forEach(elem => {
|
|
for (const op of zoomOptions) {
|
|
const option = document.createElement("option");
|
|
option.value = op.toString(10);
|
|
option.innerText = `${op * 100}%`;
|
|
option.selected = zoomLevel === op;
|
|
elem.appendChild(option);
|
|
}
|
|
});
|
|
listenTo("planner:zoom:change", e => {
|
|
const value = Number(e.detail.currentTarget.value);
|
|
if (zoomOptions.includes(value)) {
|
|
emitEvent("planner:zoom:set", value);
|
|
} else {
|
|
console.warn("bad planner zoom value", e);
|
|
}
|
|
});
|
|
listenTo(["planner:zoom:+", "planner:zoom:-"], e => {
|
|
const zoomIn = e.type === "ncp:planner:zoom:+";
|
|
const currentScale = Number(document.getElementById("planner").style
|
|
.getPropertyValue("--scale-factor") ?? zoomDefault);
|
|
const currentIndex = zoomOptions.indexOf(currentScale);
|
|
if (zoomIn && currentIndex < zoomOptions.length - 1) {
|
|
emitEvent("planner:zoom:set", zoomOptions[currentIndex + 1]);
|
|
}
|
|
if (!zoomIn && currentIndex > 0) {
|
|
emitEvent("planner:zoom:set", zoomOptions[currentIndex - 1]);
|
|
}
|
|
});
|
|
listenTo("planner:zoom:set", e => {
|
|
const scale = e.detail;
|
|
const scaleIndex = zoomOptions.indexOf(scale);
|
|
document.getElementById("planner").style
|
|
.setProperty("--scale-factor", scale.toString(10));
|
|
|
|
document.querySelectorAll('select[data-input="planner:zoom"]')
|
|
.forEach(elem => elem.value = scale);
|
|
|
|
getElements("button", "planner:zoom:+")
|
|
.forEach(e => e.disabled = scaleIndex >= zoomOptions.length - 1);
|
|
getElements("button", "planner:zoom:-")
|
|
.forEach(e => e.disabled = scaleIndex <= 0);
|
|
|
|
save(zoomStorageKey, scale);
|
|
});
|
|
listenTo("block-picker:selection", clearSelectedCells);
|
|
|
|
document.addEventListener("keydown", e => {
|
|
if (e.key === "H") {
|
|
suppressHover = !suppressHover;
|
|
console.log("Suppressing hover:", suppressHover);
|
|
}
|
|
});
|
|
});
|
|
|
|
function buildBlockPicker() {
|
|
const filter = Filter.fromInput("block-picker:filter");
|
|
|
|
const grid = new DomList(document.getElementById("block-picker-list"));
|
|
grid.clear();
|
|
|
|
atlases.flatMap(a => a.metadata)
|
|
.filter(m => m.configuration === multiblockType && m.configurationList === "blocks")
|
|
.forEach(m => {
|
|
const item = grid.makeItem();
|
|
item.dataset.index = m.index.toString(10);
|
|
const tooltipData = [...getBlockTooltip(m)];
|
|
item.dataset.tooltip = createTooltip(tooltipData).toString(10);
|
|
if (!filter.test(...tooltipDataToPlain(tooltipData))) {
|
|
item.classList.add("hidden");
|
|
}
|
|
item.appendChild(makeAtlasImage(m.index));
|
|
item.addEventListener("click", listClickEvent);
|
|
grid.addItem(item);
|
|
});
|
|
}
|
|
|
|
function listClickEvent(e) {
|
|
const items = document.querySelectorAll("#block-picker-list [data-index]");
|
|
if (e.currentTarget.dataset.active !== undefined) {
|
|
items.forEach(i => delete i.dataset.active);
|
|
activeItem = -1;
|
|
delete e.currentTarget.dataset.active;
|
|
emitEvent("block-picker:selection", activeItem);
|
|
emitEvent("block-picker:selection:clear");
|
|
return;
|
|
}
|
|
items.forEach(i => delete i.dataset.active);
|
|
activeItem = Number(e.currentTarget.dataset.index);
|
|
e.currentTarget.dataset.active = "";
|
|
emitEvent("block-picker:selection", activeItem);
|
|
}
|
|
|
|
function buildPlanner(textures) {
|
|
// handle case when 1st argument is event
|
|
if (!Array.isArray(textures)) textures = undefined;
|
|
|
|
const planner = document.getElementById("planner");
|
|
planner.dataset.casing = "hide";
|
|
|
|
const list = new DomList(planner);
|
|
list.clear();
|
|
|
|
for (let y = 0; y < multiblockSize[1]; y++) {
|
|
const item = list.makeItem();
|
|
item.style.setProperty("--width", multiblockSize[0]);
|
|
item.dataset.y = y.toString(10);
|
|
if (y === 0 || y === multiblockSize[1] - 1) {
|
|
item.dataset.casing = "";
|
|
}
|
|
const grid = new DomList(item);
|
|
grid.clear();
|
|
for (let z = 0; z < multiblockSize[2]; z++) {
|
|
for (let x = 0; x < multiblockSize[0]; x++) {
|
|
const texture = textures ? textures[x][y][z] : -1;
|
|
if (textures && !texture) {
|
|
console.log(x, y, z, textures);
|
|
}
|
|
const cell = grid.makeItem();
|
|
cell.dataset.x = x.toString(10);
|
|
cell.dataset.y = y.toString(10);
|
|
cell.dataset.z = z.toString(10);
|
|
cell.dataset.tile = texture.toString(10);
|
|
if (x === 0 || x === multiblockSize[0] - 1
|
|
|| y === 0 || y === multiblockSize[1] - 1
|
|
|| z === 0 || z === multiblockSize[2] - 1) {
|
|
cell.dataset.casing = "";
|
|
}
|
|
cell.appendChild(makeAtlasImage(texture));
|
|
cell.addEventListener("mouseenter", cellHoverEvent);
|
|
cell.addEventListener("mouseleave", cellHoverEvent);
|
|
cell.addEventListener("mousedown", cellClickEvent);
|
|
cell.addEventListener("mouseenter", cellClickEvent);
|
|
cell.addEventListener("mouseleave", cellClickEvent);
|
|
cell.addEventListener("contextmenu", cellClickEvent);
|
|
grid.addItem(cell);
|
|
}
|
|
}
|
|
list.addItem(item);
|
|
}
|
|
}
|
|
|
|
function cellHoverEvent(e) {
|
|
if (suppressHover) return;
|
|
if (activeItem === -1) return;
|
|
|
|
const cells = document.querySelectorAll("#planner [data-x]");
|
|
cells.forEach(c => {
|
|
delete c.dataset.highlight;
|
|
delete c.dataset.highlightDistance;
|
|
});
|
|
if (e.type === "mouseenter") {
|
|
cells.forEach(c => {
|
|
if (c.dataset.y === e.target.dataset.y && c.dataset.z === e.target.dataset.z) {
|
|
c.dataset.highlight = "x";
|
|
c.dataset.highlightDistance = Math.abs(c.dataset.x - e.target.dataset.x).toString(10);
|
|
}
|
|
if (c.dataset.x === e.target.dataset.x && c.dataset.z === e.target.dataset.z) {
|
|
c.dataset.highlight = "y";
|
|
c.dataset.highlightDistance = Math.abs(c.dataset.y - e.target.dataset.y).toString(10);
|
|
}
|
|
if (c.dataset.x === e.target.dataset.x && c.dataset.y === e.target.dataset.y) {
|
|
c.dataset.highlight = "z";
|
|
c.dataset.highlightDistance = Math.abs(c.dataset.z - e.target.dataset.z).toString(10);
|
|
}
|
|
if (c === e.target) {
|
|
delete c.dataset.highlight;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function cellClickEvent(e) {
|
|
if (e.type === "contextmenu") {
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
|
|
if (e.buttons !== 1 && e.buttons !== 2 && e.type !== "mouseup") {
|
|
return;
|
|
}
|
|
|
|
if (activeItem === -1) {
|
|
cellSelectEvent(e);
|
|
return;
|
|
}
|
|
|
|
if (e.type === "mousedown") {
|
|
window.addEventListener("mouseup", cellClickEvent);
|
|
emitEvent("planner:drag:start");
|
|
clearSelectedCells();
|
|
}
|
|
if (e.type === "mouseup") {
|
|
window.removeEventListener("mouseup", cellClickEvent);
|
|
emitEvent("planner:drag:end");
|
|
|
|
const cells = document.querySelectorAll("#planner [data-x]");
|
|
cells.forEach(c => {
|
|
if (c.dataset.preview) {
|
|
c.dataset.tile = c.dataset.preview;
|
|
emitEvent("planner:cell:change", c);
|
|
delete c.dataset.preview;
|
|
}
|
|
if (c.dataset.previewRemove) {
|
|
delete c.dataset.previewRemove;
|
|
c.replaceChildren(makeAtlasImage(-1));
|
|
c.dataset.tile = (-1).toString(10);
|
|
emitEvent("planner:cell:change", c);
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (e.buttons === 2) {
|
|
// remove
|
|
|
|
if (e.target.tile === "-1") return;
|
|
|
|
e.target.dataset.previewRemove = "yes";
|
|
return;
|
|
} else if (e.buttons === 1) {
|
|
// add
|
|
if (e.target.dataset.preview === activeItem.toString(10)) return;
|
|
|
|
e.target.dataset.preview = activeItem.toString(10);
|
|
e.target.replaceChildren(makeAtlasImage(activeItem));
|
|
}
|
|
|
|
if (e.target.dataset.preview === activeItem.toString(10)) return;
|
|
|
|
e.target.dataset.preview = activeItem.toString(10);
|
|
e.target.replaceChildren(makeAtlasImage(activeItem));
|
|
}
|
|
function cellSelectEvent(e) {
|
|
if (e.type === "mousedown" && !e.ctrlKey && e.buttons !== 2) {
|
|
clearSelectedCells();
|
|
}
|
|
if (e.buttons === 2) {
|
|
// remove
|
|
const wasSelected = "selected" in e.target.dataset;
|
|
delete e.target.dataset.selected;
|
|
if (wasSelected) {
|
|
emitEvent("planner:selection:removed", e.target);
|
|
}
|
|
} else if (e.buttons === 1) {
|
|
// add
|
|
const wasSelected = "selected" in e.target.dataset;
|
|
e.target.dataset.selected = "";
|
|
if (!wasSelected) {
|
|
emitEvent("planner:selection:added", e.target);
|
|
}
|
|
}
|
|
}
|
|
function clearSelectedCells() {
|
|
getSelectedCells()
|
|
.forEach(cell => {
|
|
delete cell.dataset.selected;
|
|
emitEvent("planner:selection:removed", cell);
|
|
});
|
|
}
|
|
export function getSelectedCells() {
|
|
return document.querySelectorAll("#planner [data-selected]");
|
|
}
|
|
|
|
export function clearPlanner(size, type) {
|
|
multiblockSize = size;
|
|
multiblockType = type;
|
|
buildPlanner();
|
|
}
|
|
export function loadPlanner(size, type, textures) {
|
|
multiblockSize = size;
|
|
multiblockType = type;
|
|
buildPlanner(textures);
|
|
}
|
|
|
|
export function readPlannerTiles() {
|
|
const planner = document.getElementById("planner");
|
|
const tiles = initialize3dArray(...multiblockSize, -1);
|
|
const grids = [...planner.children].filter(e => e.dataset.y);
|
|
for (let y = 0; y < multiblockSize[1]; y++) {
|
|
const grid = grids[y];
|
|
const gridTiles = [...grid.children].filter(e => e.dataset.tile);
|
|
for (let z = 0; z < multiblockSize[2]; z++) {
|
|
for (let x = 0; x < multiblockSize[0]; x++) {
|
|
tiles[x][y][z] = Number(gridTiles[x + z * multiblockSize[0]].dataset.tile);
|
|
}
|
|
}
|
|
}
|
|
return tiles;
|
|
}
|
|
|
|
export const getPlannerMultiblockType = () => multiblockType;
|
|
export const getPlannerMultiblockSize = () => multiblockSize;
|
|
|
|
function* getBlockTooltip(metadata) {
|
|
const data = lookupFromMetadata(metadata);
|
|
|
|
if (!data) {
|
|
console.warn("failed to get data from metadata", metadata);
|
|
return;
|
|
}
|
|
|
|
yield data.modules["plannerator:display_name"].display_name;
|
|
|
|
let hasAdditionalData = false;
|
|
|
|
if ("nuclearcraft:overhaul_sfr:heat_sink" in data.modules) {
|
|
hasAdditionalData = true;
|
|
const heatSink = data.modules["nuclearcraft:overhaul_sfr:heat_sink"];
|
|
yield "Heatsink";
|
|
if (heatSink.cooling) {
|
|
yield `Cooling: ${heatSink.cooling} H/t`;
|
|
}
|
|
if (heatSink.rules) {
|
|
for (const rule of heatSink.rules) {
|
|
yield "Requires " + parseRuleAsTooltip(rule, { configurationHint: metadata.configuration });
|
|
}
|
|
}
|
|
}
|
|
|
|
if ("nuclearcraft:overhaul_sfr:casing" in data.modules) {
|
|
hasAdditionalData = true;
|
|
yield "Casing";
|
|
}
|
|
|
|
if ("nuclearcraft:overhaul_sfr:controller" in data.modules) {
|
|
hasAdditionalData = true;
|
|
yield "Controller";
|
|
}
|
|
|
|
if ("nuclearcraft:overhaul_sfr:fuel_cell" in data.modules) {
|
|
hasAdditionalData = true;
|
|
yield "Fuel Cell";
|
|
}
|
|
|
|
if ("nuclearcraft:overhaul_sfr:port" in data.modules) {
|
|
hasAdditionalData = true;
|
|
if (data.modules["nuclearcraft:overhaul_sfr:port"].output) {
|
|
yield "Output Port";
|
|
} else {
|
|
yield "Input Port";
|
|
}
|
|
}
|
|
|
|
if ("nuclearcraft:overhaul_sfr:irradiator" in data.modules) {
|
|
hasAdditionalData = true;
|
|
yield "Irradiator";
|
|
}
|
|
|
|
if ("nuclearcraft:overhaul_sfr:conductor" in data.modules) {
|
|
hasAdditionalData = true;
|
|
yield "Conductor";
|
|
}
|
|
|
|
if ("nuclearcraft:overhaul_sfr:moderator" in data.modules) {
|
|
hasAdditionalData = true;
|
|
const moderator = data.modules["nuclearcraft:overhaul_sfr:moderator"];
|
|
yield "Moderator";
|
|
yield `Flux: ${moderator.flux}`;
|
|
yield `Efficiency: ${moderator.efficiency}`;
|
|
}
|
|
|
|
if ("nuclearcraft:overhaul_sfr:reflector" in data.modules) {
|
|
hasAdditionalData = true;
|
|
const reflector = data.modules["nuclearcraft:overhaul_sfr:reflector"];
|
|
yield "Reflector";
|
|
yield `Efficiency: ${reflector.efficiency}`;
|
|
yield `Reflectivity: ${reflector.reflectivity}`;
|
|
}
|
|
|
|
if ("nuclearcraft:overhaul_sfr:neutron_shield" in data.modules) {
|
|
hasAdditionalData = true;
|
|
const shield = data.modules["nuclearcraft:overhaul_sfr:neutron_shield"];
|
|
yield "Neutron Shield";
|
|
yield `Efficiency: ${shield.efficiency}`;
|
|
yield `Heat per Flux: ${shield.heat_per_flux}`;
|
|
}
|
|
|
|
if (!hasAdditionalData) {
|
|
console.log("unknown block", data);
|
|
}
|
|
|
|
yield {
|
|
type: "italics",
|
|
text: metadata.configurationAddon.name,
|
|
};
|
|
}
|
|
|
|
function parseRuleAsTooltip(rule, props) {
|
|
switch (rule.type) {
|
|
case "and":
|
|
return rule.rules.map(r => parseRuleAsTooltip(r, props)).join(" AND ");
|
|
|
|
case "or":
|
|
return rule.rules.map(r => parseRuleAsTooltip(r, props)).join(" OR ");
|
|
|
|
case "between": {
|
|
const block = resolveBlockName(rule.block, props);
|
|
if (!block) {
|
|
console.warn("could not find block %o", rule.block);
|
|
return "???";
|
|
}
|
|
if (rule.min === rule.max) {
|
|
return `exactly ${rule.min} ${block}`;
|
|
} else if (rule.max === 6) {
|
|
return `at least ${rule.min} ${block}`;
|
|
} else if (rule.min === 0) {
|
|
return `no more than ${rule.max} ${block}`;
|
|
} else {
|
|
return `between ${rule.min} and ${rule.max} ${block}`;
|
|
}
|
|
}
|
|
|
|
case "axial": {
|
|
const block = resolveBlockName(rule.block, props);
|
|
if (!block) {
|
|
console.warn("could not find block %o", rule.block);
|
|
return "???";
|
|
}
|
|
if (rule.min === rule.max) {
|
|
return `exactly ${rule.min} axial ${block}`;
|
|
} else if (rule.max === 3) {
|
|
return `at least ${rule.min} axial ${block}`;
|
|
} else if (rule.min === 0) {
|
|
return `no more than ${rule.max} axial ${block}`;
|
|
} else {
|
|
return `between ${rule.min} and ${rule.max} axial ${block}`;
|
|
}
|
|
}
|
|
|
|
default:
|
|
console.warn("unknown rule: %o", rule);
|
|
return "???";
|
|
}
|
|
}
|