Revamp tileset options; refactor drawing a bit; work on tileset conversion
Tileset options now identify the tilesets by their appearance, rather than the fairly useless "custom 1" or whatever. At last you can draw a tile without creating a renderer. Truly this is the future. Tileset conversion is still incredibly jank, but it does a fairly decent job (at least at LL -> CC2) without too much custom fiddling yet.
This commit is contained in:
parent
5a17b9022d
commit
9763ceaa1c
324
js/main.js
324
js/main.js
@ -12,7 +12,7 @@ import { PrimaryView, DialogOverlay, ConfirmOverlay, flash_button, svg_icon, loa
|
|||||||
import { Editor } from './editor/main.js';
|
import { Editor } from './editor/main.js';
|
||||||
import CanvasRenderer from './renderer-canvas.js';
|
import CanvasRenderer from './renderer-canvas.js';
|
||||||
import SOUNDTRACK from './soundtrack.js';
|
import SOUNDTRACK from './soundtrack.js';
|
||||||
import { Tileset, TILESET_LAYOUTS, parse_tile_world_large_tileset, infer_tileset_from_image } from './tileset.js';
|
import { Tileset, TILESET_LAYOUTS, convert_tileset_to_layout, parse_tile_world_large_tileset, infer_tileset_from_image } from './tileset.js';
|
||||||
import TILE_TYPES from './tiletypes.js';
|
import TILE_TYPES from './tiletypes.js';
|
||||||
import { random_choice, mk, mk_svg } from './util.js';
|
import { random_choice, mk, mk_svg } from './util.js';
|
||||||
import * as util from './util.js';
|
import * as util from './util.js';
|
||||||
@ -54,6 +54,13 @@ function simplify_number(number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function make_button(label, onclick) {
|
||||||
|
let button = mk('button', {type: 'button'}, label);
|
||||||
|
button.addEventListener('click', onclick);
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// TODO:
|
// TODO:
|
||||||
// - level password, if any
|
// - level password, if any
|
||||||
const OBITUARIES = {
|
const OBITUARIES = {
|
||||||
@ -1005,12 +1012,6 @@ class Player extends PrimaryView {
|
|||||||
time_secs_el: this.root.querySelector('#player-debug-time-secs'),
|
time_secs_el: this.root.querySelector('#player-debug-time-secs'),
|
||||||
};
|
};
|
||||||
|
|
||||||
let make_button = (label, onclick) => {
|
|
||||||
let button = mk('button', {type: 'button'}, label);
|
|
||||||
button.addEventListener('click', onclick);
|
|
||||||
return button;
|
|
||||||
};
|
|
||||||
|
|
||||||
// -- Time --
|
// -- Time --
|
||||||
// Hook up back/forward buttons
|
// Hook up back/forward buttons
|
||||||
debug_el.querySelector('.-time-controls').addEventListener('click', ev => {
|
debug_el.querySelector('.-time-controls').addEventListener('click', ev => {
|
||||||
@ -2946,7 +2947,7 @@ const TILESET_SLOTS = [{
|
|||||||
name: "CC2",
|
name: "CC2",
|
||||||
}, {
|
}, {
|
||||||
ident: 'll',
|
ident: 'll',
|
||||||
name: "LL/editor",
|
name: "LL",
|
||||||
}];
|
}];
|
||||||
const CUSTOM_TILESET_BUCKETS = ['Custom 1', 'Custom 2', 'Custom 3'];
|
const CUSTOM_TILESET_BUCKETS = ['Custom 1', 'Custom 2', 'Custom 3'];
|
||||||
const CUSTOM_TILESET_PREFIX = "Lexy's Labyrinth custom tileset: ";
|
const CUSTOM_TILESET_PREFIX = "Lexy's Labyrinth custom tileset: ";
|
||||||
@ -3020,8 +3021,9 @@ class OptionsOverlay extends DialogOverlay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Tileset options
|
// Tileset options
|
||||||
|
this.main.append(mk('h2', "Tilesets"));
|
||||||
this.tileset_els = {};
|
this.tileset_els = {};
|
||||||
this.renderers = {};
|
//this.renderer = new CanvasRenderer(conductor.tilesets[slot.ident], 1);
|
||||||
this.available_tilesets = {};
|
this.available_tilesets = {};
|
||||||
for (let [ident, def] of Object.entries(BUILTIN_TILESETS)) {
|
for (let [ident, def] of Object.entries(BUILTIN_TILESETS)) {
|
||||||
let newdef = { ...def, is_builtin: true };
|
let newdef = { ...def, is_builtin: true };
|
||||||
@ -3047,41 +3049,32 @@ class OptionsOverlay extends DialogOverlay {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let thead = mk('tr', mk('th', "Preview"), mk('th', "Format"));
|
||||||
|
this.tileset_table = mk('table.option-tilesets', thead);
|
||||||
|
this.main.append(this.tileset_table);
|
||||||
for (let slot of TILESET_SLOTS) {
|
for (let slot of TILESET_SLOTS) {
|
||||||
let renderer = new CanvasRenderer(conductor.tilesets[slot.ident], 1);
|
thead.append(mk('th.-slot', slot.name));
|
||||||
this.renderers[slot.ident] = renderer;
|
}
|
||||||
|
for (let [ident, def] of Object.entries(this.available_tilesets)) {
|
||||||
let select = mk('select', {name: `tileset-${slot.ident}`});
|
this._add_tileset_row(ident, def);
|
||||||
for (let [ident, def] of Object.entries(this.available_tilesets)) {
|
|
||||||
if (def.tileset.layout['#supported-versions'].has(slot.ident)) {
|
|
||||||
select.append(mk('option', {value: ident}, def.name));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
select.value = conductor.options.tilesets[slot.ident] ?? 'lexy';
|
|
||||||
if (! conductor._loaded_tilesets[select.value]) {
|
|
||||||
select.value = 'lexy';
|
|
||||||
}
|
|
||||||
select.addEventListener('change', () => {
|
|
||||||
this.update_selected_tileset(slot.ident);
|
|
||||||
});
|
|
||||||
|
|
||||||
let el = mk('dd.option-tileset', select, " ");
|
|
||||||
this.tileset_els[slot.ident] = el;
|
|
||||||
this.update_selected_tileset(slot.ident);
|
|
||||||
|
|
||||||
dl.append(
|
|
||||||
mk('dt', `${slot.name} tileset`),
|
|
||||||
el,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
this.custom_tileset_counter = 1;
|
this.custom_tileset_counter = 1;
|
||||||
dl.append(mk('dd',
|
// FIXME allow drag-drop into... this window? area? idk
|
||||||
mk('p', "You can also load a custom tileset, which will be saved in browser storage."),
|
let custom_tileset_button = mk('button', {type: 'button'}, "Load custom tileset");
|
||||||
mk('p', "MSCC, Tile World, and Steam layouts are all supported."),
|
custom_tileset_button.addEventListener('click', () => this.root.elements['custom-tileset'].click());
|
||||||
mk('p', "(Steam tilesets can be found in ", mk('code', "data/bmp"), " within the game's local files)."),
|
this.main.append(
|
||||||
mk('p', mk('input', {type: 'file', name: 'custom-tileset'})),
|
mk('p',
|
||||||
|
mk('input', {type: 'file', name: 'custom-tileset'}),
|
||||||
|
custom_tileset_button,
|
||||||
|
" — Any format: MSCC, Tile World, or Steam.",
|
||||||
|
),
|
||||||
|
mk('p', "(Steam CC tilesets are in the game files under ", mk('code', "data/bmp"), ".)"),
|
||||||
mk('div.option-load-tileset'),
|
mk('div.option-load-tileset'),
|
||||||
));
|
);
|
||||||
|
this.root.elements['custom-tileset'].addEventListener('change', ev => {
|
||||||
|
this._load_custom_tileset(ev.target.files[0]);
|
||||||
|
});
|
||||||
|
|
||||||
// Load current values
|
// Load current values
|
||||||
this.root.elements['music-volume'].value = this.conductor.options.music_volume ?? 1.0;
|
this.root.elements['music-volume'].value = this.conductor.options.music_volume ?? 1.0;
|
||||||
@ -3092,80 +3085,25 @@ class OptionsOverlay extends DialogOverlay {
|
|||||||
this.root.elements['show-captions'].checked = this.conductor.options.show_captions ?? false;
|
this.root.elements['show-captions'].checked = this.conductor.options.show_captions ?? false;
|
||||||
this.root.elements['use-cc2-anim-speed'].checked = this.conductor.options.use_cc2_anim_speed ?? false;
|
this.root.elements['use-cc2-anim-speed'].checked = this.conductor.options.use_cc2_anim_speed ?? false;
|
||||||
|
|
||||||
this.root.elements['custom-tileset'].addEventListener('change', ev => {
|
for (let slot of TILESET_SLOTS) {
|
||||||
this._load_custom_tileset(ev.target.files[0]);
|
let radioset = this.root.elements[`tileset-${slot.ident}`];
|
||||||
});
|
let value = conductor.options.tilesets[slot.ident] ?? 'lexy';
|
||||||
|
if (! conductor._loaded_tilesets[value]) {
|
||||||
this.add_button("save", () => {
|
value = 'lexy';
|
||||||
let options = this.conductor.options;
|
}
|
||||||
options.music_volume = parseFloat(this.root.elements['music-volume'].value);
|
if (radioset instanceof Element) {
|
||||||
options.music_enabled = this.root.elements['music-enabled'].checked;
|
// There's only one radio button so we just got that back
|
||||||
options.sound_volume = parseFloat(this.root.elements['sound-volume'].value);
|
if (radioset.value === value) {
|
||||||
options.sound_enabled = this.root.elements['sound-enabled'].checked;
|
radioset.checked = true;
|
||||||
options.spatial_mode = parseInt(this.root.elements['spatial-mode'].value, 10);
|
|
||||||
options.show_captions = this.root.elements['show-captions'].checked;
|
|
||||||
options.use_cc2_anim_speed = this.root.elements['use-cc2-anim-speed'].checked;
|
|
||||||
|
|
||||||
// Tileset stuff: slightly more complicated. Save custom ones to localStorage as data
|
|
||||||
// URIs, and /delete/ any custom ones we're not using any more, both of which require
|
|
||||||
// knowing which slots we're already using first
|
|
||||||
let buckets_in_use = new Set;
|
|
||||||
let chosen_tilesets = {};
|
|
||||||
for (let slot of TILESET_SLOTS) {
|
|
||||||
let tileset_ident = this.root.elements[`tileset-${slot.ident}`].value;
|
|
||||||
let tilesetdef = this.available_tilesets[tileset_ident];
|
|
||||||
if (! tilesetdef) {
|
|
||||||
tilesetdef = this.available_tilesets['lexy'];
|
|
||||||
}
|
|
||||||
|
|
||||||
chosen_tilesets[slot.ident] = tilesetdef;
|
|
||||||
if (tilesetdef.is_already_stored) {
|
|
||||||
buckets_in_use.add(tilesetdef.ident);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Clear out _loaded_tilesets first so it no longer refers to any custom tilesets we end
|
else {
|
||||||
// up deleting
|
// This should be an actual radioset
|
||||||
this.conductor._loaded_tilesets = {};
|
radioset.value = value;
|
||||||
for (let [slot_ident, tilesetdef] of Object.entries(chosen_tilesets)) {
|
|
||||||
if (tilesetdef.is_builtin || tilesetdef.is_already_stored) {
|
|
||||||
options.tilesets[slot_ident] = tilesetdef.ident;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// This is a newly uploaded one
|
|
||||||
let data_uri = tilesetdef.data_uri ?? tilesetdef.canvas.toDataURL('image/png');
|
|
||||||
let storage_bucket = CUSTOM_TILESET_BUCKETS.find(
|
|
||||||
bucket => ! buckets_in_use.has(bucket));
|
|
||||||
if (! storage_bucket) {
|
|
||||||
console.error("Somehow ran out of storage buckets, this should be impossible??");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
buckets_in_use.add(storage_bucket);
|
|
||||||
save_json_to_storage(CUSTOM_TILESET_PREFIX + storage_bucket, {
|
|
||||||
src: data_uri,
|
|
||||||
name: storage_bucket,
|
|
||||||
layout: tilesetdef.layout,
|
|
||||||
tile_width: tilesetdef.tile_width,
|
|
||||||
tile_height: tilesetdef.tile_height,
|
|
||||||
});
|
|
||||||
options.tilesets[slot_ident] = storage_bucket;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the conductor's loaded tilesets
|
|
||||||
this.conductor.tilesets[slot_ident] = tilesetdef.tileset;
|
|
||||||
this.conductor._loaded_tilesets[options.tilesets[slot_ident]] = tilesetdef.tileset;
|
|
||||||
}
|
|
||||||
// Delete old custom set URIs
|
|
||||||
for (let bucket of CUSTOM_TILESET_BUCKETS) {
|
|
||||||
if (! buckets_in_use.has(bucket)) {
|
|
||||||
window.localStorage.removeItem(CUSTOM_TILESET_PREFIX + bucket);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.conductor.save_stash();
|
this.add_button("save", () => this.save(), true);
|
||||||
this.conductor.reload_all_options();
|
|
||||||
|
|
||||||
this.close();
|
|
||||||
}, true);
|
|
||||||
this.add_button("forget it", () => {
|
this.add_button("forget it", () => {
|
||||||
// Restore the player's music volume just in case
|
// Restore the player's music volume just in case
|
||||||
if (this.original_music_volume !== undefined) {
|
if (this.original_music_volume !== undefined) {
|
||||||
@ -3197,6 +3135,64 @@ class OptionsOverlay extends DialogOverlay {
|
|||||||
sfx.enabled = was_enabled;
|
sfx.enabled = was_enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_add_tileset_row(ident, def) {
|
||||||
|
let tr = mk('tr');
|
||||||
|
this.tileset_table.append(tr);
|
||||||
|
|
||||||
|
tr.append(mk('td',
|
||||||
|
// TODO maybe draw these all to a single canvas
|
||||||
|
CanvasRenderer.draw_single_tile(def.tileset, 'player'),
|
||||||
|
CanvasRenderer.draw_single_tile(def.tileset, 'chip'),
|
||||||
|
CanvasRenderer.draw_single_tile(def.tileset, 'exit'),
|
||||||
|
));
|
||||||
|
|
||||||
|
tr.append(mk('td.-format',
|
||||||
|
def.tileset.layout['#name'],
|
||||||
|
mk('br'),
|
||||||
|
`${def.tileset.size_x}×${def.tileset.size_y}px`,
|
||||||
|
));
|
||||||
|
|
||||||
|
for (let slot of TILESET_SLOTS) {
|
||||||
|
let td = mk('td.-slot');
|
||||||
|
tr.append(td);
|
||||||
|
if (def.tileset.layout['#supported-versions'].has(slot.ident)) {
|
||||||
|
td.append(mk('label', mk('input', {
|
||||||
|
type: 'radio',
|
||||||
|
name: `tileset-${slot.ident}`,
|
||||||
|
value: ident,
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME make buttons work
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (def.is_builtin) {
|
||||||
|
tr.append(mk('td'));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// TODO this doesn't do anything yet. currently we just delete any tilesets not
|
||||||
|
// assigned to a slot
|
||||||
|
tr.append(mk('td', mk('button', {type: 'button'}, "Forget")));
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.append(mk('td',
|
||||||
|
make_button("LL", () => {
|
||||||
|
convert_tileset_to_layout(def.tileset, 'lexy');
|
||||||
|
}),
|
||||||
|
make_button("CC2", () => {
|
||||||
|
let canvas = convert_tileset_to_layout(def.tileset, 'cc2');
|
||||||
|
mk('a', {href: canvas.toDataURL(), target: '_new'}).click();
|
||||||
|
}),
|
||||||
|
make_button("MSCC", () => {
|
||||||
|
convert_tileset_to_layout(def.tileset, 'tw-static');
|
||||||
|
}),
|
||||||
|
make_button("TW", () => {
|
||||||
|
convert_tileset_to_layout(def.tileset, 'tw-animated');
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
async _load_custom_tileset(file) {
|
async _load_custom_tileset(file) {
|
||||||
// This is dumb and roundabout, but such is the web
|
// This is dumb and roundabout, but such is the web
|
||||||
let reader = new FileReader;
|
let reader = new FileReader;
|
||||||
@ -3212,49 +3208,20 @@ class OptionsOverlay extends DialogOverlay {
|
|||||||
// ratio, hopefully. Note that the LL layout is currently in progress so we can't
|
// ratio, hopefully. Note that the LL layout is currently in progress so we can't
|
||||||
// really detect that, but there can't really be alternatives to it either
|
// really detect that, but there can't really be alternatives to it either
|
||||||
let result_el = this.root.querySelector('.option-load-tileset');
|
let result_el = this.root.querySelector('.option-load-tileset');
|
||||||
|
result_el.textContent = '';
|
||||||
let tileset;
|
let tileset;
|
||||||
try {
|
try {
|
||||||
tileset = infer_tileset_from_image(img, (w, h) => mk('canvas', {width: w, height: h}));
|
tileset = infer_tileset_from_image(img, (w, h) => mk('canvas', {width: w, height: h}));
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
result_el.textContent = '';
|
|
||||||
result_el.append(mk('p', "This doesn't look like a tileset layout I understand, sorry!"));
|
result_el.append(mk('p', "This doesn't look like a tileset layout I understand, sorry!"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let renderer = new CanvasRenderer(tileset, 1);
|
|
||||||
result_el.textContent = '';
|
|
||||||
let buttons = mk('p');
|
|
||||||
result_el.append(
|
|
||||||
mk('p', `This looks like a ${tileset.layout['#name']} tileset with ${tileset.size_x}×${tileset.size_y} tiles.`),
|
|
||||||
mk('p',
|
|
||||||
renderer.draw_single_tile_type('player'),
|
|
||||||
renderer.draw_single_tile_type('chip'),
|
|
||||||
renderer.draw_single_tile_type('exit'),
|
|
||||||
),
|
|
||||||
buttons,
|
|
||||||
);
|
|
||||||
|
|
||||||
let tileset_ident = `new-custom-${this.custom_tileset_counter}`;
|
let tileset_ident = `new-custom-${this.custom_tileset_counter}`;
|
||||||
let tileset_name = `New custom ${this.custom_tileset_counter}`;
|
let tileset_name = `New custom ${this.custom_tileset_counter}`;
|
||||||
this.custom_tileset_counter += 1;
|
let tilesetdef = {
|
||||||
for (let slot of TILESET_SLOTS) {
|
|
||||||
if (! tileset.layout['#supported-versions'].has(slot.ident))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
let dd = this.tileset_els[slot.ident];
|
|
||||||
let select = dd.querySelector('select');
|
|
||||||
select.append(mk('option', {value: tileset_ident}, tileset_name));
|
|
||||||
|
|
||||||
let button = util.mk_button(`Use for ${slot.name}`, () => {
|
|
||||||
select.value = tileset_ident;
|
|
||||||
this.update_selected_tileset(slot.ident);
|
|
||||||
});
|
|
||||||
buttons.append(button);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.available_tilesets[tileset_ident] = {
|
|
||||||
ident: tileset_ident,
|
ident: tileset_ident,
|
||||||
name: tileset_name,
|
name: tileset_name,
|
||||||
canvas: tileset.image,
|
canvas: tileset.image,
|
||||||
@ -3263,6 +3230,10 @@ class OptionsOverlay extends DialogOverlay {
|
|||||||
tile_width: tileset.size_x,
|
tile_width: tileset.size_x,
|
||||||
tile_height: tileset.size_y,
|
tile_height: tileset.size_y,
|
||||||
};
|
};
|
||||||
|
this.available_tilesets[tileset_ident] = tilesetdef;
|
||||||
|
|
||||||
|
this.custom_tileset_counter += 1;
|
||||||
|
this._add_tileset_row(tileset_ident, tilesetdef);
|
||||||
}
|
}
|
||||||
|
|
||||||
update_selected_tileset(slot_ident) {
|
update_selected_tileset(slot_ident) {
|
||||||
@ -3283,6 +3254,77 @@ class OptionsOverlay extends DialogOverlay {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
save() {
|
||||||
|
let options = this.conductor.options;
|
||||||
|
options.music_volume = parseFloat(this.root.elements['music-volume'].value);
|
||||||
|
options.music_enabled = this.root.elements['music-enabled'].checked;
|
||||||
|
options.sound_volume = parseFloat(this.root.elements['sound-volume'].value);
|
||||||
|
options.sound_enabled = this.root.elements['sound-enabled'].checked;
|
||||||
|
options.spatial_mode = parseInt(this.root.elements['spatial-mode'].value, 10);
|
||||||
|
options.show_captions = this.root.elements['show-captions'].checked;
|
||||||
|
options.use_cc2_anim_speed = this.root.elements['use-cc2-anim-speed'].checked;
|
||||||
|
|
||||||
|
// Tileset stuff: slightly more complicated. Save custom ones to localStorage as data URIs,
|
||||||
|
// and /delete/ any custom ones we're not using any more, both of which require knowing
|
||||||
|
// which slots we're already using first
|
||||||
|
let buckets_in_use = new Set;
|
||||||
|
let chosen_tilesets = {};
|
||||||
|
for (let slot of TILESET_SLOTS) {
|
||||||
|
let tileset_ident = this.root.elements[`tileset-${slot.ident}`].value;
|
||||||
|
let tilesetdef = this.available_tilesets[tileset_ident];
|
||||||
|
if (! tilesetdef) {
|
||||||
|
tilesetdef = this.available_tilesets['lexy'];
|
||||||
|
}
|
||||||
|
|
||||||
|
chosen_tilesets[slot.ident] = tilesetdef;
|
||||||
|
if (tilesetdef.is_already_stored) {
|
||||||
|
buckets_in_use.add(tilesetdef.ident);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Clear out _loaded_tilesets first so it no longer refers to any custom tilesets we end
|
||||||
|
// up deleting
|
||||||
|
this.conductor._loaded_tilesets = {};
|
||||||
|
for (let [slot_ident, tilesetdef] of Object.entries(chosen_tilesets)) {
|
||||||
|
if (tilesetdef.is_builtin || tilesetdef.is_already_stored) {
|
||||||
|
options.tilesets[slot_ident] = tilesetdef.ident;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// This is a newly uploaded one
|
||||||
|
let data_uri = tilesetdef.data_uri ?? tilesetdef.canvas.toDataURL('image/png');
|
||||||
|
let storage_bucket = CUSTOM_TILESET_BUCKETS.find(
|
||||||
|
bucket => ! buckets_in_use.has(bucket));
|
||||||
|
if (! storage_bucket) {
|
||||||
|
console.error("Somehow ran out of storage buckets, this should be impossible??");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
buckets_in_use.add(storage_bucket);
|
||||||
|
save_json_to_storage(CUSTOM_TILESET_PREFIX + storage_bucket, {
|
||||||
|
src: data_uri,
|
||||||
|
name: storage_bucket,
|
||||||
|
layout: tilesetdef.layout,
|
||||||
|
tile_width: tilesetdef.tile_width,
|
||||||
|
tile_height: tilesetdef.tile_height,
|
||||||
|
});
|
||||||
|
options.tilesets[slot_ident] = storage_bucket;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the conductor's loaded tilesets
|
||||||
|
this.conductor.tilesets[slot_ident] = tilesetdef.tileset;
|
||||||
|
this.conductor._loaded_tilesets[options.tilesets[slot_ident]] = tilesetdef.tileset;
|
||||||
|
}
|
||||||
|
// Delete old custom set URIs
|
||||||
|
for (let bucket of CUSTOM_TILESET_BUCKETS) {
|
||||||
|
if (! buckets_in_use.has(bucket)) {
|
||||||
|
window.localStorage.removeItem(CUSTOM_TILESET_PREFIX + bucket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.conductor.save_stash();
|
||||||
|
this.conductor.reload_all_options();
|
||||||
|
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
// Ensure the player's music is set back how we left it
|
// Ensure the player's music is set back how we left it
|
||||||
this.conductor.player.update_music_playback_state();
|
this.conductor.player.update_music_playback_state();
|
||||||
|
|||||||
@ -3,10 +3,10 @@ import { mk } from './util.js';
|
|||||||
import { DrawPacket } from './tileset.js';
|
import { DrawPacket } from './tileset.js';
|
||||||
import TILE_TYPES from './tiletypes.js';
|
import TILE_TYPES from './tiletypes.js';
|
||||||
|
|
||||||
class CanvasRendererDrawPacket extends DrawPacket {
|
export class CanvasDrawPacket extends DrawPacket {
|
||||||
constructor(renderer, ctx, perception, clock, update_progress, update_rate) {
|
constructor(tileset, ctx, perception, hide_logic, clock, update_progress, update_rate) {
|
||||||
super(perception, renderer.hide_logic, clock, update_progress, update_rate);
|
super(perception, hide_logic, clock, update_progress, update_rate);
|
||||||
this.renderer = renderer;
|
this.tileset = tileset;
|
||||||
this.ctx = ctx;
|
this.ctx = ctx;
|
||||||
// Canvas position of the cell being drawn
|
// Canvas position of the cell being drawn
|
||||||
this.x = 0;
|
this.x = 0;
|
||||||
@ -14,20 +14,17 @@ class CanvasRendererDrawPacket extends DrawPacket {
|
|||||||
// Offset within the cell, for actors in motion
|
// Offset within the cell, for actors in motion
|
||||||
this.offsetx = 0;
|
this.offsetx = 0;
|
||||||
this.offsety = 0;
|
this.offsety = 0;
|
||||||
// Compatibility settings
|
|
||||||
this.use_cc2_anim_speed = renderer.use_cc2_anim_speed;
|
|
||||||
this.show_facing = renderer.show_facing;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
blit(tx, ty, mx = 0, my = 0, mw = 1, mh = mw, mdx = mx, mdy = my) {
|
blit(tx, ty, mx = 0, my = 0, mw = 1, mh = mw, mdx = mx, mdy = my) {
|
||||||
this.renderer.blit(this.ctx,
|
this.tileset.blit_to_canvas(this.ctx,
|
||||||
tx + mx, ty + my,
|
tx + mx, ty + my,
|
||||||
this.x + this.offsetx + mdx, this.y + this.offsety + mdy,
|
this.x + this.offsetx + mdx, this.y + this.offsety + mdy,
|
||||||
mw, mh);
|
mw, mh);
|
||||||
}
|
}
|
||||||
|
|
||||||
blit_aligned(tx, ty, mx = 0, my = 0, mw = 1, mh = mw, mdx = mx, mdy = my) {
|
blit_aligned(tx, ty, mx = 0, my = 0, mw = 1, mh = mw, mdx = mx, mdy = my) {
|
||||||
this.renderer.blit(this.ctx,
|
this.tileset.blit_to_canvas(this.ctx,
|
||||||
tx + mx, ty + my,
|
tx + mx, ty + my,
|
||||||
this.x + mdx, this.y + mdy,
|
this.x + mdx, this.y + mdy,
|
||||||
mw, mh);
|
mw, mh);
|
||||||
@ -79,6 +76,41 @@ export class CanvasRenderer {
|
|||||||
return mk('canvas', {width: w, height: h});
|
return mk('canvas', {width: w, height: h});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw a single tile, or even the name of a tile type. Either a canvas or a context may be given.
|
||||||
|
// If neither is given, a new canvas is returned.
|
||||||
|
static draw_single_tile(tileset, name_or_tile, canvas = null, x = 0, y = 0) {
|
||||||
|
let ctx;
|
||||||
|
if (! canvas) {
|
||||||
|
canvas = this.make_canvas(tileset.size_x, tileset.size_y);
|
||||||
|
ctx = canvas.getContext('2d');
|
||||||
|
}
|
||||||
|
else if (canvas instanceof CanvasRenderingContext2D) {
|
||||||
|
ctx = canvas;
|
||||||
|
canvas = ctx.canvas;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ctx = canvas.getContext('2d');
|
||||||
|
}
|
||||||
|
|
||||||
|
let name, tile;
|
||||||
|
if (typeof name_or_tile === 'string' || name_or_tile instanceof String) {
|
||||||
|
name = name_or_tile;
|
||||||
|
tile = null;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
tile = name_or_tile;
|
||||||
|
name = tile.type.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Individual tile types always reveal what they are
|
||||||
|
let packet = new CanvasDrawPacket(tileset, ctx, 'palette');
|
||||||
|
packet.x = x;
|
||||||
|
packet.y = y;
|
||||||
|
tileset.draw_type(name, tile, packet);
|
||||||
|
|
||||||
|
return canvas;
|
||||||
|
}
|
||||||
|
|
||||||
set_level(level) {
|
set_level(level) {
|
||||||
this.level = level;
|
this.level = level;
|
||||||
// TODO update viewport size... or maybe Game should do that since you might be cheating
|
// TODO update viewport size... or maybe Game should do that since you might be cheating
|
||||||
@ -148,16 +180,6 @@ export class CanvasRenderer {
|
|||||||
return [x, y];
|
return [x, y];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw to a canvas using tile coordinates
|
|
||||||
blit(ctx, sx, sy, dx, dy, w = 1, h = w) {
|
|
||||||
let tw = this.tileset.size_x;
|
|
||||||
let th = this.tileset.size_y;
|
|
||||||
ctx.drawImage(
|
|
||||||
this.tileset.image,
|
|
||||||
sx * tw, sy * th, w * tw, h * th,
|
|
||||||
dx * tw, dy * th, w * tw, h * th);
|
|
||||||
}
|
|
||||||
|
|
||||||
_adjust_viewport_if_dirty() {
|
_adjust_viewport_if_dirty() {
|
||||||
if (! this.viewport_dirty)
|
if (! this.viewport_dirty)
|
||||||
return;
|
return;
|
||||||
@ -185,8 +207,11 @@ export class CanvasRenderer {
|
|||||||
// game starts, because we're trying to interpolate backwards from 0, hence the Math.max()
|
// game starts, because we're trying to interpolate backwards from 0, hence the Math.max()
|
||||||
let clock = (this.level.tic_counter ?? 0) + (
|
let clock = (this.level.tic_counter ?? 0) + (
|
||||||
(this.level.frame_offset ?? 0) + (update_progress - 1) * this.update_rate) / 3;
|
(this.level.frame_offset ?? 0) + (update_progress - 1) * this.update_rate) / 3;
|
||||||
let packet = new CanvasRendererDrawPacket(
|
let packet = new CanvasDrawPacket(
|
||||||
this, this.ctx, this.perception, Math.max(0, clock), update_progress, this.update_rate);
|
this.tileset, this.ctx, this.perception, this.hide_logic,
|
||||||
|
Math.max(0, clock), update_progress, this.update_rate);
|
||||||
|
packet.use_cc2_anim_speed = this.use_cc2_anim_speed;
|
||||||
|
packet.show_facing = this.show_facing;
|
||||||
|
|
||||||
let tw = this.tileset.size_x;
|
let tw = this.tileset.size_x;
|
||||||
let th = this.tileset.size_y;
|
let th = this.tileset.size_y;
|
||||||
@ -386,7 +411,8 @@ export class CanvasRenderer {
|
|||||||
width = width ?? this.level.size_x;
|
width = width ?? this.level.size_x;
|
||||||
cells = cells ?? this.level.linear_cells;
|
cells = cells ?? this.level.linear_cells;
|
||||||
|
|
||||||
let packet = new CanvasRendererDrawPacket(this, ctx, perception);
|
let packet = new CanvasDrawPacket(this.tileset, ctx, perception);
|
||||||
|
packet.show_facing = show_facing;
|
||||||
for (let x = x0; x <= x1; x++) {
|
for (let x = x0; x <= x1; x++) {
|
||||||
for (let y = y0; y <= y1; y++) {
|
for (let y = y0; y <= y1; y++) {
|
||||||
let cell = cells[y * width + x];
|
let cell = cells[y * width + x];
|
||||||
@ -441,7 +467,8 @@ export class CanvasRenderer {
|
|||||||
let ctx = canvas.getContext('2d');
|
let ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
// Individual tile types always reveal what they are
|
// Individual tile types always reveal what they are
|
||||||
let packet = new CanvasRendererDrawPacket(this, ctx, 'palette');
|
let packet = new CanvasDrawPacket(this.tileset, ctx, 'palette');
|
||||||
|
packet.show_facing = this.show_facing;
|
||||||
packet.x = x;
|
packet.x = x;
|
||||||
packet.y = y;
|
packet.y = y;
|
||||||
this.tileset.draw_type(name, tile, packet);
|
this.tileset.draw_type(name, tile, packet);
|
||||||
|
|||||||
370
js/tileset.js
370
js/tileset.js
@ -294,7 +294,7 @@ export const CC2_TILESET_LAYOUT = {
|
|||||||
},
|
},
|
||||||
// Thin walls are built piecemeal from two tiles; the first is N/S, the second is E/W
|
// Thin walls are built piecemeal from two tiles; the first is N/S, the second is E/W
|
||||||
thin_walls: {
|
thin_walls: {
|
||||||
__special__: 'thin_walls',
|
__special__: 'thin-walls',
|
||||||
thin_walls_ns: [1, 10],
|
thin_walls_ns: [1, 10],
|
||||||
thin_walls_ew: [2, 10],
|
thin_walls_ew: [2, 10],
|
||||||
},
|
},
|
||||||
@ -847,7 +847,7 @@ export const TILE_WORLD_TILESET_LAYOUT = {
|
|||||||
wall_invisible_revealed: [0, 1],
|
wall_invisible_revealed: [0, 1],
|
||||||
// FIXME in cc1 tilesets these are opaque so they should draw at the terrain layer
|
// FIXME in cc1 tilesets these are opaque so they should draw at the terrain layer
|
||||||
thin_walls: {
|
thin_walls: {
|
||||||
__special__: 'thin_walls_cc1',
|
__special__: 'thin-walls-cc1',
|
||||||
north: [0, 6],
|
north: [0, 6],
|
||||||
west: [0, 7],
|
west: [0, 7],
|
||||||
south: [0, 8],
|
south: [0, 8],
|
||||||
@ -1166,12 +1166,12 @@ export const LL_TILESET_LAYOUT = {
|
|||||||
grass: [2, 7],
|
grass: [2, 7],
|
||||||
|
|
||||||
thin_walls: {
|
thin_walls: {
|
||||||
__special__: 'thin_walls',
|
__special__: 'thin-walls',
|
||||||
thin_walls_ns: [8, 4],
|
thin_walls_ns: [8, 4],
|
||||||
thin_walls_ew: [8, 5],
|
thin_walls_ew: [8, 5],
|
||||||
},
|
},
|
||||||
one_way_walls: {
|
one_way_walls: {
|
||||||
__special__: 'thin_walls',
|
__special__: 'thin-walls',
|
||||||
thin_walls_ns: [9, 4],
|
thin_walls_ns: [9, 4],
|
||||||
thin_walls_ew: [9, 5],
|
thin_walls_ew: [9, 5],
|
||||||
},
|
},
|
||||||
@ -1246,6 +1246,7 @@ export const LL_TILESET_LAYOUT = {
|
|||||||
all: new Array(252).fill([12, 8]).concat([
|
all: new Array(252).fill([12, 8]).concat([
|
||||||
[8, 9], [9, 9], [10, 9], [11, 9],
|
[8, 9], [9, 9], [10, 9], [11, 9],
|
||||||
]),
|
]),
|
||||||
|
_distinct: [[12, 8], [8, 9], [9, 9], [10, 9], [11, 9]],
|
||||||
},
|
},
|
||||||
cracked_ice: [12, 9],
|
cracked_ice: [12, 9],
|
||||||
ice_se: [13, 8],
|
ice_se: [13, 8],
|
||||||
@ -2089,13 +2090,22 @@ export class DrawPacket {
|
|||||||
|
|
||||||
export class Tileset {
|
export class Tileset {
|
||||||
constructor(image, layout, size_x, size_y) {
|
constructor(image, layout, size_x, size_y) {
|
||||||
// XXX curiously, i note that .image is never used within this class
|
|
||||||
this.image = image;
|
this.image = image;
|
||||||
this.layout = layout;
|
this.layout = layout;
|
||||||
this.size_x = size_x;
|
this.size_x = size_x;
|
||||||
this.size_y = size_y;
|
this.size_y = size_y;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw to a canvas using tile coordinates
|
||||||
|
blit_to_canvas(ctx, sx, sy, dx, dy, w = 1, h = w) {
|
||||||
|
ctx.drawImage(
|
||||||
|
this.image,
|
||||||
|
sx * this.size_x, sy * this.size_y, w * this.size_x, h * this.size_y,
|
||||||
|
dx * this.size_x, dy * this.size_y, w * this.size_x, h * this.size_y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Everything from here on uses the DrawPacket API
|
||||||
|
|
||||||
draw(tile, packet) {
|
draw(tile, packet) {
|
||||||
this.draw_type(tile.type.name, tile, packet);
|
this.draw_type(tile.type.name, tile, packet);
|
||||||
}
|
}
|
||||||
@ -2698,10 +2708,10 @@ export class Tileset {
|
|||||||
else if (drawspec.__special__ === 'letter') {
|
else if (drawspec.__special__ === 'letter') {
|
||||||
this._draw_letter(drawspec, name, tile, packet);
|
this._draw_letter(drawspec, name, tile, packet);
|
||||||
}
|
}
|
||||||
else if (drawspec.__special__ === 'thin_walls') {
|
else if (drawspec.__special__ === 'thin-walls') {
|
||||||
this._draw_thin_walls(drawspec, name, tile, packet);
|
this._draw_thin_walls(drawspec, name, tile, packet);
|
||||||
}
|
}
|
||||||
else if (drawspec.__special__ === 'thin_walls_cc1') {
|
else if (drawspec.__special__ === 'thin-walls-cc1') {
|
||||||
this._draw_thin_walls_cc1(drawspec, name, tile, packet);
|
this._draw_thin_walls_cc1(drawspec, name, tile, packet);
|
||||||
}
|
}
|
||||||
else if (drawspec.__special__ === 'bomb-fuse') {
|
else if (drawspec.__special__ === 'bomb-fuse') {
|
||||||
@ -2815,7 +2825,7 @@ export function parse_tile_world_large_tileset(canvas) {
|
|||||||
// fell: n/a
|
// fell: n/a
|
||||||
},
|
},
|
||||||
thin_walls: {
|
thin_walls: {
|
||||||
__special__: 'thin_walls_cc1',
|
__special__: 'thin-walls-cc1',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
let image_data = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
let image_data = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||||
@ -3194,3 +3204,347 @@ export function infer_tileset_from_image(img, make_canvas) {
|
|||||||
// Anything else could be Tile World's "large" layout, which has no fixed dimensions
|
// Anything else could be Tile World's "large" layout, which has no fixed dimensions
|
||||||
return parse_tile_world_large_tileset(canvas);
|
return parse_tile_world_large_tileset(canvas);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------------------------------
|
||||||
|
// Tileset conversion
|
||||||
|
|
||||||
|
// Copy tiles from a source image/canvas to a context. The specs should be either a flat list
|
||||||
|
// (static coordinates) or a list of lists (animation).
|
||||||
|
function blit_tile_between_layouts(tileset, old_spec, new_spec, ctx) {
|
||||||
|
// First fix the nesting to be consistent both ways
|
||||||
|
if (! (old_spec[0] instanceof Array)) {
|
||||||
|
old_spec = [old_spec];
|
||||||
|
}
|
||||||
|
if (! (new_spec[0] instanceof Array)) {
|
||||||
|
new_spec = [new_spec];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now blit each frame of the new spec's animation, picking the closest frame from the original
|
||||||
|
for (let [i, dest] of new_spec.entries()) {
|
||||||
|
let src = old_spec[Math.floor(i * old_spec.length / new_spec.length)];
|
||||||
|
tileset.blit_to_canvas(ctx, ...src, ...dest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const DOUBLE_SIZE_FALLBACK = {
|
||||||
|
horizontal: 'east',
|
||||||
|
vertical: 'south',
|
||||||
|
north: 'vertical',
|
||||||
|
south: 'vertical',
|
||||||
|
west: 'horizontal',
|
||||||
|
east: 'horizontal',
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
// bombs
|
||||||
|
// vfx collision
|
||||||
|
// no enemy underlay
|
||||||
|
// no green/purple block overlay
|
||||||
|
// wrong double-recessed wall, somehow??
|
||||||
|
// no turtles
|
||||||
|
// missing base for double-size
|
||||||
|
// missing editor cursors
|
||||||
|
// timid teeth uses different frames, huh.
|
||||||
|
// inactive red tele + trans are here and shouldn't be
|
||||||
|
// sliding player is drawn over walking player
|
||||||
|
// no wire icon
|
||||||
|
// no clone arrows
|
||||||
|
function convert_drawspec(tileset, old_spec, new_spec, ctx) {
|
||||||
|
// If the new spec is null, the tile doesn't exist there, which is fine
|
||||||
|
if (! new_spec)
|
||||||
|
return;
|
||||||
|
|
||||||
|
let recurse = (...keys) => {
|
||||||
|
for (let key of keys) {
|
||||||
|
convert_drawspec(tileset, old_spec[key], new_spec[key], ctx);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (new_spec instanceof Array && old_spec instanceof Array) {
|
||||||
|
// Simple frames
|
||||||
|
// TODO what if old_spec is *not* an array??
|
||||||
|
blit_tile_between_layouts(tileset, old_spec, new_spec, ctx);
|
||||||
|
}
|
||||||
|
else if ((new_spec.__special__ ?? 'animated') === (old_spec.__special__ ?? 'animated')) {
|
||||||
|
if (! new_spec.__special__ || new_spec.__special__ === 'animated') {
|
||||||
|
// Actor facings
|
||||||
|
if (old_spec instanceof Array) {
|
||||||
|
old_spec = {all: old_spec};
|
||||||
|
}
|
||||||
|
if (new_spec instanceof Array) {
|
||||||
|
new_spec = {all: new_spec};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (old_spec.all && new_spec.all) {
|
||||||
|
recurse('all');
|
||||||
|
}
|
||||||
|
else if (! old_spec.all && ! new_spec.all) {
|
||||||
|
recurse('north', 'south', 'east', 'west');
|
||||||
|
}
|
||||||
|
else if (old_spec.all && ! new_spec.all) {
|
||||||
|
convert_drawspec(tileset, old_spec.all, new_spec.north, ctx);
|
||||||
|
convert_drawspec(tileset, old_spec.all, new_spec.south, ctx);
|
||||||
|
convert_drawspec(tileset, old_spec.all, new_spec.east, ctx);
|
||||||
|
convert_drawspec(tileset, old_spec.all, new_spec.west, ctx);
|
||||||
|
}
|
||||||
|
else { // ! old_spec.all && new_spec.all
|
||||||
|
convert_drawspec(tileset, old_spec.south, new_spec.all, ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (new_spec.__special__ === 'arrows') {
|
||||||
|
recurse('base', 'arrows');
|
||||||
|
}
|
||||||
|
else if (new_spec.__special__ === 'double-size-monster') {
|
||||||
|
convert_drawspec(tileset, old_spec.base, new_spec.base, ctx);
|
||||||
|
for (let [direction, fallback] of Object.entries(DOUBLE_SIZE_FALLBACK)) {
|
||||||
|
convert_drawspec(
|
||||||
|
tileset, old_spec[direction] ?? old_spec[fallback], new_spec[direction], ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (new_spec.__special__ === 'letter') {
|
||||||
|
recurse('base');
|
||||||
|
// Technically this doesn't work for two layouts with letters laid out differently, but
|
||||||
|
// no two such layouts exist, so, whatever
|
||||||
|
for (let [glyph, new_coords] of Object.entries(new_spec.letter_glyphs)) {
|
||||||
|
let old_coords = old_spec.letter_glyphs[glyph];
|
||||||
|
tileset.blit_to_canvas(ctx, ...old_coords, ...new_coords, 0.5, 0.5);
|
||||||
|
}
|
||||||
|
for (let [i, new_range] of new_spec.letter_ranges.entries()) {
|
||||||
|
let old_range = old_spec.letter_ranges[i];
|
||||||
|
tileset.blit_to_canvas(ctx,
|
||||||
|
old_range.x0, old_range.y0, new_range.x0, new_range.y0,
|
||||||
|
new_range.columns * new_range.w,
|
||||||
|
Math.ceil((new_range.range[1] - new_range.range[0]) / new_range.columns) * new_range.h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (new_spec.__special__ === 'logic-gate') {
|
||||||
|
tileset.blit_to_canvas(ctx,
|
||||||
|
old_spec.counter_numbers.x, old_spec.counter_numbers.y,
|
||||||
|
new_spec.counter_numbers.x, new_spec.counter_numbers.y,
|
||||||
|
old_spec.counter_numbers.width * 12, old_spec.counter_numbers.height);
|
||||||
|
for (let gate_type of ['not', 'and', 'or', 'xor', 'nand', 'latch-ccw', 'latch-cw', 'counter']) {
|
||||||
|
convert_drawspec(
|
||||||
|
tileset, old_spec.logic_gate_tiles[gate_type], new_spec.logic_gate_tiles[gate_type], ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (new_spec.__special__ === 'perception') {
|
||||||
|
recurse('hidden', 'revealed');
|
||||||
|
}
|
||||||
|
else if (new_spec.__special__ === 'railroad') {
|
||||||
|
recurse('base', 'railroad_switch');
|
||||||
|
for (let key of ['railroad_ties', 'railroad_inactive', 'railroad_active']) {
|
||||||
|
for (let dir of ['ne', 'se', 'sw', 'nw', 'ew', 'ns']) {
|
||||||
|
convert_drawspec(tileset, old_spec[key][dir], new_spec[key][dir], ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (new_spec.__special__ === 'rover') {
|
||||||
|
// No one is ever gonna come up with an alternate rover so just copy enough to hit all
|
||||||
|
// of CC2's frames
|
||||||
|
recurse('glider', 'walker', 'direction');
|
||||||
|
}
|
||||||
|
else if (new_spec.__special__ === 'scroll') {
|
||||||
|
let sx = old_spec.base[0] + Math.min(0, old_spec.scroll_region[0]);
|
||||||
|
let sy = old_spec.base[1] + Math.min(0, old_spec.scroll_region[1]);
|
||||||
|
let dx = new_spec.base[0] + Math.min(0, new_spec.scroll_region[0]);
|
||||||
|
let dy = new_spec.base[1] + Math.min(0, new_spec.scroll_region[1]);
|
||||||
|
tileset.blit_to_canvas(
|
||||||
|
ctx, sx, sy, dx, dy,
|
||||||
|
Math.abs(old_spec.scroll_region[0]) + 1,
|
||||||
|
Math.abs(old_spec.scroll_region[1]) + 1);
|
||||||
|
}
|
||||||
|
else if (new_spec.__special__ === 'thin-walls') {
|
||||||
|
recurse('thin_walls_ns', 'thin_walls_ew');
|
||||||
|
}
|
||||||
|
else if (new_spec.__special__ === 'thin-walls-cc1') {
|
||||||
|
recurse('north', 'south', 'east', 'west', 'southeast');
|
||||||
|
}
|
||||||
|
else if (new_spec.__special__ === 'visual-state') {
|
||||||
|
for (let key of Object.keys(new_spec)) {
|
||||||
|
if (key === '__special__')
|
||||||
|
continue;
|
||||||
|
|
||||||
|
let old_state = old_spec[key];
|
||||||
|
let new_state = new_spec[key];
|
||||||
|
// These might be strings, meaning aliases...
|
||||||
|
if (typeof new_state === 'string') {
|
||||||
|
// New tileset doesn't have dedicated space for this, so nothing to do
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else if (typeof old_state === 'string') {
|
||||||
|
// New tileset wants it, but old tileset aliases it, so deref
|
||||||
|
old_state = old_spec[old_state];
|
||||||
|
}
|
||||||
|
convert_drawspec(tileset, old_state, new_state, ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (new_spec.__special__ === 'wires') {
|
||||||
|
recurse('base', 'wired', 'wired_cross');
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
else if (drawspec.__special__ === 'overlay') {
|
||||||
|
this._draw_overlay(drawspec, name, tile, packet);
|
||||||
|
}
|
||||||
|
else if (drawspec.__special__ === 'scroll') {
|
||||||
|
this._draw_scroll(drawspec, name, tile, packet);
|
||||||
|
}
|
||||||
|
else if (drawspec.__special__ === 'bomb-fuse') {
|
||||||
|
this._draw_bomb_fuse(drawspec, name, tile, packet);
|
||||||
|
}
|
||||||
|
else if (drawspec.__special__ === 'double-size-monster') {
|
||||||
|
this._draw_double_size_monster(drawspec, name, tile, packet);
|
||||||
|
}
|
||||||
|
else if (drawspec.__special__ === 'rover') {
|
||||||
|
this._draw_rover(drawspec, name, tile, packet);
|
||||||
|
}
|
||||||
|
else if (drawspec.__special__ === 'railroad') {
|
||||||
|
this._draw_railroad(drawspec, name, tile, packet);
|
||||||
|
}
|
||||||
|
else if (drawspec.__special__ === 'encased_item') {
|
||||||
|
this._draw_encased_item(drawspec, name, tile, packet);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.error(`No such special ${drawspec.__special__} for ${name}`);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (new_spec.__special__ === 'double-size-monster') {
|
||||||
|
// Converting an old single-size monster to a new double-size one is relatively easy; we can
|
||||||
|
// just draw the small one offset within the double-size space. Unfortunately for layouts
|
||||||
|
// like CC2 we can only show one vertical and one horizontal direction...
|
||||||
|
|
||||||
|
for (let [direction, fallback] of Object.entries(DOUBLE_SIZE_FALLBACK)) {
|
||||||
|
let new_frames = new_spec[direction];
|
||||||
|
if (! new_frames)
|
||||||
|
continue;
|
||||||
|
let old_frames = old_spec[direction] ?? old_spec[fallback];
|
||||||
|
for (let [i, dest] of new_frames.entries()) {
|
||||||
|
if (dest === null)
|
||||||
|
// This means "use the base sprite"
|
||||||
|
continue;
|
||||||
|
let src = old_frames[Math.floor(i * old_frames.length / new_frames.length)];
|
||||||
|
let [dx, dy] = dest;
|
||||||
|
if (direction === 'horizontal' || fallback === 'horizontal') {
|
||||||
|
dx += i / new_frames.length;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
dy += i / new_frames.length;
|
||||||
|
}
|
||||||
|
tileset.blit_to_canvas(ctx, ...src, dx, dy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO the other way, yikes
|
||||||
|
// Convert buttons to/from LL, which adds depressed states
|
||||||
|
else if (! old_spec.__special__ && new_spec.__special__ === 'visual-state') {
|
||||||
|
// Draw the static tile to every state in the new tileset
|
||||||
|
for (let [key, subspec] of Object.entries(new_spec)) {
|
||||||
|
if (key === '__special__' || typeof subspect === 'string')
|
||||||
|
continue;
|
||||||
|
convert_drawspec(tileset, old_spec, subspec, ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (! new_spec.__special__ && old_spec.__special__ === 'visual-state') {
|
||||||
|
// Draw the most fundamental state as the static tile
|
||||||
|
let representative_spec = (
|
||||||
|
old_spec.open // trap
|
||||||
|
|| old_spec.released // button
|
||||||
|
|| old_spec.normal // player i guess??
|
||||||
|
);
|
||||||
|
convert_drawspec(tileset, representative_spec, new_spec, ctx);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convert_tileset_to_tile_world_animated(tileset) {
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convert_tileset_to_layout(tileset, layout_ident) {
|
||||||
|
if (layout_ident === tileset.layout['#ident']) {
|
||||||
|
return tileset.image;
|
||||||
|
}
|
||||||
|
if (layout_ident === 'tw-animated') {
|
||||||
|
return convert_tileset_to_tile_world_animated(tileset);
|
||||||
|
}
|
||||||
|
|
||||||
|
let layout = TILESET_LAYOUTS[layout_ident];
|
||||||
|
let canvas = document.createElement('canvas');
|
||||||
|
canvas.width = layout['#dimensions'][0] * tileset.size_x;
|
||||||
|
canvas.height = layout['#dimensions'][1] * tileset.size_y;
|
||||||
|
let ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
let comparison = {};
|
||||||
|
let summarize = spec => {
|
||||||
|
if (! spec) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
else if (spec instanceof Array) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return spec.__special__;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let [name, spec] of Object.entries(layout)) {
|
||||||
|
// These things aren't tiles
|
||||||
|
if (name === '#ident' || name === '#name' || name === '#dimensions' ||
|
||||||
|
name === '#supported-versions' || name === '#wire-width')
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// These sequences only really exist in LL and were faked with other tiles in other tilesets
|
||||||
|
// TODO so include the fake in LL, right?
|
||||||
|
if (name === 'player1_exit' || name === 'player2_exit') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let old_spec = tileset.layout[name];
|
||||||
|
if (! old_spec)
|
||||||
|
// Guess we can't, uh, do much about this?
|
||||||
|
// TODO warn? dummy tiles?
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Manually adjust some incompatible tilesets
|
||||||
|
// LL's tileset adds animation for ice, which others don't have, and it's difficult to
|
||||||
|
// convert directly because it repeats its tiles a lot
|
||||||
|
if (name === 'ice' && layout_ident === 'lexy') {
|
||||||
|
// To convert TO LL, copy a lone ice tile to every position
|
||||||
|
for (let coords of spec._distinct) {
|
||||||
|
tileset.blit_to_canvas(ctx, ...old_spec, ...coords);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else if (name === 'ice' && tileset.layout['#ident'] === 'lexy') {
|
||||||
|
// To convert FROM LL, pretend it only has its one tile
|
||||||
|
tileset.blit_to_canvas(ctx, ...old_spec._distinct[0], ...spec);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// OK, do the automatic thing
|
||||||
|
if (convert_drawspec(tileset, old_spec, spec, ctx) === false) {
|
||||||
|
comparison[name] = {
|
||||||
|
old: summarize(old_spec),
|
||||||
|
'new': summarize(spec),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.table(comparison);
|
||||||
|
|
||||||
|
console.log(canvas);
|
||||||
|
console.log('%c ', `
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 1px;
|
||||||
|
padding: ${canvas.width}px ${canvas.height}px;
|
||||||
|
background: url(${canvas.toDataURL()});
|
||||||
|
`);
|
||||||
|
console.log(canvas.toDataURL());
|
||||||
|
return canvas;
|
||||||
|
}
|
||||||
|
|||||||
24
style.css
24
style.css
@ -600,7 +600,26 @@ img.compat-icon,
|
|||||||
.option-volume > input[type=range] {
|
.option-volume > input[type=range] {
|
||||||
flex: auto;
|
flex: auto;
|
||||||
}
|
}
|
||||||
.option-tileset canvas {
|
table.option-tilesets th,
|
||||||
|
table.option-tilesets td {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
}
|
||||||
|
table.option-tilesets > tr > .-format {
|
||||||
|
font-size: 0.75em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
table.option-tilesets > tr > .-slot {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
table.option-tilesets > tr > .-slot > label {
|
||||||
|
display: grid;
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
padding: 0.5em 1em;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
table.option-tilesets canvas {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
label.option {
|
label.option {
|
||||||
@ -623,6 +642,9 @@ label.option .option-label {
|
|||||||
.option-help.--visible {
|
.option-help.--visible {
|
||||||
/* TODO */
|
/* TODO */
|
||||||
}
|
}
|
||||||
|
.dialog-options input[type=file][name=custom-tileset] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 800px) {
|
@media (max-width: 800px) {
|
||||||
.dialog {
|
.dialog {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user