diff options
author | MetroWind <chris.corsair@gmail.com> | 2025-08-31 09:10:28 -0700 |
---|---|---|
committer | MetroWind <chris.corsair@gmail.com> | 2025-08-31 09:10:28 -0700 |
commit | 3204043e5cd855855b045d8f410b320dcb772d25 (patch) | |
tree | 5588ae66f826ecd1cd431d14b71bb7aae49cf33b | |
parent | 6a6cc2f22e806be895f9fddc9ed6cabab395b6cb (diff) |
Support multiple floors. Use 98 style.
-rw-r--r-- | index.html | 5 | ||||
-rw-r--r-- | scripts/main.js | 56 | ||||
-rw-r--r-- | styles.css | 309 |
3 files changed, 357 insertions, 13 deletions
@@ -16,7 +16,10 @@ | |||
16 | <title>My test page</title> | 16 | <title>My test page</title> |
17 | </head> | 17 | </head> |
18 | <body> | 18 | <body> |
19 | <div id="Plan"> | 19 | <div class="Window Dialog"> |
20 | <div class="Titlebar">NMS Freighter Planner</div> | ||
21 | <div id="Plan"> | ||
22 | </div> | ||
20 | </div> | 23 | </div> |
21 | </body> | 24 | </body> |
22 | </html> | 25 | </html> |
diff --git a/scripts/main.js b/scripts/main.js index 7347f37..b8de9e4 100644 --- a/scripts/main.js +++ b/scripts/main.js | |||
@@ -1,6 +1,7 @@ | |||
1 | const DEFAULT_APP_STATE = { | 1 | const DEFAULT_APP_STATE = { |
2 | floor: 1, | 2 | floor: 0, // This is 0-based. |
3 | plan: new Array(GRID_SIZE_X * GRID_SIZE_Y).fill(0), | 3 | plan: new Array(FLOOR_COUNT).fill( |
4 | new Array(GRID_SIZE_X * GRID_SIZE_Y).fill(0)), | ||
4 | // mouse_state can be “normal”, or “dnd”. | 5 | // mouse_state can be “normal”, or “dnd”. |
5 | mouse_state: "normal", | 6 | mouse_state: "normal", |
6 | plan_code: "", | 7 | plan_code: "", |
@@ -9,7 +10,9 @@ const DEFAULT_APP_STATE = { | |||
9 | async function serializePlan(app_state) | 10 | async function serializePlan(app_state) |
10 | { | 11 | { |
11 | let rest = new Array(GRID_SIZE_X * GRID_SIZE_Y * (FLOOR_COUNT - 1)).fill(0); | 12 | let rest = new Array(GRID_SIZE_X * GRID_SIZE_Y * (FLOOR_COUNT - 1)).fill(0); |
12 | return compressBytes(Uint8Array.from(app_state.plan.concat(rest))); | 13 | let concated_plans = |
14 | app_state.plan.reduce((big, floor_plan) => big.concat(floor_plan), []); | ||
15 | return compressBytes(Uint8Array.from(concated_plans)); | ||
13 | } | 16 | } |
14 | 17 | ||
15 | function genGrid(x_count, y_count, cell_size) | 18 | function genGrid(x_count, y_count, cell_size) |
@@ -105,11 +108,21 @@ ${cell_y * (CELL_SIZE + 1) + 1})`}, ASSET_WHITE_BG, TILES[idx].inner_svg); | |||
105 | 108 | ||
106 | function PlanView({plan, mouse_state}) | 109 | function PlanView({plan, mouse_state}) |
107 | { | 110 | { |
111 | console.debug("Looking at plan ", plan); | ||
108 | return h("div", {}, | 112 | return h("div", {}, |
109 | h("p", {}, "The plan:"), | ||
110 | h(PlanGridView, {content: plan, mouse_state: mouse_state})); | 113 | h(PlanGridView, {content: plan, mouse_state: mouse_state})); |
111 | } | 114 | } |
112 | 115 | ||
116 | // on_floor_change takes one argument, which is the 0-based floor number. | ||
117 | function FloorSelector({floor, on_floor_change}) | ||
118 | { | ||
119 | return h("div", {"id": "FloorSelectorWrapper", "class": "ButtonRow"}, | ||
120 | h("label", {}, "Floor"), | ||
121 | h("input", {"id": "InputFloor", "type": "number", "step": 1, "min": 1, | ||
122 | "max": 15, "value": floor, | ||
123 | onchange: e => on_floor_change(e.target.value - 1),},)); | ||
124 | } | ||
125 | |||
113 | function App({initial_state}) | 126 | function App({initial_state}) |
114 | { | 127 | { |
115 | const [state, setState] = preactHooks.useState(initial_state); | 128 | const [state, setState] = preactHooks.useState(initial_state); |
@@ -134,7 +147,7 @@ function App({initial_state}) | |||
134 | let asset_index = parseInt(e.target.getAttribute("data-asset-index")); | 147 | let asset_index = parseInt(e.target.getAttribute("data-asset-index")); |
135 | 148 | ||
136 | let new_state = structuredClone(state); | 149 | let new_state = structuredClone(state); |
137 | new_state.plan[cell_index] = asset_index; | 150 | new_state.plan[new_state.floor][cell_index] = asset_index; |
138 | new_state.mouse_state = "normal"; | 151 | new_state.mouse_state = "normal"; |
139 | serializePlan(new_state).then((s) => { | 152 | serializePlan(new_state).then((s) => { |
140 | new_state.plan_code = s; | 153 | new_state.plan_code = s; |
@@ -168,16 +181,27 @@ function App({initial_state}) | |||
168 | encodeURIComponent(svg_str); | 181 | encodeURIComponent(svg_str); |
169 | } | 182 | } |
170 | 183 | ||
184 | // new_floor is 0-based. | ||
185 | function onFloorChange(new_floor) | ||
186 | { | ||
187 | let new_state = structuredClone(state); | ||
188 | new_state.floor = new_floor; | ||
189 | setState(new_state); | ||
190 | } | ||
191 | |||
171 | return h("div", {}, | 192 | return h("div", {}, |
172 | h("h2", {}, `Floor ${state.floor}`), | 193 | h(FloorSelector, {floor: state.floor + 1, |
194 | on_floor_change: onFloorChange}), | ||
173 | h(AssetsView, {on_drag_begin: onDragAssetBegin, | 195 | h(AssetsView, {on_drag_begin: onDragAssetBegin, |
174 | on_drag_end: onDragAssetEnd}), | 196 | on_drag_end: onDragAssetEnd}), |
175 | h(PlanView, {plan: state.plan, mouse_state: state.mouse_state}), | 197 | h("hr", {}), |
176 | h("div", {}, | 198 | h(PlanView, {plan: state.plan[state.floor], |
177 | h("a", {"href": "javascript:void(0);", onclick: onClickSaveImg}, | 199 | mouse_state: state.mouse_state}), |
178 | "Open Image!")), | ||
179 | h("div", {}, | 200 | h("div", {}, |
180 | h("textarea", {readonly: true}, state.plan_code)), | 201 | h("textarea", {readonly: true}, state.plan_code)), |
202 | h("div", {"class": "ButtonRow"}, | ||
203 | h("button", {onclick: onClickSaveImg, type: "button"}, | ||
204 | "Open as Image!")), | ||
181 | ); | 205 | ); |
182 | } | 206 | } |
183 | 207 | ||
@@ -198,7 +222,17 @@ else | |||
198 | { | 222 | { |
199 | decompressBytes(encoded_plan).then(a => { | 223 | decompressBytes(encoded_plan).then(a => { |
200 | let state = structuredClone(DEFAULT_APP_STATE); | 224 | let state = structuredClone(DEFAULT_APP_STATE); |
201 | state.plan = Array.from(a.subarray(0, GRID_SIZE_X * GRID_SIZE_Y)); | 225 | let concated_plan = Array.from(a); |
226 | console.debug(concated_plan); | ||
227 | state.plan = []; | ||
228 | for(let i = 0; i < GRID_SIZE_X * GRID_SIZE_Y * FLOOR_COUNT; | ||
229 | i += GRID_SIZE_X * GRID_SIZE_Y) | ||
230 | { | ||
231 | let floor_slice = | ||
232 | concated_plan.slice(i, i + GRID_SIZE_X * GRID_SIZE_Y); | ||
233 | console.debug(floor_slice); | ||
234 | state.plan.push(floor_slice); | ||
235 | } | ||
202 | state.plan_code = encoded_plan; | 236 | state.plan_code = encoded_plan; |
203 | render(state); | 237 | render(state); |
204 | }); | 238 | }); |
@@ -1,5 +1,64 @@ | |||
1 | body | 1 | /* http://meyerweb.com/eric/tools/css/reset/ |
2 | v2.0 | 20110126 | ||
3 | License: none (public domain) | ||
4 | */ | ||
5 | |||
6 | html, body, div, span, applet, object, iframe, | ||
7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, | ||
8 | a, abbr, acronym, address, big, cite, code, | ||
9 | del, dfn, em, img, ins, kbd, q, s, samp, | ||
10 | small, strike, strong, sub, sup, tt, var, | ||
11 | b, u, i, center, | ||
12 | dl, dt, dd, ol, ul, li, | ||
13 | fieldset, form, label, legend, | ||
14 | table, caption, tbody, tfoot, thead, tr, th, td, | ||
15 | article, aside, canvas, details, embed, | ||
16 | figure, figcaption, footer, header, hgroup, | ||
17 | menu, nav, output, ruby, section, summary, | ||
18 | time, mark, audio, video { | ||
19 | margin: 0; | ||
20 | padding: 0; | ||
21 | border: 0; | ||
22 | font-size: unset; | ||
23 | font: inherit; | ||
24 | vertical-align: baseline; | ||
25 | } | ||
26 | /* HTML5 display-role reset for older browsers */ | ||
27 | article, aside, details, figcaption, figure, | ||
28 | footer, header, hgroup, menu, nav, section { | ||
29 | display: block; | ||
30 | } | ||
31 | body { | ||
32 | line-height: 1; | ||
33 | } | ||
34 | ol, ul { | ||
35 | list-style: none; | ||
36 | } | ||
37 | blockquote, q { | ||
38 | quotes: none; | ||
39 | } | ||
40 | blockquote:before, blockquote:after, | ||
41 | q:before, q:after { | ||
42 | content: ''; | ||
43 | content: none; | ||
44 | } | ||
45 | table { | ||
46 | border-collapse: collapse; | ||
47 | border-spacing: 0; | ||
48 | } | ||
49 | |||
50 | /* ========== Actual styles ======================================> */ | ||
51 | |||
52 | :root | ||
2 | { | 53 | { |
54 | --color-bg: #c0c0c0; | ||
55 | --color-fg: black; | ||
56 | --color-border-dark-1: #808080; | ||
57 | --color-border-dark-2: black; | ||
58 | --color-border-light-1: #dfdfdf; | ||
59 | --color-border-light-2: white; | ||
60 | --color-titlebar-1: navy; | ||
61 | --color-titlebar-2: #1084d0; | ||
3 | } | 62 | } |
4 | 63 | ||
5 | * | 64 | * |
@@ -7,6 +66,188 @@ body | |||
7 | box-sizing: border-box; | 66 | box-sizing: border-box; |
8 | } | 67 | } |
9 | 68 | ||
69 | html | ||
70 | { | ||
71 | background: #008080; | ||
72 | color: var(--color-fg); | ||
73 | font-family: sans-serif; | ||
74 | font-size: 12px; | ||
75 | padding: 12px; | ||
76 | } | ||
77 | |||
78 | .Window | ||
79 | { | ||
80 | box-shadow: inset -1px -1px var(--color-border-dark-2), | ||
81 | inset 1px 1px var(--color-border-light-1), | ||
82 | inset -2px -2px var(--color-border-dark-1), | ||
83 | inset 2px 2px var(--color-border-light-2); | ||
84 | background: var(--color-bg); | ||
85 | padding: 10px; | ||
86 | |||
87 | .Titlebar | ||
88 | { | ||
89 | background: linear-gradient(90deg, var(--color-titlebar-1), var(--color-titlebar-2)); | ||
90 | margin: -6px -6px 10px -6px; | ||
91 | color: white; | ||
92 | padding: 3px; | ||
93 | } | ||
94 | } | ||
95 | |||
96 | .Dialog | ||
97 | { | ||
98 | max-width: 800px; | ||
99 | margin: 0px auto; | ||
100 | } | ||
101 | |||
102 | input[type="number"] | ||
103 | { | ||
104 | appearance: none; | ||
105 | border: none; | ||
106 | outline: none; | ||
107 | box-shadow: inset -1px -1px var(--color-border-light-2), | ||
108 | inset 1px 1px var(--color-border-dark-1), | ||
109 | inset -2px -2px var(--color-border-light-1), | ||
110 | inset 2px 2px var(--color-border-dark-2); | ||
111 | padding: 4px 0px 4px 6px; | ||
112 | font-family: monospace; | ||
113 | font-size: 0.9rem; | ||
114 | } | ||
115 | |||
116 | input[type="text"], input[type="url"], input[type="email"], textarea | ||
117 | { | ||
118 | appearance: none; | ||
119 | border: none; | ||
120 | outline: none; | ||
121 | box-shadow: inset -1px -1px var(--color-border-light-2), | ||
122 | inset 1px 1px var(--color-border-dark-1), | ||
123 | inset -2px -2px var(--color-border-light-1), | ||
124 | inset 2px 2px var(--color-border-dark-2); | ||
125 | width: 100%; | ||
126 | padding: 4px 6px; | ||
127 | font-family: monospace; | ||
128 | font-size: 0.9rem; | ||
129 | } | ||
130 | |||
131 | a.Button, a.FloatButton | ||
132 | { | ||
133 | color: black; | ||
134 | text-decoration: none; | ||
135 | } | ||
136 | |||
137 | button, input[type="submit"] | ||
138 | { | ||
139 | appearance: none; | ||
140 | border: none; | ||
141 | outline: none; | ||
142 | box-shadow: inset -1px -1px var(--color-border-dark-2), | ||
143 | inset 1px 1px var(--color-border-light-1), | ||
144 | inset -2px -2px var(--color-border-dark-1), | ||
145 | inset 2px 2px var(--color-border-light-2); | ||
146 | background: var(--color-bg); | ||
147 | min-height: 23px; | ||
148 | min-width: 75px; | ||
149 | padding: 0 12px; | ||
150 | text-align: center; | ||
151 | } | ||
152 | |||
153 | .IconButton | ||
154 | { | ||
155 | padding: 4px; | ||
156 | min-width: unset; | ||
157 | min-height: unset; | ||
158 | } | ||
159 | |||
160 | .FloatButton | ||
161 | { | ||
162 | padding: 4px; | ||
163 | min-width: unset; | ||
164 | min-height: unset; | ||
165 | box-shadow: none; | ||
166 | } | ||
167 | |||
168 | .FloatButton:hover | ||
169 | { | ||
170 | padding: 4px; | ||
171 | min-width: unset; | ||
172 | min-height: unset; | ||
173 | box-shadow: inset 1px 1px var(--color-border-light-1), | ||
174 | inset -1px -1px var(--color-border-dark-1); | ||
175 | } | ||
176 | |||
177 | button:active, .FloatButton:active | ||
178 | { | ||
179 | box-shadow: inset -1px -1px var(--color-border-light-2), | ||
180 | inset 1px 1px var(--color-border-dark-1), | ||
181 | inset -2px -2px var(--color-border-light-1), | ||
182 | inset 2px 2px var(--color-border-dark-2); | ||
183 | } | ||
184 | |||
185 | hr | ||
186 | { | ||
187 | border: none; | ||
188 | border-top: solid var(--color-border-dark-1) 1px; | ||
189 | border-bottom: solid var(--color-border-light-1) 1px; | ||
190 | } | ||
191 | |||
192 | .ButtonRow | ||
193 | { | ||
194 | display: flex; | ||
195 | justify-content: right; | ||
196 | gap: 10px; | ||
197 | margin: 10px 0; | ||
198 | align-items: baseline; | ||
199 | } | ||
200 | |||
201 | .ButtonRowLeft | ||
202 | { | ||
203 | text-align: Left; | ||
204 | margin-top: 10px; | ||
205 | } | ||
206 | |||
207 | .Toolbar | ||
208 | { | ||
209 | display: flex; | ||
210 | border-bottom: solid var(--color-border-dark-1) 1px; | ||
211 | box-shadow: 0px 1px var(--color-border-light-1); | ||
212 | position: relative; | ||
213 | left: -10px; | ||
214 | top: -10px; | ||
215 | width: calc(100% + 2ex); | ||
216 | padding: 2px 8px; | ||
217 | } | ||
218 | |||
219 | table.InputFields | ||
220 | { | ||
221 | border-collapse: collapse; | ||
222 | width: 100%; | ||
223 | table-layout: auto; | ||
224 | |||
225 | td | ||
226 | { | ||
227 | padding: 2px 2px; | ||
228 | } | ||
229 | |||
230 | td:nth-child(2) | ||
231 | { | ||
232 | width: 100%; | ||
233 | } | ||
234 | |||
235 | } | ||
236 | |||
237 | .StatusBar | ||
238 | { | ||
239 | margin: 10px -6px -7px -6px; | ||
240 | display: flex; | ||
241 | } | ||
242 | |||
243 | .StatusCell | ||
244 | { | ||
245 | box-shadow: inset -2px -2px var(--color-border-light-2), | ||
246 | inset 2px 2px var(--color-border-dark-1); | ||
247 | padding: 4px; | ||
248 | flex-grow: 1; | ||
249 | } | ||
250 | |||
10 | .Asset | 251 | .Asset |
11 | { | 252 | { |
12 | display: inline-block; | 253 | display: inline-block; |
@@ -15,8 +256,74 @@ body | |||
15 | height: 34px; | 256 | height: 34px; |
16 | } | 257 | } |
17 | 258 | ||
259 | |||
260 | |||
18 | /* Prevent SVG elements from interferring with drag n drop. */ | 261 | /* Prevent SVG elements from interferring with drag n drop. */ |
19 | #Grid * | 262 | #Grid * |
20 | { | 263 | { |
21 | pointer-events: none; | 264 | pointer-events: none; |
22 | } | 265 | } |
266 | |||
267 | nav | ||
268 | { | ||
269 | display: flex; | ||
270 | justify-content: space-between; | ||
271 | } | ||
272 | |||
273 | #Links | ||
274 | { | ||
275 | box-shadow: inset -1px -1px var(--color-border-light-2), | ||
276 | inset 1px 1px var(--color-border-dark-1), | ||
277 | inset -2px -2px var(--color-border-light-1), | ||
278 | inset 2px 2px var(--color-border-dark-2); | ||
279 | padding: 2px; | ||
280 | } | ||
281 | |||
282 | table.TableView | ||
283 | { | ||
284 | background-color: white; | ||
285 | border-collapse: collapse; | ||
286 | width: 100%; | ||
287 | |||
288 | thead th | ||
289 | { | ||
290 | box-shadow: inset -1px -1px var(--color-border-dark-2), | ||
291 | inset 1px 1px var(--color-border-light-1), | ||
292 | inset -2px -2px var(--color-border-dark-1), | ||
293 | inset 2px 2px var(--color-border-light-2); | ||
294 | background-color: var(--color-bg); | ||
295 | padding: 2px 4px 4px 4px; | ||
296 | text-align: left; | ||
297 | } | ||
298 | |||
299 | td | ||
300 | { | ||
301 | padding: 2px; | ||
302 | } | ||
303 | } | ||
304 | |||
305 | #Links table | ||
306 | { | ||
307 | table-layout: fixed; | ||
308 | |||
309 | th:nth-child(1) | ||
310 | { | ||
311 | width: 10em; | ||
312 | } | ||
313 | |||
314 | td:nth-child(2) | ||
315 | { | ||
316 | overflow: hidden; | ||
317 | white-space: nowrap; | ||
318 | } | ||
319 | |||
320 | th:nth-child(3), th:nth-child(4) | ||
321 | { | ||
322 | width: 55px; | ||
323 | } | ||
324 | |||
325 | td:nth-child(3), td:nth-child(4) | ||
326 | { | ||
327 | text-align: center; | ||
328 | } | ||
329 | } | ||