Allow editing level comments; touch up level props dialog (fixes #47)

This commit is contained in:
Eevee (Evelyn Woods) 2021-03-13 18:02:49 -07:00
parent 3b257df8d3
commit fa06eb8d7a
4 changed files with 126 additions and 61 deletions

View File

@ -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;

View File

@ -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) {

View File

@ -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',
],
}, {

View File

@ -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 <dt> 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 {