begin file loading implementation

This commit is contained in:
CodedSakura 2026-07-09 23:24:12 +03:00
parent ba3adc63c4
commit 585335c21c
11 changed files with 215 additions and 25 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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"}},{"metadata":1,"name":"nuclearcraft:solid_fission_sink","type":"legacy_block","blockstate":{"type":"iron"}},{"metadata":7,"name":"nuclearcraft:solid_fission_sink","type":"legacy_block","blockstate":{"type":"lapis"}},{"metadata":8,"name":"nuclearcraft:solid_fission_sink","type":"legacy_block","blockstate":{"type":"gold"}},{"metadata":12,"name":"nuclearcraft:solid_fission_sink","type":"legacy_block","blockstate":{"type":"purpur"}},{"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_reflector","type":"legacy_block","blockstate":{"type":"beryllium_carbon"}}],"coolant_recipes":[{"elements":[{"name":"water","type":"legacy_fluid"},{"name":"condensate_water","type":"legacy_fluid"}],"type":"list"}]}},"designs":[{"design":[[[3,3,3,3,3],[3,0,1,2,3],[3,4,4,11,3],[3,3,3,3,3]],[[3,12,4,4,3],[5,10,13,14,4],[4,7,6,9,4],[3,4,4,4,3]],[[3,4,4,4,3],[4,7,6,9,4],[4,-1,8,-1,4],[3,4,4,4,3]],[[3,3,3,3,3],[3,4,4,4,3],[3,4,4,4,3],[3,3,3,3,3]]],"block_recipes":[[[0]],[[0],[0]]],"type":"nuclearcraft:overhaul_sfr","coolant_recipe":0,"dimensions":[4,4,5]}],"version":1}

View File

@ -54,7 +54,7 @@
<input type="file" data-input="configuration" accept=".ncpf.json" />
</div>
<div data-state="planning">
<div class="list" id="planner-list">
<div class="list" id="planner">
<template>
<div class="grid m2">
<template>
@ -71,5 +71,20 @@
<footer id="bottom-menu">
<span data-resize="bottom-menu"></span>
</footer>
<div class="dialog hidden" data-dialog="fileLoad:chooseDesign">
<section>
<header>Choose a design to open</header>
<main class="list-choice">
<template>
<button>
<span data-text="Name" class="bold"></span> by <span data-text="Author"></span>
</button>
</template>
</main>
<footer>
<button data-dialog-action="cancel">Cancel</button>
</footer>
</section>
</div>
</body>
</html>

40
scripts/dialogs.js Normal file
View File

@ -0,0 +1,40 @@
import { DomList } from "./dom-list.js";
import { addEventToElement, setText } from "./dom-utils.js";
import { emitEvent, listenTo } from "./events.js";
export function initializeDialogs() {
listenTo("fileLoad:chooseDesign", designChoiceDialog);
}
function designChoiceDialog(e) {
const dialog = document.querySelector('[data-dialog="fileLoad:chooseDesign"]');
const dismiss = (data) => {
dialog.classList.add("hidden");
emitEvent("fileLoad:chooseDesign:choice", data);
}
addEventToElement(
"dialog-action", "cancel", "click",
() => dismiss(-1),
{ element: dialog },
);
dialog.addEventListener("click", e => {
if (e.target !== e.currentTarget) return;
dismiss(-1);
});
const list = new DomList(dialog.querySelector(":scope > section > main"));
list.clear();
for (const designIndex in e.detail.designs) {
const design = e.detail.designs[designIndex];
const button = list.makeItem();
setText("Name", design.modules["plannerator:metadata"].Name, { element: button });
setText("Author", design.modules["plannerator:metadata"].Author, { element: button });
button.addEventListener("click", () => dismiss(designIndex));
list.addItem(button);
}
dialog.classList.remove("hidden");
}

View File

