Allow editing level comments; touch up level props dialog (fixes #47)
This commit is contained in:
parent
3b257df8d3
commit
fa06eb8d7a
@ -99,7 +99,8 @@ export class StoredLevel extends LevelInterface {
|
|||||||
this.title = '';
|
this.title = '';
|
||||||
this.author = '';
|
this.author = '';
|
||||||
this.password = null;
|
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
|
// A number is a specified count; the default of null means that the chips are counted on
|
||||||
// level init, as in CC2
|
// level init, as in CC2
|
||||||
this.chips_required = null;
|
this.chips_required = null;
|
||||||
|
|||||||
@ -1053,7 +1053,7 @@ export function parse_level(buf, number = 1) {
|
|||||||
type === 'CLUE' || type === 'NOTE')
|
type === 'CLUE' || type === 'NOTE')
|
||||||
{
|
{
|
||||||
// These are all singular strings (with a terminating NUL, for some reason)
|
// 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");
|
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
|
// TODO store more of this, at least for idempotence, maybe
|
||||||
@ -1079,10 +1079,23 @@ export function parse_level(buf, number = 1) {
|
|||||||
level.hint = str;
|
level.hint = str;
|
||||||
}
|
}
|
||||||
else if (type === 'NOTE') {
|
else if (type === 'NOTE') {
|
||||||
// Author's comments... but might also include multiple hints for levels with
|
// Author's comments... but might also include tags delimiting special blocks, most
|
||||||
// multiple hint tiles, delineated by [CLUE] (anywhere in the line (!)).
|
// notably for storing multiple hints. Note that this parsing might lose data in
|
||||||
// LL treats extra hints as tile properties, so store them for later
|
// two cases: if other text is in the same line as the tag (which still counts!),
|
||||||
[level.comment, ...extra_hints] = str.split(/\n?^.*\[CLUE\].*$\n?/mg);
|
// 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;
|
continue;
|
||||||
}
|
}
|
||||||
@ -1425,6 +1438,7 @@ class C2M {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (typeof buf === 'string' || buf instanceof String) {
|
if (typeof buf === 'string' || buf instanceof String) {
|
||||||
|
// FIXME encode as latin1, maybe with some kludge for anything that doesn't fit
|
||||||
let str = buf;
|
let str = buf;
|
||||||
// C2M also includes the trailing NUL
|
// C2M also includes the trailing NUL
|
||||||
buf = new ArrayBuffer(str.length + 1);
|
buf = new ArrayBuffer(str.length + 1);
|
||||||
@ -1640,13 +1654,18 @@ export function synthesize_level(stored_level) {
|
|||||||
}
|
}
|
||||||
map_bytes = map_bytes.subarray(0, p);
|
map_bytes = map_bytes.subarray(0, p);
|
||||||
|
|
||||||
// Collect hints first so we can put them in the comment field
|
let comment = stored_level.comment;
|
||||||
// FIXME this does not respect global hint, but then, neither does the editor.
|
if (hints.length) {
|
||||||
hints = hints.map(hint => hint ?? '');
|
// Collect hints first so we can put them in the comment field
|
||||||
hints.push('');
|
// FIXME this does not respect global hint, but then, neither does the editor.
|
||||||
hints.unshift('');
|
hints = hints.map(hint => hint ?? '');
|
||||||
// Must use Windows linebreaks here 🙄
|
hints.push('');
|
||||||
c2m.add_section('NOTE', hints.join('\r\n[CLUE]\r\n'));
|
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);
|
let compressed_map = compress(map_bytes);
|
||||||
if (compressed_map) {
|
if (compressed_map) {
|
||||||
|
|||||||
@ -47,10 +47,15 @@ class EditorLevelMetaOverlay extends DialogOverlay {
|
|||||||
let dl = mk('dl.formgrid');
|
let dl = mk('dl.formgrid');
|
||||||
this.main.append(dl);
|
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 time_limit_output = mk('output');
|
||||||
let update_time_limit = () => {
|
let update_time_limit = () => {
|
||||||
let time_limit = parseInt(time_limit_input.value, 10);
|
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;
|
let text;
|
||||||
if (time_limit === 0) {
|
if (time_limit === 0) {
|
||||||
text = "No time limit";
|
text = "No time limit";
|
||||||
@ -63,6 +68,28 @@ class EditorLevelMetaOverlay extends DialogOverlay {
|
|||||||
update_time_limit();
|
update_time_limit();
|
||||||
time_limit_input.addEventListener('input', 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 make_radio_set = (name, options) => {
|
||||||
let elements = [];
|
let elements = [];
|
||||||
for (let [label, value] of options) {
|
for (let [label, value] of options) {
|
||||||
@ -72,52 +99,48 @@ class EditorLevelMetaOverlay extends DialogOverlay {
|
|||||||
|
|
||||||
dl.append(
|
dl.append(
|
||||||
mk('dt', "Title"),
|
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('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('dt', "Time limit"),
|
||||||
mk('dd',
|
mk('dd.-with-buttons',
|
||||||
time_limit_input,
|
mk('div.-left',
|
||||||
" ",
|
time_limit_input,
|
||||||
time_limit_output,
|
" ",
|
||||||
mk('br'),
|
time_limit_output,
|
||||||
mk_button("None", ev => {
|
),
|
||||||
this.root.elements['time_limit'].value = 0;
|
mk('div.-right',
|
||||||
update_time_limit();
|
mk_button("None", ev => {
|
||||||
}),
|
this.root.elements['time_limit'].value = 0;
|
||||||
mk_button("−30s", ev => {
|
update_time_limit();
|
||||||
this.root.elements['time_limit'].value = Math.max(0,
|
}),
|
||||||
parseInt(this.root.elements['time_limit'].value, 10) - 30);
|
mk_button("−30s", ev => {
|
||||||
update_time_limit();
|
this.root.elements['time_limit'].value = Math.max(0,
|
||||||
}),
|
parseInt(this.root.elements['time_limit'].value, 10) - 30);
|
||||||
mk_button("+30s", ev => {
|
update_time_limit();
|
||||||
this.root.elements['time_limit'].value = Math.min(999,
|
}),
|
||||||
parseInt(this.root.elements['time_limit'].value, 10) + 30);
|
mk_button("+30s", ev => {
|
||||||
update_time_limit();
|
this.root.elements['time_limit'].value = Math.min(999,
|
||||||
}),
|
parseInt(this.root.elements['time_limit'].value, 10) + 30);
|
||||||
mk_button("Max", ev => {
|
update_time_limit();
|
||||||
this.root.elements['time_limit'].value = 999;
|
}),
|
||||||
update_time_limit();
|
mk_button("Max", ev => {
|
||||||
}),
|
this.root.elements['time_limit'].value = 999;
|
||||||
|
update_time_limit();
|
||||||
|
}),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
mk('dt', "Size"),
|
mk('dt', "Size"),
|
||||||
mk('dd',
|
mk('dd.-with-buttons',
|
||||||
mk('input', {name: 'size_x', type: 'number', min: 10, max: 100, value: stored_level.size_x}),
|
mk('div.-left', make_size_input('size_x'), " × ", make_size_input('size_y')),
|
||||||
" × ",
|
mk('div.-right', ...[10, 32, 50, 100].map(size =>
|
||||||
mk('input', {name: 'size_y', type: 'number', min: 10, max: 100, value: stored_level.size_y}),
|
mk_button(`${size}²`, ev => {
|
||||||
mk('br'),
|
this.root.elements['size_x'].value = size;
|
||||||
mk_button("10×10", ev => {
|
this.root.elements['size_y'].value = size;
|
||||||
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('dt', "Viewport"),
|
mk('dt', "Viewport"),
|
||||||
mk('dd',
|
mk('dd',
|
||||||
@ -181,10 +204,11 @@ class EditorLevelMetaOverlay extends DialogOverlay {
|
|||||||
stored_level.author = author;
|
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_x = Math.max(1, Math.min(100, parseInt(els.size_x.value, 10)));
|
||||||
let size_y = parseInt(els.size_y.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) {
|
if (size_x !== stored_level.size_x || size_y !== stored_level.size_y) {
|
||||||
this.conductor.editor.resize_level(size_x, 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.blob_behavior = parseInt(els.blob_behavior.value, 10);
|
||||||
stored_level.hide_logic = els.hide_logic.checked;
|
stored_level.hide_logic = els.hide_logic.checked;
|
||||||
stored_level.use_cc1_boots = els.use_cc1_boots.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.conductor.player.update_viewport_size();
|
||||||
|
|
||||||
this.close();
|
this.close();
|
||||||
@ -1749,7 +1777,7 @@ const EDITOR_PALETTE = [{
|
|||||||
|
|
||||||
'water', 'turtle', 'fire',
|
'water', 'turtle', 'fire',
|
||||||
'ice', 'ice_nw',
|
'ice', 'ice_nw',
|
||||||
'force_floor_n', 'force_floor_all',
|
'force_floor_s', 'force_floor_all',
|
||||||
'canopy',
|
'canopy',
|
||||||
],
|
],
|
||||||
}, {
|
}, {
|
||||||
|
|||||||
17
style.css
17
style.css
@ -301,6 +301,8 @@ dl.formgrid {
|
|||||||
}
|
}
|
||||||
dl.formgrid > dt {
|
dl.formgrid > dt {
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
|
text-align: right;
|
||||||
|
color: hsl(225, 50%, 25%);
|
||||||
}
|
}
|
||||||
dl.formgrid > dd {
|
dl.formgrid > dd {
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
@ -312,6 +314,21 @@ dl.formgrid > dd button {
|
|||||||
dl.formgrid > dd button + button {
|
dl.formgrid > dd button + button {
|
||||||
margin-left: 0.25em;
|
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 */
|
/* Individual overlays */
|
||||||
table.level-browser {
|
table.level-browser {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user