From fa06eb8d7a1685f2dd5f6c2089025e7b21a27bac Mon Sep 17 00:00:00 2001 From: "Eevee (Evelyn Woods)" Date: Sat, 13 Mar 2021 18:02:49 -0700 Subject: [PATCH] Allow editing level comments; touch up level props dialog (fixes #47) --- js/format-base.js | 3 +- js/format-c2g.js | 43 +++++++++++----- js/main-editor.js | 124 ++++++++++++++++++++++++++++------------------ style.css | 17 +++++++ 4 files changed, 126 insertions(+), 61 deletions(-) diff --git a/js/format-base.js b/js/format-base.js index 8068c4c..347f9fa 100644 --- a/js/format-base.js +++ b/js/format-base.js @@ -99,7 +99,8 @@ export class StoredLevel extends LevelInterface { this.title = ''; this.author = ''; this.password = null; - this.hint = ''; + this.comment = ''; + this.hint = ''; // XXX does this actually belong here, since hints contain the text? does anything set it? // A number is a specified count; the default of null means that the chips are counted on // level init, as in CC2 this.chips_required = null; diff --git a/js/format-c2g.js b/js/format-c2g.js index 6059b00..1172a23 100644 --- a/js/format-c2g.js +++ b/js/format-c2g.js @@ -1053,7 +1053,7 @@ export function parse_level(buf, number = 1) { type === 'CLUE' || type === 'NOTE') { // These are all singular strings (with a terminating NUL, for some reason) - // XXX character encoding?? + // XXX character encoding?? seems to be latin1, ugh let str = util.string_from_buffer_ascii(bytes, 0, bytes.length - 1).replace(/\r\n/g, "\n"); // TODO store more of this, at least for idempotence, maybe @@ -1079,10 +1079,23 @@ export function parse_level(buf, number = 1) { level.hint = str; } else if (type === 'NOTE') { - // Author's comments... but might also include multiple hints for levels with - // multiple hint tiles, delineated by [CLUE] (anywhere in the line (!)). - // LL treats extra hints as tile properties, so store them for later - [level.comment, ...extra_hints] = str.split(/\n?^.*\[CLUE\].*$\n?/mg); + // Author's comments... but might also include tags delimiting special blocks, most + // notably for storing multiple hints. Note that this parsing might lose data in + // two cases: if other text is in the same line as the tag (which still counts!), + // it's silently ignored; if there are more [CLUE] blocks than the level has hints, + // the extras are silently dropped, because hint text is a tile prop in LL. + let parts = str.split(/\n?^.*\[(CLUE|JETLIFE|COM)\].*$\n?/mg); + level.comment = parts[0]; + extra_hints = []; + for (let i = 1; i < parts.length; i += 2) { + let type = parts[i]; + let text = parts[i + 1]; + if (type === 'CLUE') { + extra_hints.push(text); + } + // TODO do something with COM (c2g commands) and JETLIFE (easter egg, make flame + // jets propagate like game of life) + } } continue; } @@ -1425,6 +1438,7 @@ class C2M { } if (typeof buf === 'string' || buf instanceof String) { + // FIXME encode as latin1, maybe with some kludge for anything that doesn't fit let str = buf; // C2M also includes the trailing NUL buf = new ArrayBuffer(str.length + 1); @@ -1640,13 +1654,18 @@ export function synthesize_level(stored_level) { } map_bytes = map_bytes.subarray(0, p); - // Collect hints first so we can put them in the comment field - // FIXME this does not respect global hint, but then, neither does the editor. - hints = hints.map(hint => hint ?? ''); - hints.push(''); - hints.unshift(''); - // Must use Windows linebreaks here 🙄 - c2m.add_section('NOTE', hints.join('\r\n[CLUE]\r\n')); + let comment = stored_level.comment; + if (hints.length) { + // Collect hints first so we can put them in the comment field + // FIXME this does not respect global hint, but then, neither does the editor. + hints = hints.map(hint => hint ?? ''); + hints.push(''); + hints.unshift(''); + // Must use Windows linebreaks here 🙄 + comment += hints.join('\r\n[CLUE]\r\n'); + } + // TODO support COM and JETLIFE + c2m.add_section('NOTE', comment); let compressed_map = compress(map_bytes); if (compressed_map) { diff --git a/js/main-editor.js b/js/main-editor.js index a74d16c..48aa342 100644 --- a/js/main-editor.js +++ b/js/main-editor.js @@ -47,10 +47,15 @@ class EditorLevelMetaOverlay extends DialogOverlay { let dl = mk('dl.formgrid'); this.main.append(dl); - let time_limit_input = mk('input', {name: 'time_limit', type: 'number', min: 0, max: 999, value: stored_level.time_limit}); + let time_limit_input = mk('input', {name: 'time_limit', type: 'number', min: 0, max: 65535, value: stored_level.time_limit}); let time_limit_output = mk('output'); let update_time_limit = () => { let time_limit = parseInt(time_limit_input.value, 10); + // FIXME need a change event for this tbh? + // FIXME handle NaN; maybe block keydown of not-numbers + time_limit = Math.max(0, Math.min(65535, time_limit)); + time_limit_input.value = time_limit; + let text; if (time_limit === 0) { text = "No time limit"; @@ -63,6 +68,28 @@ class EditorLevelMetaOverlay extends DialogOverlay { update_time_limit(); time_limit_input.addEventListener('input', update_time_limit); + let make_size_input = (name) => { + let input = mk('input', {name: name, type: 'number', min: 10, max: 100, value: stored_level[name]}); + // TODO maybe block keydown of non-numbers too? + // Note that this is a change event, not an input event, so we don't prevent them from + // erasing the whole value to type a new one + input.addEventListener('change', ev => { + let value = parseInt(ev.target.value, 10); + if (isNaN(value)) { + ev.target.value = stored_level[name]; + } + else if (value < 1) { + // Smaller than 10×10 isn't supported by CC2, but LL doesn't mind, so let it + // through if they try it manually + ev.target.value = 1; + } + else if (value > 100) { + ev.target.value = 100; + } + }); + return input; + }; + let make_radio_set = (name, options) => { let elements = []; for (let [label, value] of options) { @@ -72,52 +99,48 @@ class EditorLevelMetaOverlay extends DialogOverlay { dl.append( mk('dt', "Title"), - mk('dd', mk('input', {name: 'title', type: 'text', value: stored_level.title})), + mk('dd.-one-field', mk('input', {name: 'title', type: 'text', value: stored_level.title})), mk('dt', "Author"), - mk('dd', mk('input', {name: 'author', type: 'text', value: stored_level.author})), + mk('dd.-one-field', mk('input', {name: 'author', type: 'text', value: stored_level.author})), + mk('dt', "Comment"), + mk('dd.-textarea', mk('textarea', {name: 'comment', rows: 4, cols: 20}, stored_level.comment)), mk('dt', "Time limit"), - mk('dd', - time_limit_input, - " ", - time_limit_output, - mk('br'), - mk_button("None", ev => { - this.root.elements['time_limit'].value = 0; - update_time_limit(); - }), - mk_button("−30s", ev => { - this.root.elements['time_limit'].value = Math.max(0, - parseInt(this.root.elements['time_limit'].value, 10) - 30); - update_time_limit(); - }), - mk_button("+30s", ev => { - this.root.elements['time_limit'].value = Math.min(999, - parseInt(this.root.elements['time_limit'].value, 10) + 30); - update_time_limit(); - }), - mk_button("Max", ev => { - this.root.elements['time_limit'].value = 999; - update_time_limit(); - }), + mk('dd.-with-buttons', + mk('div.-left', + time_limit_input, + " ", + time_limit_output, + ), + mk('div.-right', + mk_button("None", ev => { + this.root.elements['time_limit'].value = 0; + update_time_limit(); + }), + mk_button("−30s", ev => { + this.root.elements['time_limit'].value = Math.max(0, + parseInt(this.root.elements['time_limit'].value, 10) - 30); + update_time_limit(); + }), + mk_button("+30s", ev => { + this.root.elements['time_limit'].value = Math.min(999, + parseInt(this.root.elements['time_limit'].value, 10) + 30); + update_time_limit(); + }), + mk_button("Max", ev => { + this.root.elements['time_limit'].value = 999; + update_time_limit(); + }), + ), ), mk('dt', "Size"), - mk('dd', - mk('input', {name: 'size_x', type: 'number', min: 10, max: 100, value: stored_level.size_x}), - " × ", - mk('input', {name: 'size_y', type: 'number', min: 10, max: 100, value: stored_level.size_y}), - mk('br'), - mk_button("10×10", ev => { - this.root.elements['size_x'].value = 10; - this.root.elements['size_y'].value = 10; - }), - mk_button("32×32", ev => { - this.root.elements['size_x'].value = 32; - this.root.elements['size_y'].value = 32; - }), - mk_button("100×100", ev => { - this.root.elements['size_x'].value = 100; - this.root.elements['size_y'].value = 100; - }), + mk('dd.-with-buttons', + mk('div.-left', make_size_input('size_x'), " × ", make_size_input('size_y')), + mk('div.-right', ...[10, 32, 50, 100].map(size => + mk_button(`${size}²`, ev => { + this.root.elements['size_x'].value = size; + this.root.elements['size_y'].value = size; + }), + )), ), mk('dt', "Viewport"), mk('dd', @@ -181,10 +204,11 @@ class EditorLevelMetaOverlay extends DialogOverlay { stored_level.author = author; } - stored_level.time_limit = parseInt(els.time_limit.value, 10); + // FIXME gotta deal with NaNs here too, sigh, might just need a teeny tiny form library + stored_level.time_limit = Math.max(0, Math.min(65535, parseInt(els.time_limit.value, 10))); - let size_x = parseInt(els.size_x.value, 10); - let size_y = parseInt(els.size_y.value, 10); + let size_x = Math.max(1, Math.min(100, parseInt(els.size_x.value, 10))); + let size_y = Math.max(1, Math.min(100, parseInt(els.size_y.value, 10))); if (size_x !== stored_level.size_x || size_y !== stored_level.size_y) { this.conductor.editor.resize_level(size_x, size_y); } @@ -192,7 +216,11 @@ class EditorLevelMetaOverlay extends DialogOverlay { stored_level.blob_behavior = parseInt(els.blob_behavior.value, 10); stored_level.hide_logic = els.hide_logic.checked; stored_level.use_cc1_boots = els.use_cc1_boots.checked; - stored_level.viewport_size = parseInt(els.viewport.value, 10); + let viewport_size = parseInt(els.viewport.value, 10); + if (viewport_size !== 9 && viewport_size !== 10) { + viewport_size = 10; + } + stored_level.viewport_size = viewport_size; this.conductor.player.update_viewport_size(); this.close(); @@ -1749,7 +1777,7 @@ const EDITOR_PALETTE = [{ 'water', 'turtle', 'fire', 'ice', 'ice_nw', - 'force_floor_n', 'force_floor_all', + 'force_floor_s', 'force_floor_all', 'canopy', ], }, { diff --git a/style.css b/style.css index 3c3fb28..196f174 100644 --- a/style.css +++ b/style.css @@ -301,6 +301,8 @@ dl.formgrid { } dl.formgrid > dt { grid-column: 1; + text-align: right; + color: hsl(225, 50%, 25%); } dl.formgrid > dd { grid-column: 2; @@ -312,6 +314,21 @@ dl.formgrid > dd button { dl.formgrid > dd button + button { margin-left: 0.25em; } +dl.formgrid > dd.-one-field { + display: flex; +} +dl.formgrid > dd.-textarea { + display: flex; + align-self: end; /* make the
align to the top */ +} +dl.formgrid > dd.-one-field > *, +dl.formgrid > dd.-textarea > * { + flex: auto; +} +dl.formgrid > dd.-with-buttons { + display: flex; + justify-content: space-between; +} /* Individual overlays */ table.level-browser {