@ -1,16 +1,21 @@
export function getElements(dataType, dataValue, props = {}) {
export function getElements(dataType, dataValue, {
additionalDataFilters = [],
additionalPropertyFilters = [],
element = document,
} = {}) {
// props: { additionalDataFilters: [key, value][], additionalPropertyFilters: [key, value][] }
const queryString = [
...[[dataType, dataValue], ...(props.additionalDataFilters ?? [])]
...[[dataType, dataValue], ...additionalDataFilters]
.map(([k, v]) => [`data-${k}`, v]),
...(props.additionalPropertyFilters ?? []),
...additionalPropertyFilters,
].map(([k, v]) => v === '*' ? `[${k}]` : `[${k}='${v}']`).join("");
return document.querySelectorAll(queryString);
return element.querySelectorAll(queryString);
}
export function addEventToElement(dataType, dataValue, event, callback, props = {}) {
getElements(dataType, dataValue, props).forEach(e => e.addEventListener(event, callback));
getElements(dataType, dataValue, props)
.forEach(e => e.addEventListener(event, callback));
}
export function addEventToInput(dataValue, event, callback, props = {}) {
@ -20,6 +25,11 @@ export function addEventToButton(dataValue, event, callback, props = {}) {
addEventToElement("button", dataValue, event, callback, props);
}
export function setText(dataValue, text, props = {}) {
getElements("text", dataValue, props)
.forEach(e => e.innerText = text);
}
export function debounce(fn, milliseconds = 200) {
let timeout = null;
return (e) => {

44
scripts/files.js Normal file
View File

@ -0,0 +1,44 @@
import { awaitEvent, emitEvent } from "./events.js";
import { clearPlanner } from "./planner.js";
export function showOpenFileDialog() {
const input = document.createElement("input");
input.type = "file";
input.accept = ".ncpf.json";
input.addEventListener("change", async () => {
const data = await new Promise(resolve => {
const reader = new FileReader();
reader.addEventListener("load", () => {
resolve(JSON.parse(reader.result));
});
reader.readAsText(input.files[0]);
});
void loadFile(data);
});
input.click();
}
export async function loadExampleFile(filename) {
const data = await fetch(`./examples/${filename}.ncpf.json`)
.then(data => data.json());
return loadFile(data);
}
export async function loadFile(data) {
if (!data.designs) throw new Error("invalid file");
if (data.designs.length === 0) throw new Error("no designs");
let design = data.designs[0];
if (data.designs.length > 1) {
const prepChoice = awaitEvent("fileLoad:chooseDesign:choice");
emitEvent("fileLoad:chooseDesign", data);
const res = await prepChoice;
design = data.designs[res.detail];
}
if (!design) throw new Error("no design");
clearPlanner(design.dimensions, design.type);
console.log(data);
console.log(design);
}

View File

@ -1,6 +1,8 @@
import { loadConfiguration } from "./configuration.js";
import { initializeDialogs } from "./dialogs.js";
import { addEventToButton, addEventToInput, debounce, getElements } from "./dom-utils.js";
import { emitEvent, listenTo } from "./events.js";
import { awaitEvent, emitEvent, listenTo } from "./events.js";
import { loadExampleFile, showOpenFileDialog } from "./files.js";
import { initializePlanner } from "./planner.js";
import { initializeMenuResizing } from "./resize-menus.js";
import { getState, setState, STATE } from "./state.js";
@ -22,6 +24,7 @@ window.addEventListener("load", () => {
addEventToInput("block-filter", "input", debounce(e => {
emitEvent("block-picker:filter", e.target.value);
}));
addEventToButton("open-file", "click", showOpenFileDialog);
listenTo("state", () => {
console.log("State changed", getState());
@ -39,8 +42,12 @@ window.addEventListener("load", () => {
initializeMenuResizing();
initializeTooltips();
initializeMenuBar();
initializeDialogs();
emitEvent("setup-end");
void loadConfiguration("addons");
void loadConfiguration("catastrophe");
awaitEvent("config:end").then(() => loadExampleFile("tiny reactor"));
// awaitEvent("config:end").then(() => loadExampleFile("multi-reactor"));
});

View File

@ -6,13 +6,14 @@ import { createTooltip } from "./tooltip.js";
import { lookupFromMetadata, resolveBlockName } from "./ncpf-utils.js";
let suppressHover = false;
let multiblockSize = [7, 7, 5]; // width (screen x), depth (screen y), height (xy grids)
let multiblockSize = [9, 7, 9]; // width (screen x), height (xy grids), depth (screen y)
let multiblockType = "nuclearcraft:overhaul_sfr";
let activeItem = -1;
function cellHoverEvent(e) {
if (suppressHover) return;
const cells = document.querySelectorAll("#planner-list [data-x]");
const cells = document.querySelectorAll("#planner [data-x]");
cells.forEach(c => {
delete c.dataset.highlight;
delete c.dataset.highlightDistance;
@ -58,7 +59,7 @@ function cellClickEvent(e) {
window.removeEventListener("mouseup", cellClickEvent);
emitEvent("planner:drag:end");
const cells = document.querySelectorAll("#planner-list [data-x]");
const cells = document.querySelectorAll("#planner [data-x]");
cells.forEach(c => {
if (c.dataset.preview) {
c.dataset.tile = c.dataset.preview;
@ -118,7 +119,7 @@ function buildBlockPicker() {
grid.clear();
const blocks = atlases.flatMap(a => a.metadata)
.filter(m => m.configuration === "nuclearcraft:overhaul_sfr" && m.configurationList === "blocks");
.filter(m => m.configuration === multiblockType && m.configurationList === "blocks");
console.log(blocks);
blocks.forEach(m => {
const item = grid.makeItem();
@ -141,21 +142,32 @@ function buildPlanner() {
}
});
const list = new DomList(document.querySelector("#planner-list"));
const planner = document.querySelector("#planner");
planner.dataset.casing = "hide";
const list = new DomList(planner);
list.clear();
for (let z = 0; z < multiblockSize[2]; z++) {
for (let y = 0; y < multiblockSize[1]; y++) {
const item = list.makeItem();
item.style.setProperty("--width", multiblockSize[1]);
item.style.setProperty("--width", multiblockSize[0]);
if (y === 0 || y === multiblockSize[1] - 1) {
item.dataset.casing = "";
}
const grid = new DomList(item);
grid.clear();
for (let y = 0; y < multiblockSize[1]; y++) {
for (let z = 0; z < multiblockSize[2]; z++) {
for (let x = 0; x < multiblockSize[0]; x++) {
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 = (-1).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(-1));
cell.addEventListener("mouseenter", cellHoverEvent);
cell.addEventListener("mouseleave", cellHoverEvent);
@ -170,6 +182,12 @@ function buildPlanner() {
}
}
export function clearPlanner(size, type) {
multiblockSize = size;
multiblockType = type;
buildPlanner();
}
function* getBlockTooltip(metadata) {
const data = lookupFromMetadata(metadata);

View File

@ -24,6 +24,10 @@
display: none;
}
.bold {
font-weight: bold;
}
.list {
display: flex;
flex-wrap: wrap;
@ -33,7 +37,7 @@
}
.grid {
display: grid;
grid-template-columns: repeat(var(--width), calc(var(--scale-factor) * var(--default-size)));
grid-template-columns: repeat(var(--width-override, var(--width)), calc(var(--scale-factor) * var(--default-size)));
}
.m1 {
@ -194,6 +198,14 @@ input:not([type="checkbox"]):not([type="radio"]):not([type="file"]) {
background-color: var(--highlight-color, #f808);
}
[data-highlight='y'] .atlas-texture:after, [data-highlight='y'] .atlas-air:after {
inset:
calc(var(--scale-factor) * var(--size-y, var(--default-size)) * 0.4)
calc(var(--scale-factor) * var(--size-x, var(--default-size)) * 0.4)
calc(var(--scale-factor) * var(--size-y, var(--default-size)) * 0.4)
calc(var(--scale-factor) * var(--size-x, var(--default-size)) * 0.4);
background-color: var(--highlight-color, #f808);
}
[data-highlight='z'] .atlas-texture:after, [data-highlight='z'] .atlas-air:after {
inset:
0
calc(var(--scale-factor) * var(--size-x, var(--default-size)) * 0.4)
@ -201,13 +213,11 @@ input:not([type="checkbox"]):not([type="radio"]):not([type="file"]) {
calc(var(--scale-factor) * var(--size-x, var(--default-size)) * 0.4);
background-color: var(--highlight-color, #f808);
}
[data-highlight='z'] .atlas-texture:after, [data-highlight='z'] .atlas-air:after {
inset:
calc(var(--scale-factor) * var(--size-y, var(--default-size)) * 0.4)
calc(var(--scale-factor) * var(--size-x, var(--default-size)) * 0.4)
calc(var(--scale-factor) * var(--size-y, var(--default-size)) * 0.4)
calc(var(--scale-factor) * var(--size-x, var(--default-size)) * 0.4);
background-color: var(--highlight-color, #f808);
[data-casing="hide"] > * {
--width-override: calc(var(--width) - 2);
}
[data-casing="hide"] [data-casing] {
display: none;
}
#block-list [data-active] {
@ -249,7 +259,7 @@ input:not([type="checkbox"]):not([type="radio"]):not([type="file"]) {
display: flex;
flex-direction: column;
z-index: 100;
box-shadow: var(--menu-shadow, 0 2px 4px rgba(0, 0, 0, 0.2));
box-shadow: var(--menu-shadow, 0 2px 4px #0002);
}
.menu-bar button {
all: initial;
@ -274,3 +284,45 @@ input:not([type="checkbox"]):not([type="radio"]):not([type="file"]) {
.menu-bar .alt-key {
text-decoration: underline;
}
.dialog.hidden {
display: none;
}
.dialog {
position: fixed;
inset: 0;
background: var(--dialog-backdrop, #0008);
display: flex;
justify-content: center;
align-items: center;
z-index: 10;
padding: var(--dialog-margin, 1em);
}
.dialog > section {
z-index: 11;
padding: var(--dialog-padding, 1em);
background: var(--dialog-background, #fff);
color: var(--dialog-foreground, #000);
border-radius: var(--dialog-border-radius, .25em);
box-shadow: var(--dialog-box-shaodw, 0 4px 8px #0002);
max-width: 100%;
max-height: 100%;
}
.dialog > section > header {
font-size: var(--dialog-header-size, 1.25em);
}
.dialog > section > main {
padding: 1em 0;
}
.dialog > section > footer {
display: flex;
flex-direction: row-reverse;
}
.dialog > section > .list-choice {
display: flex;
flex-direction: column;
gap: .25em;
}
.dialog > section > .list-choice > * {
font-size: 1em;
}