From 5f168246cca42ba54b67bd84fbd69fd6a5b9f820 Mon Sep 17 00:00:00 2001 From: MetroWind Date: Thu, 25 Sep 2025 16:16:58 -0700 Subject: This repo will be used for all small web apps. The NMS Freighter Planner is moved into its own dir. --- nms-freighter-planner/scripts/lib.js | 40 ++++ nms-freighter-planner/scripts/main.js | 256 ++++++++++++++++++++++++ nms-freighter-planner/scripts/tiles.js | 347 +++++++++++++++++++++++++++++++++ 3 files changed, 643 insertions(+) create mode 100644 nms-freighter-planner/scripts/lib.js create mode 100644 nms-freighter-planner/scripts/main.js create mode 100644 nms-freighter-planner/scripts/tiles.js (limited to 'nms-freighter-planner/scripts') diff --git a/nms-freighter-planner/scripts/lib.js b/nms-freighter-planner/scripts/lib.js new file mode 100644 index 0000000..9a5e957 --- /dev/null +++ b/nms-freighter-planner/scripts/lib.js @@ -0,0 +1,40 @@ +const h = preact.h; +const CELL_SIZE = 32; +const GRID_SIZE_X = 21; +const GRID_SIZE_Y = 21; +const FLOOR_COUNT = 15; + +const ASSET_SVG_PROPS = {"width": CELL_SIZE, "height": CELL_SIZE, + "version": "1.1"}; +const ASSET_WHITE_BG = h("rect", {"x": 0, "y": 0, "width": CELL_SIZE, + "height": CELL_SIZE, "fill": "white", + "stroke-width": 0 }); + +// Bytes is a Uint8Array +async function compressBytes(bytes) +{ + let blob = new Blob([bytes]); + const ds = new CompressionStream("deflate"); + const compressed = blob.stream().pipeThrough(ds); + let compressed_blob = await new Response(compressed).blob(); + const reader = new FileReader(); + return new Promise((resolve, _) => { + reader.onloadend = (event) => { + const result = event.target.result; + resolve(result.replace(/^data:.+;base64,/, '') + .replaceAll("/", "-").replaceAll("+", "_")); + } + reader.readAsDataURL(compressed_blob); + }); +} + +// Decompress into Uint8Array +function decompressBytes(s) +{ + const decoded = window.atob(s.replaceAll("_", "+").replaceAll("-", "/")); + const decoded_array = Uint8Array.from(decoded, c => c.charCodeAt(0)); + let blob = new Blob([decoded_array]); + const cs = new DecompressionStream("deflate"); + const decompressed = blob.stream().pipeThrough(cs); + return new Response(decompressed).bytes(); +} diff --git a/nms-freighter-planner/scripts/main.js b/nms-freighter-planner/scripts/main.js new file mode 100644 index 0000000..d6b6787 --- /dev/null +++ b/nms-freighter-planner/scripts/main.js @@ -0,0 +1,256 @@ +const DEFAULT_APP_STATE = { + floor: 0, // This is 0-based. + plan: new Array(FLOOR_COUNT).fill(null).map(() => + new Array(GRID_SIZE_X * GRID_SIZE_Y).fill(0)), + // mouse_state can be “normal”, or “dnd”. + mouse_state: "normal", + dragged_asset_index: null, + plan_code: "", +}; + +async function serializePlan(app_state) +{ + return compressBytes(Uint8Array.from(app_state.plan.flat())); +} + +function genGrid(x_count, y_count, cell_size) +{ + let lines = []; + for(let x = 0; x < x_count+1; x++) + { + lines.push(h("line", { + "x1": x * cell_size + 0.5, "x2": x * cell_size + 0.5, "y1": 0.5, + "y2": y_count * cell_size + 0.5, + })); + } + for(let y = 0; y < y_count+1; y++) + { + lines.push(h("line", { + "y1": y * cell_size + 0.5, "y2": y * cell_size + 0.5, "x1": 0.5, + "x2": x_count * cell_size + 0.5, + })); + } + return h("g", {"stroke": "black", "stroke-width": 1}, lines); +} + +function AssetView({asset_index, on_drag_begin, on_drag_end}) +{ + let bg = ASSET_WHITE_BG; + if(TILES[asset_index].bg === null) + { + bg = null; + } + return h("div", {"class": "Asset", "draggable": true, + "data-asset-index": asset_index, + "ondragstart": on_drag_begin, + "ondragend": on_drag_end, + "title": TILES[asset_index].name,}, + h("svg", ASSET_SVG_PROPS, bg, TILES[asset_index].inner_svg)); +} + +function AssetsView({on_drag_begin, on_drag_end}) +{ + let views = TILES.map((t, i) => + h(AssetView, {asset_index: i, on_drag_begin: on_drag_begin, + on_drag_end: on_drag_end})); + return h("div", {}, views); +} + +function PlanGridView({content, mouse_state, on_drop_asset}) +{ + const [mouse_coord, setMouseCoord] = preactHooks.useState(null); + + function onDragOver(e) + { + e.preventDefault(); + let b = e.target.getBoundingClientRect(); + let x = e.clientX - b.left; + let y = e.clientY - b.top; + setMouseCoord([x, y]); + } + + function onDragLeave(e) + { + setMouseCoord(null); + } + + let hilight_box = null; + + if(mouse_coord !== null && mouse_state != "dnd") + { + setMouseCoord(null); + } + + if(mouse_coord !== null && mouse_state == "dnd") + { + let cell_x = Math.floor(mouse_coord[0] / (CELL_SIZE + 1)); + let cell_y = Math.floor(mouse_coord[1] / (CELL_SIZE + 1)); + hilight_box = h("rect", { + "x": cell_x * (CELL_SIZE + 1) + 0.5, "y": cell_y * (CELL_SIZE + 1) + 0.5, + "width": CELL_SIZE + 1, "height": CELL_SIZE + 1, + "stroke": "#2ed573", "stroke-width": 3, "fill": "transparent"}); + } + let grid_content = content.map((idx, i) => { + if(idx == 0) return null; + let cell_x = i % GRID_SIZE_X; + let cell_y = Math.floor(i / GRID_SIZE_Y); + let bg = ASSET_WHITE_BG; + if(TILES[idx].bg === null) + { + bg = null; + } + return h("g", {"transform": `translate(${cell_x * (CELL_SIZE + 1) + 1} \ +${cell_y * (CELL_SIZE + 1) + 1})`}, bg, TILES[idx].inner_svg); + }); + + let img_width = GRID_SIZE_X * (CELL_SIZE + 1) + 1; + let img_height = GRID_SIZE_Y * (CELL_SIZE + 1) + 1; + return h("svg", {"width": img_width, "height": img_height, + "version": "1.1", "ondragover": onDragOver, + "ondrop": on_drop_asset, + "id": "Grid", "ondragleave": onDragLeave,}, + h("rect", {x: 0, y: 0, width: img_width, height: img_height, + fill: "#c0c0c0", "stroke-width": 0}), + grid_content, genGrid(GRID_SIZE_X, GRID_SIZE_Y, CELL_SIZE + 1), + hilight_box); +} + +function PlanView({plan, mouse_state, on_drop_asset}) +{ + return h("div", {"id": "PlanView"}, + h("div", {"id": "EntryIndicator"}, "⬇️"), + h(PlanGridView, {content: plan, mouse_state: mouse_state, + on_drop_asset: on_drop_asset})); +} + +// on_floor_change takes one argument, which is the 0-based floor number. +function FloorSelector({floor, on_floor_change}) +{ + return h("div", {"id": "FloorSelectorWrapper", "class": "ButtonRowLeft"}, + h("label", {}, "Floor"), + h("input", {"id": "InputFloor", "type": "number", "step": 1, "min": 1, + "max": 15, "value": floor, + onchange: e => on_floor_change(e.target.value - 1),},)); +} + +function App({initial_state}) +{ + const [state, setState] = preactHooks.useState(initial_state); + + // This event is targeted at the dragged asset. + function onDragAssetBegin(e) + { + let new_state = structuredClone(state); + new_state.mouse_state = "dnd"; + new_state.dragged_asset_index = + parseInt(e.target.getAttribute("data-asset-index")); + setState(new_state); + } + + // This event is targeted at the grid. + function onDropAssetOnGrid(e) + { + if(e.dragEffect === "none") return; + let grid = document.getElementById("Grid"); + let b = grid.getBoundingClientRect(); + let x = e.clientX - b.left; + let y = e.clientY - b.top; + let cell_x = Math.floor(x / (CELL_SIZE + 1)); + let cell_y = Math.floor(y / (CELL_SIZE + 1)); + let cell_index = cell_y * GRID_SIZE_X + cell_x; + let new_state = structuredClone(state); + new_state.plan[new_state.floor][cell_index] = state.dragged_asset_index; + new_state.mouse_state = "normal"; + new_state.dragged_asset_index = null; + serializePlan(new_state).then((s) => { + new_state.plan_code = s; + setState(new_state); + const url = new URL(location); + url.searchParams.set("plan", s); + window.history.pushState({}, "", url); + }); + } + + function onClickSaveImg() + { + let grid_width = GRID_SIZE_X * (CELL_SIZE + 1) + 1; + let grid_height = GRID_SIZE_Y * (CELL_SIZE + 1) + 1; + let canvas = document.createElement('canvas'); + canvas.width = grid_width; + canvas.height = grid_height; + let context = canvas.getContext("2d"); + var svg_str = new XMLSerializer().serializeToString( + document.getElementById("Grid")); + var img = new Image(); + img.onload = function() + { + context.drawImage(this, 0, 0); + // Open PNG image in new tab. + var dataURL = canvas.toDataURL("image/png"); + var new_tab = window.open('about:blank', 'image from canvas'); + new_tab.document.write("from canvas"); + } + img.src = 'data:image/svg+xml; charset=utf8, ' + + encodeURIComponent(svg_str); + } + + // new_floor is 0-based. + function onFloorChange(new_floor) + { + let new_state = structuredClone(state); + new_state.floor = new_floor; + setState(new_state); + } + + return h("div", {}, + h("div", {"class": "MainWrapper"}, + h("aside", {}, + h(FloorSelector, {floor: state.floor + 1, + on_floor_change: onFloorChange}), + h("div", {"class": "LabeledPanel"}, + h("h2", {}, "Rooms"), + h(AssetsView, {on_drag_begin: onDragAssetBegin,}))), + h(PlanView, {plan: state.plan[state.floor], + mouse_state: state.mouse_state, + on_drop_asset: onDropAssetOnGrid,})), + h("hr", {}), + h("div", {}, + h("textarea", {readonly: true}, state.plan_code)), + h("div", {"class": "ButtonRow"}, + h("button", {onclick: onClickSaveImg, type: "button"}, + "Open as Image!")), + ); +} + +function render(app_state) +{ + preact.render(h(App, {initial_state: app_state}), + document.getElementById("Plan")); +} + +const paramsString = window.location.search; +const searchParams = new URLSearchParams(paramsString); +const encoded_plan = searchParams.get("plan"); +if(encoded_plan === null) +{ + render(DEFAULT_APP_STATE); +} +else +{ + decompressBytes(encoded_plan).then(a => { + let state = structuredClone(DEFAULT_APP_STATE); + let concated_plan = Array.from(a); + console.debug(concated_plan); + state.plan = []; + for(let i = 0; i < GRID_SIZE_X * GRID_SIZE_Y * FLOOR_COUNT; + i += GRID_SIZE_X * GRID_SIZE_Y) + { + let floor_slice = + concated_plan.slice(i, i + GRID_SIZE_X * GRID_SIZE_Y); + console.debug(floor_slice); + state.plan.push(floor_slice); + } + state.plan_code = encoded_plan; + render(state); + }); +} diff --git a/nms-freighter-planner/scripts/tiles.js b/nms-freighter-planner/scripts/tiles.js new file mode 100644 index 0000000..2ed5691 --- /dev/null +++ b/nms-freighter-planner/scripts/tiles.js @@ -0,0 +1,347 @@ +// TODO: how does internal walls & ladder work?? +const TILES = [ + { + id: "null", + name: "Nothing", + bg: null, + inner_svg: null, + }, { + id: "blank", + name: "Generic", + inner_svg: null, + }, { + id: "corridor_ns", + name: "Corridor N-S", + bg: null, + inner_svg: h("rect", { + x: CELL_SIZE / 4, y: 0, width: CELL_SIZE / 2, height: CELL_SIZE, + fill: "white"}), + }, { + id: "corridor_we", + name: "Corridor W-E", + bg: null, + inner_svg: h("rect", { + y: CELL_SIZE / 4, x: 0, height: CELL_SIZE / 2, width: CELL_SIZE, + fill: "white"}), + }, { + id: "corridor_ne", + name: "Corridor N-E", + bg: null, + inner_svg: h("path", { + d: `M ${CELL_SIZE * 0.5} 0 \ +A ${CELL_SIZE * 0.5} ${CELL_SIZE * 0.5} 0 0 0 \ +${CELL_SIZE} ${CELL_SIZE * 0.5}`, + stroke: "white", "stroke-width": CELL_SIZE * 0.5}), + }, { + id: "corridor_se", + name: "Corridor S-E", + bg: null, + inner_svg: h("path", { + d: `M ${CELL_SIZE * 0.5} ${CELL_SIZE} \ +A ${CELL_SIZE * 0.5} ${CELL_SIZE * 0.5} 0 0 1 \ +${CELL_SIZE} ${CELL_SIZE * 0.5}`, + stroke: "white", "stroke-width": CELL_SIZE * 0.5}), + }, { + id: "corridor_sw", + name: "Corridor S-W", + bg: null, + inner_svg: h("path", { + d: `M ${CELL_SIZE * 0.5} ${CELL_SIZE} \ +A ${CELL_SIZE * 0.5} ${CELL_SIZE * 0.5} 0 0 0 \ +0 ${CELL_SIZE * 0.5}`, + stroke: "white", "stroke-width": CELL_SIZE * 0.5}), + }, { + id: "corridor_nw", + name: "Corridor N-W", + bg: null, + inner_svg: h("path", { + d: `M ${CELL_SIZE * 0.5} 0 \ +A ${CELL_SIZE * 0.5} ${CELL_SIZE * 0.5} 0 0 1 \ +0 ${CELL_SIZE * 0.5}`, + stroke: "white", "stroke-width": CELL_SIZE * 0.5}), + }, { + id: "corridor_3n", + name: "Three-way corridor 1", + bg: null, + inner_svg: + h(preact.Fragment, {}, + h("rect", { + y: CELL_SIZE / 4, x: 0, height: CELL_SIZE / 2, width: CELL_SIZE, + fill: "white"}), + h("rect", { + x: CELL_SIZE / 4, y: 0, width: CELL_SIZE * 0.5, + height: CELL_SIZE * 0.5, fill: "white"})), + }, { + id: "corridor_3e", + name: "Three-way corridor 2", + bg: null, + inner_svg: + h(preact.Fragment, {}, + h("rect", { + x: CELL_SIZE / 4, y: 0, width: CELL_SIZE / 2, height: CELL_SIZE, + fill: "white"}), + h("rect", { + y: CELL_SIZE / 4, x: CELL_SIZE * 0.5, height: CELL_SIZE / 2, + width: CELL_SIZE * 0.5, fill: "white"})) + }, { + id: "corridor_3s", + name: "Three-way corridor 3", + bg: null, + inner_svg: + h(preact.Fragment, {}, + h("rect", { + y: CELL_SIZE / 4, x: 0, height: CELL_SIZE / 2, width: CELL_SIZE, + fill: "white"}), + h("rect", { + x: CELL_SIZE / 4, y: CELL_SIZE * 0.5, width: CELL_SIZE * 0.5, + height: CELL_SIZE * 0.5, fill: "white"})), + }, { + id: "corridor_3e", + name: "Three-way corridor 4", + bg: null, + inner_svg: + h(preact.Fragment, {}, + h("rect", { + x: CELL_SIZE / 4, y: 0, width: CELL_SIZE / 2, height: CELL_SIZE, + fill: "white"}), + h("rect", { + y: CELL_SIZE / 4, x: 0, height: CELL_SIZE / 2, + width: CELL_SIZE * 0.5, fill: "white"})) + }, { + id: "stair_up", + name: "Stair up", + inner_svg: + h(preact.Fragment, {}, + h("polygon", { + points: `5, ${CELL_SIZE} \ +5, ${5 + (CELL_SIZE - 5) / 3 * 2} \ +${5 + (CELL_SIZE - 5) / 3}, ${5 + (CELL_SIZE - 5) / 3 * 2} \ +${5 + (CELL_SIZE - 5) / 3}, ${5 + (CELL_SIZE - 5) / 3} \ +${5 + (CELL_SIZE - 5) / 3 * 2}, ${5 + (CELL_SIZE - 5) / 3} \ +${5 + (CELL_SIZE - 5) / 3 * 2}, 5 \ +${CELL_SIZE}, 5 \ +${CELL_SIZE}, ${CELL_SIZE}`, + fill: "black", "stroke-width": 0}), + h("line", {x1: 3, y1: CELL_SIZE - 15, x2: CELL_SIZE - 15, y2: 3, + stroke: "black", "stroke-width": 2}), + h("polyline", {points: `${CELL_SIZE - 15 - 6}, 3 \ +${CELL_SIZE - 15}, 3 \ +${CELL_SIZE - 15}, ${3 + 6}`, + stroke: "black", "stroke-width": 2})), + + }, { + id: "stair_down", + name: "Stair down", + inner_svg: + h(preact.Fragment, {}, + h("polygon", { + points: `5, ${CELL_SIZE} \ +5, ${5 + (CELL_SIZE - 5) / 3 * 2} \ +${5 + (CELL_SIZE - 5) / 3}, ${5 + (CELL_SIZE - 5) / 3 * 2} \ +${5 + (CELL_SIZE - 5) / 3}, ${5 + (CELL_SIZE - 5) / 3} \ +${5 + (CELL_SIZE - 5) / 3 * 2}, ${5 + (CELL_SIZE - 5) / 3} \ +${5 + (CELL_SIZE - 5) / 3 * 2}, 5 \ +${CELL_SIZE}, 5 \ +${CELL_SIZE}, ${CELL_SIZE}`, + fill: "black", "stroke-width": 0}), + h("line", {x1: 3, y1: CELL_SIZE - 15, x2: CELL_SIZE - 15, y2: 3, + stroke: "black", "stroke-width": 2}), + h("polyline", {points: `3, ${CELL_SIZE - 15 - 6} \ +3, ${CELL_SIZE - 15} \ +${3 + 6}, ${CELL_SIZE - 15}`, + stroke: "black", "stroke-width": 2})), + + }, { + id: "tech", + name: "Technology room", + inner_svg: h("text", {x: CELL_SIZE * 0.5, y: CELL_SIZE * 0.5, dy: 3, + "font-family": "monospace", "font-weight": "bold", + "font-size": 32, "text-anchor": "middle", + "dominant-baseline": "middle", fill: "#c0c0c0", }, + "T"), + }, { + id: "industry", + name: "Industrial room", + inner_svg: h("text", {x: CELL_SIZE * 0.5, y: CELL_SIZE * 0.5, dy: 3, + "font-family": "monospace", "font-weight": "bold", + "font-size": 32, "text-anchor": "middle", + "dominant-baseline": "middle", fill: "#c0c0c0", }, + "I"), + }, { + id: "biology", + name: "Biological room", + inner_svg: h("text", {x: CELL_SIZE * 0.5, y: CELL_SIZE * 0.5, dy: 3, + "font-family": "monospace", "font-weight": "bold", + "font-size": 32, "text-anchor": "middle", + "dominant-baseline": "middle", fill: "#c0c0c0", }, + "B"), + }, { + id: "appearance", + name: "Appearance modifier", + inner_svg: h("g", {"fill": "black", "stroke-width": 0, + "transform": "translate(2.921 1.0547)"}, + h("path", {d: "m28.151 27.442 4e-3 -5e-3c-2.0135-2.2928-4.2222-1.0005-6.0958-0.27575-1.651 0.721-3.213 1.402-4.92 0.749-0.871-0.33-1.423-0.809-1.645-1.419-0.184-0.502-0.135-1.045-0.028-1.488l1.228 0.22c0.619 0.109 1.218-0.307 1.326-0.925l1.508-8.447c3.688-0.714 6.474-3.955 6.474-7.851 0-4.419-3.582-8-8-8-2.524 0-4.772 1.173-6.239 3h-7.763v9h7.079c0.565 0.977 1.34 1.813 2.25 2.474l-1.556 8.71c-0.11 0.618 0.306 1.216 0.925 1.326l1.081 0.191c-0.15 0.686-0.201 1.527 0.099 2.362 0.394 1.104 1.287 1.931 2.647 2.451 2.365 0.899 4.412 6e-3 6.218-0.78 1.475-0.6585 2.9852-1.5128 4.1748-0.11225l6e-3 -7e-3c0.15 0.136 0.35 0.218 0.566 0.218 0.473 0 0.854-0.382 0.854-0.852 0-0.203-0.072-0.391-0.193-0.539zm-10.151-23.442c2.206 0 4 1.794 4 4 0 2.205-1.794 4-4 4s-4-1.795-4-4c0-2.206 1.794-4 4-4z"}), + h("circle", {cx: 18, cy: 8, r: 2}), + h("path", {d: "m 0,4.001 v 6.997 C 0,11.552 0.448,12 1.001,12 H 3 V 3 H 1.001 C 0.448,3 0,3.448 0,4.001 Z"})), + }, { + id: "construction", + name: "Construction specialist’s room", + inner_svg: h("text", {x: CELL_SIZE * 0.5, y: CELL_SIZE * 0.5, dy: 3, + "font-family": "monospace", "font-weight": "bold", + "font-size": 32, "text-anchor": "middle", + "dominant-baseline": "middle", fill: "black",}, + "C"), + }, { + id: "science", + name: "Scientist’s room", + inner_svg: h("text", {x: CELL_SIZE * 0.5, y: CELL_SIZE * 0.5, dy: 3, + "font-family": "monospace", "font-weight": "bold", + "font-size": 32, "text-anchor": "middle", + "dominant-baseline": "middle", fill: "black",}, + "S"), + }, { + id: "agricultural", + name: "Agricultural specialist’s room", + inner_svg: h("text", {x: CELL_SIZE * 0.5, y: CELL_SIZE * 0.5, dy: 3, + "font-family": "monospace", "font-weight": "bold", + "font-size": 32, "text-anchor": "middle", + "dominant-baseline": "middle", fill: "black",}, + "A"), + }, { + id: "exocrafter", + name: "Exocraft specialist’s room", + inner_svg: h("text", {x: CELL_SIZE * 0.5, y: CELL_SIZE * 0.5, dy: 3, + "font-family": "monospace", "font-weight": "bold", + "font-size": 32, "text-anchor": "middle", + "dominant-baseline": "middle", fill: "black",}, + "E"), + }, { + id: "trade", + name: "Galaxtic trade", + inner_svg: h("text", {x: CELL_SIZE * 0.5, y: CELL_SIZE * 0.5, dy: 3, + "font-family": "monospace", "font-weight": "bold", + "font-size": 28, "text-anchor": "middle", + "dominant-baseline": "middle", fill: "black",}, + "$"), + }, { + id: "teleporter", + name: "Teleporter", + inner_svg: h("circle", { + "cx": CELL_SIZE * 0.5, "cy": CELL_SIZE * 0.5, + "r": (CELL_SIZE - 10) * 0.5, "stroke": "black", + "fill": "transparent", "stroke-width": 6, + }), + }, { + id: "fleet_command", + name: "Fleet command", + inner_svg: h("polyline", { + "points": `3, ${CELL_SIZE} \ +3, ${CELL_SIZE - 15} \ +13, ${CELL_SIZE - 25} \ +13, ${CELL_SIZE - 5} \ +${CELL_SIZE - 13}, ${CELL_SIZE - 5} \ +${CELL_SIZE - 13}, ${CELL_SIZE - 25} \ +${CELL_SIZE - 3}, ${CELL_SIZE - 15} \ +${CELL_SIZE - 3}, ${CELL_SIZE}`, + "fill": "black", "stroke-width": 0, + }), + }, { + id: "scanner", + name: "Scanner", + inner_svg: + h(preact.Fragment, {}, + h("circle", {"cx": CELL_SIZE * 0.5, "cy": CELL_SIZE * 0.5, + "r": CELL_SIZE * 0.5 - 2, "fill": "transparent", + "stroke": "black", "stroke-width": 2}), + h("circle", {"cx": CELL_SIZE * 0.5, "cy": CELL_SIZE * 0.5, + "r": CELL_SIZE * 0.25, "fill": "transparent", + "stroke": "black", "stroke-width": 2}), + h("line", {"x1": 2, "y1": CELL_SIZE * 0.5, + "x2": CELL_SIZE - 2, "y2": CELL_SIZE * 0.5, + "stroke": "black", "stroke-width": 2}), + h("line", {"y1": 2, "x1": CELL_SIZE * 0.5, + "y2": CELL_SIZE - 2, "x2": CELL_SIZE * 0.5, + "stroke": "black", "stroke-width": 2}), + h("line", {"x1": CELL_SIZE * 0.5, "y1": CELL_SIZE * 0.5, + "x2": CELL_SIZE * 0.5 + (CELL_SIZE * 0.5 - 2) * Math.cos(Math.PI / 6), + "y2": CELL_SIZE * 0.5 - (CELL_SIZE * 0.5 - 2) * Math.sin(Math.PI / 6), + "stroke": "black", "stroke-width": 2}), + h("circle", {"cx": 22, "cy": 6, "r": 2, "fill": "black", + "stroke-width": 0}), + h("circle", {"cx": 10, "cy": 22, "r": 4, "fill": "black", + "stroke-width": 0}), + + ), + }, { + id: "extractor", + name: "Stellar extractor", + inner_svg: + h(preact.Fragment, {}, + h("circle", {"cx": CELL_SIZE * 0.5, "cy": 21, + "r": 10, "fill": "black", "stroke-width": 0}), + h("polygon", {"points": `${CELL_SIZE * 0.5 - 4}, 20 \ +${CELL_SIZE * 0.5 - 4}, 10 \ +${CELL_SIZE * 0.5 - 8}, 10 \ +${CELL_SIZE * 0.5}, 3 \ +${CELL_SIZE * 0.5 + 8}, 10 \ +${CELL_SIZE * 0.5 + 4}, 10 \ +${CELL_SIZE * 0.5 + 4}, 20`, + "stroke": "black", "stroke-width": 2, + "fill": "white"}), + ), + }, { + id: "storage", + name: "Storage", + inner_svg: + h(preact.Fragment, {}, + h("rect", {"x": 4, "y": 4, "width": CELL_SIZE - 8, + "height": CELL_SIZE - 8, "rx": 2, "ry": 2, + "fill": "black", "stroke-width": 0}), + h("line", {"x1": 4, "y1": 14, "x2": CELL_SIZE - 4, "y2": 14, + "stroke": "white", "stroke-width": 2}), + h("rect", {"x": CELL_SIZE * 0.5 - 4, "y": 10, "width": 8, "height": 8, + "fill": "white", "stroke-width": 0}), + ), + }, { + id: "plant", + name: "Cultivation chamber", + inner_svg: h("path", {"d": "m3.02 17.7a13 13 0 0013 13 13 13 0 00-13-13m13 13a13 13 0 0013-13 13 13 0 00-13 13m8.66-27.4v7.21a8.66 8.66 0 01-8.66 8.66 8.66 8.66 0 01-8.66-8.66v-7.21c1.07 0 2.12.173 3.12.563.793.332 1.5.821 2.09 1.44l3.43-3.43 3.43 3.43c.591-.622 1.3-1.11 2.09-1.44.996-.388 2.05-.563 3.12-.563z", + "fill": "black", "stroke-width": 0}), + }, { + id: "refiner", + name: "Refiner", + inner_svg: + h(preact.Fragment, {}, + h("polyline", { + points: `5, 5 \ +${CELL_SIZE * 0.5}, 5 \ +${CELL_SIZE * 0.5}, ${CELL_SIZE - 5} \ +5, ${CELL_SIZE - 5}`, fill: "transparent", stroke: "black", "stroke-width": 2}), + h("line", {x1: 5, y1: CELL_SIZE * 0.5, x2: CELL_SIZE - 5, + y2: CELL_SIZE * 0.5, stroke: "black", "stroke-width": 2}), + h("circle", {cx: 5, cy: CELL_SIZE * 0.5, r: 3, fill: "black", + "stroke-width": 0}), + h("circle", {cx: CELL_SIZE - 5, cy: CELL_SIZE * 0.5, r: 3, + fill: "black", "stroke-width": 0}), + h("circle", {cx: 5, cy: 5, r: 3, fill: "black", "stroke-width": 0}), + h("circle", {cx: 5, cy: CELL_SIZE - 5, r: 3, fill: "black", + "stroke-width": 0}), + ), + }, { + id: "nutrition", + name: "Nutrition", + inner_svg: + h("path", { + d: "M 11 10 C 11.2727 7.45014 13.3891 5.34782 16 5.34782 C 18.6108 5.30432 20.7272 7.40664 20.7272 10 L 21.9091 10 C 24.5199 10.0435 26.6363 12.1457 26.6363 14.7392 C 26.6363 16.8978 24.5199 19 21.9091 19 L 21 19 L 21 23 L 11 23 L 11 19 L 10.0909 19 C 7.48012 19.4347 5.36364 17.3325 5.36364 14.7392 C 5.36364 12.1457 7.48012 10.0435 10.0909 10.0435 Z M 9.29722 7.73927 C 10.2581 4.98084 12.8962 3 16 3 C 19.1037 3 21.7419 4.98084 22.7028 7.73929 C 26.2454 8.1312 29 11.1157 29 14.7392 C 28.9091 18.229 26.3537 21.1263 23 21.6852 L 23 28.826 L 21.9091 30 L 10.0909 30 L 9 28.826 L 9 21.6852 C 5.55544 21.1263 3 18.229 3 14.7392 C 3 11.1157 5.7546 8.1312 9.29722 7.73927 Z M 11 27.6521 L 11 25 L 21 25 L 21 27.6521 Z", + fill: "black", "stroke-width": 0, "fill-rule": "evenodd"}), + }, { + id: "exocraft", + name: "Exocraft materialiser", + inner_svg: h("path", {d: "m 11.71,23.3 h 9.95 m -9.95,0 c 0,1.37 -1.11,2.49 -2.49,2.49 -1.37,0 -2.49,-1.11 -2.49,-2.49 m 4.98,0 c 0,-1.37 -1.11,-2.49 -2.49,-2.49 -1.37,0 -2.49,1.11 -2.49,2.49 m 14.93,0 c 0,1.37 1.11,2.49 2.49,2.49 1.37,0 2.49,-1.11 2.49,-2.49 m -4.98,0 c 0,-1.37 1.11,-2.49 2.49,-2.49 1.37,0 2.49,1.11 2.49,2.49 M 14.2,8.37 v 7.47 m -7.47,0 0.41,-2.47 C 7.44,11.59 7.59,10.7 8.03,10.04 8.42,9.45 8.97,8.98 9.61,8.7 10.34,8.37 11.24,8.37 13.04,8.37 h 4.19 c 1.17,0 1.75,0 2.28,0.16 0.47,0.14 0.91,0.38 1.29,0.69 0.43,0.35 0.75,0.84 1.4,1.81 l 3.2,4.81 M 6.74,23.3 H 6.24 c -0.7,0 -1.05,0 -1.31,-0.14 C 4.69,23.05 4.5,22.86 4.38,22.62 4.25,22.36 4.25,22.01 4.25,21.31 v -1.49 c 0,-1.39 0,-2.09 0.27,-2.62 0.24,-0.47 0.62,-0.85 1.09,-1.09 0.53,-0.27 1.23,-0.27 2.62,-0.27 h 14.93 c 0.92,0 1.39,0 1.77,0.06 2.13,0.34 3.8,2.01 4.14,4.14 0.06,0.39 0.06,0.85 0.06,1.77 0,0.23 0,0.35 -0.02,0.44 -0.08,0.53 -0.5,0.95 -1.03,1.03 -0.1,0.02 -0.21,0.02 -0.44,0.02 H 26.64", + "stroke": "black", + "fill": "transparent", "stroke-width": 2.48853, + }), + }, +]; + +const TILE_MAP = Object.fromEntries(TILES.slice(1).map(t => [t.id, t])); -- cgit v1.2.3-70-g09d2