upgrade atlas to support multi-resolution, finalize heatsink block tooltips
This commit is contained in:
parent
b93cfafc5a
commit
53b5386465
|
|
@ -2,6 +2,7 @@
|
|||
<dictionary name="project">
|
||||
<words>
|
||||
<w>ncpf</w>
|
||||
<w>nuclearcraft</w>
|
||||
</words>
|
||||
</dictionary>
|
||||
</component>
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
146
scripts/atlas.js
146
scripts/atlas.js
|
|
@ -2,8 +2,7 @@ import { emitEvent } from "./events.js";
|
|||
|
||||
let atlasSize = 0; // in items
|
||||
let atlasResolution = 0; // in px
|
||||
export let atlasBlobURL = "about:blank";
|
||||
export let atlasMetadata = [];
|
||||
export let atlases = [];
|
||||
|
||||
async function getTextureSize(data) {
|
||||
const texture = data.texture;
|
||||
|
|
@ -35,53 +34,65 @@ function putTextureOnCanvas(ctx, atlasSize, index, data) {
|
|||
|
||||
export async function buildAtlas(data) {
|
||||
emitEvent("config:process:atlas:start");
|
||||
const overhaulSFRBlocks = [
|
||||
...data.configuration["nuclearcraft:overhaul_sfr"]?.blocks,
|
||||
...data.addons.flatMap(a => a.configuration["nuclearcraft:overhaul_sfr"]?.blocks)
|
||||
const textures = [
|
||||
...Object.entries(data.configuration),
|
||||
...data.addons.flatMap(a => Object.entries(a.configuration)),
|
||||
]
|
||||
.filter(b => b && b.modules && b.modules["plannerator:texture"])
|
||||
.map(b => (
|
||||
{
|
||||
...b,
|
||||
texture: b.modules["plannerator:texture"].texture,
|
||||
}
|
||||
));
|
||||
.flatMap(([config, data]) => Object.entries(data)
|
||||
.map(([type, entries]) => [config, type, entries]))
|
||||
.flatMap(([c, t, e]) => Array.isArray(e)
|
||||
? [[c, t, e]]
|
||||
: t === "modules" && e["plannerator:global_elements"]
|
||||
? [[c, "plannerator:global_elements", e["plannerator:global_elements"].elements]]
|
||||
: [])
|
||||
.flatMap(([c, t, l]) => l.map((v) => [c, t, v]))
|
||||
.filter(([,, b]) => b && b.modules && b.modules["plannerator:texture"])
|
||||
.map(([c, t, b]) => ({
|
||||
configuration: c,
|
||||
configurationList: t,
|
||||
texture: b.modules["plannerator:texture"].texture,
|
||||
...(Object.fromEntries(Object.entries(b).filter(([k]) => k !== "modules")))
|
||||
}));
|
||||
|
||||
if (overhaulSFRBlocks.length === 0) {
|
||||
if (textures.length === 0) {
|
||||
throw new Error("Configuration processing failed - no textured blocks found");
|
||||
}
|
||||
|
||||
await Promise.all(overhaulSFRBlocks.map(getTextureSize));
|
||||
await Promise.all(textures.map(getTextureSize));
|
||||
|
||||
// TODO: handle configurations with unique sizes
|
||||
if (new Set(overhaulSFRBlocks.map(v => v.size.toString())).size > 1) {
|
||||
throw new Error("Configuration processing failed - unable to handle multi-sized textures yet");
|
||||
atlases = [];
|
||||
let offset = 0;
|
||||
for (const t of Object.values(Object.groupBy(textures, t => t.size.toString()))) {
|
||||
if (t[0].size[0] !== t[0].size[1]) {
|
||||
throw new Error("Configuration processing failed - textures must be square");
|
||||
}
|
||||
|
||||
const atlasEdgeSizeBlocks = 2 ** Math.ceil(Math.log2(Math.sqrt(t.length)));
|
||||
const atlasEdgeSize = atlasEdgeSizeBlocks * t[0].size[0];
|
||||
|
||||
const canvas = new OffscreenCanvas(atlasEdgeSize, atlasEdgeSize);
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
await Promise.all(t.map((b, i) => putTextureOnCanvas(ctx, atlasEdgeSizeBlocks, i, b)));
|
||||
|
||||
t.forEach((m, i) => {
|
||||
m.index = offset + i;
|
||||
delete m.texture;
|
||||
});
|
||||
|
||||
const blob = await canvas.convertToBlob({ type: "image/png" });
|
||||
atlases.push({
|
||||
offset,
|
||||
url: URL.createObjectURL(blob),
|
||||
metadata: t,
|
||||
size: atlasEdgeSizeBlocks,
|
||||
resolution: atlasEdgeSize,
|
||||
length: t.length,
|
||||
});
|
||||
offset += t.length;
|
||||
}
|
||||
|
||||
if (overhaulSFRBlocks[0].size[0] !== overhaulSFRBlocks[0].size[1]) {
|
||||
throw new Error("Configuration processing failed - textures must be square");
|
||||
}
|
||||
|
||||
// TODO: handle sizes other than 16px
|
||||
if (overhaulSFRBlocks[0].size[0] !== 16) {
|
||||
throw new Error("Configuration processing failed - textures must be 16px");
|
||||
}
|
||||
|
||||
const atlasEdgeSizeBlocks = 2 ** Math.ceil(Math.log2(Math.sqrt(overhaulSFRBlocks.length)));
|
||||
const atlasEdgeSize = atlasEdgeSizeBlocks * overhaulSFRBlocks[0].size[0];
|
||||
|
||||
const canvas = new OffscreenCanvas(atlasEdgeSize, atlasEdgeSize);
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
await Promise.all(overhaulSFRBlocks.map((b, i) => putTextureOnCanvas(ctx, atlasEdgeSizeBlocks, i, b)));
|
||||
|
||||
const blob = await canvas.convertToBlob({ type: "image/png" });
|
||||
atlasSize = atlasEdgeSizeBlocks;
|
||||
atlasResolution = atlasEdgeSize;
|
||||
atlasBlobURL = URL.createObjectURL(blob);
|
||||
atlasMetadata = overhaulSFRBlocks;
|
||||
|
||||
console.log("Atlas blob: %s", atlasBlobURL);
|
||||
console.log(atlases);
|
||||
|
||||
emitEvent("config:process:atlas:end");
|
||||
}
|
||||
|
|
@ -96,21 +107,44 @@ export function makeAtlasImage(index) {
|
|||
return div;
|
||||
}
|
||||
|
||||
const atlasIndexX = index % atlasSize;
|
||||
const atlasIndexY = (index - atlasIndexX) / atlasSize;
|
||||
const size = atlasMetadata[index].size;
|
||||
const x = atlasIndexX * size[0];
|
||||
const y = atlasIndexY * size[1];
|
||||
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 x = atlasIndexX * size[0];
|
||||
const y = atlasIndexY * size[1];
|
||||
|
||||
const div = document.createElement("div");
|
||||
div.draggable = false;
|
||||
div.classList.add("atlas-texture");
|
||||
div.style.backgroundImage = `url(${atlasBlobURL})`;
|
||||
div.style.setProperty("--atlas-resolution", `${atlasResolution}px`);
|
||||
div.style.setProperty("--raw-x", `${x}px`);
|
||||
div.style.setProperty("--raw-y", `${y}px`);
|
||||
div.style.setProperty("--size-x", `${size[0]}px`);
|
||||
div.style.setProperty("--size-y", `${size[1]}px`);
|
||||
const div = document.createElement("div");
|
||||
div.draggable = false;
|
||||
div.dataset.originalIndex = originalIndex;
|
||||
div.classList.add("atlas-texture");
|
||||
div.style.backgroundImage = `url(${atlas.url})`;
|
||||
div.style.setProperty("--atlas-resolution", `${atlas.resolution}px`);
|
||||
div.style.setProperty("--raw-x", `${x}px`);
|
||||
div.style.setProperty("--raw-y", `${y}px`);
|
||||
div.style.setProperty("--size-x", `${size[0]}px`);
|
||||
div.style.setProperty("--size-y", `${size[1]}px`);
|
||||
|
||||
return div;
|
||||
return div;
|
||||
}
|
||||
index -= atlas.length;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getAtlasMetadata(index) {
|
||||
if (index < -1) return null;
|
||||
if (index === -1) return {};
|
||||
|
||||
for (const atlas of atlases) {
|
||||
if (index < atlas.length - 1) {
|
||||
return atlas.metadata[index];
|
||||
}
|
||||
index -= atlas.length;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,11 @@ export class Filter {
|
|||
|
||||
test(...data) {
|
||||
for (const datum of data) {
|
||||
if (typeof datum !== "string") {
|
||||
console.log("cannot test %o", datum);
|
||||
continue;
|
||||
}
|
||||
|
||||
const normalized = datum.trim().toLocaleLowerCase().trim();
|
||||
for (const n of this.negate) {
|
||||
if (normalized.includes(n)) {
|
||||
|
|
|
|||
|
|
@ -40,5 +40,5 @@ window.addEventListener("load", () => {
|
|||
|
||||
emitEvent("setup-end");
|
||||
|
||||
void loadConfiguration("default");
|
||||
void loadConfiguration("addons");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
import { configuration } from "./configuration.js";
|
||||
|
||||
function findInPossible(entryList, entry) {
|
||||
switch (entry.type) {
|
||||
case "legacy_block":
|
||||
return entryList.find(v => v.name === entry.name
|
||||
&& v.metadata === entry.metadata);
|
||||
|
||||
case "oredict":
|
||||
return entryList.find(v => v.oredict === entry.oredict);
|
||||
|
||||
default:
|
||||
console.warn("unknown type", entry.type);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function resolveBlockName(block, { configurationHint }) {
|
||||
|
||||
if (block.type === "module") {
|
||||
switch (block.name) {
|
||||
case "nuclearcraft:overhaul_sfr:fuel_cell":
|
||||
return "Fuel Cell";
|
||||
|
||||
case "nuclearcraft:overhaul_sfr:moderator":
|
||||
return "Moderator";
|
||||
|
||||
case "nuclearcraft:overhaul_sfr:casing":
|
||||
return "Casing";
|
||||
|
||||
case "nuclearcraft:overhaul_sfr:reflector":
|
||||
return "Reflector";
|
||||
|
||||
case "nuclearcraft:overhaul_sfr:neutron_shield":
|
||||
return "Neutron Shield";
|
||||
|
||||
case "nuclearcraft:overhaul_sfr:irradiator":
|
||||
return "Irradiator";
|
||||
|
||||
default:
|
||||
console.warn("unknown module", block);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const possibleBlocks = [
|
||||
...Object.entries(configuration.configuration),
|
||||
...configuration.addons.flatMap(a => Object.entries(a.configuration)),
|
||||
]
|
||||
.filter(([k, v]) => (!configurationHint || k === configurationHint) && "blocks" in v)
|
||||
.flatMap(([, v]) => v.blocks);
|
||||
|
||||
const entry = findInPossible(possibleBlocks, block);
|
||||
if (!entry) return null;
|
||||
|
||||
return entry.modules["plannerator:display_name"].display_name;
|
||||
}
|
||||
|
||||
export function lookupFromMetadata(metadata) {
|
||||
if (!metadata.configuration || !metadata.configurationList || !metadata.type)
|
||||
return null;
|
||||
|
||||
const possibleEntries = [
|
||||
...Object.entries(configuration.configuration),
|
||||
...configuration.addons.flatMap(a => Object.entries(a.configuration)),
|
||||
]
|
||||
.filter(([k, v]) => k === metadata.configuration && metadata.configurationList in v)
|
||||
.flatMap(([, v]) => v[metadata.configurationList])
|
||||
.filter(v => v.type === metadata.type);
|
||||
|
||||
return findInPossible(possibleEntries, metadata);
|
||||
}
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
import {atlasMetadata, makeAtlasImage} from "./atlas.js";
|
||||
import {DomList} from "./dom-list.js";
|
||||
import { atlases, makeAtlasImage } from "./atlas.js";
|
||||
import { DomList } from "./dom-list.js";
|
||||
import { emitEvent, listenTo } from "./events.js";
|
||||
import { Filter } from "./filter.js";
|
||||
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)
|
||||
|
|
@ -103,9 +104,7 @@ function listClickEvent(e) {
|
|||
}
|
||||
|
||||
export function initializePlanner() {
|
||||
listenTo("block-picker:filter", e => {
|
||||
buildBlockPicker();
|
||||
});
|
||||
listenTo("block-picker:filter", buildBlockPicker);
|
||||
|
||||
listenTo("config:end", buildBlockPicker);
|
||||
|
||||
|
|
@ -114,81 +113,25 @@ export function initializePlanner() {
|
|||
|
||||
function buildBlockPicker() {
|
||||
const filter = Filter.fromInput("block-filter");
|
||||
console.log(filter);
|
||||
|
||||
const grid = new DomList(document.querySelector("#block-list"));
|
||||
grid.clear();
|
||||
|
||||
atlasMetadata.forEach((m, i) => {
|
||||
const item = grid.makeItem();
|
||||
item.dataset.index = i.toString(10);
|
||||
if (m.modules["plannerator:display_name"].display_name === "Lithium Heat Sink") {
|
||||
console.log(m);
|
||||
void {
|
||||
"cooling": 130,
|
||||
"rules": [
|
||||
{
|
||||
"rules": [
|
||||
{
|
||||
"rules": [
|
||||
{
|
||||
"min": 2,
|
||||
"max": 2,
|
||||
"block": {
|
||||
"metadata": 1,
|
||||
"name": "nuclearcraft:solid_fission_sink2",
|
||||
"type": "legacy_block",
|
||||
"blockstate": {
|
||||
"type": "lead"
|
||||
}
|
||||
},
|
||||
"type": "between"
|
||||
},
|
||||
{
|
||||
"min": 1,
|
||||
"max": 1,
|
||||
"block": {
|
||||
"metadata": 1,
|
||||
"name": "nuclearcraft:solid_fission_sink2",
|
||||
"type": "legacy_block",
|
||||
"blockstate": {
|
||||
"type": "lead"
|
||||
}
|
||||
},
|
||||
"type": "axial"
|
||||
}
|
||||
],
|
||||
"type": "and"
|
||||
},
|
||||
{
|
||||
"min": 1,
|
||||
"max": 6,
|
||||
"block": {
|
||||
"name": "nuclearcraft:overhaul_sfr:casing",
|
||||
"type": "module"
|
||||
},
|
||||
"type": "between"
|
||||
const blocks = atlases.flatMap(a => a.metadata)
|
||||
.filter(m => m.configuration === "nuclearcraft:overhaul_sfr" && m.configurationList === "blocks");
|
||||
console.log(blocks);
|
||||
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(...tooltipData)) {
|
||||
item.classList.add("hidden");
|
||||
}
|
||||
],
|
||||
"type": "and"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
item.dataset.tooltip = createTooltip([
|
||||
m.modules["plannerator:display_name"].display_name,
|
||||
...("nuclearcraft:overhaul_sfr:heat_sink" in m.modules ? [
|
||||
"Heatsink",
|
||||
`Cooling: ${m.modules["nuclearcraft:overhaul_sfr:heat_sink"].cooling ?? "???"} H/t`,
|
||||
] : []),
|
||||
]).toString(10);
|
||||
if (!filter.test(m.modules["plannerator:display_name"].display_name)) {
|
||||
item.classList.add("hidden");
|
||||
}
|
||||
item.appendChild(makeAtlasImage(i));
|
||||
item.addEventListener("click", listClickEvent);
|
||||
grid.addItem(item);
|
||||
});
|
||||
item.appendChild(makeAtlasImage(m.index));
|
||||
item.addEventListener("click", listClickEvent);
|
||||
grid.addItem(item);
|
||||
});
|
||||
}
|
||||
|
||||
function buildPlanner() {
|
||||
|
|
@ -226,3 +169,127 @@ function buildPlanner() {
|
|||
list.addItem(item);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
if ("nuclearcraft:overhaul_sfr:heat_sink" in data.modules) {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parseRuleAsTooltip(rule, props) {
|
||||
void {
|
||||
"cooling": 130,
|
||||
"rules": [
|
||||
{
|
||||
"rules": [
|
||||
{
|
||||
"rules": [
|
||||
{
|
||||
"min": 2,
|
||||
"max": 2,
|
||||
"block": {
|
||||
"metadata": 1,
|
||||
"name": "nuclearcraft:solid_fission_sink2",
|
||||
"type": "legacy_block",
|
||||
"blockstate": {
|
||||
"type": "lead"
|
||||
}
|
||||
},
|
||||
"type": "between"
|
||||
},
|
||||
{
|
||||
"min": 1,
|
||||
"max": 1,
|
||||
"block": {
|
||||
"metadata": 1,
|
||||
"name": "nuclearcraft:solid_fission_sink2",
|
||||
"type": "legacy_block",
|
||||
"blockstate": {
|
||||
"type": "lead"
|
||||
}
|
||||
},
|
||||
"type": "axial"
|
||||
}
|
||||
],
|
||||
"type": "and"
|
||||
},
|
||||
{
|
||||
"min": 1,
|
||||
"max": 6,
|
||||
"block": {
|
||||
"name": "nuclearcraft:overhaul_sfr:casing",
|
||||
"type": "module"
|
||||
},
|
||||
"type": "between"
|
||||
}
|
||||
],
|
||||
"type": "and"
|
||||
}
|
||||
]
|
||||
}
|
||||
// Requires exactly 2 lead heat sink AND exactly 1 pairs of axial lead heat sink AND at least 1 casing
|
||||
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 === 6) {
|
||||
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}`;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
default:
|
||||
console.warn("unknown rule: %o", rule);
|
||||
return "???";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue