diff --git a/js/format-base.js b/js/format-base.js index 15401e1..e071a31 100644 --- a/js/format-base.js +++ b/js/format-base.js @@ -8,6 +8,7 @@ export class StoredLevel { // TODO still not sure this belongs here this.number = number; // one-based this.title = ''; + this.author = ''; this.password = null; this.hint = ''; this.chips_required = 0; diff --git a/js/format-c2g.js b/js/format-c2g.js index d31ebb9..0748511 100644 --- a/js/format-c2g.js +++ b/js/format-c2g.js @@ -843,6 +843,7 @@ export function parse_level(buf, number = 1) { level.time_limit = view.getUint16(0, true); // TODO 0 - 10x10, 1 - 9x9, 2 - split, otherwise unknown which needs handling + // FIXME does this default to 0 if no OPTN block is present? let viewport = view.getUint8(2, true); if (viewport === 0) { level.viewport_size = 10; @@ -1219,6 +1220,22 @@ export function synthesize_level(stored_level) { if (stored_level.title) { c2m.add_section('TITL', stored_level.title); } + if (stored_level.author) { + c2m.add_section('AUTH', stored_level.author); + } + + // Options block + let options = new Uint8Array(3); + new DataView(options.buffer).setUint16(0, stored_level.time_limit, true); + if (stored_level.viewport_size === 10) { + options[2] = 0; + } + else if (stored_level.viewport_size === 9) { + options[2] = 1; + } + // TODO split + // TODO for size purposes, omit the block entirely if all options are defaults? + c2m.add_section('OPTN', options); // Store camera regions // TODO LL feature, should be distinguished somehow @@ -1240,6 +1257,7 @@ export function synthesize_level(stored_level) { let map_view = new DataView(map_bytes.buffer); map_bytes[0] = stored_level.size_x; map_bytes[1] = stored_level.size_y; + let hints = []; let p = 2; for (let cell of stored_level.linear_cells) { for (let i = cell.length - 1; i >= 0; i--) { @@ -1283,10 +1301,20 @@ export function synthesize_level(stored_level) { } } + if (tile.type.name === 'hint') { + hints.push(tile.hint_text); + } + // TODO assert that the bottom tile has no next, and all the others do } } + // 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(''); + c2m.add_section('NOTE', hints.join('\n[CLUE]\n')); + // FIXME ack, ArrayBuffer.slice makes a copy actually! and i use it a lot in this file i think!! let map_buf = map_bytes.buffer.slice(0, p); let compressed_map = compress(map_buf); diff --git a/js/main-editor.js b/js/main-editor.js index ecf084a..77b4913 100644 --- a/js/main-editor.js +++ b/js/main-editor.js @@ -30,9 +30,18 @@ class EditorLevelMetaOverlay extends DialogOverlay { update_time_limit(); time_limit_input.addEventListener('input', update_time_limit); + let make_radio_set = (name, options) => { + let elements = []; + for (let [label, value] of options) { + elements.push(); + } + }; + dl.append( mk('dt', "Title"), mk('dd', 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('dt', "Time limit"), mk('dd', time_limit_input, " ", time_limit_output), mk('dt', "Size"), @@ -43,7 +52,37 @@ class EditorLevelMetaOverlay extends DialogOverlay { "Height: ", mk('input', {name: 'size_y', type: 'number', min: 10, max: 100, value: stored_level.size_y}), ), + mk('dt', "Viewport"), + mk('dd', + mk('label', + mk('input', {name: 'viewport', type: 'radio', value: '10'}), + " 10×10 (Chip's Challenge 2 size)"), + mk('br'), + mk('label', + mk('input', {name: 'viewport', type: 'radio', value: '9'}), + " 9×9 (Chip's Challenge 1 size)"), + mk('br'), + mk('label', + mk('input', {name: 'viewport', type: 'radio', value: '', disabled: 'disabled'}), + " Split 10×10 (not yet supported)"), + ), + mk('dt', "Blob behavior"), + mk('dd', + mk('label', + mk('input', {name: 'blob_behavior', type: 'radio', value: '0'}), + " Deterministic (PRNG + simple convolution)"), + mk('br'), + mk('label', + mk('input', {name: 'blob_behavior', type: 'radio', value: '1'}), + " 4 patterns (default; PRNG + rotating offset)"), + mk('br'), + mk('label', + mk('input', {name: 'blob_behavior', type: 'radio', value: '2'}), + " Extra random (initial seed is truly random)"), + ), ); + this.root.elements['viewport'].value = stored_level.viewport_size; + this.root.elements['blob_behavior'].value = stored_level.blob_behavior; // TODO: // - author // - chips? @@ -64,6 +103,10 @@ class EditorLevelMetaOverlay extends DialogOverlay { stored_level.title = title; this.conductor.update_level_title(); } + let author = els.author.value; + if (author !== stored_level.author) { + stored_level.author = author; + } stored_level.time_limit = parseInt(els.time_limit.value, 10); @@ -73,6 +116,10 @@ class EditorLevelMetaOverlay extends DialogOverlay { this.conductor.editor.resize_level(size_x, size_y); } + stored_level.blob_behavior = parseInt(els.blob_behavior.value, 10); + stored_level.viewport_size = parseInt(els.viewport.value, 10); + this.conductor.player.update_viewport_size(); + this.close(); }); this.footer.append(ok); @@ -409,8 +456,8 @@ const ADJUST_TOGGLES_CCW = {}; ['thief_keys', 'thief_tools'], ['swivel_nw', 'swivel_ne', 'swivel_se', 'swivel_sw'], ['ice_nw', 'ice_ne', 'ice_se', 'ice_sw'], - ['ff_north', 'ff_east', 'ff_south', 'ff_west'], - ['ice', 'ff_all'], + ['force_floor_n', 'force_floor_e', 'force_floor_s', 'force_floor_w'], + ['ice', 'force_floor_all'], ['water', 'turtle'], ['no_player1_sign', 'no_player2_sign'], ['flame_jet_off', 'flame_jet_on'], @@ -743,11 +790,22 @@ const EDITOR_PALETTE = [{ title: "Terrain", tiles: [ 'popwall', - 'fake_floor', 'fake_wall', - 'wall_invisible', 'wall_appearing', + 'steel', + 'wall_invisible', + 'wall_appearing', + 'fake_floor', + 'fake_wall', + 'popdown_floor', + 'popdown_wall', + + 'floor_letter', 'gravel', 'dirt', - 'dirt', + 'slime', + 'thief_keys', + 'thief_tools', + 'no_player1_sign', + 'no_player2_sign', 'floor_custom_green', 'floor_custom_pink', 'floor_custom_yellow', 'floor_custom_blue', 'wall_custom_green', 'wall_custom_pink', 'wall_custom_yellow', 'wall_custom_blue', @@ -765,36 +823,52 @@ const EDITOR_PALETTE = [{ 'bribe', 'railroad_sign', 'hiking_boots', 'speed_boots', 'xray_eye', 'helmet', 'foil', 'lightning_bolt', 'bowling_ball', 'dynamite', 'no_sign', 'bestowal_bow', + 'score_10', 'score_100', 'score_1000', 'score_2x', ], }, { title: "Creatures", tiles: [ 'tank_blue', + 'tank_yellow', 'ball', + 'walker', 'fireball', 'glider', 'bug', 'paramecium', - 'walker', - 'teeth', 'blob', + 'teeth', ], }, { title: "Mechanisms", tiles: [ - 'bomb', 'dirt_block', 'ice_block', + /* + * FIXME this won't work for all kinds of reasons + { name: 'directional_block', arrows: new Set }, + { name: 'directional_block', arrows: new Set(['north']) }, + { name: 'directional_block', arrows: new Set(['north', 'east']) }, + { name: 'directional_block', arrows: new Set(['north', 'south']) }, + { name: 'directional_block', arrows: new Set(['north', 'east', 'south']) }, + { name: 'directional_block', arrows: new Set(['north', 'east', 'south', 'west']) }, + */ + 'bomb', 'button_gray', 'button_green', 'green_floor', 'green_wall', 'green_chip', 'green_bomb', + 'button_yellow', 'button_blue', 'button_red', 'cloner', 'button_brown', 'trap', 'button_orange', 'flame_jet_off', 'flame_jet_on', + 'button_pink', + 'button_black', + 'purple_floor', + 'purple_wall', 'teleport_blue', 'teleport_red', 'teleport_green', @@ -1025,6 +1099,7 @@ export class Editor extends PrimaryView { let stored_level = new format_base.StoredLevel(number); stored_level.size_x = size_x; stored_level.size_y = size_y; + stored_level.viewport_size = 10; for (let i = 0; i < size_x * size_y; i++) { stored_level.linear_cells.push(this._make_cell()); } diff --git a/js/main.js b/js/main.js index 9afae09..6f95ea7 100644 --- a/js/main.js +++ b/js/main.js @@ -676,15 +676,20 @@ class Player extends PrimaryView { this.level = new Level(stored_level, this.gather_compat_options(stored_level)); this.level.sfx = this.sfx_player; this.renderer.set_level(this.level); - this.renderer.set_viewport_size(stored_level.viewport_size, stored_level.viewport_size); - this.renderer.canvas.style.setProperty('--viewport-width', stored_level.viewport_size); - this.renderer.canvas.style.setProperty('--viewport-height', stored_level.viewport_size); + this.update_viewport_size(); this.root.classList.toggle('--has-demo', !!this.level.stored_level.demo); // TODO base this on a hash of the UA + some identifier for the pack + the level index. StoredLevel doesn't know its own index atm... this.change_music(this.conductor.level_index % SOUNDTRACK.length); this._clear_state(); } + update_viewport_size() { + let size = this.conductor.stored_level.viewport_size; + this.renderer.set_viewport_size(size, size); + this.renderer.canvas.style.setProperty('--viewport-width', size); + this.renderer.canvas.style.setProperty('--viewport-height', size); + } + restart_level() { this.level.restart(this.gather_compat_options(this.level.stored_level)); this._clear_state();